diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..15dbf3325 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +node_modules +.vagrant + +npm-debug.log +*.bak + +browserify +report.tap +coverage +.nyc_output +instrumented +reports +.idea/ diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..e91bbb7a5 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,6 @@ +/examples/browserChannelClient/node_modules/** + +/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/node_modules/** +/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/public/lib/** +/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/webTests/lib/** +/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/node_modules/** diff --git a/.eslintrc-md.json b/.eslintrc-md.json new file mode 100644 index 000000000..28f4e3896 --- /dev/null +++ b/.eslintrc-md.json @@ -0,0 +1,15 @@ +{ + "extends": "eslint-config-fluid", + "useEslintrc": false, + "env": { + "browser": true + }, + "rules": { + "no-undef": "off", + "strict": "off", + "no-unused-vars": "off" + }, + "plugins": [ + "markdown" + ] +} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..0ffbac405 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "eslint-config-fluid", + "env": { + "node": true + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..a66295bef --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Github thinks our long .js files are generated and hides them from diffs in PRs. Disable that behavior. +*.js linguist-generated=false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..6e6aa36a9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: gpii-universal tests + +on: [push, pull_request] + +jobs: + tests: + runs-on: ubuntu-latest + env: + HEADLESS: true + + steps: + - uses: actions/checkout@v2 + + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: '12.x' + + - name: Run npm install + run: npm install + + - name: Pass linter + run: $(npm bin)/grunt lint + + - name: Run node tests + run: npm run test:node + + - name: Run browser tests + run: npm run test:browser diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..ff4878197 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +/node_modules/ +/build/ +/instrumented/ + +/examples/browserChannelClient/node_modules/ + +/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/node_modules/ + +npm-debug.log +package-lock.json +*.bak + +Vagrantfile.local +.vagrant/ + +report.tap +coverage +.nyc_output +reports +.idea/ +browserify diff --git a/.nycrc b/.nycrc new file mode 100644 index 000000000..147cb57c0 --- /dev/null +++ b/.nycrc @@ -0,0 +1,132 @@ +{ + "include": [ + "**/*.js", + "gpii/node_modules/**/*.js" + ], + "exclude": [ + "!**/gpii/node_modules/accessRequester/index.js", + "!**/gpii/node_modules/accessRequester/src/accessRequester.js", + "!**/gpii/node_modules/couchConnector/index.js", + "!**/gpii/node_modules/couchConnector/src/js/couchConnector.js", + "!**/gpii/node_modules/deviceReporter/index.js", + "!**/gpii/node_modules/deviceReporter/src/DeviceReporter.js", + "!**/gpii/node_modules/deviceReporter/src/DeviceReporterUtilities.js", + "!**/gpii/node_modules/eventLog/index.js", + "!**/gpii/node_modules/eventLog/src/eventLog.js", + "!**/gpii/node_modules/eventLog/src/installID.js", + "!**/gpii/node_modules/eventLog/src/metrics.js", + "!**/gpii/node_modules/flatMatchMaker/index.js", + "!**/gpii/node_modules/flatMatchMaker/src/FlatMatchMaker.js", + "!**/gpii/node_modules/flowManager/index.js", + "!**/gpii/node_modules/flowManager/src/BrowserChannel.js", + "!**/gpii/node_modules/flowManager/src/CloudBasedFlowManager.js", + "!**/gpii/node_modules/flowManager/src/DefaultSettingsLoader.js", + "!**/gpii/node_modules/flowManager/src/FlowManager.js", + "!**/gpii/node_modules/flowManager/src/HealthGetHandler.js", + "!**/gpii/node_modules/flowManager/src/MatchMaking.js", + "!**/gpii/node_modules/flowManager/src/PrefsServerDataSource.js", + "!**/gpii/node_modules/flowManager/src/PSPChannel.js", + "!**/gpii/node_modules/flowManager/src/ReadyGetHandler.js", + "!**/gpii/node_modules/flowManager/src/SessionAware.js", + "!**/gpii/node_modules/flowManager/src/SettingsDataSource.js", + "!**/gpii/node_modules/flowManager/src/SettingsGetHandler.js", + "!**/gpii/node_modules/flowManager/src/SettingsPutHandler.js", + "!**/gpii/node_modules/flowManager/src/SolutionsRegistryDataSource.js", + "!**/gpii/node_modules/flowManager/src/SystemUtils.js", + "!**/gpii/node_modules/flowManager/src/UntrustedFlowManager.js", + "!**/gpii/node_modules/flowManager/src/UserLogonHandlers.js", + "!**/gpii/node_modules/gpii-db-operation/index.js", + "!**/gpii/node_modules/gpii-db-operation/src/DataStore.js", + "!**/gpii/node_modules/gpii-db-operation/src/DbConst.js", + "!**/gpii/node_modules/gpii-db-operation/src/DbDataStore.js", + "!**/gpii/node_modules/gpii-db-operation/src/DbDataStoreUtils.js", + "!**/gpii/node_modules/gpii-db-operation/src/DbUtils.js", + "!**/gpii/node_modules/gpii-ini-file/index.js", + "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/index.js", + "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/AuthGrantFinder.js", + "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/AuthorizationService.js", + "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/AuthServer.js", + "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/ClientService.js", + "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/CodeGenerator.js", + "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/index.js", + "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Const.js", + "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Express.js", + "!**/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Utilities.js", + "!**/gpii/node_modules/gpii-oauth2/index.js", + "!**/gpii/node_modules/gpii-user-errors/index.js", + "!**/gpii/node_modules/gpii-user-errors/src/UserErrors.js", + "!**/gpii/node_modules/journal/index.js", + "!**/gpii/node_modules/journal/src/Journal.js", + "!**/gpii/node_modules/journal/src/JournalIdParser.js", + "!**/gpii/node_modules/journal/src/SettingsDir.js", + "!**/gpii/node_modules/lifecycleActions/index.js", + "!**/gpii/node_modules/lifecycleActions/src/LifecycleActions.js", + "!**/gpii/node_modules/lifecycleManager/index.js", + "!**/gpii/node_modules/lifecycleManager/src/LifecycleManager.js", + "!**/gpii/node_modules/lifecycleManager/src/LifecycleManagerSession.js", + "!**/gpii/node_modules/lifecycleManager/src/PrivateMatchMaker.js", + "!**/gpii/node_modules/lifecycleManager/src/Resolvers.js", + "!**/gpii/node_modules/lifecycleManager/src/UserLogonRequest.js", + "!**/gpii/node_modules/lifecycleManager/src/UserLogonStateChange.js", + "!**/gpii/node_modules/matchMakerFramework/index.js", + "!**/gpii/node_modules/matchMakerFramework/src/MatchMakerFramework.js", + "!**/gpii/node_modules/matchMakerFramework/src/MatchMakerUtilities.js", + "!**/gpii/node_modules/ontologyHandler/index.js", + "!**/gpii/node_modules/ontologyHandler/src/ontologyHandler.js", + "!**/gpii/node_modules/ontologyHandler/src/ontologyHandlerUtilities.js", + "!**/gpii/node_modules/preferencesServer/index.js", + "!**/gpii/node_modules/preferencesServer/src/preferencesGetHandler.js", + "!**/gpii/node_modules/preferencesServer/src/preferencesPostHandler.js", + "!**/gpii/node_modules/preferencesServer/src/preferencesPutHandler.js", + "!**/gpii/node_modules/preferencesServer/src/preferencesServer.js", + "!**/gpii/node_modules/preferencesServer/src/preferencesServerConst.js", + "!**/gpii/node_modules/preferencesServer/src/preferencesService.js", + "!**/gpii/node_modules/preferencesServer/src/readyGetHandler.js", + "!**/gpii/node_modules/processReporter/index.js", + "!**/gpii/node_modules/processReporter/src/ProcessReporter.js", + "!**/gpii/node_modules/processReporter/src/processesBridge.js", + "!**/gpii/node_modules/settingsHandlers/index.js", + "!**/gpii/node_modules/settingsHandlers/src/ExplodingSettingsHandler.js", + "!**/gpii/node_modules/settingsHandlers/src/INISettingsHandler.js", + "!**/gpii/node_modules/settingsHandlers/src/JSONSettingsHandler.js", + "!**/gpii/node_modules/settingsHandlers/src/LaunchHandlers.js", + "!**/gpii/node_modules/settingsHandlers/src/NoSettingsHandler.js", + "!**/gpii/node_modules/settingsHandlers/src/WebSocketsSettingsHandler.js", + "!**/gpii/node_modules/settingsHandlers/src/XMLSettingsHandler.js", + "!**/gpii/node_modules/settingsHandlers/src/settingsHandlerUtilities.js", + "!**/gpii/node_modules/singleInstance/index.js", + "!**/gpii/node_modules/singleInstance/src/singleInstance.js", + "!**/gpii/node_modules/testing/index.js", + "!**/gpii/node_modules/testing/src/Acceptance.js", + "!**/gpii/node_modules/testing/src/BrowserIncludes.js", + "!**/gpii/node_modules/testing/src/CloudBased.js", + "!**/gpii/node_modules/testing/src/CloudBasedOAuth2TestsUtils.js", + "!**/gpii/node_modules/testing/src/Fixtures.js", + "!**/gpii/node_modules/testing/src/Integration.js", + "!**/gpii/node_modules/testing/src/Mocks.js", + "!**/gpii/node_modules/testing/src/NockUtils.js", + "!**/gpii/node_modules/testing/src/RunTestDefs.js", + "!**/gpii/node_modules/testing/src/TestUtils.js", + "!**/gpii/node_modules/transformer/index.js", + "!**/gpii/node_modules/transformer/src/js/Transformer.js", + "!**/gpii/node_modules/userListeners/index.js", + "!**/gpii/node_modules/userListeners/src/listeners.js", + "!**/gpii/node_modules/userListeners/src/pcsc.js", + "!**/gpii/node_modules/userListeners/src/usb.js", + "testData", + "tests", + "reports", + "coverage", + "instrumented", + ".nyc_output", + "**/browserify/**", + "scripts", + "examples", + "gpii.js", + "Gruntfile.js" + ], + "reporter": "none", + "report-dir": "reports", + "temp-directory": "coverage", + "clean": false +} diff --git a/.project b/.project deleted file mode 100644 index 3d9434dfe..000000000 --- a/.project +++ /dev/null @@ -1,11 +0,0 @@ - - - gpii-universal - - - - - - - - diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..e128c8417 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM node:8-alpine + +WORKDIR /app +COPY . /app +ARG gitFullRev + +RUN apk add --no-cache --virtual build-dependencies python make git g++ && \ + apk add --no-cache curl jq && \ + chown -R node:node . && \ + su node -c 'npm install' && \ + npm cache clean --force && \ + apk del build-dependencies && \ + echo "{\"sha256\": \"$gitFullRev\"}" > gpii-revision.json + +USER node + +CMD ["npm","start"] diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 000000000..b2b815d18 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,31 @@ +/*! +GPII Universal project Gruntfile + +Copyright 2014 RTF-US + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ +"use strict"; +module.exports = function (grunt) { + grunt.initConfig({ + lintAll: { + sources: { + md: [ "./*.md","./documentation/*.md", "./examples/**/*.md", "./gpii/**/*.md"], + js: ["!./browserify/**/*.js", "./gpii/**/*.js", "./tests/**/*.js", "./examples/**/*.js", "*.js"], + json: ["./gpii/**/*.json", "./tests/**/*.json", "./testData/**/*.json", "./*.json"], + json5: ["./gpii/**/*.json5", "./tests/**/*.json5", "./testData/**/*.json5", "./*.json5"], + other: ["./.*"] + } + } + }); + + grunt.loadNpmTasks("gpii-grunt-lint-all"); + grunt.registerTask("lint", "Perform all standard lint checks.", ["lint-all"]); +}; diff --git a/LICENSE.txt b/LICENSE.txt index 338c9fbd8..051ad3955 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,16 +1,23 @@ -Copyright (c) 2012, OCAD University +All modules are Copyright 2014 Raising the Floor - International except +where noted otherwise in the code itself, or if the modules reside in a +separate directory, they may contain explicit declarations of copyright +in both the LICENSE file in the directory in which they reside and in the +code itself. No external contributions are allowed under licenses which are +fundamentally incompatible with the BSD or Apache licenses that the GPII is +distributed under. + All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of OCAD University nor the names of its contributors may - be used to endorse or promote products derived from this software without + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of OCAD University nor the names of its contributors may + be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" @@ -22,4 +29,7 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The research leading to these results has received funding from +the European Union's Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. \ No newline at end of file diff --git a/README.md b/README.md index d19427b17..62d628bba 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,329 @@ -GPII Universal -============== +# GPII Universal -Cross platform, core components of the GPII personalization infrastructure. +The package contains cross-platform core components of the GPII personalization infrastructure. This repository should +not be used directly, but in conjunction with one of the top-level GPII architecture-specific repositories. +Additional documentation is available [on our wiki](http://wiki.gpii.net/w/Architecture). -Installation ------------- +## Installation -Run the following command in your newly checked out universal repository. This -will pull all dependencies that are required by universal. +In production, this repository should be installed using one of the top-level GPII architecture-specific repositories: - npm install +* [windows](https://github.com/GPII/windows) +* [linux](https://github.com/GPII/linux) +* [android](https://github.com/GPII/android) -Usage ------ +For development purposes this repository can also be cloned directly. -To use any of the gpii components or funcionality in node, use the +## Quick Start + +To verify the basic installation of GPII universal, you can start the core framework with + +`npm start` + +If all is well, you will see a message like + + 01:51:07.289: Kettle Server 2us8uqry-22 is listening on port 8081 + +Note that this installation will not include any OS-specific features, but can be used to verify system function with +basic preference sets which only start solutions which require filesystem-based configuration (XML, JSON or .INI files). + +## Environment Variables + +Through the use of Kettle +[resolvers](https://github.com/fluid-project/kettle/blob/master/docs/ConfigsAndApplications.md#referring-to-external-data-via-resolvers), +some pre-defined configuration files offer the ability to read environment variables to change commonly used settings. + +### Preferences Server + +The Preferences Server with the `gpii.config.cloudBased.flowManager.production` configuration uses the following +variables: + +* `GPII_PREFERENCESSERVER_LISTEN_PORT`: TCP port to listen on (default: `8081`) +* `GPII_DATASOURCE_HOSTNAME`: The host name of CouchDB (default: `http://localhost`) +* `GPII_DATASOURCE_PORT`: The port of CouchDB (default: `5984`) + +Example: + +```snippet +GPII_PREFERENCESSERVER_LISTEN_PORT=9081 \ +GPII_DATASOURCE_HOSTNAME=https://localhost \ +GPII_DATASOURCE_PORT=5984 \ +NODE_ENV=gpii.config.cloudBased.flowManager.production \ +npm start +``` + +### Flow Manager + +The Flow Manager with the `gpii.config.cloudBased.flowManager.production` configuration uses the following variables: + +* `GPII_FLOWMANAGER_LISTEN_PORT`: TCP port to listen on (default: 8081) +* `GPII_FLOWMANAGER_TO_PREFERENCESSERVER_URL`: The preferences server URL used by the cloud based flow manager to + communicate with the preferences server + preferences (default: `http://localhost:8081`) +* `GPII_DATASOURCE_HOSTNAME`: The host name of CouchDB (default: `http://localhost`) +* `GPII_DATASOURCE_PORT`: The port of CouchDB (default: 5984) +* `GPII_CLOUD_URL`: The URL to GPII Cloud (default: `http://localhost:8084`). Used by an untrusted local flow manager + to communicate with GPII Cloud. + +#### Example: + +```snippet +GPII_FLOWMANAGER_LISTEN_PORT=9091 \ +GPII_FLOWMANAGER_TO_PREFERENCESSERVER_URL=http://localhost:8081 \ +GPII_DATASOURCE_HOSTNAME=https://localhost \ +GPII_DATASOURCE_PORT=5984 \ +NODE_ENV=gpii.config.cloudBased.flowManager.production \ +npm start +``` + +## Recovering From System Corruption Using the Journal + +Either when operating the live GPII system or running test cases, you may end up corrupting your desktop settings in the +case there is a system crash. In this case, you can navigate to + + http://localhost:8081/journal/journals.html + +to browse a set of journal recovery snapshots. Clicking on the first link on this page will restore your system +to the state it was in prior to the most recent GPII login. + +## Testing + +There are currently 3 different sets of tests: + +* Tests that run in the browser +* Tests that run in node.js +* Production tests, that run in node.js, but which interact with external services. + +The `npm test` command will run the browser and Node based tests. Please note, the node tests may behave oddly if you +have set the `NODE_ENV` variable. The last two types of tests require you to have either Docker or Vagrant installed +and available on your path, or else have a local instance of CouchDB listening on port 25984. +If you are using Vagrant, you'll need to set the `GPII_TEST_COUCH_USE_VAGRANT` environment +variable to `true`, or if you wish to bind tests against a local CouchDB on port 25984, you can +set the `GPII_TEST_COUCH_USE_EXTERNAL` environment variable to `true`. See [the documentation for the +gpii-couchdb-test-harness package](https://github.com/GPII/gpii-couchdb-test-harness) for more details. + +### Convert Preferences Data + +GPII has two sets of source preferences JSON5 data files, located at `%gpii-universal/testData/preferences` and +`%gpii-universal/tests/data/preferences/`. These are converted and used in various configurations. + +The preferences files in `%gpii-universaluniversal/testData/preferences/` are converted into both `snapset` and +`user` preferences: + +* `%gpii-universal/build/dbData/snapset/gpiiKeys.json` +* `%gpii-universal/build/dbData/snapset/prefsSafes.json` +* `%gpii-universal/build/dbData/user/gpiiKeys.json` +* `%gpii-universal/build/dbData/user/prefsSafes.json` + +The above `snapset` preferences safes and GPII keys are: + +1. loaded into production and staging CouchDB instances in cloud environments. +2. loaded into a local CouchDB instance when GPII runs locally, regardless of which configuration is used. + +The above `user` preferences are loaded into the local CouchDB for running GPII integration tests. + +The preferences in `%gpii-universal/tests/data/preferences/` are converted into `user` preferences: + +* `%gpii-universal/build/tests/dbData/user/gpiiKeys.json` +* `%gpii-universal/build/tests/dbData/user/prefsSafes.json` + +These `user` preferences and the above `snapset` preferences are used with CouchDB when GPII runs in a development configuration. + +When any preferences file in either one of the two source directories (`%gpii-universaluniversal/testData/preferences/` +or `%gpii-universal/tests/data/preferences/`) are modified, running `npm run postinstall` will generate +gpiiKeys.json and prefsSafes.json files, whose contents are structured for loading into CouchDB. +This step is needed for any preferences modifications that are to be applied to GPII. + +### Running browser tests + +You can run the browser tests in this package with a range of browsers using [Testem](https://github.com/testem/testem), +and the configuration file `tests/testem.js`, which will ensure that code coverage information is collected. From the +root of the `universal` folder, run the following command: + +`npm run test:browser` + +Please note, when running the browser tests locally using this command, the tests will fail on some browsers +(notably Firefox, Safari, and Opera) unless the browser has focus. You can manually click the browser when it launches +to give it focus. You should not give another window focus until the tests complete. This is not a problem when +running the browser tests in Vagrant (see below). It's also not a problem when running the tests in Chrome, which you +can do using a command like: + +`npm run test:browser -- --launch Chrome` + +You can also run (and debug) the tests manually from the root of the repository using a command like: + +`node node_modules/testem/testem.js --file tests/testem.js` + +The required test fixtures and Testem will start, and Testem will provide a URL you can open in a browser. + +If you would like to debug individual tests or view the test summary in a browser without using Testem, you can: + +1. Host the working directory, for example, using a command like the following from the root of the repository: + `python -m SimpleHTTPServer 4103` +2. Open the "rollup" file `tests/all-tests.html` that runs all tests in a browser. Continuing the above example, you + would load the URL `http://localhost:4103/tests/web/html/all-tests.html`. + +Please note, when hosting the tests yourself, you may see multiple 404 errors related to the `/testem.js` and +`/coverage/client/coverageSender.js` files which are provided by Testem and the `gpii.testem` harness respectively. +These 404 errors will not cause any tests to fail and can be safely ignored. + +### Running node-based tests + +From the root of the `universal` folder, run the following command: + +`npm run test:node` + +Please note, the node tests may behave oddly if you have set the `NODE_ENV` variable. + +### Running tests using a VM + +A VM can be automatically created using tools provided by the +[Prosperity4All Quality Infrastructure](https://github.com/GPII/qi-development-environments/). Please ensure the +[requirements](https://github.com/GPII/qi-development-environments/#requirements) have been met. The `vagrant up` +command can then be used to provision a new VM. + +Within the VM, the application will be installed in the directory specified by the `nodejs_app_install_dir` variable, +which is defined in `provisioning/vagrant-vars.yml` configuration file in this repository. By default the installation + directory is set to `/home/vagrant/sync/universal`. + +Following the provisioning phase, tests can be run in the VM directly from the host system, without the need to log +into the VM or interact with its console. + +From your project's top-level directory (where the `Vagrantfile` and `package.json` files reside), run: + +* Node and browser tests: `npm run test:vagrant` +* Node tests only: `npm run test:vagrantNode` +* browser tests only: `npm run test:vagrantBrowser` +* production tests: `npm run test:vagrantProduction` + +The `test:vagrantProduction` target uses the `vagrantCloudBasedContainers.sh` script to spin up docker container-based +GPII components inside the VM. + +**WARNING:** There is also a `test:productionConfig` npm script but do not run it +directly. It is used to execute the production tests *within* the docker container-based GPII components +created by `vagrantCloudBasedContainers.sh`. It is executed as the last step of +`vagrantCloudBasedContainers.sh`. + +You can also run `vagrant ssh` to connect to the VM (or open the VirtualBox console and interface with the desktop +environment) and run the tests manually if you wish. + +## Usage + +To use any of the gpii components or functionality in Node.js, use the following statements to get access to fluid and/or gpii objects. - var fluid = require("universal"); +```javascript +var fluid = require("infusion"), gpii = fluid.registerNamespace("gpii"); - // Now you should have access to both fluid - // and gpii namespace. -Testing -------- +fluid.require("%gpii-universal"); +// Now you will have access to both fluid and gpii namespaces. +``` + +### Running node-based production tests + +The purpose of these tests are to test production config setups of the system. This involves using the cloud based flow +manager when fetching or updating user settings, so there are extended requirements for these tests. + +These tests are a supplement to the `all-tests.js` (and hence not part of that test suite) and should be run separately +when testing the system and having the below requirements available. + +The tests are run in one of two ways, using the following commands: + +1. From a host machine running a guest VM: `npm run test:vagrantProduction` +2. From within the VM: `./scripts/vagrantCloudBasedContainers.sh` + +### Coverage Reporting + +The preferred way to consistently generate a code coverage report is to use Vagrant as described above. When you start +a VM using `vagrant up` and run `npm run test:vagrant`, the full test suite will run in the VM, and at then end of +the run: + +1. A summary of the report will be displayed to the console. +2. A coverage report will be saved to the `reports` directory (open the `index.html` file in that directory in a browser + to see the full details). + +The report displays the code coverage for: + +1. Code reached from the tests run directly in Node. +2. Code reached within a browser when running the tests in Testem. +3. Server fixtures exercised by browser requests when running the Testem tests. + +A code coverage report will also be generated when you run the `npm test` command on your local machine, but you will +need to ensure that browsers receive focus when they are launched (see above). + +The `npm test` command has [two additional associated scripts](https://docs.npmjs.com/misc/scripts). The `pretest` +script runs before the command defined for the `test` script. The `posttest` script runs after. In our case +we use a `pretest` script to clean up previous coverage data before we run the tests, and a `posttest` script to +compile the actual report. Both the `pretest` and `posttest` steps must run in order to generate the report. If you +only care about the test results, you should not need to run either of these scripts. + +## Docker Containers + +The provided Dockerfile can be used to run GPII Universal directly. + +### Build image + +#### Method 1: Via the universal VM + +Start the universal VM by running `vagrant up` in the universal root directory. + +Inside the universal VM, run: + +```snippet +cd sync/universal +./scripts/vagrantCloudBasedContainers.sh +docker images +``` + +Running `docker images` will show the universal docker image named `vagrant-universal` has been built. + +Running `./scripts/vagrantCloudBasedContainers.sh`: + +1. Starts the preferences server in the production mode on the port 9081 inside the VM. To test it, open a browser and + access the URL: `http://localhost:9081/preferences/carla`. The preferences for `carla` should be returned. +2. Starts the flow manager in the production mode on the port 9082 inside the VM. To test it, open a terminal and run: + + ```snippet + curl -H "Content-Type: application/x-www-form-urlencoded" -X POST + -d "username=li&password=dummy&client_id=pilot-computer&client_secret=pilot-computer-secret&grant_type=password" + -X POST http://localhost:9082/access_token + ``` + + An access token should be returned. An example: + + ``` snippet + { + "access_token": "19199532c97ed2e0f0d360df6679f058", + "expiresIn":3600, + "token_type":"Bearer" + } + ``` + +3. The CouchDB data can be accessed via the URL: `http://localhost:5984/_utils/` + +**Note**: + +1. All 3 ports are forwarded from the VM to the host machine. Running the same test URLs on the host machine should + return the same result. +2. In the case that you would like to start these 3 servers without rebuilding the universal docker image, run the + script with `--no-rebuild` option: `./scripts/vagrantCloudBasedContainers.sh --no-rebuild` + +#### Method 2: On a host machine that has docker installed + +To build a Docker image simply run: `docker build -t my-universal .` + +The following command can be used to build an image locally as long as it is run relative to the repository's +`Dockerfile`: + +`docker build --rm -t gpii/universal:$(git rev-parse --short HEAD) .` -There are currently 2 different sets of tests: the ones that run in the browser -and the ones that can run in node. +That will use the Git repository's current abbreviated commit hash as a +[Docker tag](https://docs.docker.com/reference/commandline/cli/#tag). If you would like to download the latest public +Universal image you can use this command: -Running browser tests: -open `{universal}/tests/web/html/all-tests.html` in your preferred browser: +`docker pull gpii/universal` -Running node tests: +Or use the following command to download a particular image identified using a Git commit hash: - node tests/all-tests.js \ No newline at end of file +`docker pull gpii/universal:` diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 000000000..c3a0967e0 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,87 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +require 'yaml' + +vars = YAML.load_file("provisioning/vars.yml") +vagrant_vars = YAML.load_file("provisioning/vagrant-vars.yml") + +app_name = vars["nodejs_app_name"] + +app_directory = vagrant_vars["nodejs_app_install_dir"] + +app_start_script = vars["nodejs_app_start_script"] + +# Prepare the port forwarding for the preferences server +# Check for the existence of the 'VM_PREFERENCES_SERVER_PORT' environment variable. If it +# doesn't exist but 'nodejs_preferences_server_port' is defined in vars.yml, then use that +# port. The default port for the preferences server is 9081. +host_preferences_server_port = ENV["VM_PREFERENCES_SERVER_PORT"] || vars["nodejs_preferences_server_port"] || 9081 +guest_preferences_server_port = vars["nodejs_preferences_server_port"] || 9081 + +# Prepare the port forwarding for the preferences server +host_flow_manager_port = ENV["VM_FLOW_MANAGER_PORT"] || vars["nodejs_flow_manager_port"] || 9082 +guest_flow_manager_port = vars["nodejs_flow_manager_port"] || 9082 + +# Prepare the port forwarding for CouchDB +host_couchdb_port = ENV["VM_FLOW_MANAGER_PORT"] || vars["nodejs_couchdb_port"] || 9082 +guest_couchdb_port = vars["nodejs_couchdb_port"] || 9082 + +# By default this VM will use 2 processor cores and 2GB of RAM. The 'VM_CPUS' and +# "VM_RAM" environment variables can be used to change that behaviour. +cpus = ENV["VM_CPUS"] || 2 +ram = ENV["VM_RAM"] || 2048 + +Vagrant.configure(2) do |config| + + config.vm.box = "inclusivedesign/fedora31" + + # Your working directory will be synced to /home/vagrant/sync in the VM. + config.vm.synced_folder ".", "#{app_directory}" + + # Mounts node_modules in /var/tmp to work around issues in the VirtualBox shared folders + config.vm.provision "shell", run: "always", inline: <<-SHELL + sudo mkdir -p /var/tmp/#{app_name}/node_modules #{app_directory}/node_modules + sudo chown vagrant:vagrant -R /var/tmp/#{app_name}/node_modules #{app_directory}/node_modules + sudo mount -o bind /var/tmp/#{app_name}/node_modules #{app_directory}/node_modules + SHELL + + # List additional directories to sync to the VM in your "Vagrantfile.local" file + # using the following format: + # config.vm.synced_folder "../path/on/your/host/os/your-project", "/home/vagrant/sync/your-project" + + if File.exist? "Vagrantfile.local" + instance_eval File.read("Vagrantfile.local"), "Vagrantfile.local" + end + + # Port forwarding takes place here. The 'guest' port is used inside the VM + # whereas the 'host' port is used by your host operating system. + config.vm.network "forwarded_port", guest: guest_preferences_server_port, host: host_preferences_server_port, protocol: "tcp", + auto_correct: true + config.vm.network "forwarded_port", guest: guest_flow_manager_port, host: host_flow_manager_port, protocol: "tcp", + auto_correct: true + config.vm.network "forwarded_port", guest: guest_couchdb_port, host: host_couchdb_port, protocol: "tcp", + auto_correct: true + + # Port 19531 is needed so logs can be viewed using systemd-journal-gateway + #config.vm.network "forwarded_port", guest: 19531, host: 19531, protocol: "tcp", + # auto_correct: true + + config.vm.hostname = app_name + + config.vm.provider :virtualbox do |vm| + vm.customize ["modifyvm", :id, "--memory", ram] + vm.customize ["modifyvm", :id, "--cpus", cpus] + vm.customize ["modifyvm", :id, "--vram", "256"] + vm.customize ["modifyvm", :id, "--accelerate3d", "off"] + vm.customize ["modifyvm", :id, "--audio", "null", "--audiocontroller", "ac97"] + vm.customize ["modifyvm", :id, "--ioapic", "on"] + vm.customize ["setextradata", "global", "GUI/SuppressMessages", "all"] + end + + config.vm.provision "shell", inline: <<-SHELL + sudo ansible-galaxy install -fr #{app_directory}/provisioning/requirements.yml + sudo UNIVERSAL_VARS_FILE=vagrant-vars.yml PYTHONUNBUFFERED=1 ansible-playbook #{app_directory}/provisioning/playbook.yml --tags="install,configure" --inventory="localhost ansible_connection=local," + SHELL + +end diff --git a/documentation/Apptology.md b/documentation/Apptology.md new file mode 100644 index 000000000..4dd98aa0a --- /dev/null +++ b/documentation/Apptology.md @@ -0,0 +1,24 @@ +# The Apptology + +To allow [the flat matchmaker](./FlatMatchMaker.md) to distinguish what type of application a solution is (and hence +avoid launching two of the same types of applications), the Apptology was introduced. + +The apptology is an ontology that expresses what type of application a solution is (or what principal roles a solution +is to fullfil), such as "screenReader", "magnifier", etc. There is nothing in the apptology preventing a solution from +being of multiple types (e.g. both a screenreader and magnifier). + +The file that defines the mapping between the application type and common terms is located at +[flat-apptology.json5](../testData/ontologies/mappings/flat-apptology.json5). + +Note that while the apptology can be used to describe any type or capability of a solution, it should only be used to +express mutually exclusive application types. So, for example, it only makes sense that a single screenreader is running +on the system at any given time, whereas it is ok for multiple text-editors to be running on the system at the same +time. The reason for this is that the flat matchmaker uses the apptology to decide which applications to launch on the +system, or more specifically, when two solutions are conflicting in their capabilities/roles and only one should be +launched. A more detailed description of the Flat Matchmaker flow can be found here: [Flat Matchmaker +Documentation](FlatMatchMaker.md). + +There are several known and serious issues with the apptology and its current usage, amongst others that it doesn't +express how to turn on and off certain features of a solution, which could affect its role (or solution type) - such as +turning off the voice in a combined screenreader/magnifier. The issues are described in this JIRA: +[GPII-1998](https://issues.gpii.net/browse/GPII-1998) diff --git a/documentation/AuthGrantFinder.md b/documentation/AuthGrantFinder.md new file mode 100644 index 000000000..8606d7f4b --- /dev/null +++ b/documentation/AuthGrantFinder.md @@ -0,0 +1,30 @@ +# Authorization Grant Finder + +The authorization grant finder provides API that allows users to use access tokens to retrieve their corresponding +authorization information. The access tokens can be requested via [Resource Owner GPII key +Grant](https://wiki.gpii.net/w/GPII_OAuth_2_Guide#Resource_Owner_GPII_Key_Grant). + +## getGrantForAccessToken(accessToken) + +* **description**: Retrieve the authorization information of an access token. +* **parameters:** + * accessToken: String. A string representing an authorization issued to the client. +* **return:** A promise object. Once resolved, this object contains the authorization information for the access token. + Return `undefined` when the access token is not found. + * An example of the returned object: + +```JSON +{ + "accessToken": "the_input_accessToken", + "gpiiKey": "bob_gpii_key", + "allowSettingsGet": true, + "allowSettingsPut": true, + "allowedPrefsToWrite": [ + "http://registry.gpii.net/common/language", + "http://registry.gpii.net/common/DPIScale", + "http://registry.gpii.net/common/highContrast/enabled", + "http://registry.gpii.net/common/selfVoicing/enabled", + "http://registry.gpii.net/applications/com.microsoft.windows.mouseSettings" + ] +} +``` diff --git a/documentation/AuthServer.md b/documentation/AuthServer.md new file mode 100644 index 000000000..db3274e85 --- /dev/null +++ b/documentation/AuthServer.md @@ -0,0 +1,51 @@ +# Authorization Server: + +The authorization server adds access control to the cloud-based flow manger to secure access to user preferences. We are +implementing the authorization server using the [OAuth 2.0 Authorization Framework](http://oauth.net/2/). See [GPII +OAuth 2 Guide](https://wiki.gpii.net/w/GPII_OAuth_2_Guide) for the implementation of GPII supported OAuth 2.0 grant +types. + +## Supported GPII Clients + +The authorization server authorizes **GPII App installations** + +* **Custom OAuth2 grant**: [Resource Owner GPII Key + Grant](https://wiki.gpii.net/w/GPII_OAuth_2_Guide#Resource_Owner_GPII_Key_Grant). +* **Capabilities**: GPII App installations can retrieve untrusted settings from GPII Cloud. +* Local Flow Manger within the GPII App installation interacts with GPII Cloud-Based Flow Manager to: + * Retrieve user settings + * Update user preferences + +## APIs + +### Request an access token (POST /access_token) + +* **description**: Returns an access token. Requesting an access token is the first step of using Resource Owner GPII + Key Grant. This access token is required by HTTP requests that retrieve or update user settings. +* **route:** `/access_token` +* **body:** + * `grant_type`: Must be "password" + * `client_id`: The OAuth2 client ID. + * `client_secret`: The OAuth2 client secret. + * `username`: A GPII key. + * `password`: Any string. Note that the Resource Owner GPII Key Grant is customized upon [the OAuth 2.0 Resource + Owner Password Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.3) provided by [the oauth2orize + library](https://github.com/jaredhanson/oauth2orize). The value of the `password` field can NOT be left empty due + to the requirement of this library. + +An example of the POST body: + +```snippet +grant_type=password&client_id=pilot-computer&client_secret=pilot-computer-secret&username=li&password=dummy +``` + +* **method:** `POST` +* **return:** An object, containing the access token and its lifetime in second. For example: + +```snippet +{ + "access_token": {String}, + "expiresIn":3600, + "token_type":"Bearer" +} +``` diff --git a/documentation/AuthorizationService.md b/documentation/AuthorizationService.md new file mode 100644 index 000000000..4709486c5 --- /dev/null +++ b/documentation/AuthorizationService.md @@ -0,0 +1,80 @@ +# Authorization Service + +The authorization service provides API that allows to add and retrieve authorization data for GPII App installations. +GPII App Installations are authorized using [Resource Owner GPII key +Grant](https://wiki.gpii.net/w/GPII_OAuth_2_Guide#Resource_Owner_GPII_Key_Grant). + +* [getInfoByAccessToken(accessToken)](#getinfobyaccesstokenaccesstoken) +* [grantGpiiAppInstallationAuthorization(gpiiKey, clientId, + clientCredentialId)](#grantgpiiappinstallationauthorizationgpiikey-clientid-clientcredentialid) + +## getInfoByAccessToken(accessToken) + +* **description**: Get the authorization information that is associated with the access token. +* **parameters:** + * accessToken: String. A string representing an authorization issued to the client. +* **return: (one of the below)** + * An object. For GPII app installation clients, the object contains: the access token that matches the given + parameter, the client and authorization data. An example: + ```json + { + "accessToken": "gpii-app-installation-accessToken-1", + "clientCredential": { + "type": "clientCredential", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-1", + "oauth2ClientId": "net.gpii.ajc.bakersfield", + "oauth2ClientSecret": "client_secret_ajc_bakersfield", + "allowedIPBlocks": null, + "allowedPrefsToWrite": null, + "isCreateGpiiKeyAllowed": false, + "isCreatePrefsSafeAllowed": false, + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampRevoked": null, + "id": "clientCredential-1" + }, + "authorization": { + "type": "gpiiAppInstallationAuthorization", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-1", + "gpiiKey": "chrome_high_contrast", + "accessToken": "gpii-app-installation-accessToken-1", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-05-29T17:54:00.000Z", + "timestampRevoked": null, + "timestampExpires": "3020-05-30T17:54:00.000Z", + "id": "gpiiAppInstallationAuthorization-1" + } + } + ``` + * `undefined`. `undefined` is returned in any of these cases: + * The authorization that matches the given access token is not found; + * The authorization has been revoked. + +## grantGpiiAppInstallationAuthorization(gpiiKey, clientId, clientCredentialId) + +* **description**: Grant an authorization to a GPII app installation. The authorization allows a GPII app installation + to access user preferences associated with the given GPII key. A new authorization will always be generated and + returned regardless whether the previous authorization assigned for the same gpiiKey + clientId combination has + expired. +* **parameters:** + * gpiiKey: String. A GPII key that associates with user preferences. + * clientId: String. A system generated unique string that identifies the client. + * clientCredentialId: String. A system generated unique string that identifies the valid client credential of this + client with clientId. +* **return:** Object. Contains an access token and the number of seconds that the access token will expire. For example: + +```json +{ + "accessToken": "8ea3457bf283db5d34ea5a4079fa36b2", + "expiresIn": 3600 +} +``` + +Return an object that contains the error message and HTTP status code if, + +* The GPII key is not found, or, +* The client is not a GPII App Installation Client. diff --git a/documentation/BrowserChannel.md b/documentation/BrowserChannel.md new file mode 100644 index 000000000..34a1f8588 --- /dev/null +++ b/documentation/BrowserChannel.md @@ -0,0 +1,122 @@ +# The browserChannel and the WebSockets settings handler + +This document describes how the __Browser Channel__ and the __WebSockets__ settings handler work. + +This feature consists on: + +* A route in the Flow Manager that serves as the entry point for clients: `/browserChannel` +* The components behind this route are the _gpii.flowManager.browserChannel.handler_ + and the _gpii.settingsHandlers.webSockets.component_ + +## The browser channel + +This handler processes every request to `ws://localhost:8081/browserChannel` and is responsible for: + +* Processing every request and determining whether a client is allowed or not to connect +* Registering and removing the clients as they connect or disconnect. +* Processing modifications of settings that are caused by other aspects of the system, e.g. a new user logs in. + +The browser channel handler supports the following request messages and sends the +associated responses. When an error occurs, the handler sends an response and +closes the web sockets connection. + +* A client sends a connection request. In this example, the client is UIO+: + * request: `{type: "connect", solutionId: "net.gpii.uioPlus"}` + * response: `{type: "connectionSucceeded, "payload": {initial settings values for the solutionId}}` +* Client sends a request to change settings values: + * request: `{type: "changeSettings", "payload": {settings values to change}}` + * response: `{type: "changeSettingsReceived", "payload": {settings values after changing}}` +* Some other component of the system changes a setting relevant to connected clients: + * response: `{type: "onChangeSettings", "payload:" {settings values after changing}}` +* Error response when connecting with an unknown solution: + * response: `{isError: true, message: "Rejecting a connection request from _solutionId_. + The solution id was not found in the solutions registry"}` +* Error response when trying to connect more than once: + * response: `{isError: true, message: "Connection already established - cannot send a second connect message"}` + +## The WebSockets settings handler + +This settings handler follows the standard settings handler API and exposes both the .get and .set methods to the rest +of the system. The settings handler is an instance of `gpii.settingsHandler.webSockets.component`, which can be found +in _gpii/node_modules/settingsHandlers/src/WebSocketsComponent.js_. + +This component stores the information about clients and keeps a list of settings for every solution that makes use of +this settings handler. Also, this component notifies connected clients whenever their settings change. + +## Usage + +This small and documented client illustrates the workflow. + +```javascript +var Ws = require("ws"); + +// The client starts the communication + +var socket = new Ws("ws://localhost:8081/browserChannel"); + +// When the connection is done, the client tells to the flow manager its id + +socket.on("open", function () { + console.log("## Socket connected"); + socket.send(JSON.stringify({ + type: "connect", + payload: { + solutionId: "net.gpii.uioPlus" + } + })); +}); + +socket.on("message", function (data) { + console.log("## Received the following message: " + data); + var message = JSON.parse(data); + // Right after sending the id to the flow manager, the server will return back + // the current settings in the system (if any) + if (message.type === "connectionSucceeded") { + console.log("## Got initial settings ", message.payload, " on connection"); + } + // By listening to this message type, the client will be notified when the system has + // new settings to be applied on the client side + else if (message.type === "onSettingsChanged") { + console.log("## Got changed settings ", message.payload); + } +}); +``` + +The workflow between the client and server can be summarised as follows: + +* After connecting to the flow manager, the client sends a socket message to the channel, which is a payload containing + the *id* of the client, in this instance `net.gpii.uioPlus`. +* The client will be registered if the solution's id can be found of the solutions registry, otherwise, the registration + will be rejected and the system will emit en error, and the client will disconnect. +* The client can request changes to its settings by sending a _changeSettings_ message type. If successful, the client + is sent a _changeSettingsReceived_ message type. +* When a _connectionSucceeded_, _changeSettingsReceived_, or an _onSettingsChanged_ signal is sent to the client, the + current available settings for the client are sent as well, e.g.: + ```json + { + "characterSpace":1, + "clickToSelectEnabled":false, + "contrastTheme":"wb", + "fontSize":1.1, + "inputsLargerEnabled":false, + "lineSpace":1, + "selectionTheme":"default", + "selfVoicingEnabled":false, + "simplifiedUiEnabled":false, + "syllabificationEnabled":false, + "tableOfContentsEnabled":false, + "wordSpace":1 + } + ``` +* When a client disconnects, it is removed from the list of registered clients + +## Running the sample client + +An example client is avaiable at [../examples/browserChannelClient](../examples/browserChannelClient). To try it out, first +start the GPII test configuration from the root of universal with + + npm start + +Then start the client from [../examples/browserChannelClient](../examples/browserChannelClient) with + + node browserChannelClient.js diff --git a/documentation/CloudBasedFlow.md b/documentation/CloudBasedFlow.md new file mode 100644 index 000000000..4d3c5756a --- /dev/null +++ b/documentation/CloudBasedFlow.md @@ -0,0 +1,48 @@ +# Cloud Based Flowmanager Flow + +This page describes the flow when GPII is run in cloud based flowmanager mode. This mainly involves two files: + +* `CloudBasedFlowManager.js` - This will be referred to as `Cloud Based FlowManager` in the below +* `MatchMaking.js` - referred to in the below as `FlowManagerRequests`. + +## Overview and APIs + +The Cloud Based FlowManager can be considered as a standalone, cloud-only configuration of the GPII. This is relevant +for applications that are built to support this, and running on a system without the core GPII installed. So for +example, a simple javaphone that has a built-in support for GPII, but is not running the core architecture. This can +send a GPII key and platform information to the Cloud Based FlowManager, and get instructions back on how it should be +set up. + +The endpoint for this service is at `:gpiiKey/settings/:device` (where `:gpiiKey` is the GPII key to log in and +`:device` is information about the current platform.) + +## Detailed walkthrough of flow: + +The process of retrieving application lifecycle instructions in the Cloud Based FlowManager mode is as follows: + +1. GET request is sent to the `:gpiiKey/settings/:device` URL where `:gpiiKey` is the GPII key to log in and `:device` + is a device reporter payload, like: `{"OS":{"id":"web"},"solutions":[{"id":"org.chrome.cloud4chrome"}]}`. It is + handled by the `onSettings` (Cloud Based FlowManager) function which ensures the device payload is valid and fires + two events: `onGpiiKey` and `onDeviceContext` +2. `onGpiiKey` event has two listeners: + * `getPreferences` (FlowManagerRequests) which fetches the preferences and fires the `onPreferences` event when the + preferences are fetched. + * `setGpiiKey` (FlowManagerRequests) which sets the gpiiKey property in the handler +3. `onDeviceContext` event has one listener: + * `getSolutions` (FlowManagerRequests) which fetches the solutions registry and filters it based on the device + reporter info. The `onSolutions` event is fired with the result. The `onReadyToMatch` event is listening to the + three events described above: `onDeviceContext`, `onPreferences` and `onSolutions`. When these three events have + been fired, the `onReadyToMatch` event will be triggered. +4. This event signal that all the resources has been fetched and the matchmaking related portion of the workflow starts. + This is done via the `processMatch` pseudo event, which is listening to the `onReadyToMatch` event and triggered from + it. The `processMatch` event kickes off a set of functions fired sequentially as dictated by the + `flowManager.processMatch.priorities`. As with everything else in this flow, the sequence and ordering of the steps + in the matchmaking process can be modified by the config/setup being used, and this document wont dive into the + details of this flow except for some general observatiosn: + * Generally some prioritized steps: `preProcess`, `matchMakerDispatcher`, `updateActivePrefsSetName` and `transform` + will be run (in that order). + * The end result of this process is a description of the configuration to be applied to system, described per + application. The event `onMatchDone` signals that we have the lifecycle instructions ready. For more details on the + MatchMaker frameworks internal workings, see: [MatchMaker Framework Documentation](MatchMakerFramework.md) +5. `onMatchDone` is being listened to by the `matchToSettings` function (Cloud Based FlowManager) which takes and puts + the lifecycle instructions in the response to the requesting application. diff --git a/documentation/Configs.md b/documentation/Configs.md new file mode 100644 index 000000000..0223ccf5b --- /dev/null +++ b/documentation/Configs.md @@ -0,0 +1,86 @@ +# System Launch and Configs + +## Overview + +The system can generally run in three different modes: + +* **Locally Installed GPII**: The most common mode is as a locally installed system. This is what end users would have + running on their system, and is able to allow a user to log in, auto-configure the system, launch applications, etc. +* **Cloud-based Flowmanager**: This is a mode which is supposed to be run in the cloud. It does not allow a user to log + in, and does no configuration (i.e. has no lifecycle manager). Instead, on request, it is able to serve a user's + preferences in a format required by one or more applications. This is useful for applications that run without a + locally installed GPII (for example, web applications). +* **Custom Configs**: In addition to running the system as a local install or with a cloud-based flowmanager, it is also + possible to run the system in other configurations. Examples are: Preferences server only, Solution Registry only, + Flat MatchMaker only. These are generally useful for production mode where you might want individual (remote) servers + set up for each of the different services used by the system. + +Note that for both the locally installed system and cloud-based flowmanager mode, depending on the exact configuration +used, components such as the solutions registry, preferences server, etc., can be set up to run both locally or +remotely. A description of how to run the system in these modes and of the configs is given below. + +## System Launch + +### Locally Installed + +Since running the system in local install mode is expected to configure the machine and launch applications on login, it +is dependent on the platform. For this reason, running the system locally is done from the platform-specific folders - +that is windows, linux or android. + +If you have installed GPII in the folder : + +* In Microsoft Windows, go to \windows +* In GNU/Linux and other Unix-like systems, go to /linux +* In Android, go to /android + +Then run the command: `node gpii.js` + +By default this will start up the system with everything running locally, using the +`gpii.config.development.manualTesting.json5` configuration file of universal repository (see the `universal/gpii/configs` +folder). If you would like to run the system using a different configuration, say `dev.remote.prefs` run the following: + +* in Microsoft Windows: `SET NODE_ENV=dev.remote.prefs` +* in GNU/Linux and other Unix-like systems: `export NODE_ENV=dev.remote.prefs` + +Followed by the `node gpii.js` command. + +### Cloud-Based Flowmanager + +Since the Cloud-Based Flowmanager does not require any platform-specific bindings (it only returns a modified set of +settings - but does not configure anything), you can run it from universal. Given that you have installed GPII in the +folder , go to: `/node_modules/universal`. + +Change the NODE_ENV environment variable to the cloudbased config file: + +* in Microsoft Windows: `SET NODE_ENV=gpii.config.cloudBased.development.manualTesting` +* in GNU/Linux and other Unix-like systems: `export NODE_ENV=gpii.config.cloudBased.development.manualTesting` + +Then start up the server by running the following command from the universal folder: + +`node gpii.js` + +This will set up the system to run in cloud-based mode, but with everything running on the same (local) device. + +## Special Configs + +### Hide Key-in, Key-out Endpoints + +**Config file**: [``%flowManager/configs/gpii.flowManager.config.suppressHttpEndpoints.base.json5`](../gpii/node_modules/flowManager/configs/gpii.flowManager.config.suppressHttpEndpoints.base.json5) + +**Purpose**: This config is often used when starting the system in the production environment. For security reason, +it stops exposing these key-in and key-out http endpoints: /login, /logout, /proximityTriggered. + +### Reset the Computer to Default Settings When the System Starts + +**Config file**: [``%flowManager/configs/gpii.flowManager.config.resetAtStart.base.json5`](../gpii/node_modules/flowManager/configs/gpii.flowManager.config.resetAtStart.base.json5) + +**Purpose**: This config resets the computer to the default settings at the system startup. See [Reset Computer](ResetComputer.md) +for the reset API and how to use it. + +### Fetch Default Settings from a Remote URL + +**Config file**: [``%flowManager/configs/gpii.flowManager.config.remoteDefaultSettings.base.json5`](../gpii/node_modules/flowManager/configs/gpii.flowManager.config.remoteDefaultSettings.base.json5) + +**Purpose**: This config fetches default settings from a remote URL. The default URL is set to +`https://raw.githubusercontent.com/GPII/universal/master/testData/defaultSettings/defaultSettings.win32.json5` +in this config. See [Reset Computer](ResetComputer.md) for what are default settings. diff --git a/documentation/CouchConnector.md b/documentation/CouchConnector.md new file mode 100644 index 000000000..188913897 --- /dev/null +++ b/documentation/CouchConnector.md @@ -0,0 +1,49 @@ +# Couch Connector: + +When GPII runs using a development configuration, the couchConnector starts up a test instance of CouchDB and +provisions it with sample data. This "Couch Connector" consists of: + +* An instance of CouchDB provided by the [gpii-couchdb-test-harness](https://github.com/GPII/gpii-couchdb-test-harness) + package, which runs in either Docker or a combination of Vagrant and Docker depending on your local setup. This is + configured to listen on port `25984`. +* An express server that provides a `/reset-couch` API to restore the initial data set. By default, it listens on port + `8060`. + +Note that when GPII runs using a production configuration, the authorization server expects to work with a local CouchDB +instance. The couchConnector is not needed or started for those configurations. + +## APIs + +The Docker/Vagrant test harness runs an instance of CouchDB, which provides its own [well-documented +API](http://docs.couchdb.org/en/stable/api/index.html). + +In addition, there is a small REST API that can be used to reload the data in the CouchDB instance, which provides a +single endpoint. + +### GET /reset-couch + +Reprovisions the CouchDB instance with the initial data set, listening on port 8060 by default. + +On success returns a 200 status code and a message like: + +`Success: CouchDB has been reprovisioned with the initial data set.` + +## `gpii.couchConnector` + +The Infusion component responsible for provisioning and resetting a CouchDB instance on request. + +### Component Options + +| Option | Type | Description | Default | +| ----------------- | ---------- | ----------- | ------- | +| `resetServerPort` | Integer | Required. The port on which the express server for restoring the initial data set will run. | 8060 | +| `databases` | Object | [optional] The list of databases to provision. See [the gpii-couchdb-test-harness documentation for more details](https://github.com/the-t-in-rtf/gpii-couchdb-test-harness/blob/GPII-3531/docs/harness.md#the-databases-option). | None | + +### Supported Events + +| Event | Description | Parameters | Parameters Description | +| ----------------------------- | ----------- | ---------- | ---------------------- | +| `onCouchStartupComplete` | Fires when the CouchDB instance has completed startup and is ready to handle requests. | None | | +| `onExpressResetServerStarted` | Fires when the express server used to resetting the data set is ready. | None | | +| `onReady` | Fires when both CouchDB and the express "reset" server are ready. | None | | +| `onCouchProvisioningComplete` | Fires once the CouchDB instance has been reprovisioned, typically in response to a "reset" request. | None | | diff --git a/documentation/DataLoader.md b/documentation/DataLoader.md new file mode 100644 index 000000000..bb7b23a5e --- /dev/null +++ b/documentation/DataLoader.md @@ -0,0 +1,92 @@ +# CouchDB Data Loader + +(`scripts/deleteAndLoadSnapsets.sh`) + +This script is used to setup CouchDB database and is executed as a Kubernetes batch Job every time a new version of the +universal image is deployed to the cluster (also when cluster is initially created). + +It does the following: + +1. Converts the preferences in universal into `snapset` Prefs Safes and their associated GPII Keys, +2. Optionally deletes the existing database, +3. Creates a CouchDB database if none exists, +4. Updates the database with respect to its `_design/views` document, as required, +5. Deletes the `snapset` Prefs Safes and their associated GPII Keys, if any, currently in the database, +6. Loads the latest snapsets and associated keys created at step 1. into the database. + +Steps 4, 5, and 6 are handled by, and documented further in [`scripts/deleteAndLoadSnapsets.js`](https://github.com/GPII/universal/blob/master/scripts/deleteAndLoadSnapsets.js#L11). + +## Environment Variables + +With the exception of `GPII_COUCHDB_URL`, the following environment variables have default values defined within +`scripts/deleteAndLoadSnapsets.sh`. The database, `GPII_COUCHDB_URL`, must be set outside of the script. Developers +can set these variables as needed for testing and experimentation. + +The use of environment variables for data directories is also useful if you want to mount the database data using a Docker +volume and point the data loader at it. + +WARNING: setting `GPII_CLEAR_INDEX` to `true` will erase all the contents of the database. Use with caution, and with +your own database for development. In a staging or production environment, these variables are set appropriately for +those contexts; in particular `GPII_CLEAR_INDEX` will not be set. + +- `GPII_COUCHDB_URL`: URL of the CouchDB database. (required) +- `GPII_CLEAR_INDEX`: If set to `true`, the database at `$GPII_COUCHDB_URL` will be deleted and replaced with an empty + database. (optional) +- `GPII_STATIC_DATA_DIR`: The directory where the static data to be loaded into CouchDB resides. (optional) +- `GPII_PREFERENCES_DATA_DIR`: The directory containing the "raw" preferences that are converted into `snapset` Prefs + Safes and their associated GPII Keys (step 1 above). (optional) +- `GPII_SNAPSET_DATA_DIR`: The directory where the data built from the conversion step reside. (optional) +- `GPII_APP_DIR`: The main directory, typically `universal`. (optional) + +Note that since [the docker doesn't support the environment variable type of +array](https://github.com/moby/moby/issues/20169), separate environment variables are used for inputting data +directories instead of one array that holds these directories. + +## Running + +### Example using containers + +```bash +$ docker run -d -p 5984:5984 --name couchdb couchdb +$ docker run --rm --link couchdb -e GPII_COUCHDB_URL=http://couchdb:5984/gpii \ + -e GPII_CLEAR_INDEX=true vagrant-universal scripts/deleteAndLoadSnapsets.sh +$ docker run -d -p 8081:8081 --name preferences --link couchdb \ + -e NODE_ENV=gpii.config.preferencesServer.standalone.production \ + -e PREFERENCESSERVER_LISTEN_PORT=8081 -e DATASOURCE_HOSTNAME=http://couchdb \ + -e DATASOURCE_PORT=5984 vagrant-universal +``` + +Below are two versions of loading couchdb data from a different location (e.g. +/home/vagrant/sync/universal/testData/dbData for static data directory and /home/vagrant/sync/universal/build/dbData for +build data directory). The first version has the optional `GPII_CLEAR_INDEX` set to true to erase and reset the +database prior to other database changes: + +```bash +$ docker run --name dataloader --link couchdb \ + -v /home/vagrant/sync/universal/testData/dbData:/static_data -e GPII_STATIC_DATA_DIR=/static_data \ + -v /home/vagrant/sync/universal/build/dbData:/build_data -e GPII_SNAPSET_DATA_DIR=/build_data \ + -e GPII_COUCHDB_URL=http://couchdb:5984/gpii \ + -e GPII_CLEAR_INDEX=true vagrant-universal scripts/deleteAndLoadSnapsets.sh +``` + +The second version does not set `GPII_CLEAR_INDEX` such that any existing database is left intact prior to subsequent +changes to it (e.g., deleting the snapsets): + +```bash +$ docker run --name dataloader --link couchdb \ + -v /home/vagrant/sync/universal/testData/dbData:/static_data -e GPII_STATIC_DATA_DIR=/static_data \ + -v /home/vagrant/sync/universal/build/dbData:/build_data -e GPII_SNAPSET_DATA_DIR=/build_data \ + -e GPII_COUCHDB_URL=http://couchdb:5984/gpii \ + vagrant-universal scripts/deleteAndLoadSnapsets.sh +``` + +### Example using a CouchDB installation + +You can provision a CouchDB installation using your local clone of `universal` for testing and development as well. + +```bash +GPII_COUCHDB_URL="http://localhost:5984/gpii" GPII_APP_DIR=$(pwd) bash -c ./scripts/deleteAndLoadSnapsets.sh +``` + +In the above example we've created a `gpii` database in our local CouchDB database, and are working in the top level +`universal` directory. diff --git a/documentation/DataModel.md b/documentation/DataModel.md new file mode 100644 index 000000000..455ea3bc8 --- /dev/null +++ b/documentation/DataModel.md @@ -0,0 +1,6 @@ +# GPII Data Model + +GPII uses CouchDB to store data in JSON documents. The two GPII components that read and write data from the data +storage are the Preferences Server and the Authorization Server. + +The details of the GPII data model can be found [here](https://wiki.gpii.net/w/Keys,_KeyTokens,_and_Preferences). diff --git a/documentation/FlatMatchMaker.md b/documentation/FlatMatchMaker.md new file mode 100644 index 000000000..9e8271aeb --- /dev/null +++ b/documentation/FlatMatchMaker.md @@ -0,0 +1,23 @@ +# Flat Matchmaker + +## Motivation and Description + +This is a very simple reference matchmaker written by the architecture team when building the initial architecture. It +was created to ensure that the framework was able to support the various aspects of the matchmaking process, to make +available some potentially more globally useful functionality to the matchmaker teams, have a base matchmaker that could +be used while the development of the more sophisticated matchmakers was being done. + +## Selection of solutions to configure and launch + +This involves four steps: + +1. Finding the leaves of all the (common term) preferences - 'leaves' meaning all the keys of the preference set that + holds a primitive as it's value. +2. Finding all the capabilities of the solutions on the machine. The capabilities are taken from two sources, both part + of the solutions registry entry of each solution: + * Those explicitly expressed in the solution entry's `capabilitities` block + * The input-paths, as calculated from the solution entry's `capabilitiesTransformations` block. +3. Knowing what preferences the user has (in common terms) and what capabilities each solution has, the flat matchmaker + now simply selects those solutions that have a capability matching a user preference. +4. Any solution present on the device for which the user has application-specific preferences is added. +5. Apply [Apptology](Apptology.md) to select only one solution from each solution type. diff --git a/documentation/FlowManager.md b/documentation/FlowManager.md new file mode 100644 index 000000000..6b65e7bef --- /dev/null +++ b/documentation/FlowManager.md @@ -0,0 +1,295 @@ +# Flow Manager + +The flow manager is the central point of coordination in the system for managing flow between different architecture +components. For example, it coordinates the steps involved during logging in, which require retrieving preferences, +solutions, device data, etc. and passing these to the [MatchMaker Framework](MatchMakerFramework.md). Following those +steps, the payload is sent to the [LifecycleManager](LifecycleManager.md). + +## Important flows + +Depending on what the usage of the system is, the flows will be different. For example user login, user log off, and +retrieving settings from the system in "cloud based flowmanager" mode are all different. Each "flow" is managed in a +different file, with the common events, functions, etc., located in `FlowManager.js` and `MatchMaking.js`. The +different kinds of flows are: + +* **User Login** (`UserLogonHandlers.js`) - the flow for a user keying in to the system. The flow is described in + details in the [loginAndLogoutFlow](LoginAndLogoutFlow.md) document +* **User Logout** (`UserLogonHandlers.js`) - the flow for a user keying out of the system +* **User Logon State Change** (`UserLogonHandlers.js`) - the flow for changing a user's logon state +* **Retrieve Settings** (`CloudBasedFlowManager.js`) - used to retrieve the settings when the system is running in + cloud-based mode. See [CloudBasedFlow](CloudBasedFlow.md) for more details +* **Update Preferences** (`CloudBasedFlowManager.js`) - used to update the preferences when the system is running in + cloud-based mode. See [CloudBasedFlow](CloudBasedFlow.md) for more details + +## Reserved GPII Keys + +### noUser + +The reserved GPII key "noUser" is automatically keyed into the system when there is not an actual key keyed in. This includes: + +* When GPII starts +* Once an actual GPII key is keyed out + +The present of "noUser" key allows users to continue to change settings via QSS (Quick Strip Set) when no actual GPII +key is keyed into the system. + +### reset + +The reserved GPII key "reset" is used by [the flow manager login API](ResetComputer.md#reset-via-http-request) to +reset the computer. + +See [Reset Computer Documentation](ResetComputer.md) for more details about the reset workflow. + +Note that a separate logout of "reset" is not necessary. The final condition of using the "reset" key is to have the +"noUser" key log back in the system. + +### restore + +The reserved GPII key "restore" is used by the journal API to restore a specific journal. The API is: + +GET /journal/restore/:journalId + +Note that a separate logout of "restore" is not necessary. The final condition of using the "reset" key is to have the +"noUser" key log back in the system. + +### readSetting + +The reserved GPII key "readSetting" is used by the PSPChannel read API to read a preference value. + +Note that "readSetting" GPII key does not log into the system at any time. It's only used to construct an initial +payload structure to start a matchMaking process. + +## APIs on Local Flow Manager + +### User Logon state change (GET /user/:gpiiKey/proximityTriggered) + +* **description**: Change the logon state for the user with the given `:gpiiKey`. Note that there is a debounce + functionality implemented following these rules: any RFID actions is ignored for if a login/logout for + is in progress OR if the last login/logout process for finished less than 1.5 seconds ago. For + more details for rules on keying and out, see [LoginAndLogoutFlow](LoginAndLogoutFlow.md) +* **route:** `/user/:gpiiKey/proximityTriggered` where `:gpiiKey` should be the GPII key of the user for which to change + the logon state +* **return:** Message on success or failure of the login/logout + +### User Login (GET /user/:gpiiKey/login) + +* **description**: Log in a user to the system +* **route:** `/user/:gpiiKey/login` where :gpiiKey should be the GPII key of the user +* **method:** `GET` +* **return:** Message saying that user successfully logged into the system or an error message. + +### User Logout (GET /user/:gpiiKey/logout) + +* **description**: Log out a user of the system +* **route:** `/user/:gpiiKey/logout` where `:gpiiKey` should be the GPII key of the user +* **method:** `GET` +* **return:** Message saying that user successfully logged out of the system or an error message. + +## APIs on Cloud Based Flow Manager + +### Check the readiness of Cloud Based Flow Manager (GET /ready) + +* **description**: Check whether Cloud Based Flow Manager is ready to handle requests. + * When Cloud Based Flow Manager and Preferences Server are running together as one server, the readiness endpoint + checks the database connection. + * When Cloud Based Flow Manager and Preferences Server are running as separate servers, the readiness endpoint + checks the communication with Preferences Server. +* **route:** `/ready` +* **method:** `GET` +* **return:** Return http status code 200 when Cloud Based Flow Manager is ready to handle requests. Otherwise, return + http status code 404. + +### Check the liveness of Cloud Based Flow Manager (GET /health) + +* **description**: Check whether Cloud Based Flow Manager itself is running. A running Cloud Based Flow Manager may or may + not be ready to handle requests because the liveness endpoint does not check communications between Cloud Based + Flow Manager with other modules such as Preferences Server and the database. +* **route:** `/health` +* **method:** `GET` +* **return:** Return http status code 200 when Cloud Based Flow Manager itself is running. Otherwise, return http status + code 500. + +### Retrieve the revision of the Cloud Based Flow Manager (GET /revision) + +* **description**: Serve the full `SHA256` of the revision of the source code repository used by this deployment of the + cloud based components of the GPII. +* **route:** `/revision` +* **method:** `GET` +* **return:** A JSON document containing the revision: + +```json +{ + "sha256": "2602bdf868aec49993d8780feec42d4e9f995e21" +} +``` + +### Get an access token (POST /access_token) + +* **description**: Access tokens are credentials used to protect user preferences. An access token represents an + authorization issued to a GPII application. It needs to be provided at retrieving or updating user preferences. + An access token will not be granted in these cases: + * The OAuth2 client associates with an allowed IP range and the ip of the incoming request doesn't belong to this + range. + * The OAuth2 client requests access to a nonexistent GPII key but this client doesn't have privilege to create new + GPII keys or preferences safes. +* **route:** `/access_token` with these parameters in the `POST` body using the `application/x-www-form-urlencoded` Content-Type. + * `grant_type`: must be set to "password". + * `client_id`: the OAuth2 client id. + * `client_secret`: the OAuth2 client_secret. Confidential shared secret, used to verify the identity of the OAuth2 + client + * `username`: the GPII key. + * `password`: any string. +* **method:** `POST` +* **return:** A JSON document with an access token: + +```json5 +{ + "access_token": "carla", + "expiresIn": 3600, + "token_type": "Bearer" +} +``` + +### Get lifecycle instructions from Cloud Based Flow Manager (GET /:gpiiKey/settings/:device) + +* **description**: Get settings in the ontology of preferences from the cloud based flow manager. These settings are + untransformed lifecycle instructions. See [an example of the return payload of this endpoint.](https://github.com/GPII/gpii-payloads/blob/master/CloudBasedFlowManagerUntrustedSettings.md#user-content-return-payload) +* **route:** `/:gpiiKey/settings/:device` where: + * `:gpiiKey` should be the GPII key of the user for which the settings are requested. + * `:device` should be a device reporter payload - for example: + `{"OS":{"id":"linux"},"solutions":[{"id":"org.gnome.desktop.a11y.magnifier"}]}` would retrieve the settings for the + solution with ID `org.gnome.desktop.a11y.magnifier` which is a solution for `linux`. +* **header:** Authorization: Bearer < access_token > + * `access_token` The access token can be first requested via /access_token endpoint. It represents the authorization + that grants a GPII app to access settings associated with a GPII key. Refer to [GPII OAuth2 + Guide](https://wiki.gpii.net/w/GPII_OAuth_2_Guide#Resource_Owner_GPII_Key_Grant) about the detail steps. +* **method:** `GET` +* **return:** An object, containing the GPII key and solution registry entries. Each block in the solution registry + entries contains the relevant lifecycle instructions in a format understandable by the solution. For example: + +```json5 +{ + "gpiiKey": "carla", + "solutionsRegistryEntries": { + "org.nvda-project": { + "name": "NVDA Screen Reader", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configs": { + "type": "gpii.settingsHandlers.INISettingsHandler", + "options": { + "filename": "${{environment}.APPDATA}\\nvda\\nvda.ini", + "allowNumberSignComments": true, + "allowSubSections": true + }, + // ... + } + }, + "configure": [ + "settings.configs" + ], + "restore": [ + "settings.configs" + ], + "start": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe\\}\"" + } + ], + "stop": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "nvda_service.exe" + }, + { + "type": "gpii.windows.closeProcessByName", + "filename": "nvda.exe" + } + ], + "isInstalled": [ + { + "type": "gpii.deviceReporter.registryKeyExists", + "hKey": "HKEY_LOCAL_MACHINE", + "path": "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe", + "subPath": "", + "dataType": "REG_SZ" + } + ], + "isRunning": [ + { + "type": "gpii.processReporter.find", + "command": "nvda" + } + ] + } + // ... + }, + "matchMakerOutput": { + "inferredConfiguration": { + "gpii-default": { + "applications": {} + }, + "turn-down-light": { + "applications": {} + } + } + } +} +``` + +### Update preferences on Cloud Based Flow Manager (PUT /:gpiiKey/settings) + +* **description**: Call the preferences server API to update user preferences, or to create a GPII key and its + associated preferences safe if the GPII key does not exist but the OAuth2 client has privilege to create new GPII + keys and preferences safes. In the case of update existing preferences, the preferences server API merges the + incoming preferences with the existing user preferences and update the merged preferences on the cloud based flow + manager. +* **route:** `/:gpiiKey/settings` where: + * `:gpiiKey` should be the GPII key of the user for which the preferences are updated. +* **header:** Authorization: Bearer < access_token > + * `access_token` The access token can be first requested via /access_token endpoint. It represents the authorization + that grants a GPII app to update settings associated with a GPII key. Refer to [GPII OAuth2 + Guide](https://wiki.gpii.net/w/GPII_OAuth_2_Guide#Resource_Owner_GPII_Key_Grant) about the detail steps. +* **method:** `PUT` +* **request body:** An object, containing a subset of to-be-updated preferences. For example: + +```json +{ + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850 + } + } + } +} +``` + +* **return:** An object, containing the GPII key and a status message. For example: + +```json +{ + "gpiiKey": "carla", + "message": "Successfully updated." +} +``` + +The returned payload when the request is rejected: + +```json +{ + "isError": true, + "message": "Unauthorized" +} +``` diff --git a/documentation/LifecycleManager.md b/documentation/LifecycleManager.md new file mode 100644 index 000000000..9957b5a93 --- /dev/null +++ b/documentation/LifecycleManager.md @@ -0,0 +1,121 @@ +# Lifecycle Manager + +The Lifecycle Manager is responsible for actually performing user login, logout, retrieving the active GPII key +requests. It also configs the users system via setting handlers and launch handlers. + +The Lifecycle Manager is the only component in the system that keeps state. This is done in the "session" member of +the Lifecycle Manager component, and tracks what changes have been done to the system, what the original configuration +of the system was and which user is currently logged in. + +## Lifecycle Manager Model + +This section lists a few important model items that the Lifecycle Manager keeps track of. + +### Current User Logon + +The model path `currentUserLogon` holds information about the last actual user logon state for the proximityTriggered +logon request to verify its [debounce rule](FlowManager.md#user-logon-state-change-get-usergpiikeyproximitytriggered). +Note that this model item does not keep track of the logon of "noUser" key. + +This model structure has the following format: + +```snippet +{ + currentUserLogon: { + type: , // "login" or "logout" + gpiiKey: , // A GPII key` + timeStamp: // Timestamp that the logon occurs + } +} +``` + +### Logon Change + +The model path `logonChange` holds information about the most recent login/logout action including the "noUser" key. + +This model structure has the following format: + +```snippet +{ + logonChange: { + type: , // "login" or "logout" + inProgress: // Whether the handling of the logon change is in progress + gpiiKey: , // A GPII key` + timeStamp: // Timestamp that the logon occurs + } +} +``` + +## Lifecycle Manager Maintained Queues + +### User Logon Request Queue + +The Lifecycle Manager user logon request queue is used to hold all requests for user logging in and logging out. +The queue is handled sequentially to avoid racing issues (e.g. the next request will wait until the previous request +finishes before the processing). + +The logging in and logging out items in the queue should have the following format: + +```snippet +{ + gpiiKey: , // A GPII key + logonState: // "login" or "logout" +} +``` + +### Action Queue + +The Lifecycle Manager action queue is used to hold the high-level action that needs to happen: starting the login process, +starting logout process, starting the update process. Since the steps of these processes are asynchronous, the action +queue was implemented to avoid racing issues between these processes (e.g. if a logout attempt is started before login +is complete, etc). + +Each item in the action queue should have the following format: + +```snippet +{ + invokerName: , + func: , + arg: +} +``` + +The variable `` is an unresolved (i.e. string) single-argument function that returns a +promise. The promise should be resolved when the function is complete (including side-effects). `` is the +argument to pass to the function. `` is the name of the invoker that was called on the Lifecycle Manager for +triggering the adding of the item to the action queue. + +The action queue is run sequentially, and an item is considered "done" once the promise returned by its function is resolved. + +## Main Functions of Lifecycle Manager: + +### Handle User Logon Requests + +The lifecycle manager provides three invokers to handle three type of user logon requests: "login", "logout" and ["proximityTriggered"]((FlowManager.md#user-logon-state-change-get-usergpiikeyproximitytriggered)): + +* `performLogin`: Handle login requests +* `performLogout`: Handle logout requests +* `performProximityTriggered`: Handle proximityTriggered requests + +These invokers perform checks and add corresponding login and logout requests to the user logon request queue. Each +request in the queue triggers the creation of a dynamic user logon request component that instantiates the corresponding +request handler component: + +* `"gpii.lifecycleManager.loginRequest"`: handle login requests +* `"gpii.lifecycleManager.logoutRequest"`: handle logout requests + +The dynamic request component will be automatically destroyed once the handling of the request completes. + +### Start, Stop and Update Processes + +The lifecycle manager has three invokers that are generally the ones that will be called from the general system, namely +"start", "stop" and "update". These invokers are manually created, so are not obvious to spot on the component defaults +block, but they are the ones to be used (instead of the processStart, processStop and processUpdate). They will add the +relevant task to the Lifecycle Managers action queue: + +* `start`: Should be called when keying in (configuring the system) +* `stop`: Should be called when keying out (restoring the system) +* `update`: Should be called when changing the settings of an already configured system (updating the applied settings) + +Note that these are unrelated to, and should not be confused with the "start", "stop" and "update" directives from +solutions registry entries. diff --git a/documentation/LoginAndLogoutFlow.md b/documentation/LoginAndLogoutFlow.md new file mode 100644 index 000000000..2f33db549 --- /dev/null +++ b/documentation/LoginAndLogoutFlow.md @@ -0,0 +1,84 @@ +# User login and Logout flow + +This document describes the flow on a locally installed GPII system when the user logs in and logs out. First an +overview of the endpoints and related semantics are given and this will be followed by a more in-depth technical +walkthrough of the flow. + +## Overview and APIs + +We support 3 different logon related URLs, namely: + +* `/user/:gpiiKey/proximityTriggered` - change the logon state of the given GPII key (i.e. log in or out) + * **Debounce rule**: any RFID actions is ignored for if a login/logout for is in progress OR + if the last login/logout process for finished less than 1.5 seconds ago + * If no user is logged in and debounce doesn't apply, log in + * If is logged in and debounce doesn't apply, log out + * If another user is already logged in or in the process of logging in or out, log that user out and log in + +* `/user/:gpiiKey/login` - log the GPII key in + * If no user is logged in, will be logged in + * If another user is logged in, nothing will happen and an error is returned +* `/user/:gpiiKey/logout` - log the GPII key out + * If no user is logged in, nothing happens + * if user myGpiiKey is logged in, he will be logged out + * If another user is logged in, nothing will happen and an error is returned + +In general, the the `proximityTriggered` URL should be used by proximity devices. This will take the appropriate action +depending on whether the user is logged into the system or not. + +The reason for preferring the `/proximityTriggered` URL over the `/logout` and `/login` URL, is that we support multiple +user listeners and means of logging in and out of the system, so unless the service calling these URLs does a lot of +work checking for currently logged in users, e.g. a `/logout` call when no user (or some other user) is logged in +becomes meaningless. The implementation allows for these kinds of bogus calls to login and logout, simply resulting in +an error response to the request. + +The reason for continuing to support the specific /login and /logout, instead of only supporting the +`/proximityTriggered` URL is that they make sense for some user listeners, for example when using the the USB listener, +one wouldn't expect to log a user out when inserting the USB, or log a user in when pulling the USB out. So for that, it +seems to make sense that it attempts to log in (i.e. call `/login`) or log out (i.e. call `/logout` when +inserting/removing the USB, respectively + +## Technical description + +The core part of the flow is defined in these files: + +* `UserLogonHandlers.js` contains the handling of the logon related endpoints kicks off the related process. It + contains individual handlers for the `login`, `logout` and `proximityTriggered` URLs. These handlers add user logon + request to Lifecycle Manager user logon request queue. Lifecycle Manager then processes the queue in sequence to + perform actual logging in, loggin out and retrieving the current logged in GPII key. +* `UserLogonStateChange.js` contains the functionality for the actual logging in, logging out and retrieving the active + GPII key. +* `MatchMaking.js` describes the remaining part of the flow (e.g. fetching resources and preferences, matchmaking, + etc.). + +The user login process is as follows: + +1. a GET request is sent to either `/user/:gpiiKey/login` or `/user/:gpiiKey/proximityTriggered`. This is retrieved by + the relevant handler in `UserLogonHandlers`. The handler adds a relevant login or logout request to Lifecycle + Manager user logon request queue, which trigger the actual logging in or logging out. If it is found that the GPII + key needs to be logged in, the `onGpiiKey` event is fired (via the + `gpii.lifecycleManager.userLogonHandling.loginUser` function) +2. the `onGpiiKey` event has three listeners: + 1. UserLogonStateChange's `getDeviceContext`, which fetches the device reporter data. When this has been fetched an + `onDeviceContext` event is fired. + 2. `getPreferences` (FlowManagerRequests), which fetches the preferences and fires the `onPreferences` event when the + preferences are fetched. + 3. `setGpiiKey` (FlowManagerRequests) which sets the gpiiKey property in the handler +3. the `onDeviceContext` event has one listener: + 1. `getSolutions` (FlowManagerRequests), which fetches the solutions registry and filters it based on the device + reporter info. The `onSolutions` event is fired with the result. +4. The `onReadyToMatch` event is listening to the three events described above: `onDeviceContext`, `onPreferences` and + `onSolutions`. When these three events have been fired, the `onReadyToMatch` event will be triggered. +5. This event signal that all the resources has been fetched and the matchmaking related portion of the workflow starts. + This is done via the `processMatch` pseudo event, which is listening to the `onReadyToMatch` event and triggered from + it. The `processMatch` event kickes off a set of functions fired sequentially as dictated by the + `flowManager.processMatch.priorities`. As with everything else in this flow, the sequence and ordering of the steps + in the matchmaking process can be modified by the config/setup being used, and this document won't dive into the + details of this flow except for some general observation: + 1. Generally some prioritized steps: `preProcess`, `matchMakerDispatcher`, `updateActivePrefsSetName` and `transform` + will be run (in that order). + 2. The end result of this process is a description of the configuration to be applied to system, described per + application. The even `onMatchDone` signals that we have the lifecycle instructions ready. For more details on the + MatchMaker frameworks internal workings, see: [MatchMaker Framework Documentation](MatchMakerFramework.md) +6. `onMatchDone` is being listened to by the `startLifecycle` (UserLogonStateChange), which applies the settings to the + system via the functionality in the LifecycleManager. diff --git a/documentation/MatchMakerFramework.md b/documentation/MatchMakerFramework.md new file mode 100644 index 000000000..e35e24f3e --- /dev/null +++ b/documentation/MatchMakerFramework.md @@ -0,0 +1,114 @@ +# Matchmaker Framework + +The matchmaker framework is a component and a set of utitilities that can run either locally or remotely depending on +the configuration. Together with the actual matchmaker, it is responsible for deciding how the users device should be +configured based on their preferences, the specified set of preferences therein, and the solution registry data. More +specifically, the matchmaker framework has the following responsibility: + +* Doing the preprocessing - that is, preparing and augmenting the input payload for the specific matchmaker +* Making the decision of which matchmaker to call, and then call that matchmaker (for example the + [Flat MatchMaker](FlatMatchMaker.md)) +* Doing the post-processing - that is, taking the return payload from the matchmakers, parse it and pass it on in the + login (or other) flow. +* Generally providing utilities that can be used by matchmaker implementations. + +## Configuration + +The matchmaker framework itself is not a Kettle app, but it expect at least matchmaker implementation to be available on +a URL. The available matchmaker implementation should be provided in the options block under a `matchMakers` directive. +Here, matchmaker implementation can be listed by some identifier (key) and an object with a `{ url: "SOME URL" }` object +each. For example the options of having two matchmakers available, where the default one will be on port 8081 will be +done like: + +```json +{ + "type": "my.matchmakerframework.config", + "options": { + "distributeOptions": { + "untrusted.development.matchMakers": { + "record": { + "default": { + "url": "http://localhost:8081" + }, + "otherMM": { + "url": "http://localhost:8084" + } + }, + "target": "{that flowManager}.options.matchMakers" + } + } + } +} + +``` + +## Detailed Description: + +### Main steps of the matchMaker frameworks matching process + +1. The matchmaking process is triggered by the `processMatch` event of the FlowManager. The actual matchmaking flow is + dictated by a combination of the priorities in `gpii.flowmanager.processmatch.priorities`, and the `processMatch` + listeners (which are different depending on whether the system runs in trusted or untrusted mode) +* Preprocess: + * Triggered when the initial data has been collected (preferences, named preferences set, solution registry entries, etc) + * TODO: _Full solutions registry is currently being used when inferring common terms. This is because the users + preference set might contain e.g. Linux app preferences, even if the user is logging into a windows box. We then + need the linux solutions registry entries to get information (if available) about how to transform the linux app + preferences into common terms (that in turn can be translated into app settings for windows)._ +* matchMakerDispatcher: + * Sends the matchmaker input data to the actual matchmaker. _While there is functionality in place for allowing + multiple matchmakers, the system is currently hardcoded to use the “default” one, which is the Flat matchmaker in + all the default configs_. + * A promise is returned, which will contain the original matchmaker input along with the response from the matchmaker + in a block keyed by `matchMakerOutput`. + * The return payload is built in: `gpii.matchMakerFramework.utils.buildReturnPayload` + +### Important utilities and concepts in the MatchMaker Framework + +**The Flat Matchmaker**: While this is not part of the matchmaker framework, it is important to mention the Flat +MatchMaker, which is an (the only) implementation of a matchmaker, which uses most of the utilities described in this +document. It is described in details here: +[https://github.com/GPII/universal/blob/master/documentation/FlatMatchMaker.md](./FlatMatchMaker.md) + +**Solution Types**: To ensure that several conflicting solutions (e.g. two screenreaders) are not launched, "Types" can +be calculated for each solution, based on the capabilities they have (and in the future, on their explicitly stated +types). The inference of solution type based on capabilities is based on the "apptology" tranformation, from the "flat" +format into a solution type ontology. In the matchmaking process, each time a solution is 'accepted', any other +conflicting solutions (i.e. of the same type) will be rejected. Adding solution type information can be done via the +`gpii.matchMakerFramework.utils.addSolutionTypeInformation` function. + +The apptology is described in more details here: [Apptology.md](Apptology.md). + +**gpii.matchmakerframework.utils.disposeSolution:** In many ways the “Heart” of the matchmaking process. It is +responsible for adding priorities, augmenting the solution and preference data and finally passing it to some +disposition (matchmaker) strategy that is passed as a parameter. In more details the following happens: + +1. The solutions are run through `gpii.matchMakerFramework.utils.expandSolutions` augmenting them to, besides the + original solution registry data, a skeleton of the solutions capabilities is built is built - i.e. a hierarchical + object matching the solutions capabilities. +2. Next solution priorities are added to each solution according to application settings via + `gpii.matchMakerFramework.utils.expandSolutions`. That is, if a user has any application settings the solution is + considered to have a priority of 512. +3. Following this, explicit priorities of the users preference set is added to each solution. That is, if a user has + application priorities in their metadata, this is added. These values will always be above 1024, and hence take priority + (overwrite) the implicit priorities from the step above. +4. A leaves structure of the preferences is calculated in the `gpii.matchMakerFramework.utils.computeLeaves` function. + This is a structure with a key for each preference in an EL-path format and a value of true. This, in conjunction with + the solutions skeleton is useful for looking up matches via fluids' getPath functionality. +5. Once all this data has been prepared, the information is sent to the strategy (e.g. flat MatchMakers algorithm) +6. Finally a disposition is assigned to each solution based on the output of the strategy. + +**Preference filtering:** When using a the untrusted flowmanager, security dictates that no "irrelevant" or unnused +preferences of settings should be sent to the local device. Since the users preference set does need to be sent to the +local device for use in the PSP, a filtering needs to happen, to ensure that the only settings reaching the local +device are those that have been used to configure the device. This filtering can be done using the +`gpii.matchMakerFramework.utils.filterPreferencesForSolution` function. + +**matchMakerDispatcher:** Sends the matchmaker input data to the actual matchmaker. While there is functionality in +place for allowing multiple matchmakers, the system is currently hardcoded to use the “default” one, which is the +Flat matchmaker. + +A promise is returned, which will contain the original matchmaker input along with the matchmaker response in a block +keyed by matchMakerOutput + +The dispatching happens in the function `gpii.matchMakerFramework.matchMakerDispatcher`. diff --git a/documentation/PSPChannel.md b/documentation/PSPChannel.md new file mode 100644 index 000000000..659eee0ed --- /dev/null +++ b/documentation/PSPChannel.md @@ -0,0 +1,333 @@ +# PSP Channel + +PSPChannel is used to establish persistent WebSocket connection between PSPChannel server and clients. Once the +connection is established, the PSPChannel client can request various actions by transmitting the specified data to the +pspChannel server. These actions include: apply preferences, switch to a different preference set, save changed +preferences and read a preference value. Examples of how to construct PSPChannel connections and send/receive data +from PSPChannel server can be found at +[examples/pspChannelClient](../examples/pspChannelClient). + +## PSPChannel WebSocket APIs + +### Open up a PSPChannel WebSocket Connection + +Assume GPII is running on the port `localhost:8081`: + +```js +var WS = require("ws"); +var socket = new WS("ws://localhost:8081/pspChannel"); +``` + +Add socket listeners to process the communicated data with the server: + +```js +socket.on("open", function () { + // Indicate the PSPChannel WebSocket Connection has been established +}); + +socket.on("message", function (data) { + // Process messages received from the server +}); + +socket.on("error", function (error) { + // Error handler +}); +``` + +Once the PSPChannel Connection is established, the server will send the first message to the client indicating +the current lifecycle manager session state. An example of a typical first message that shows "noUser" is keyed +in the system and no preferences have been read or applied: + +```json +{ + "type": "modelChanged", + "payload": { + "path": [], + "type": "ADD", + "value": { + "gpiiKey": "noUser", + "activePrefsSetName": "gpii-default", + "settingControls": {}, + "preferences": {} + } + } +} +``` + +### Close a PSPChannel WebSocket Connection + +```js +socket.close(); +``` + +### Send Data to PSPChannel Server + +The PSPChannel client can request various actions by transmitting the specified data to the server. + +```js +socket.send(data); +``` + +* Parameters: + + **data**: {Object}. The data to send to the server. It contains two paths: `type` and `value`. + + **data.type**: {String}. It can be one of these values: `modelChanged`, `pullModel`. + + _`modelChanged`_ typed messages are used to apply preferences, switch to a different preference set and save changed preferences. + + _`pullModel`_ typed messages are used to read preference values. + + **data.value**: {Object}. The actual value for performing actions. The detailed value structure is described below: + +#### Apply a Preference + +* The data transmitted from the client to the server: + +An example of changing the volume: + +```json +{ + "type": "modelChanged", + "value": { + "settingControls": { + "http://registry\\.gpii\\.net/common/volume": { + "value": 0.5 + } + } + } +} +``` + + An example of changing a nested keyed preference: + +```json +{ + "type": "modelChanged", + "value": { + "settingControls": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.office.word-ribbon": { + "value": "StandardSet" + } + } + } +} +``` + +* The server response: + +When preferences are applied, the server will send back a `modelChanged` message. An example: + +```json +{ + "type": "modelChanged", + "payload": { + "path": [], + "type": "ADD", + "value": { + "gpiiKey": "noUser", + "activePrefsSetName": "gpii-default", + "settingControls": { + "http://registry\\.gpii\\.net/common/volume": { + "value": 0.5, + "schema": { + "title": "Volume", + "description": "General volume of the operating system", + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "liveness": "live" + } + }, + "preferences": { + "contexts": { + "gpii-default": {} + } + } + } + } +} +``` + +#### Read a Preference + +* The data transmitted from the client to the server: +1. An example of reading the magnification value: + +```json +{ + "type": "pullModel", + "value": { + "settingControls": { + "http://registry\\.gpii\\.net/common/volume": { + "value": 1 + } + } + } +} +``` + +The value at the path `value.settingControls.{preference}.value` can be any valid value constrained by this setting's +schema defined in its solution registry entry. This value is required for sending the requested preference through the +matchmaking process but it doesn't have any effect on the system. + +* The server response: + +The server will send back the current preference value. This value doesn't have any connection with the value in the +incoming request. The typed messages from the server vary in different use cases: + +1. If the preference value is changed from the last time when it was read or applied, the server will send back + `modelChanged` and `preferenceReadSuccess` typed messages; +2. If the preference value stays unchanged from the last time when it was read or applied, the server will only send + back the `preferenceReadSuccess` typed message; +3. If GPII is not able to read the preference value, the server will send back the `preferenceReadFail` typed message. + +An example of a `modelChanged` typed message: + +```json +{ + "type": "modelChanged", + "payload": { + "path": [], + "type": "ADD", + "value": { + "gpiiKey": "noUser", + "activePrefsSetName": "gpii-default", + "settingControls": { + "http://registry\\.gpii\\.net/common/volume": { + "value": 0.5, + "schema": { + "title": "Volume", + "description": "General volume of the operating system", + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "liveness": "live" + } + }, + "preferences": { + "contexts": { + "gpii-default": {} + } + } + } + } +} +``` + +An example of a `preferenceReadSuccess` typed message: + +```json +{ + "type": "preferenceReadSuccess", + "payload": {} +} +``` + +An example of a `preferenceReadFail` typed message: + +```json +{ + "type": "preferenceReadFail", + "payload": {} +} +``` + +#### Switch to a different preference set + +* The data transmitted from the client to the server: + +An example of switching to a preference set named "bright": + +```json +{ + "type": "modelChanged", + "value": { + "activePrefsSetName": "bright" + } +} +``` + +The value at the path `value.activePrefsSetName` specifies the name of the preference set to switch to. + +* The server response: + +Once the switch completes, the server will send back a `modelChanged` typed message with session state with the new +preference set. + +An example of a `modelChanged` typed message: + +```json +{ + "type": "modelChanged", + "payload": { + "path": [], + "type": "ADD", + "value": { + "gpiiKey": "context1", + "activePrefsSetName": "bright", + "settingControls": { + "http://registry\\.gpii\\.net/common/magnification": { + "schema": { + "title": "Magnification", + "description": "Level of magnification", + "type": "number", + "default": 1, + "minimum": 1, + "multipleOf": 0.1 + }, + "liveness": "live", + "value": 2 + } + }, + "preferences": { + "name": "Multiple Contexts", + "contexts": { + "gpii-default": { + "name": "Default preferences" + }, + "bright": { + "name": "bright" + }, + "noise": { + "name": "noise" + }, + "brightandnoise": { + "name": "bright and noise" + } + } + } + } + } +} +``` + +#### Save changed preferences + +This request is to save all changed preferences into the database. + +* The data transmitted from the client to the server: + +```json +{ + "type": "modelChanged", + "value": { + "saveButtonClickCount": 1 + } +} +``` + +The integer at the path `value.saveButtonClickCount` should be incremented at each new request. + +* The server response: + +When the save completes, the server will send back a `preferencesApplied` typed message. An example: + +```json +{ + "type": "preferencesApplied", + "payload": {} +} +``` diff --git a/documentation/PreferencesServer.md b/documentation/PreferencesServer.md new file mode 100644 index 000000000..00a94a8a1 --- /dev/null +++ b/documentation/PreferencesServer.md @@ -0,0 +1,346 @@ +# Preferences Server: + +The preferences server currently preferences APIs. The URLS are as follows: + +* **URL: `/preferences`**: The main URL to be used for the preferences server. Any new development should be using this + URL. + +## Description + +The Preferences Server is a server meant to be the public-facing, REST-based interface for consumers of preferences. Its +main purpose is to be able to present a filtered view of the user preferences based on the requirements of the consumer, +i.e. the preferences shown in a specific ontology, filtered according to a specified set of preferences. + +The bulk of work of the Preferences Server is done in other components of the GPII system, leaving the Preferences +Server with the task of acting on the parameters supplied to the REST calls, calling the relevant functions in other +components accordingly, etc. + +The main types of filtering provided by the Preferences Server are the following: + +* **?view=:view** - the ontology the preferences is given/required in. For each REST API call, there is the option of + providing desired 'view' or 'ontology' of the preferences. If none is given the _'flat'_ view is defaulted to. The + Preferences Server draws on the ontologyHandler component to take care of the bulk of this filtering, merging, etc., + of the prefs and metadata sections. + +## APIs + +### GET /ready + +Check whether Preferences Server is ready to handle requests. The readiness endpoint checks the database connection. + +It returns http status code 200 when Preferences Server is ready to handle requests. Otherwise, returns http status code +404. + +### GET /health + +Check whether Preferences Server itself is running. A running Preferences Server may or may not be ready to handle requests +because the liveness endpoint does not check the connection between Preferences Server and the database. + +It returns http status code 200 when Preferences Server itself is running. Otherwise, returns http status code 500. + +### GET /preferences/:gpiiKey[?view=:view] + +Retrieves the preference sets for the GPII key (`:gpiiKey`). The optional `view` parameter is to retrieve the +preferences in a different view (ontology). If no `view` is specified the 'flat' ontology will be defaulted to. + +#### Example of GET request with no view provided + +Example of a basic GET request to the Preferences Server (given that the Preferences Server is located on +preferences.gpii.net): + +`http://preferences.gpii.net/preferences/sammy` + +Return payload: + +```json +{ + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/unknown": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + } + } + } +} +``` + +#### Example of GET request with view parameter + +An example of a GET request (given that the Preferences Server is located on preferences.gpii.net): + +`http://preferences.gpii.net/preferences/myGpiiKey?view=ISO24751` + +Return payload: + +```json +{ + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "control": { + "onscreenKeyboard": true, + "mouseEmulation": { + "cursorSpeed": 0.85, + "-provisional-initDelay": 120, + "-provisional-mouseEmulation/enabled": true + } + }, + "applications": { + "org.alsa-project": { + "parameters": { + "masterVolume": 14 + } + } + } + } + } + } +} +``` + +### POST /preferences/[?view=:view] + +This is used to post new preferences to the Preferences Server. A new GPII key will automatically be generated and +returned in the payload along with the saved preferences. + +As with GET, this takes an optional `view` parameter denoting the ontology of the provided settings. If no `view` is +provided, the preferences will be stored and interpreted as being in the `flat` format. + +#### Example POST query + +Below is an example of a post query to the following url (given that a Preferences Server is available at +preferences.gpii.net): + +`http://preferences.gpii.net/preferences/?view=flat` + +The body of the POST should contain the preferences to be stored. They should be in the format specified by `view` or, +if no `view` is provided, in the flat format. + +Example POST body: + +```json +{ + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/unknown": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + } + } + } +} +``` + +The return payload will contain the stored preferences (keyed by `preferences`) and the newly generated GPII key +(keyed by `gpiiKey`) under which the preference set is stored. + +Given that the above preferences was stored with the GPII key `123e4567-e89b-12d3-a456-426655440000`, the return +payload would be: + +```json +{ + "gpiiKey": "123e4567-e89b-12d3-a456-426655440000", + "preferences": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/unknown": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + } + } + } + } +} +``` + +### PUT /preferences/:gpiiKey[?merge=:mergeview=:view] + +This is used to create a new GPII key with its preferences, or update preferences for an existing GPII key to the +Preferences Server. At the update, if no preferences safe is associated with the provided GPII key, a new safe will +be automatically created. + +As with GET and PUT, this takes an optional `view` parameter denoting the ontology of the settings provided in the +payload of the request body. If no `view` is provided, the preferences will be stored and interpreted as being in the +`flat` format. + +As for the update, this takes an optional boolean `merge` parameter denoting whether the incoming preferences should be +merged with the existing preferences. If `merge` is `true`, the incoming preferences will be merged with the existing +ones. Otherwise, the incoming preferences will override the existing ones. If no `merge` is provided, the default value +will be `false`. + +When preferences are PUT to the preferences server, all the preferences in that view will be overwritten. In other +words, if I have preferences A, B and C already existing in my preference set (in a given view), and a put request is +made containing only settings B and D, the resulting preference set will contain only settings B and D. + +The preferences are allowed to be stored in different ontologies. A (transformable) user preference will only be stored +once in the preference set. This also means that if a put request is make containing a preference A1, which already +exists in the preference set, but in a different ontology (lets call this same setting in a different ontology Ax), the +preference A1 will be stored in the provided ontology, while Ax will be removed from the preference set. An example will +be given below to help make this clearer. + +#### Example PUT query + +Below is an example of a put query to the following url (given that a Preferences Server is available at +preferences.gpii.net): + +`http://preferences.gpii.net/preferences/myGpiiKey?merge=true&view=flat` + +Note that here, we store preferences to the GPII key 'myKey'. The body of the PUT should contain the preferences to be +stored. They should be in the format specified by `view` or in the flat format if no `view` is provided. + +putBody: + +```json +{ + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 18, + "http://registry.gpii.net/common/cursorSpeed": 0.850 + } + } + } +} +``` + +The return payload would then be the following: + +```json +{ + "gpiiKey": "myKey", + "preferences": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 18, + "http://registry.gpii.net/common/cursorSpeed": 0.850 + } + } + } + } +} +``` + +If we look at what's going on at the preferences server level, imagine that the original preference set looked like this +before the PUT request: + +```json +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850 + } + } + } + }, + "ISO24751": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "display": { + "screenEnhancement": { + "fontSize": 24 + } + }, + "control": { + "onscreenKeyboard": true + } + } + } + } + }, + "bogus": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "foo": "bar" + } + } + } + } +} +``` + +After the PUT request, it would be changed to: + +```json +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 18, + "http://registry.gpii.net/common/cursorSpeed": 0.2 + } + } + } + }, + "ISO24751": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "control": { + "onscreenKeyboard": true + } + } + } + } + }, + "bogus": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "foo": "bar" + } + } + } + } +} +``` + +There are two important things to note here: + +1. As the PUT request was made in the flat format, all the existing preferences in the flat format were replaced by the + ones in the body of the PUT request. This means that a setting like _cursorAcceleration_ is no longer present in the + preference set. +2. In the example, we consider the ISO-24751 setting "display.screenEnhancement.fontSize" to be ontologically equivalent + to `http://registry.gpii.net/common/fontSize`. Since we do not allow the same setting to be present multiple times in + the NP set, the fontSize has been stored in the flat ontology and removed from the ISO24751 block. + +## Other relevant documentation: + +* [The Preferences Server Framework](PreferencesServerFramework.md) diff --git a/documentation/PreferencesServerFramework.md b/documentation/PreferencesServerFramework.md new file mode 100644 index 000000000..1d313c8de --- /dev/null +++ b/documentation/PreferencesServerFramework.md @@ -0,0 +1,57 @@ +# Preferences Server Framework + +## Introduction + +As the name suggests, the purpose of the preferences server framework is to server the user needs and preferences. Only +the preferences that are relevant and permitted to the consumer are provided, and they can be provided in the ontology +desired by the consumer, given that an ontology (or 'view') has been provided for this. + +Currently, the preferences server Framework consist of two components: + +* The Ontology Handler (see below) +* [The Preferences Server](PreferencesServer.md) + +_The Ontology Handler_ is the component containing the functionality for translating between one ontology (or 'view') +and another. Certain consumers prefer viewing the preferences in one view more suitable for them (i.e. ISO-24751) and +the Ontology Handler is able to translate these settings (given that an ontology transformation has been provided) from +one view to another. + +_The Preferences Server_ is the public facing component of the preferences server framework. It provides APIs, allowing +requests for preference sets in some desired view (via API parameters), that only gives the consumer access to the +relevant settings. It is responsible for (via the ontology handler) to translate the settings to the desired view, +ensuring that no duplicate preferences are stored, etc. For a full description of the Preferences Server, see: [The +Preferences Server](PreferencesServer.md). + +## Motivation and rules + +We have identified a set of requirements to a new Preferences Framework and Ontology Handler implementations, and the +components using it: + +* **Each ontology should have a (ontology-global) unique ID** for the framework to be able to identify it +* **preference sets should consist of containers keyed by ontology IDs** - as opposed to before, where it's guessed by + the pattern of the preferences what ontology is used, the preference sets should explicitly declare what ontology + they're in. +* **A single preference safe can contain multiple ontologies.** +* **A single (transformable) value will only be present once in the preference safe** +* **Ontology, preference safes and security gateway should always be set up together** - seeing how we would now have + the preferences keyed by ontology, any instance of the preferences server would need to be deployed along with a + ontology server, as well as a little front-end allowing the transformation of the preferences into a desired ontology. + This will allow anyone who needs to read the preferences to get them in a format they understand. This has some + implications: +* **Any consumer of preferences is expected to declare what format it wants them in** (or absense thereof implies the + use of the 'flat' ontology) - this includes the MMs and preference editors + +## Ontology Handler: + +Component with the ability of performing various operations on preference sets. It is NOT implemented as a REST API +service, as it is expected to be present on all any instance running the Preferences Server. In other words, either the +ontology handling is expected to be done based on parameters given when talking to the [Preferences +Server](PreferencesServer.md) or by using the public functions of the OntologyHandler directly. + +### Ontologies: + +The ontology transform specs are stored in the `testData/ontologies` folder of the `universal` repository. The filename +of a transformation spec file should describe what transformation that file contains. More specifically, a filename for +ontology transformations should have the following format: -.json where should be replaced with the +name of the ontology that is expected as the input model, and should be the name of the ontology that will be +output from the transformation. diff --git a/documentation/ProductionSetupInstructions.md b/documentation/ProductionSetupInstructions.md new file mode 100644 index 000000000..71015f737 --- /dev/null +++ b/documentation/ProductionSetupInstructions.md @@ -0,0 +1,134 @@ +# Production Setup Instructions + +## Introduction + +The *Cloud Based Flow Manager* can be deployed in a VM using production configurations. Once the VM is running, the +following services will be available in the VM: + +* *Preferences Server* listening on port 9081 +* *Cloud Based Flow Manager* listening on port 9082 +* *CouchDB* as the backend database listening on port 5984 + +**Note:** + +* The *Preferences Server* is a Node.js process +* The *Cloud Based Flow Manager* is a separate Node.js process +* The above mentioned ports will be forwarded from the VM to your host machine allowing access to deployed services +* Before starting the VM please make sure the ports in question are not being used on your host machine +* All the Vagrant commands listed below should be run in the universal root directory. + +## Requirements + +In order to start the VM, make sure [all these requirements for setting up Quality Infrastructure +Environments](https://github.com/GPII/qi-development-environments/blob/master/README.md#requirements) are satisfied. + +## Start the VM + +To start a VM, run the following command in the universal root directory: + +`vagrant up` + +## Test the VM + +### Test the Preferences Server + +Visiting the following link in a browser: + +`http://localhost:9081/preferences/carla` + +should return: + +```json +{ + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.texthelp.readWriteGold": { + "ApplicationSettings.AppBar.Width.$t": 788, + "ApplicationSettings.AppBar.ShowText.$t": true, + "ApplicationSettings.AppBar.optToolbarShowText.$t": true, + "ApplicationSettings.AppBar.LargeIcons.$t": true, + "ApplicationSettings.AppBar.optToolbarLargeIcons.$t": true, + "ApplicationSettings.Speech.optSAPI5Speed.$t": 50, + "ApplicationSettings.Speech.optAutoUseScreenReading.$t": false + }, + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier": { + "show-cross-hairs": true, + "lens-mode": false, + "mag-factor": 2, + "mouse-tracking": "proportional", + "screen-position": "right-half", + "scroll-at-edges": true + }, + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "Magnification": 200, + "ZoomIncrement": 50, + "Invert": 0, + "FollowMouse": 1, + "FollowFocus": 1, + "FollowCaret": 1, + "MagnificationMode": 1 + }, + "http://registry.gpii.net/common/fontSize": 24, + "http://registry.gpii.net/common/foregroundColor": "white", + "http://registry.gpii.net/common/backgroundColor": "black", + "http://registry.gpii.net/common/fontFaceFontName": ["Comic Sans"], + "http://registry.gpii.net/common/fontFaceGenericFontFace": "sans serif", + "http://registry.gpii.net/common/magnification": 2, + "http://registry.gpii.net/common/tracking": ["mouse"], + "http://registry.gpii.net/common/invertColours": true, + "http://registry.gpii.net/common/adaptationPreference": [{ + "adaptationType": "caption", + "language": "en" + }, {}], + "http://registry.gpii.net/common/tableOfContents": false + } + } + }, + "name": "Carla" +} +``` + +### Test the Cloud Based Flow Manager + +Open a terminal and copy, paste this command: + +```snippet +curl -X POST -d "grant_type=password&client_id=pilot-computer&client_secret=pilot-computer-secret&username=li&password=dummy" http://localhost:9082/access_token +``` + +should return: + +```snippet +{ + "access_token": {String}, + "expiresIn":3600, + "token_type":"Bearer" +} +``` + +### Test CouchDB + +Opening this link in a browser: + +`http://localhost:5984/_utils/` + +should show the [CouchDB Web GUI Administration Panel](http://docs.couchdb.org/en/1.6.1/intro/futon.html). The following +database should be present: + +* `gpii`: Contains all data for GPII Server that runs for the Preferences Server and the Cloud Based Flow Manager, + including GPII keys, prefs safes, GPII App installation clients, GPII App installation client authorizations and + client credentials + +## Stop the VM + +To stop the VM, run the following command: + +`vagrant halt` + +## Delete the VM + +Once you no longer need the VM, you can reclaim storage resources using the following command: + +`vagrant destroy -f` diff --git a/documentation/README.md b/documentation/README.md new file mode 100644 index 000000000..6909109df --- /dev/null +++ b/documentation/README.md @@ -0,0 +1,22 @@ +# Documentation Overview + +* [System Startup and configs](Configs.md) +* [Production Setup of Cloudbased FM and Preferences server](ProductionSetupInstructions.md) +* [Flow Manager](FlowManager.md) + * [Cloud Based Flow Description](CloudBasedFlow.md) + * [Login and Logout Flow Description](loginAndLogoutFlow.md) +* [Preferences Server Framework](PreferencesServerFramework.md) + * [Preferences Server](PreferencesServer.md) +* [Data Model for Preferences and OAuth Data](DataModel.md) + * [Couch Connector](CouchConnector.md) + * [Data Loader](DataLoader.md) +* [MatchMakerFramework](MatchMakerFramework.md) + * [Flat MatchMaker](FlatMatchMaker.md) + * [Apptology](Apptology.md) +* [Lifecycle Manager](LifecycleManager.md) +* [Transformer](Transformer.md) +* [BrowserChannel and WebSockets settings handler](BrowserChannel.md) +* [Auth Server](AuthServer.md) + * [Auth Grant Finder](AuthGrantFinder.md) + * [Authorization Service](AuthorizationService.md) +* [Solutions Registry Format](SolutionsRegistryFormat.md) diff --git a/documentation/ResetComputer.md b/documentation/ResetComputer.md new file mode 100644 index 000000000..6adf9f253 --- /dev/null +++ b/documentation/ResetComputer.md @@ -0,0 +1,55 @@ +# Reset Computer + +GPII provides [reset APIs](FlowManager.md#reset) to reset the computer to default settings when needed. The reset workflow +is described below. + +## Define the Default Settings File + +The default settings JSON5 file should be created at `testData/defaultSettings/defaultSettings.json5` that is in a +format of a standard preferences set. An example of its content for starting gnome magnifier on Linux: + +```json +{ + "contexts": { + "gpii-default": { + "preferences": { + "http://registry.gpii.net/common/magnification/enabled": true + } + } + } +} +``` + +## Reset on System Start + +The local flow manager provides a boolean option "resetAtStart" that serves as a flag indicating whether the computer +should be reset on system start. This flag is set to `false` by default. Setting it to `true` will reset the computer +on system start. This flag is set to true in GPII configs that run the local flow manager in the production mode. + +When GPII starts, `defaultSettings.json5` is automatically copied from `testData/defaultSettings` directory to the GPII +settings directory if it hasn't been copied. GPII reads the default settings from `defaultSettings.json5` located +at the GPII settings directory. Users are invited to edit the default settings file at the settings directory instead +of at `testData/defaultSettings` directory to keep the code base clean and consistent. + +To make it easier to create windows installers, `testData/defaultSettings/defaultSettings.win32.json5` is pre-created. It +should be renamed (or copied) to `testData/defaultSettings/defaultSettings.json5` before building windows installers. + +Note: + +* When `defaultSettings.json5` in the settings directory is wanted to be re-copied from `testData/defaultSettings` directory, + removing `defaultSettings.json5` from the settings directory will trigger the recopy automatically next time when + GPII starts. + +* The actual location of the settings directory can be found at the beginning of the log output when GPII starts + +## Reset via HTTP Request + +Once the default settings file is ready, the reset can be initiated by sending a HTTP request to the login API provided +by the local flow manager. This API is: + +* GET /user/reset/login + +The reset actions performed by the local flow manager are: + +1. If there's a GPII key currently keyed in, key it out; +2. If default settings are defined, apply them to reset the computer. diff --git a/documentation/SolutionsRegistryFormat.md b/documentation/SolutionsRegistryFormat.md new file mode 100644 index 000000000..9d0a9f89a --- /dev/null +++ b/documentation/SolutionsRegistryFormat.md @@ -0,0 +1,403 @@ +# Solutions Registry format + +## Overall format: + +Each entry in the solution registry should have a unique ID (`Solution.id` in the below example), as well as a name +(`name`), and a description of which context it requires to run (`context`). Besides these, information can be provided +describing different potential aspects of its lifecycle. This can for example be information about how to start and stop +the solution, detect whether it is running, set its settings, etc. These will all be described in the below. The overall +structure and allowed keys in a solution description can be seen here. + +```snippet +"Solution.id": { + "name": "My Solution" + "contexts": { ... }, + "settingsHandlers": { ... }, + "launchHandlers": { ... }, + "capabilities": [ .. ], + "configure": [ .. ], + "restore": [ .. ], + "update": [ .. ], + "start": [ .. ], + "stop": [ .. ], + "isRunning": [ ..], + "isInstalled": [ .. ], + + // Not yet supported. + "install": [ ... ], + "uninstall": [ ... ], + "makeConfigurable": [ ... ], + "isConfigurable": [ ... ] +} +``` + +## contexts + +The `contexts` block describes what the required context is for the solution to run. Currently only one type of context +is supported, namely `OS`. The context block is **mandatory**. + +**Example Context**: + +```snippet +"contexts": { + "OS": [ + { + "id": "win32" + } + ] +} +``` + +## settingsHandlers + +The `settingsHandlers` block is unique and one of the most important blocks in the solutions registry entry. It consists +of zero or more settingsHandler entries, each keyed by an arbitrary name (that is unique within this solutions +settingsHandlers block). Inside each settingsHandler entry, the properties for that settingsHandler is provided. The +entries in the settingsHandlers block can be referred to from the lifecycle blocks of the solutions registry entry. The +settingsHandlers block is mandatory, but can be empty. + +**Example settingsHandlers block**: + +```snippet +"settingsHandlers": { + "myconf": { + "type": "gpii.settingsHandlers.INISettingsHandler", + "liveness": "manualRestart", + "options": { + "filename": "${{environment}.APPDATA}\\nvda\\nvda.ini", + "allowNumberSignComments": true, + "allowSubSections": true + }, + "supportedSettings": : { + "speech.espeak.pitch": { ... metadata for setting... }, + "presentation.reportHelpBalloons": { ... metadata for setting ... }, + "speech.nonTransformableSetting": { ... metadata for setting ... } + }, + "capabilitiesTransformations": { + "speech.espeak.pitch": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 100 + } + }, + "presentation.reportHelpBalloons": "http://registry\\.gpii\\.net/common/speakTutorialMessages" + } + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/pitch": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "speech.espeak.pitch", + "factor": 0.01 + } + } + }, + "http://registry\\.gpii\\.net/common/speakTutorialMessages": "presentation.reportHelpBalloons" + } + }, + "otherconf": { + "type": "gpii.settingsHandlers.XMLHandler", + "liveness: "liveRestart", + "options": { + "filename": "${{environment}.HOME}\\mySettings.ini" + }, + "supportedSettings": { + "appsettingA": { ... appsettingA metadata ... }, + "appsettingB": { ... appsettingB metadata ... }, + ... + "appsettingZ": { ... appsettingZ metadata ... } + } + } +} +``` + +An important thing to notice here is that this solution example has two settingsHandlers - one XMLHandler which has been +given a reference `myconf` and an INIHandler referred to as `otherconf`. + +Each settingsHandler block can contain the following information: + +* **type (mandatory):** the type of settingshandler +* **liveness (mandatory):** Describes the update behavior of this solution: `"live"` means that the settings can be + applied live without needing to restart the solution. `"liveRestart"` means that the a restart is required but + considered low-impact enough for e.g. the PSP to trigger this automatically but not on a framerate of e.g. dragging a + slider. `"manualRestart"` means that a change in settings requires a restart and that the restart of the solution is + considered high-impact and slow. `"OSRestart"` means that a restart of the operating system required. +* **options:** Any options that should be passed to the settingsHandler. This is specific to the type of settingshandler + specified in the "type" block. +* **capabilitiesTransformations**: Transformations from common terms to application specific settings can be defined + here. These will enable the framework to automatically translate common terms from a user's preference set into + application settings. Any common terms listed here, will automatically be added to the `capabilities` of the solution. +* **inverseCapabilitiesTransformations**: This block describes transformations from application settings to common + terms. If this block is present, the transformations specified will be used by the framework to deduce common terms + based on any application specific settings in the users preference set. If this key is not present, the framework will + attempt to do the inversion itself, based on the `capabilitiesTransformations`. If this block is present, but empty, + the system will make no attempt to automatically invert the `capabilitiesTransformations`. +* **supportedSettings (mandatory when multiple settingsHandlers)**: This block is used to determine which application + specific settings are relevant to the settingshandler, and also serves as location for providing metadata (such as + default values, data type, validation information, etc) about the setting. Currently no relevant metadata is + supported, so the empty object (`{}`) should be used as value. If a solution only has a single settingsHandler block, + all the settings will be passed to that handler by default. But in case there are multiple settingsHandlers, the + system needs some way of determining which settings to apply to which handler. The `supportedSettings` directive is + used for this: + * If a `supportedSettings` option is supplied, only those settings listed there will be applied to the settingsHandler + * If a solution registry entry has multiple settings handlers, the `supportedSettings` entry is mandatory for each settingshandler. + +## launchHandlers: + +The `launchHandlers` are very similar to the `settingsHandlers` block in both form, functionality and implementation, +but have a different area of responsibility. As the name suggests, rather than being responsible for modifying settings, +they are responsible for the 'launch' state of the application. That is, they are responsible for any actions related to +stopping or starting the application, and detecting whether it is running. + +There are two main difference from settingshandlers: (1) internally, launch handlers only have one setting (`running`) +which can be true or false depending on the (desired) state of solution and (2) launch handlers do not get their +settings from the users NP directly, rather they get the value for `running` from the matchmaker. This is because the +decision of which applications to run/stop/update on login depends on what is available on the system and what the +matchmaker decides best works for the user. + +On a technical level, launch handlers work exactly as settings handlers, in that they have two methods `get` and `set` +for getting and setting the current run-state of and application, respectively. They ignore all "settings" passed in the +payload, except for the `running` setting, which should be a boolean value. An implementation of a launch handler should +support 3 actions: reading the current run-state of an application (i.e. the `get` call), starting an application (i.e. +when `set` is called with a `true` value) and stopping an application (i.e. when `set` is called with a `false` value). + +**Example launchHandlers block**: + +```snippet +"launchHandlers:" { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_CURRENT_USER\\Software\\Texthelp\\Read&Write11\\InstallPath}\\ReadAndWrite.exe\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "ReadAndWrite.exe" + } + ], + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "ReadAndWrite" + } + ] + } + } +} +``` + +As can be seen, the structure of the `launchHandlers` block is very similar to the `settingsHandlers` block. It supports +any number of launchHandler entries, keyed by some reference string (in this case "launcher", but it could be anything) +that can be used in the lifecycle blocks to reference it. + +Each launch handler will have a `type` entry, describing its type, as well as an `options` block. The content of the +`options` block will depend on the launch handler. + +## Capabilities + +While most of the users preferences for a certain application or group of applications are handled in the various +`settingsHandlers` entries, there are some preferences that can affect the application in other ways than in its +configuration. These are `enabled` terms, such as `http://registry.gpii.net/common/magnification/enabled`, which can +have a special meaning. If a setting like this should affect whether the solution should be started at all, it should be +listed in the solutions capabilities block. If a preference like this should just affect a certain feature/setting of +the application, it should _not_ be listed in the capabilities. + +For clarity, lets take two different solutions: + +1. `Solution A` is a combined screen reader and magnifier. It supports a `magOff` setting which will turn the + magnification feature off, but still read what is on the screen. In this case, it would have + `http://registry.gpii.net/common/magnification/enabled` in one of its settingsHandlers' capabilitiesTransformation + block, where it would be transformed into an appropriate `magOff` value. This will affect whether the magnification + feature is enabled, but otherwise allow the application to run normally. It should _not_ have the + `http://registry\\.gpii\\.net/common/magnification/enabled` listed in its capabilities, since a value of `false` + would mean that the application would not be started at all - meaning that the screenreader features would not be + usable either. +2. `Solution B` is a magnifier application. If the user does not want magnification enabled, there is no sense in having + this application running. In this case, it _should_ have the + `http://registry\\.gpii\\.net/common/magnification/enabled` listed in its capabilities. This effect is that if the + user have a preference for this term, it would affect whether the `Solution B` is launched or not. Note that + `Solution B` would still be configured according to the users preferences (just not launched) on user login, in case + the user manually starts it later. + +**Example capabilities block**: + +```snippet +"capabilities": [ + "http://registry\\.gpii\\.net/common/magnification/enabled" +] +``` + +## Lifecycle Blocks: configure, restore, start, stop, update and isRunning + +Lifecycle blocks describe what should happen when the system needs to configure, start, update, etc., an application. +Neither of these blocks are mandatory as the system will infer their content in case they are not specified. + +### configure and restore + +These blocks describe how to configure and restore a solution, that is: + +* `configure`: Configure the solution with the users setting (e.g. on login) +* `restore`: Restore the settings of the system from before the user logged in + +Each of these lifecycle blocks allow the same content - which is an array with entries that are either references to +settingsHandlers blocks or customized lifecycle blocks. To reference a settingsHandler block, the keyword +`settings.` is used, where `` should be replaced with the name of a settingsHandler block. The +meaning of referencing a settingsHandler is telling the system that the users preference set will be applied to that +solution via the referenced settingshandler. Alternative to referencings setting and restoring settings, arbitrary +lifecycle actions are allowed - the syntax for this is an object that contains at least a `type` key for the function to +call and any further key/value pairs that are needed by the type. + +If the `configure` and/or `restore` blocks are omitted from a solution entry, they will be inferred as containing +references to all the solutions settingshandlers (if any). + +**Example blocks**: + +```snippet +"configure": [ + "settings.myconf" +], +"restore": [ + "settings.myconf" +] +``` + +### start, stop and isRunning + +These blocks all have to do with the run-state of a solution. Their meanings are the following: + +* `start`: Launch/start the solution +* `stop`: Stop/kill the solution +* `isRunning`: Detect whether the application is currently running + +Similar to the configuration related blocks, each of these lifecycle blocks allow the same content - which is an array +with entries that are either references to launcHandler blocks or customized lifecycle blocks. To reference a +launchHandler block, the keyword `launchers.` is used, where `` should be replaced with the name +of a `launchHandler` block. Internally, when referencing a launchHandler, different things will happen depending on +which lifecycle block the reference is from. A reference from `start` or `stop` will call the launch handlers `.set` +method with a `running` value of `true` or `false`, respectively. This should have the effect of starting or stopping +the process. In case of a reference from `isRunning`, a call will be made to the launch handlers `.set` method. +Alternative to referencing launch handler blocks, arbitrary lifecycle actions are allowed - the syntax for this is an +object that contains at least a `type` key for the function to call. + +None of these blocks are mandatory. If one is omitted from the solution registry entry, it will be inferred as +containing references to all launchHandlers specified for that solution (if any). + +**Example blocks**: + +```snippet +"start": [ + "launchers.myLauncher" +], +"stop": [ + "launchers.myLauncher" +], +"isRunning": [ + "launchers.myLauncher", + { + "type": "gpii.runCheckers.myCustomChecker", + "command": "applicationChecker.exe /n myApplication" + } +] +``` + +### update + +The `update` block works very similarly to the lifecycle blocks. It describes what should happen when the configuration +needs to be updated (e.g. due to preferences set changes, PSP adjustments, etc). + +The format of the `update` block allows for the same entries as the other lifecycle blocks - that is: arbitrary +lifecycle action blocks and references to `settings.` and `launchers.`. Unlike for the other +lifecycle blocks, the `update` block furthermore allows references to the `start`, `stop` and `configure` blocks. This +is done by putting a string with the name of that block. When the system encounters one of these references, the entries +of that block will be run. + +**Example block**: + +```snippet +"configure": [ + "settings.myconf" +], +"update": [ + "configure", + { + "type": "gpii.launch.exec", + "command": "my_application --refresh" + } +] +``` + +In the above example, the process of updating the application settings would consists of running the contents of the +`configure` block (that is `"settings.myconf"`), followed by a custom lifecycle actions. + +If the `update` block is omitted, it will be inferred by the system. What the inferred content will be depends on the +solutions' liveness. If any of the settingsHandlers have a `liveness` value of less than "live", the inferred content +will be `[ "stop", "configure", "start" ]`, i.e. a cycle of stopping, configuring and starting the application. If all +settingsHandlers are "live", that means that it supports settings being updated live and a value of `[ "configure" ]` is +inferred. + +### isInstalled: + +This directive is used to detect whether a solution is installed. If any of these blocks evaluate to `true` (implicit +**OR**), the application is considered to be installed. + +**Example Entry**: + +```snippet +"isInstalled": [ + { + "type": "gpii.reporter.fileExists", + "fileName": "${{registry}.HKEY_CURRENT_USER\\Software\\Texthelp\\Read&Write10\\InstallPath}\\ReadAndWrite.exe" + }, // IMPLICIT OR BETWEEN THESE BLOCKS + { + "type": "gpii.packageKit.find", + "name": "orca" + } +] +``` + +***** + +### UNIMPLEMENTED BLOCKS + +There are several advanced options that we're not planning to implement in the short term, but which will make the +implementation of things like the ORCA settings handler much less horrible. + +#### isConfigurable + +This is run before configuration to ensure that the application is actually ready to be configured. This is relevant for +applications where e.g. a configuration file needs to be present, a tutorial needs to be run on the first launch, etc. + +**Example Entry**: + +```snippet +"isConfigurable": [{ + "type": "gpii.reporter.fileExists", + "path": "${{environment}.XDG_DATA_HOME}/orca/user-settings.conf"" +}] +``` + +#### makeConfigurable + +Is the actions that need to be taken to make the application configurable (such as running a wizard, creating a default +configuration file, adding a new system user, etc). + +**Example Entry**: + +```snippet +"makeConfigurable": [{ + "launch" // A special key meaning "start it, wait until isConfigurable is met, and then stop it" +}] +``` + +#### install: + +Used for describing the steps required for installing the application + +#### uninstall: + +Used for describing the steps required for uninstalling the application (i.e. completely removing it from the system) diff --git a/documentation/Transformer.md b/documentation/Transformer.md new file mode 100644 index 000000000..fd696a12e --- /dev/null +++ b/documentation/Transformer.md @@ -0,0 +1,23 @@ +# Transformer + +## Description + +The transformer component of the core framework is a supportive component for the fluid.infusions transformation +capabilities. It is responsible for supporting transformations in the core framework that aren't directly achievable +using the Infusion transformation framework. + +The responsibilities worth highlighting for this component are: + +## Translation from matchmaker output to lifecycle manager input + +One of the functionalities of the transformer is to translate from the matchmaker output into the format +required by the lifecycle manager. For this the function `gpii.transformer.toLifecycleFormat` should be used. See +detailed documentation in code comments. + +## Regular transformation functions + +The transformer also contains useful transformation functions that do not qualify to become part of the general Infusion +framework but are useful for GPII. These are: + +* `gpii.transformer.quantize` as described here: + [http://wiki.gpii.net/w/Architecture_-_Available_transformation_functions#Mapping_a_continuous_range_into_discrete_values_.28gpii.transformer.quantize.29](http://wiki.gpii.net/w/Architecture_-_Available_transformation_functions#Mapping_a_continuous_range_into_discrete_values_.28gpii.transformer.quantize.29) diff --git a/examples/browserChannelClient/README.md b/examples/browserChannelClient/README.md new file mode 100644 index 000000000..195b3110a --- /dev/null +++ b/examples/browserChannelClient/README.md @@ -0,0 +1,11 @@ +# Browser Channel Client + +This directory contains a tiny sample client for the browserChannel WebSockets connection +as described in [../documentation/BrowserChannel.md](../documentation/BrowserChannel.md). + +After running + + npm install + +in this directory, follow the instructions there to start up the CloudBased FlowManager +and connect to it with this client. diff --git a/examples/browserChannelClient/browserChannelClient.js b/examples/browserChannelClient/browserChannelClient.js new file mode 100644 index 000000000..097220269 --- /dev/null +++ b/examples/browserChannelClient/browserChannelClient.js @@ -0,0 +1,71 @@ +/*! + * Test client for BrowserChannel WebSockets + * + * Copyright 2016 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ +"use strict"; + +var ws = require("ws"); + +// The client starts the communication + +var socket = new ws("ws://localhost:8081/browserChannel"); // eslint-disable-line new-cap + +var changeSetting = false; + +// When the connection is done, the client tells to the flow manager its id + +socket.on("open", function () { + console.log("## browserChannelClient: Socket connected"); + socket.send(JSON.stringify({ + type: "connect", + payload: { + solutionId: "net.gpii.uioPlus" // must match the solution id in (every) solutions registry + } + })); +}); + +socket.on("message", function (data) { + console.log("## browserChannelClient: Received the following message: " + data); + var message = JSON.parse(data); + // Right after sending the id to the flow manager, the server will return back + // the current settings in the system (if any) + if (message.type === "connectionSucceeded") { + changeSetting = true; + console.log("## browserChannelClient: Got initial settings ", message.payload, " on connection"); + } + // By listening to this message type, the client will be notified when the system has + // new settings to be applied on the client side + else if (message.type === "onSettingsChanged") { + console.log("## browserChannelClient: Got changed settings ", message.payload); + } + // Log acknowledgement that the "changeSettings" message was sent + else if (message.type === "changeSettingsReceived") { + console.log("## browserChannelClient: ChangeSettings was successfully sent ", message.payload); + } + + // Change two settings, and be done. + if (changeSetting) { + changeSetting = false; + socket.send(JSON.stringify({ + type: "changeSettings", + payload: { + settings: { + characterSpace: 1, + clickToSelectEnabled: false, + contrastTheme: "default" + } + } + })); + } +}); diff --git a/examples/browserChannelClient/package.json b/examples/browserChannelClient/package.json new file mode 100644 index 000000000..fc41fafb0 --- /dev/null +++ b/examples/browserChannelClient/package.json @@ -0,0 +1,7 @@ +{ + "name": "gpii-browser-channel-client", + "version": "0.1.0", + "dependencies": { + "ws": "6.2.1" + } +} diff --git a/examples/deviceReporter/README.txt b/examples/deviceReporter/README.txt new file mode 100644 index 000000000..e846ec293 --- /dev/null +++ b/examples/deviceReporter/README.txt @@ -0,0 +1,14 @@ +examples/deviceReporter folder +================================= + +This folder contains examples of mock deviceReporter files - like the ones used for running +the system in development mode. + +The content of this folder are: +* android_installedSolutions.json: example device reporter file for the android OS +* linux_installedSolutions.json: example device reporter file for the linux OS (gnome 3) +* win32_installedSolutions.json: example device reporter file for Windows 7 +* osx_installedSolutions.json: example device reporter file for OSX (darwin) + +NOTE that these entries are only meant as examples and not exhaustive lists of solutions available +on each platform. \ No newline at end of file diff --git a/testData/deviceReporter/android_installedSolutions.json b/examples/deviceReporter/android_installedSolutions.json similarity index 51% rename from testData/deviceReporter/android_installedSolutions.json rename to examples/deviceReporter/android_installedSolutions.json index c6c69cf79..b0d908134 100644 --- a/testData/deviceReporter/android_installedSolutions.json +++ b/examples/deviceReporter/android_installedSolutions.json @@ -12,6 +12,18 @@ }, { "id": "com.android.freespeech" + }, + { + "id": "com.android.persistentConfiguration" + }, + { + "id": "com.android.audioManager" + }, + { + "id": "com.android.settings.system" + }, + { + "id": "com.android.settings.secure" } ] } diff --git a/testData/deviceReporter/linux_installedSolutions.json b/examples/deviceReporter/linux_installedSolutions.json similarity index 66% rename from testData/deviceReporter/linux_installedSolutions.json rename to examples/deviceReporter/linux_installedSolutions.json index 6378dcb8c..6ccb07650 100644 --- a/testData/deviceReporter/linux_installedSolutions.json +++ b/examples/deviceReporter/linux_installedSolutions.json @@ -7,77 +7,65 @@ { "id": "org.gnome.desktop.interface" }, - + { "id": "org.gnome.nautilus" }, - + { "id": "org.gnome.desktop.a11y.keyboard" }, - + { - "id": "org.gnome.desktop.a11y.caribou-keyboard" + "id": "org.gnome.desktop.a11y.applications.onscreen-keyboard" }, - + { "id": "org.gnome.orca" }, - + { "id": "org.gnome.desktop.a11y.magnifier" }, - + { - "id": "com.microsoft.windows.magnifier" + "id": "org.gnome.shell.overrides" }, - + { - "id": "com.microsoft.windows.onscreenKeyboard" + "id": "org.gnome.desktop.wm.preferences" }, - + { - "id": "nvda.screenReader" + "id": "com.microsoft.windows.magnifier" }, - + { - "id": "fluid.uiOptions.windows" + "id": "com.microsoft.windows.onscreenKeyboard" }, - + { - "id": "fluid.uiOptions.linux" + "id": "nvda.screenReader" }, - + { "id": "org.gnome.desktop.interface" }, - + { "id": "org.gnome.nautilus" }, - - { - "id": "trace.easyOne.communicator.windows" - }, - + { "id": "trace.easyOne.communicator.linux" }, - - { - "id": "trace.easyOne.sudan.windows" - }, - + { "id": "trace.easyOne.sudan.linux" }, - - { - "id": "webinsight.webAnywhere.windows" - }, - + { "id": "webinsight.webAnywhere.linux" } ] -} \ No newline at end of file +} diff --git a/examples/deviceReporter/osx_installedSolutions.json b/examples/deviceReporter/osx_installedSolutions.json new file mode 100644 index 000000000..c0395c6e4 --- /dev/null +++ b/examples/deviceReporter/osx_installedSolutions.json @@ -0,0 +1,8 @@ +{ + "OS": { + "id": "darwin", + "version": "11.3.0" + }, + "solutions": [ + ] +} diff --git a/examples/deviceReporter/win32_installedSolutions.json b/examples/deviceReporter/win32_installedSolutions.json new file mode 100644 index 000000000..d59147aad --- /dev/null +++ b/examples/deviceReporter/win32_installedSolutions.json @@ -0,0 +1,47 @@ +{ + "OS": { + "id": "win32", + "version": "5.0.0" + }, + "solutions": [ + { + "id": "com.microsoft.windows.magnifier" + }, + + { + "id": "com.microsoft.windows.onscreenKeyboard" + }, + + { + "id": "org.nvda-project" + }, + + { + "id": "trace.easyOne.communicator.windows" + }, + + { + "id": "trace.easyOne.sudan.windows" + }, + + { + "id": "webinsight.webAnywhere.windows" + }, + + { + "id": "com.texthelp.readWriteGold" + }, + + { + "id": "com.microsoft.windows.highContrast" + }, + + { + "id": "com.microsoft.windows.mouseTracking" + }, + + { + "id": "com.microsoft.windows.cursors" + } + ] +} diff --git a/examples/pspChannelClient/README.md b/examples/pspChannelClient/README.md new file mode 100644 index 000000000..ab84241d0 --- /dev/null +++ b/examples/pspChannelClient/README.md @@ -0,0 +1,162 @@ +# PSP Example client + +This directory contains a tiny sample client for the pspChannel WebSockets connection. + +## Starting up the GPII and trying out the PSP channel example client. + +It can be tested by firing up any configuration of the GPII which includes a local FlowManager. Particularly +suitable are the mock configurations in %gpii-universal/gpii/configs/mocks - for example you can run + + `node gpii.js gpii/configs/mocks gpii.config.development.manualTesting.mock.windows` + +or + + `node gpii.js gpii/configs/mocks gpii.config.untrusted.development.manualTesting.mock.windows` + +from the root of this repository. This will start up the servers in trusted or untrusted mode, respectively. + +After that, you can fire up the client at any time during the lifetime of the FlowManager by typing + + `node pspChannelClient.js` + +from this directory in another shell. + +Before or after that, you can experiment with logging in and out of the GPII using endpoints such as + + `http://localhost:8081/user/snapset_1a/login` + +and + + `http://localhost:8081/user/snapset_1a/logout` + +Note that the preference sets used with the above configurations should be in the testData/preferences folder of this +repository. + +## Example payloads received by the PSP + +Here are some sample payloads collected from this client during such testing. + +Firstly, connecting the client when no user is keyed into the system produces the payload + +```json +{ + "type": "modelChanged", + "payload": { + "path": [], + "type": "ADD", + "value": { + "gpiiKey": "noUser", + "activePrefsSetName": "gpii-default", + "settingControls": {}, + "preferences": {} + } + } +} +``` + +After logging in `snapset_1a`, the client receives the following update: + +```json +{ + "type": "modelChanged", + "payload": { + "path": [], + "type": "ADD", + "value": { + "gpiiKey": "snapset_1a", + "activePrefsSetName": "gpii-default", + "settingControls": { + "http://registry\\.gpii\\.net/common/DPIScale": { + "value": 1.25, + "schema": { + "title": "DPI Scale", + "description": "DPI scale factor on default monitor", + "type": "number", + "min": 1, + "max": 2, + "divisibleBy": 0.25 + } + }, + "http://registry\\.gpii\\.net/common/cursorSize": { + "value": 1, + "schema": { + "title": "Cursor Size", + "description": "Cursor size", + "type": "number", + "min": 0, + "max": 1, + "divisibleBy": 0.1 + } + } + }, + "preferences": { + "name": "Larger 125%", + "contexts": { + "gpii-default": { + "name": "Default preferences" + } + } + } + } + } +} +``` + +After logging out `snapset_1a`, the client receives the following update: + +```json +{ + "type": "modelChanged", + "payload": { + "path": [], + "value": null, + "type": "DELETE" + } +} +``` + +When the PSP issues an update to the preferences (see below), a message will be sent to the PSP once the settings change +has been applied to the system: + +```json +{ + "type": "preferencesApplied" +} +``` + +## Example payloads sent by the PSP + +The PSP has can send two different payloads to the GPII core architecture. Namely a change in the set of preferences +to use or a change in a setting. + +Changing the preferences set via the psp is done with the following payload: + +```json +{ + "type": "modelChanged", + "value": { + "activePrefsSetName": "bright" + } +} +``` + +This will change the preferences set to that named by the key "bright". To change the set to something else, simply +exchange "bright" with the name of the desired preferences set. + +The PSP API also supports changing preferences (one at a time). This is done via the following payload: + +```json +{ + "type": "modelChanged", + "value": { + "settingControls": { + "http://registry\\.gpii\\.net/common/magnification": { + "value": 4 + } + } + } +} +``` + +Where the value for the "preferences" key is the setting that should change, and the value for the "value" is the new +value the setting should take. So in the above example the common term magnification is set to 4. diff --git a/examples/pspChannelClient/package.json b/examples/pspChannelClient/package.json new file mode 100644 index 000000000..08f307d26 --- /dev/null +++ b/examples/pspChannelClient/package.json @@ -0,0 +1,7 @@ +{ + "name": "gpii-psp-channel-client", + "version": "0.1.0", + "dependencies": { + "ws": "6.2.1" + } +} diff --git a/examples/pspChannelClient/pspChannelClientApplyPrefs.js b/examples/pspChannelClient/pspChannelClientApplyPrefs.js new file mode 100644 index 000000000..246a2be09 --- /dev/null +++ b/examples/pspChannelClient/pspChannelClientApplyPrefs.js @@ -0,0 +1,55 @@ +/*! + * Test client for PSPChannel WebSockets + * + * Copyright 2017 Raising the Floor - International + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ +"use strict"; + +var ws = require("ws"); + +// The client starts the communication +var socket = new ws("ws://localhost:8081/pspChannel"); // eslint-disable-line new-cap + +// When the connection is done, the server will send the initial data of the current session if any +socket.on("open", function () { + console.log("## pspChannelClientApplyPrefs: Socket connected"); +}); + +socket.on("message", function (data) { + var message = JSON.parse(data); + console.log("## pspChannelClientApplyPrefs: Received the following message: " + JSON.stringify(message, null, 4)); + + if (message.type === "preferencesApplied") { + console.log("## pspChannelClientApplyPrefs: Preferences have been applied"); + socket.close(); + return; + } else { + console.log("## pspChannelClientApplyPrefs: Message type '" + message.type + "' not 'preferencesApplied'"); + } + + console.log("## pspChannelClientApplyPrefs: Sending 'modelChanged' request"); + socket.send(JSON.stringify( + { + "type": "modelChanged", + value: { + settingControls: { + "http://registry\\.gpii\\.net/common/DPIScale": { + value: 1.75 + } + } + } + }) + ); +}); diff --git a/examples/pspChannelClient/pspChannelClientReadPrefs.js b/examples/pspChannelClient/pspChannelClientReadPrefs.js new file mode 100644 index 000000000..da7988d63 --- /dev/null +++ b/examples/pspChannelClient/pspChannelClientReadPrefs.js @@ -0,0 +1,64 @@ +/*! + * Test client for PSPChannel WebSockets + * + * Copyright 2019 OCAD University + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ +"use strict"; + +var ws = require("ws"); + +// The client starts the communication +var socket = new ws("ws://localhost:8081/pspChannel"); // eslint-disable-line new-cap +var readRequestCount = 0; + +// When the connection is done, the server will send the initial data of the current session if any +socket.on("open", function () { + console.log("## pspChannelClientReadPrefs: Socket connected"); +}); + +socket.on("message", function (data) { + var message = JSON.parse(data); + console.log("## pspChannelClientReadPrefs: Received the following message: " + JSON.stringify(message, null, 4)); + + if (message.type === "preferenceReadSuccess") { + console.log("## pspChannelClientReadPrefs: Preference has been read"); + socket.close(); + return; + } else if (message.type === "preferenceReadFail") { + console.log("## pspChannelClientReadPrefs: Preference cannot be read"); + socket.close(); + return; + } else { + console.log("## pspChannelClientReadPrefs: Message type '" + message.type + "' not reading success/failure"); + } + + if (readRequestCount === 0) { + readRequestCount++; + // Only send the read request once + console.log("## pspChannelClientReadPrefs: Sending 'pullModel' request"); + socket.send(JSON.stringify( + { + "type": "pullModel", + value: { + settingControls: { + "http://registry\\.gpii\\.net/common/magnification": { + value: 1 + } + } + } + }) + ); + } +}); diff --git a/gpii.js b/gpii.js new file mode 100644 index 000000000..6777af152 --- /dev/null +++ b/gpii.js @@ -0,0 +1,32 @@ +/*! +GPII Universal Personalization Framework Node.js Bootstrap + +Copyright 2012 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +// GPII-3394: This is temporary code enabling Google Stackdriver tracing https://cloud.google.com/trace/ +// If this requirement expands, this code will be moved into its own module, otherwise it will be removed + +if (process.env.GPII_ENABLE_STACKDRIVER_TRACE === "true") { + console.log("Enabling @google-cloud/trace-agent"); + require("@google-cloud/trace-agent").start(); +} + +// A simple bootstrap file which allows a configuration of the GPII to be +// started from the command line from universal + +var fluid = require("./index.js"), + gpii = fluid.registerNamespace("gpii"); + +gpii.start(); diff --git a/gpii/configs/README.md b/gpii/configs/README.md new file mode 100644 index 000000000..7d174f96c --- /dev/null +++ b/gpii/configs/README.md @@ -0,0 +1,50 @@ +# GPII Configs + +This directory and its subdirectories contain variants of the configs that are used to: + +1. Start GPII application in development or production modes; +2. Write integration and acceptance tests. + +The "production" configurations are intended for use in production deployments. The "development" configurations are +used in the acceptance tests in this package. The "manualTesting" configurations provided in this directory are intended +for use in manual QA, troubleshooting, demos, et cetera. + +## The Couchdb Test Harness + +The "development" and "manualTesting" configurations in this package make use of the gpii-couchdb-test-harness package, +which ensures that a CouchDB instance is available and provisioned with the required data if no data is found. The +caseHolder used in with most "development" configurations has sequence steps that start the harness and wait for CouchDB +to be ready. The "manualTesting" configurations provided in this directory immediately start the CouchDB harness (see +below) on startup, and are intended for use in manual QA, troubleshooting, demos, et cetera. + +For all configurations that use the CouchDB test harness, you can control how Couch is started/stopped using an +environment variable: + +* (nothing, the default): Start a Docker container running CouchDB. +* `GPII_TEST_COUCH_USE_VAGRANT`: If this is set, a Vagrant VM will be created that is running Docker and CouchDB. +* `GPII_TEST_COUCH_USE_EXTERNAL`: If this is set, an external instance of CouchDB running on port 25984 will be + used. + +For more information, see [the documentation for the gpii-couchdb-test-harness +package](https://github.com/GPII/gpii-couchdb-test-harness). + +If a database already exists, the "development" configurations will not replace the data. To reset to the default data, +stop the GPII, delete the existing database or container and start the GPII. + +You can delete the `gpii` database in your CouchDB instance either by: + +* Making a call to the [CouchDB REST endpoint that deletes a + database](https://docs.couchdb.org/en/stable/api/database/common.html#delete--db), i.e. `DELETE + http://localhost:25984/gpii` +* Deleting the database from the built-in[Fauxton administrative + interface](https://docs.couchdb.org/en/stable/fauxton/index.html) in Couch, i.e. `http://localhost:25984/_utils` + +Either method will work regardless of how you're running CouchDB. You also have the option to stop the GPII, remove the +VM/container, and then start the GPII. To remove the VM/container: + +* When running in Docker (the default): + * Find the ID of the container using a command like `docker ps -a` + * Remove the container using a command like `docker rm -rf {CONTAINER_ID}` +* When running in Vagrant, destroy the VM using commands like: + * `cd node_modules/gpii-couchdb-test-harness/src/test` + * `vagrant destroy -f` diff --git a/gpii/configs/cloudBased.json b/gpii/configs/cloudBased.json deleted file mode 100644 index 93116cfff..000000000 --- a/gpii/configs/cloudBased.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "typeName": "cloudBased.development", - "includes": [ - "../node_modules/matchMaker/configs/development.json", - "../node_modules/ontologyServer/configs/development.json", - "../node_modules/lifecycleManager/configs/development.json", - "../node_modules/solutionsRegistry/configs/development.json" - ] -} \ No newline at end of file diff --git a/gpii/configs/fm.ps.sr.dr.mm.os.lms.development.json b/gpii/configs/fm.ps.sr.dr.mm.os.lms.development.json deleted file mode 100644 index a3c2f956f..000000000 --- a/gpii/configs/fm.ps.sr.dr.mm.os.lms.development.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "typeName": "flowManager.preferencesServer.solutionsRegistry.deviceReporter.matchMaker.ontologyServer.lifecycleManagerServer.development", - "options": { - "gradeNames": ["autoInit", "fluid.littleComponent"], - "components": { - "server": { - "type": "kettle.server", - "options": { - "logging": true, - "port": 8081 - } - } - } - }, - "includes": [ - "../node_modules/deviceReporter/configs/development.json", - "../node_modules/flowManager/configs/development.json", - "../node_modules/matchMaker/configs/development.json", - "../node_modules/ontologyServer/configs/development.json", - "../node_modules/lifecycleManager/configs/development.json", - "../node_modules/preferencesServer/configs/development.json", - "../node_modules/solutionsRegistry/configs/development.json" - ] -} \ No newline at end of file diff --git a/gpii/configs/fm.ps.sr.dr.mm.os.lms.production.json b/gpii/configs/fm.ps.sr.dr.mm.os.lms.production.json deleted file mode 100644 index 46f066660..000000000 --- a/gpii/configs/fm.ps.sr.dr.mm.os.lms.production.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "typeName": "flowManager.preferencesServer.solutionsRegistry.deviceReporter.matchMaker.ontologyServer.lifecycleManagerServer.production", - "options": { - "gradeNames": ["autoInit", "fluid.littleComponent"], - "components": { - "server": { - "type": "kettle.server", - "options": { - "logging": false, - "port": 8081 - } - } - } - }, - "includes": [ - "../node_modules/deviceReporter/configs/production.json", - "../node_modules/flowManager/configs/production.json", - "../node_modules/matchMaker/configs/production.json", - "../node_modules/ontologyServer/configs/development.json", - "../node_modules/lifecycleManager/configs/production.json", - "../node_modules/solutionsRegistry/configs/development.json" - ] -} \ No newline at end of file diff --git a/gpii/configs/gpii.config.cloudBased.development.manualTesting.json5 b/gpii/configs/gpii.config.cloudBased.development.manualTesting.json5 new file mode 100644 index 000000000..ae3225452 --- /dev/null +++ b/gpii/configs/gpii.config.cloudBased.development.manualTesting.json5 @@ -0,0 +1,21 @@ +/** + * This configuration runs a GPII cloud including a cloud based flow manager and a preferences server, locally + * on port 8081. It also uses a CouchDB Docker test harness for persistence, which is available at port 25984. + * + * * The following components are running: + * ** Auth Server + * ** Flat MatchMaker + * ** Flow Manager + * ** MatchMaker Framework + * ** Ontology Handler + * ** Preferences Server + * ** Solutions Registry (reading registry from file) + * ** gpii-couchdb-test-harness + */ +{ + "type": "gpii.config.cloudBased.development.manualTesting", + "mergeConfigs": [ + "%gpii-universal/gpii/configs/shared/gpii.config.cloudBased.development.json5", + "%gpii-universal/gpii/configs/shared/gpii.config.couch.development.startWithoutCleanData.json5" + ] +} diff --git a/gpii/configs/gpii.config.cloudBased.flowManager.production.json5 b/gpii/configs/gpii.config.cloudBased.flowManager.production.json5 new file mode 100644 index 000000000..585cf14a7 --- /dev/null +++ b/gpii/configs/gpii.config.cloudBased.flowManager.production.json5 @@ -0,0 +1,32 @@ +/** + * This configuration runs a cloud based flow manager locally on port 8082. It expects: + * 1. couchDB to be running on the localhost using the default port 5984. + * 2. Preferences Server running as a standalone server using `gpii.config.preferencesServer.standalone.production`. + * + * * The following components are running: + * ** Cloud Bsed Flow Manager + * ** Flat MatchMaker + */ +{ + "type": "gpii.config.cloudBased.flowManager.production", + "options": { + "gradeNames": "gpii.withCloudLogging", // Apply cloud logging for GPII-3310 + "mainServerPort": 8082, + "dbServerPort": 5984, + "dataSourceConfigProductionEnv": { + "baseUrl": "@expand:kettle.resolvers.env(GPII_DATASOURCE_HOSTNAME)", + "port": "@expand:kettle.resolvers.env(GPII_DATASOURCE_PORT)" + }, + "distributeOptions": { + "flowManager.production.dataStore": { + "source": "{that}.options.dataSourceConfigProductionEnv", + "target": "{that flowManager oauth2DataStore}.options.dataSourceConfig", + "priority": "after:flowManager.base.dataStore" + } + } + }, + "mergeConfigs": [ + "%flowManager/configs/gpii.flowManager.config.cloud.base.json5", + "%flatMatchMaker/configs/gpii.flatMatchMaker.config.base.json5" + ] +} diff --git a/gpii/configs/gpii.config.development.manualTesting.json5 b/gpii/configs/gpii.config.development.manualTesting.json5 new file mode 100644 index 000000000..a79742900 --- /dev/null +++ b/gpii/configs/gpii.config.development.manualTesting.json5 @@ -0,0 +1,23 @@ +/** + * This configuration runs the entire system locally on port 8081, including a local flow manager, a cloud based + * flow manager, and a preferences server. It also uses a CouchDB Docker test harness for persistence, which is + * available at port 25984. + * + * * The following components are running: + * ** Auth Server + * ** Flat MatchMaker + * ** Cloud Based FlowManager + * ** Local FlowManager + * ** MatchMaker Framework + * ** Ontology Handler + * ** Preferences Server + * ** Solutions Registry (reading registry from file) + * ** gpii-couchdb-test-harness + */ +{ + "type": "gpii.config.development.manualTesting", + "mergeConfigs": [ + "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5", + "%gpii-universal/gpii/configs/shared/gpii.config.couch.development.startWithoutCleanData.json5" + ] +} diff --git a/gpii/configs/gpii.config.preferencesServer.standalone.development.manualTesting.json5 b/gpii/configs/gpii.config.preferencesServer.standalone.development.manualTesting.json5 new file mode 100644 index 000000000..5a687684b --- /dev/null +++ b/gpii/configs/gpii.config.preferencesServer.standalone.development.manualTesting.json5 @@ -0,0 +1,20 @@ +/** + * This configuration runs a standalone preferences server locally on port 8081. It also starts and loads test data + * into a CouchDB instance listening on port 25984 for data persistence. + * + * * The following components are running: + * ** Preferences Server + * ** gpii-couchdb-test-harness + * + * Note: this config is created for testing PPT locally. Currently it's not used by any other configs and tests. + */ +{ + "type": "gpii.config.preferencesServer.standalone.development.manualTesting", + "options": { + "dbServerPort": 25984 + }, + "mergeConfigs": [ + "%preferencesServer/configs/gpii.preferencesServer.config.base.json5", + "%gpii-universal/gpii/configs/shared/gpii.config.couch.development.startWithoutCleanData.json5" + ] +} diff --git a/gpii/configs/gpii.config.preferencesServer.standalone.production.json5 b/gpii/configs/gpii.config.preferencesServer.standalone.production.json5 new file mode 100644 index 000000000..3beb440ee --- /dev/null +++ b/gpii/configs/gpii.config.preferencesServer.standalone.production.json5 @@ -0,0 +1,28 @@ +/** + * This configuration runs a standalone Preferences Server on port 8081. It doesn't start any backend database + * for data persistence, but expect couchDB to be running separately on the localhost using the default port 5984. + * + * * The following components are running on the local machine: + * ** Preferences Server + */ +{ + "type": "gpii.config.preferencesServer.standalone.production", + "options": { + "gradeNames": "gpii.withCloudLogging", // Apply cloud logging for GPII-3310 + "dbServerPort": 5984, + "dataSourceConfigProductionEnv": { + "baseUrl": "@expand:kettle.resolvers.env(GPII_DATASOURCE_HOSTNAME)", + "port": "@expand:kettle.resolvers.env(GPII_DATASOURCE_PORT)" + }, + "distributeOptions": { + "preferencesServer.dataStore.production": { + "source": "{that}.options.dataSourceConfigProductionEnv", + "target": "{that preferencesServer dataStore}.options.dataSourceConfig", + "priority": "after:preferencesServer.base.dataStore" + } + } + }, + "mergeConfigs": [ + "%preferencesServer/configs/gpii.preferencesServer.config.base.json5" + ] +} diff --git a/gpii/configs/gpii.config.untrusted.development.dynamicDR.manualTesting.json5 b/gpii/configs/gpii.config.untrusted.development.dynamicDR.manualTesting.json5 new file mode 100644 index 000000000..964d27279 --- /dev/null +++ b/gpii/configs/gpii.config.untrusted.development.dynamicDR.manualTesting.json5 @@ -0,0 +1,26 @@ +/** + * This configuration runs the entire system locally under the untrusted Flow Manager architecture. + * It starts these servers: + * 1. Untrusted Local Flow Manager on port 8081 + * 2. Cloud Based Flow Manager and Preferences Server on port 8084 + * 3. CouchDB running in Docker, available on port 25984 + * + * * The following components are running: + * ** Auth Server + * ** Flat MatchMaker + * ** Cloud Based Flow Manager + * ** Dynamic Device Reporter + * ** Untrusted Local Flow Manager + * ** MatchMaker Framework + * ** Ontology Handler + * ** Preferences Server + * ** Solutions Registry (reading registry from file) + * ** gpii-couchdb-test-harness + */ +{ + "type": "gpii.config.untrusted.development.dynamicDR.manualTesting", + "mergeConfigs": [ + "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.dynamicDR.local.json5", + "%gpii-universal/gpii/configs/shared/gpii.config.couch.development.startWithoutCleanData.json5" + ] +} diff --git a/gpii/configs/gpii.config.untrusted.development.manualTesting.json5 b/gpii/configs/gpii.config.untrusted.development.manualTesting.json5 new file mode 100644 index 000000000..f272b555b --- /dev/null +++ b/gpii/configs/gpii.config.untrusted.development.manualTesting.json5 @@ -0,0 +1,26 @@ +/** + * This configuration runs the entire system locally under the untrusted Flow Manager architecture. + * It starts these servers: + * 1. Untrusted Local Flow Manager on port 8081 + * 2. Cloud Based Flow Manager and Preferences Server on port 8084 + * 3. CouchDB on port 25984 + * + * * The following components are running: + * ** Auth Server + * ** Flat MatchMaker + * ** Cloud Based Flow Manager + * ** Untrusted Local Flow Manager + * ** MatchMaker Framework + * ** Ontology Handler + * ** gpii-couchdb-test-harness + * ** Preferences Server + * ** Solutions Registry (reading registry from file) + * ** Static Device Reporter (reading from file) + */ +{ + "type": "gpii.config.untrusted.development.manualTesting", + "mergeConfigs": [ + "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.local.json5", + "%gpii-universal/gpii/configs/shared/gpii.config.couch.development.startWithoutCleanData.json5" + ] +} diff --git a/gpii/configs/gpii.config.untrusted.production.dynamicDR.json5 b/gpii/configs/gpii.config.untrusted.production.dynamicDR.json5 new file mode 100644 index 000000000..c9077f6a7 --- /dev/null +++ b/gpii/configs/gpii.config.untrusted.production.dynamicDR.json5 @@ -0,0 +1,15 @@ +/** + * This configuration is the production version of `gpii.config.untrusted.development.dynamicDR`. It runs + * the local Untrusted Flow Manager locally and supresses the 'login', 'logout' and 'proximityTriggered' HTTP endpoints. + * + * * The following components are running on the local machine: + * ** Untrusted Local Flow Manager + * ** Dynamic Device Reporter + */ +{ + "type": "gpii.config.untrusted.production.dynamicDR", + "mergeConfigs": [ + "%flowManager/configs/gpii.flowManager.config.suppressHttpEndpoints.base.json5", + "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.dynamicDR.json5" + ] +} diff --git a/gpii/configs/gpii.config.untrusted.production.dynamicDR.manualTesting.json5 b/gpii/configs/gpii.config.untrusted.production.dynamicDR.manualTesting.json5 new file mode 100644 index 000000000..ef2aa7e3d --- /dev/null +++ b/gpii/configs/gpii.config.untrusted.production.dynamicDR.manualTesting.json5 @@ -0,0 +1,29 @@ +/** + * This configuration is the production version of `gpii.config.untrusted.development.dynamicDR.manualTesting`. + * It runs the entire system locally under the untrusted Flow Manager architecture and supresses + * the 'login', 'logout' and 'proximityTriggered' HTTP endpoints. + * + * It starts these servers: + * 1. Untrusted Local Flow Manager on port 8081 + * 2. Cloud Based Flow Manager and Preferences Server on port 8084 + * 3. CouchDB on port 25984 + * + * * The following components are running: + * ** Auth Server + * ** Flat MatchMaker + * ** Cloud Based Flow Manager + * ** Dynamic Device Reporter + * ** Untrusted Local Flow Manager + * ** MatchMaker Framework + * ** Ontology Handler + * ** Preferences Server + * ** Solutions Registry (reading registry from file) + * ** gpii-couchdb-test-harness + */ +{ + "type": "gpii.config.untrusted.production.dynamicDR.manualTesting", + "mergeConfigs": [ + "%flowManager/configs/gpii.flowManager.config.suppressHttpEndpoints.base.json5", + "%gpii-universal/gpii/configs/gpii.config.untrusted.development.dynamicDR.manualTesting.json5" + ] +} diff --git a/gpii/configs/mocks/README.md b/gpii/configs/mocks/README.md new file mode 100644 index 000000000..7b31f2f9e --- /dev/null +++ b/gpii/configs/mocks/README.md @@ -0,0 +1,5 @@ +# Mock Configs + +This directory contains variants of the configs in the outer directory, with the settings handler definitions replaced +by mocks. This makes them suitable for testing from the bare universal repository, without the risk of failures caused +by missing platform-specific settings handlers, or any corruption to the outer machine's state. diff --git a/gpii/configs/mocks/gpii.config.development.local.mock.windows.json5 b/gpii/configs/mocks/gpii.config.development.local.mock.windows.json5 new file mode 100644 index 000000000..6eafcd237 --- /dev/null +++ b/gpii/configs/mocks/gpii.config.development.local.mock.windows.json5 @@ -0,0 +1,21 @@ +/** + * A variety of gpii.config.development.local.json5 which supplies mock implementations of all settings handlers + * supported on the Windows platform. + * + * This configuration runs the entire system, in the "trusted local FlowManager" configuration, locally and in development mode. + * This configuration is used for testing PSP channel, which means that the fetch of solutions is sourced from the filesystem, + * and the preferences are fetched from a CouchDB instance that is created if needed and provisioned at the start of + * each test sequence. + */ +{ + "type": "gpii.config.development.local.mock.windows", + "options": { + "gradeNames": "gpii.test.integration.mockHolder.windows" + }, + "mergeConfigs": [ + "%gpii-universal/gpii/configs/shared/gpii.config.development.local" + ], + "require": [ + "%gpii-universal/gpii/node_modules/testing/src/Mocks.js" + ] +} diff --git a/gpii/configs/mocks/gpii.config.development.manualTesting.mock.windows.json5 b/gpii/configs/mocks/gpii.config.development.manualTesting.mock.windows.json5 new file mode 100644 index 000000000..14603888d --- /dev/null +++ b/gpii/configs/mocks/gpii.config.development.manualTesting.mock.windows.json5 @@ -0,0 +1,19 @@ +/** + * A variety of gpii.config.development.manualTesting which supplies mock implementations of all settings handlers + * supported on the Windows platform. + * + * This configuration runs the entire system, in the "trusted local FlowManager" configuration, locally and in development mode. + * This means that fetches of solutions, preferences, etc. are sourced from the filesystem rather than from CouchDB URLs. + */ +{ + "type": "gpii.config.development.manualTesting.mock.windows", + "options": { + "gradeNames": "gpii.test.integration.mockHolder.windows" + }, + "mergeConfigs": [ + "%gpii-universal/gpii/configs/gpii.config.development.manualTesting" + ], + "require": [ + "%gpii-universal/gpii/node_modules/testing/src/Mocks.js" + ] +} diff --git a/gpii/configs/mocks/gpii.config.untrusted.development.manualTesting.mock.windows.json5 b/gpii/configs/mocks/gpii.config.untrusted.development.manualTesting.mock.windows.json5 new file mode 100644 index 000000000..ea842f6b8 --- /dev/null +++ b/gpii/configs/mocks/gpii.config.untrusted.development.manualTesting.mock.windows.json5 @@ -0,0 +1,55 @@ +/** + * A variant of gpii.config.untrusted.development which includes mock definitions for all builtin settings handlers + * supported on the Windows platform. This allows any preference set to be logged in from a bare universal repo without + * crashing the system. + * + * This configuration runs: + * * The local flow manager on port 8081 + * * The cloud on port 8084 + * + * This mean that: + * * The keys, preferences and authentication data is stored locally in a CouchDB instance. + * * The following components are running on the local machine: + * ** flowManager + * ** preferencesServer + * ** OntologyHandler + * ** solutionsRegistry (reading registry from file) + * ** flatMatchMaker + * ** MatchMakerFramework + * ** CouchDB + * + * Note that this config incorrectly derives from the deviceReporter development config. This is because of a dependency error + * in the system design where the cloudBased flowManager still attempts to query the platform deviceReporter rather than + * recognising that it is on platform "web" - e.g. within BrowserChannel.js + */ + +{ + "type": "gpii.config.untrusted.development.manualTesting.mock.windows", + "options": { + "gradeNames": ["kettle.multiConfig.config"], + "configs": { + "localConfig": { + "configName": "gpii.config.untrusted.development.mock.windows", + "configPath": "%gpii-universal/gpii/configs/mocks" + }, + "cloudBasedConfig": { + "configName": "gpii.config.cloudBased.development", + "configPath": "%gpii-universal/gpii/configs/shared" + } + }, + "distributeOptions": { + "untrusted.development.port": { + "record": 8084, + "target": "{that cloudBasedConfig}.options.mainServerPort" + }, + "untrusted.development.prefs": { + "record": "http://localhost:8084", + "target": "{that cloudBasedConfig flowManager prefsServerDataSource}.options.prefsServerURL", + "priority": "after:flowManager.prefsServerDataSource.default" + } + } + }, + "mergeConfigs": [ + "%gpii-universal/gpii/configs/shared/gpii.config.couch.development.base.json5" + ] +} diff --git a/gpii/configs/mocks/gpii.config.untrusted.development.mock.windows.json5 b/gpii/configs/mocks/gpii.config.untrusted.development.mock.windows.json5 new file mode 100644 index 000000000..7257c2713 --- /dev/null +++ b/gpii/configs/mocks/gpii.config.untrusted.development.mock.windows.json5 @@ -0,0 +1,16 @@ +/** + * Variant of gpii.config.untrusted.development.json5 with the local (untrusted) flowManager's settings handlers + * replaced by mocks emulating the Windows platform. + */ +{ + "type": "gpii.config.untrusted.development.mock.windows", + "options": { + "gradeNames": "gpii.test.integration.mockHolder.windows" + }, + "mergeConfigs": [ + "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.json5" + ], + "require": [ + "%gpii-universal/gpii/node_modules/testing/src/Mocks.js" + ] +} diff --git a/gpii/configs/ps.sr.development.json b/gpii/configs/ps.sr.development.json deleted file mode 100644 index a56fa12a6..000000000 --- a/gpii/configs/ps.sr.development.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "typeName": "preferencesServer.solutionsRegistry.development", - "options": { - "gradeNames": ["autoInit", "fluid.littleComponent"], - "components": { - "server": { - "type": "kettle.server", - "options": { - "logging": true, - "port": 8081 - } - } - } - }, - "includes": [ - "../node_modules/preferencesServer/configs/development.json", - "../node_modules/solutionsRegistry/configs/development.json" - ] -} \ No newline at end of file diff --git a/gpii/configs/ps.sr.production.json b/gpii/configs/ps.sr.production.json deleted file mode 100644 index b54a66ee8..000000000 --- a/gpii/configs/ps.sr.production.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "typeName": "preferencesServer.solutionsRegistry.production", - "options": { - "gradeNames": ["autoInit", "fluid.littleComponent"], - "components": { - "server": { - "type": "kettle.server", - "options": { - "logging": false, - "port": 8081 - } - } - } - }, - "includes": [ - "../node_modules/preferencesServer/configs/production.json", - "../node_modules/solutionsRegistry/configs/production.json" - ] -} \ No newline at end of file diff --git a/gpii/configs/shared/README.md b/gpii/configs/shared/README.md new file mode 100644 index 000000000..cb4e0dc0c --- /dev/null +++ b/gpii/configs/shared/README.md @@ -0,0 +1,5 @@ +# "Shared" Configurations + +The configuration files in this directory are used in acceptance tests, manual testing, and production. In almost all +circumstances, you will want to use a configuration in the parent directory that merges the appropriate configuration +files rather than using these files directory in tests or manual testing. diff --git a/gpii/configs/shared/gpii.config.cloudBased.development.json5 b/gpii/configs/shared/gpii.config.cloudBased.development.json5 new file mode 100644 index 000000000..c3ff09d27 --- /dev/null +++ b/gpii/configs/shared/gpii.config.cloudBased.development.json5 @@ -0,0 +1,27 @@ +/** + * This configuration acts as a base for `gpii.config.cloudBased.development.manualTesting`. + * + * * The following components are running: + * ** Auth Server + * ** Flat MatchMaker + * ** Flow Manager + * ** MatchMaker Framework + * ** Ontology Handler + * ** Preferences Server + * ** Solutions Registry (reading registry from file) + * + * Notes on testing: + * This setup is used by the acceptance tests in universal testing the cloudBased applications. + * Each individual acceptance test may override some elements of this config, e.g. the folder + * to be used for reading prefs and solutionsRegistry). + * For more details, see the txt files describing the configs in the acceptance tests folder. + * + */ +{ + "type": "gpii.config.cloudBased.development", + "mergeConfigs": [ + "%flowManager/configs/gpii.flowManager.config.cloud.base.json5", + "%preferencesServer/configs/gpii.preferencesServer.config.base.json5", + "%flatMatchMaker/configs/gpii.flatMatchMaker.config.base.json5" + ] +} diff --git a/gpii/configs/shared/gpii.config.couch.development.base.json5 b/gpii/configs/shared/gpii.config.couch.development.base.json5 new file mode 100644 index 000000000..916a8c7ad --- /dev/null +++ b/gpii/configs/shared/gpii.config.couch.development.base.json5 @@ -0,0 +1,43 @@ +/* + + This configuration is common to both the tests and "development" configs, and adds a local CouchDB instance + provided by the gpii-couchdb-test-harness package. Note, you can control how Couch is started/stopped using an + environment variable: + + * (nothing, the default): Start a Docker container running CouchDB. + * `GPII_TEST_COUCH_USE_VAGRANT`: If this is set, a Vagrant VM will be created that is running Docker and CouchDB. + * `GPII_TEST_COUCH_USE_EXTERNAL`: If this is set, an external instance of CouchDB running on port 25984 will be + used. + + For more information, see the documentation for the gpii-couchdb-test-harness package: + + https://github.com/GPII/gpii-couchdb-test-harness + + See "gpii.test.couchEnvironment" grade in gpii/node_modules/testing/src/Fixtures.js for the CouchDB data loader + used for running acceptance and integration tests. + +*/ +{ + "type": "gpii.config.couch.development.base", + "options": { + "components": { + "harness": { + "type": "gpii.test.couchdb.harness", + "options": { + "databases": { + "gpii": { + "data": [ + "%gpii-universal/testData/dbData/clientCredentials.json", + "%gpii-universal/testData/dbData/gpiiAppInstallationClients.json", + "%gpii-universal/build/dbData/snapset/gpiiKeys.json", + "%gpii-universal/build/dbData/snapset/prefsSafes.json", + "%gpii-universal/testData/dbData/views.json" + ] + } + } + } + } + } + }, + "require": ["gpii-couchdb-test-harness", "journal"] +} diff --git a/gpii/configs/shared/gpii.config.couch.development.startWithCleanData.json5 b/gpii/configs/shared/gpii.config.couch.development.startWithCleanData.json5 new file mode 100644 index 000000000..ef9ff4f64 --- /dev/null +++ b/gpii/configs/shared/gpii.config.couch.development.startWithCleanData.json5 @@ -0,0 +1,33 @@ +/* + + A configuration that starts a Couch instance if needed on startup, and which always clean up the data if the + database already exists. Note, you can control how Couch is started/stopped using an environment variable: + + * (nothing, the default): Start a Docker container running CouchDB. + * `GPII_TEST_COUCH_USE_VAGRANT`: If this is set, create a Vagrant VM that runs Docker and CouchDB. + * `GPII_TEST_COUCH_USE_EXTERNAL`: If this is set, connect to an external instance of CouchDB running on port 25984. + + For more information, see the documentation for the gpii-couchdb-test-harness package: + + https://github.com/GPII/gpii-couchdb-test-harness + +*/ +{ + "type": "gpii.config.couch.development.startWithCleanData", + "options": { + "components": { + "harness": { + "options": { + "listeners": { + "onCreate.startHarness": { + func: "{that}.startup" + } + } + } + } + } + }, + "mergeConfigs": [ + "%gpii-universal/gpii/configs/shared/gpii.config.couch.development.base.json5" + ] +} diff --git a/gpii/configs/shared/gpii.config.couch.development.startWithoutCleanData.json5 b/gpii/configs/shared/gpii.config.couch.development.startWithoutCleanData.json5 new file mode 100644 index 000000000..4044fb54c --- /dev/null +++ b/gpii/configs/shared/gpii.config.couch.development.startWithoutCleanData.json5 @@ -0,0 +1,36 @@ +/* + + A configuration that starts a Couch instance if needed on startup, but which does not clean up the data if the + database already exists. Note, you can control how Couch is started/stopped using an environment variable: + + * (nothing, the default): Start a Docker container running CouchDB. + * `GPII_TEST_COUCH_USE_VAGRANT`: If this is set, create a Vagrant VM that runs Docker and CouchDB. + * `GPII_TEST_COUCH_USE_EXTERNAL`: If this is set, connect to an external instance of CouchDB running on port 25984. + + For more information, see the documentation for the gpii-couchdb-test-harness package: + + https://github.com/GPII/gpii-couchdb-test-harness + +*/ +{ + "type": "gpii.config.couch.development.startWithoutCleanData", + "options": { + "components": { + "harness": { + "type": "gpii.test.couchdb.harness.persistent", + "options": { + "cleanDbs": false, + // Start the harness on startup. + "listeners": { + "onCreate.startHarness": { + func: "{that}.startup" + } + } + } + } + } + }, + "mergeConfigs": [ + "%gpii-universal/gpii/configs/shared/gpii.config.couch.development.base.json5" + ] +} diff --git a/gpii/configs/shared/gpii.config.development.base.local.json5 b/gpii/configs/shared/gpii.config.development.base.local.json5 new file mode 100644 index 000000000..b02f6036e --- /dev/null +++ b/gpii/configs/shared/gpii.config.development.base.local.json5 @@ -0,0 +1,34 @@ +/** + * This configuration acts as a base for `gpii.config.development.local` and `gpii.config.development.dynamicDR.local`. + * It includes configs to locate the local flow manager, the cloud based flow manager, the preferences server and + * matchmaker all locally. The extra work required is to arrange to cross-inject the ontology handler between the + * matchmaker and preferences server. + * + * * The following components are running: + * ** Auth Server + * ** Flat MatchMaker + * ** Cloud Based Flow Manager + * ** Local Flow Manager + * ** MatchMaker Framework + * ** Ontology Handler + * ** Preferences Server + * ** Solutions Registry (reading registry from file) + */ +{ + "type": "gpii.config.development.base.local", + "options": { + "distributeOptions": { + "development.all.ontologyHandler": { + "record": "{ontologyHandler}", + "target": "{that preferencesServer}.options.components.ontologyHandler" + } + } + }, + "mergeConfigs": [ + "%flowManager/configs/gpii.flowManager.config.cloud.base.json5", + "%flowManager/configs/gpii.flowManager.config.local.base.json5", + "%preferencesServer/configs/gpii.preferencesServer.config.base.json5", + "%flatMatchMaker/configs/gpii.flatMatchMaker.config.base.json5", + "%gpii-universal/gpii/configs/shared/gpii.config.couch.development.base.json5" + ] +} diff --git a/gpii/configs/shared/gpii.config.development.dynamicDR.local.json5 b/gpii/configs/shared/gpii.config.development.dynamicDR.local.json5 new file mode 100644 index 000000000..db38156d6 --- /dev/null +++ b/gpii/configs/shared/gpii.config.development.dynamicDR.local.json5 @@ -0,0 +1,32 @@ +/** + * This configuration runs the entire system locally with a dynamic deviceReporter on port 8081. It expects couchDB + * to be running separately on localhost for data persistence. + * + * * The following components are running: + * ** Dynamic Device Reporter + * ** Flow Manager + * ** Preferences Server + * ** Ontology Handler + * ** Solutions Registry (reading registry from file) + * ** MatchMaker Framework + * ** Flat MatchMaker + * ** Lifecycle Manager + * + * Notes on testing: + * This setup is used as a basis for the platform specific acceptance tests that make use of a dynamic device reporter. + */ +{ + "type": "gpii.config.development.dynamicDR.local", + "options": { + "distributeOptions": { + "gpii.config.deviceReporter.flowManager": { + "record": "{flowManager}.solutionsRegistryDataSource", + "target": "{that gpii.deviceReporter.live}.options.components.solutionsRegistryDataSource" + } + } + }, + "mergeConfigs": [ + "%deviceReporter/configs/gpii.deviceReporter.config.dynamic.json5", + "%gpii-universal/gpii/configs/shared/gpii.config.development.base.local.json5" + ] +} diff --git a/gpii/configs/shared/gpii.config.development.local.json5 b/gpii/configs/shared/gpii.config.development.local.json5 new file mode 100644 index 000000000..c5de331d6 --- /dev/null +++ b/gpii/configs/shared/gpii.config.development.local.json5 @@ -0,0 +1,29 @@ +/** + * This configuration acts as a base for `gpii.config.development.manualTesting` and many configurations used by + * integration tests. It runs the entire system including the local flow manager, the cloud based flow manager, + * the preferences server and matchmaker all locally. It doesn't start any backend database for data persistence. + * + * * The following components are running: + * ** Auth Server + * ** Flat MatchMaker + * ** Cloud Based Flow Manager + * ** Lifecycle Manager + * ** Local Flow Manager + * ** MatchMaker Framework + * ** Ontology Handler + * ** Preferences Server + * ** Solutions Registry (reading registry from file) + * ** Static Device Reporter (reading from file) + * + * Notes on testing: + * This setup is used as a basis for the platform specific acceptance tests. Since the acceptance tests might use slightly different paths for the tests (i.e. a special folder for reading prefs and solutionsRegistry), + * what is actually being used for testing is a slight variation of this config. For more details, see the txt files describing the configs in the acceptance tests folder of both universal and the platform specific repositories. + * + */ +{ + "type": "gpii.config.development.local", + "mergeConfigs": [ + "%deviceReporter/configs/gpii.deviceReporter.config.static.json5", + "%gpii-universal/gpii/configs/shared/gpii.config.development.base.local.json5" + ] +} diff --git a/gpii/configs/shared/gpii.config.untrusted.development.dynamicDR.json5 b/gpii/configs/shared/gpii.config.untrusted.development.dynamicDR.json5 new file mode 100644 index 000000000..c6c56449d --- /dev/null +++ b/gpii/configs/shared/gpii.config.untrusted.development.dynamicDR.json5 @@ -0,0 +1,24 @@ +/** + * This configuration acts as a base for `gpii.config.untrusted.development.local`. It runs the + * local Untrusted Flow Manager locally. + * + * * The following components are running on the local machine: + * ** Untrusted Local Flow Manager + * ** Dynamic Device Reporter + */ +{ + "type": "gpii.config.untrusted.development.dynamicDR", + "options": { + "distributeOptions": { + "gpii.config.deviceReporter.flowManager": { + "record": "{flowManager}.solutionsRegistryDataSource", + "target": "{that gpii.deviceReporter.live}.options.components.solutionsRegistryDataSource" + } + } + }, + "mergeConfigs": [ + "%deviceReporter/configs/gpii.deviceReporter.config.dynamic.json5", + "%flowManager/configs/gpii.flowManager.config.untrusted.base.json5", + "%flatMatchMaker/configs/gpii.flatMatchMaker.config.base.json5" + ] +} diff --git a/gpii/configs/shared/gpii.config.untrusted.development.dynamicDR.local.json5 b/gpii/configs/shared/gpii.config.untrusted.development.dynamicDR.local.json5 new file mode 100644 index 000000000..26af42527 --- /dev/null +++ b/gpii/configs/shared/gpii.config.untrusted.development.dynamicDR.local.json5 @@ -0,0 +1,34 @@ +/** + * This configuration acts as a base for `gpii.config.untrusted.development.manualTesting`. + * + * It uses a multi-server Kettle configuration to run the Untrusted Flow Manager and + * a local version of the Cloud Based Flow Manager within the same Kettle process, but on different ports: + * It starts these servers: + * 1. Untrusted Local Flow Manager on port 8081 + * 2. Cloud with both Cloud Based Flow Manager and Preferences Server on port 8084 + * + * * The following components are running on the local machine: + * ** Untrusted Local Flow Manager + * ** Dynamic Device Reporter + * + * * The following components are running on the cloud: + * *** Auth Server + * *** Flat MatchMaker + * *** Cloud Based Flow Manager + * *** Preferences Server + * *** Solutions Registry + */ +{ + "type": "gpii.config.untrusted.development.dynamicDR.local", + "options": { + "configs": { + "localConfig": { + "configName": "gpii.config.untrusted.development.dynamicDR", + "configPath": "%gpii-universal/gpii/configs/shared" + } + } + }, + "mergeConfigs": [ + "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.local.json5" + ] +} diff --git a/gpii/configs/shared/gpii.config.untrusted.development.json5 b/gpii/configs/shared/gpii.config.untrusted.development.json5 new file mode 100644 index 000000000..82b0ce5af --- /dev/null +++ b/gpii/configs/shared/gpii.config.untrusted.development.json5 @@ -0,0 +1,16 @@ +/** + * This configuration acts as a base for `gpii.config.untrusted.development.local`. It runs the + * local Untrusted Flow Manager locally. + * + * * The following components are running on the local machine: + * ** Untrusted Local Flow Manager + * ** Static Device Reporter (reading from file) + */ +{ + "type": "gpii.config.untrusted.development", + "mergeConfigs": [ + "%deviceReporter/configs/gpii.deviceReporter.config.static.json5", + "%flowManager/configs/gpii.flowManager.config.untrusted.base.json5", + "%flatMatchMaker/configs/gpii.flatMatchMaker.config.base.json5" + ] +} diff --git a/gpii/configs/shared/gpii.config.untrusted.development.local.json5 b/gpii/configs/shared/gpii.config.untrusted.development.local.json5 new file mode 100644 index 000000000..e4bb8fdbd --- /dev/null +++ b/gpii/configs/shared/gpii.config.untrusted.development.local.json5 @@ -0,0 +1,47 @@ +/** + * This configuration acts as a base for `gpii.config.untrusted.development.manualTesting`. + * + * It uses a multi-server Kettle configuration to run the Untrusted Flow Manager and + * a local version of the Cloud Based Flow Manager within the same Kettle process, but on different ports: + * It starts these servers: + * 1. Untrusted Local Flow Manager on port 8081 + * 2. Cloud with both Cloud Based Flow Manager and Preferences Server on port 8084 + * + * * The following components are running on the local machine: + * ** Untrusted Local Flow Manager + * ** Static Device Reporter (reading from file) + * + * * The following components are running on the cloud: + * *** Auth Server + * *** Flat MatchMaker + * *** Cloud Based Flow Manager + * *** Preferences Server + * *** Solutions Registry + */ +{ + "type": "gpii.config.untrusted.development.local", + "options": { + "gradeNames": ["kettle.multiConfig.config"], + "configs": { + "localConfig": { + "configName": "gpii.config.untrusted.development", + "configPath": "%gpii-universal/gpii/configs/shared" + }, + "cloudBasedConfig": { + "configName": "gpii.config.cloudBased.development", + "configPath": "%gpii-universal/gpii/configs/shared" + } + }, + "distributeOptions": { + "untrusted.development.port": { + "record": 8084, + "target": "{that cloudBasedConfig}.options.mainServerPort" + }, + "untrusted.development.prefs": { + "record": "http://localhost:8084", + "target": "{that cloudBasedConfig flowManager prefsServerDataSource}.options.prefsServerURL", + "priority": "after:flowManager.prefsServerDataSource.default" + } + } + } +} diff --git a/gpii/index.js b/gpii/index.js deleted file mode 100644 index 0b2e53735..000000000 --- a/gpii/index.js +++ /dev/null @@ -1,14 +0,0 @@ -var fluid = require("infusion"); - -fluid.require("kettle", require); -fluid.require("matchMaker", require); -fluid.require("transformer", require); -fluid.require("deviceReporter", require); -fluid.require("lifecycleManager", require); -fluid.require("lifecycleActions", require); -fluid.require("flowManager", require); -fluid.require("settingsHandlers", require); -fluid.require("preferencesServer", require); -fluid.require("ontologyServer", require); - -module.exports = fluid; diff --git a/gpii/node_modules/accessRequester/index.js b/gpii/node_modules/accessRequester/index.js new file mode 100644 index 000000000..4870c8496 --- /dev/null +++ b/gpii/node_modules/accessRequester/index.js @@ -0,0 +1,7 @@ +"use strict"; + +var fluid = require("infusion"); + +fluid.module.register("accessRequester", __dirname, require); + +require("./src/accessRequester.js"); diff --git a/gpii/node_modules/accessRequester/package.json b/gpii/node_modules/accessRequester/package.json new file mode 100644 index 000000000..d7d6d3517 --- /dev/null +++ b/gpii/node_modules/accessRequester/package.json @@ -0,0 +1,13 @@ +{ + "name": "accessRequester", + "description": "Used by GPII app installations to request OAuth2 access tokens from GPII Cloud", + "version": "0.1.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } +} diff --git a/gpii/node_modules/accessRequester/src/accessRequester.js b/gpii/node_modules/accessRequester/src/accessRequester.js new file mode 100644 index 000000000..8e4103461 --- /dev/null +++ b/gpii/node_modules/accessRequester/src/accessRequester.js @@ -0,0 +1,93 @@ +/*! +GPII Access Requester + +Copyright 2017 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = fluid || require("infusion"), + gpii = fluid.registerNamespace("gpii"), + $ = fluid.registerNamespace("jQuery"); + +/* + * Access Requester provides getAccessToken(gpiiKey) API that returns a promise object whose resolved value is the access token. + * When this API is called, the access requester does: + * 1. Retrieves client credentials; + * 2. Communicate with GPII Cloud end point via [resource owner GPII key grant](https://wiki.gpii.net/w/GPII_OAuth_2_Guide#Resource_Owner_GPII_Key_Grant); + * 3. Retrieves and returns a promise object whose resolved value contains the access token responded by the GPII Cloud /access_token endpoint; + * + * Access Requester requires these input options: + * @url {String} The GPII cloud http end point to request access tokens granted by OAuth2 resource owner GPII key grant. + * such as https://flowmanager.gpii.net/access_token + * @clientCredentialDataSourceGrade {String or Array of Strings} The grade name of the implementation of "clientCredentialDataSource" subcomponent + * that provides a get() API that returns a promise object whose resolved value is the client credential. + */ +fluid.defaults("gpii.accessRequester", { + gradeNames: ["fluid.component"], + + // These options must be provided by integrators + url: "/access_token", // Must be provided by integrators. The API endpoint provided by the authorization server to request access tokens via Resource Owner GPII key Grant + clientCredentialDataSourceGrade: null, // Must be provided by integrators + // End of integrators provided options + + distributeOptions: { + source: "{that}.options.clientCredentialDataSourceGrade", + target: "{that > clientCredentialDataSource}.options.gradeNames" + }, + components: { + clientCredentialDataSource: { + type: "fluid.component" + }, + accessTokenDataSource: { + type: "kettle.dataSource.URL", + options: { + url: "{accessRequester}.options.url", + writable: true, + writeMethod: "POST", + dataSourceModel: { + grant_type: "password", + password: "dummy" + } + } + } + }, + invokers: { + getAccessToken: { + funcName: "gpii.accessRequester.getAccessToken", + args: ["{that}.clientCredentialDataSource", "{that}.accessTokenDataSource", "{arguments}.0"] + // gpiiKey + } + } +}); + +gpii.accessRequester.getAccessToken = function (clientCredentialDataSource, accessTokenDataSource, gpiiKey) { + var promiseTogo = fluid.promise(); + var clientCredentialPromise = clientCredentialDataSource.get(); + clientCredentialPromise.then(function (clientCredential) { + var accessTokenRequestParams = $.extend({}, accessTokenDataSource.options.dataSourceModel, clientCredential, { + username: gpiiKey + }); + var accessTokenPromise = accessTokenDataSource.set(null, accessTokenRequestParams); + fluid.promise.follow(accessTokenPromise, promiseTogo); + }, function (err) { + promiseTogo.reject(err); + }); + return promiseTogo; +}; + +/*********************************** + Client Credential File Data Source + ***********************************/ + +// The client credential is read from a file +fluid.defaults("gpii.accessRequester.clientCredentialDataSource.file", { + gradeNames: ["kettle.dataSource.file.moduleTerms"], + path: "%gpii-universal/testData/clientCredentials/pilot.json" +}); diff --git a/gpii/node_modules/accessRequester/test/AccessRequesterTests.js b/gpii/node_modules/accessRequester/test/AccessRequesterTests.js new file mode 100644 index 000000000..33af81498 --- /dev/null +++ b/gpii/node_modules/accessRequester/test/AccessRequesterTests.js @@ -0,0 +1,218 @@ +/*! +GPII Access Requester + +Copyright 2017 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + kettle = require("kettle"), + nock = require("nock"); + +require("accessRequester"); + +fluid.require("%gpii-universal/gpii/node_modules/testing/src/NockUtils.js"); + +kettle.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.accessRequester"); + +gpii.tests.accessRequester.hostname = "http://gpii.net"; +gpii.tests.accessRequester.path = "/access_token"; +gpii.tests.accessRequester.gpiiKey = "gpii-key-test"; + +// Set up mock ajax responses +gpii.tests.accessRequester.setUpNock = function (config) { + var cloudMock = nock(gpii.tests.accessRequester.hostname); + + // log nock matches + cloudMock.log(console.log); + + // mock POST requests to "/access_token" + cloudMock.post(gpii.tests.accessRequester.path, config.request) + .reply(config.status, config.response); +}; + +// The customized accessRequester component for tests +fluid.defaults("gpii.tests.accessRequester", { + gradeNames: ["gpii.accessRequester"], + gpiiKey: gpii.tests.accessRequester.gpiiKey, + url: gpii.tests.accessRequester.hostname + gpii.tests.accessRequester.path, + clientCredentialDataSourceGrade: "gpii.accessRequester.clientCredentialDataSource.file" +}); + +// The base testEnvironment grade to be inherited by all tests +fluid.defaults("gpii.tests.accessRequesterTests", { + gradeNames: ["fluid.test.testEnvironment"], + clientCredentialFilePath: null, // supplied by individual tests + testCaseHolderGrade: null, // supplied by individual tests + distributeOptions: { + clientCredentialFilePath: { + source: "{that}.options.clientCredentialFilePath", + target: "{that clientCredentialDataSource}.options.path" + }, + testCaseHolderGrade: { + source: "{that}.options.testCaseHolderGrade", + target: "{that > testCaseHolder}.type" + } + }, + components: { + accessRequester: { + type: "gpii.tests.accessRequester" + }, + testCaseHolder: { + type: "fluid.test.testCaseHolder" + } + } +}); + +// 1. A successful workflow +gpii.tests.accessRequester.success = { + clientCredentialFilePath: "%gpii-universal/gpii/node_modules/accessRequester/test/data/clientCredential-correct.json", + nockConfig: { + request: { + "grant_type": "password", + "password": "dummy", + "client_id": "pilot-computer", + "client_secret": "pilot-computer-secret", + "username": gpii.tests.accessRequester.gpiiKey + }, + status: 200, + response: { + access_token: "access-token-test-in-accessRequester", + expiresIn: 3600, + token_type: "Bearer" + } + } +}; + +fluid.defaults("gpii.tests.accessRequester.testCaseHolder.success", { + gradeNames: "fluid.test.testCaseHolder", + modules: [{ + name: "The access requester module tests - a successful workflow", + expect: 1, + tests: [{ + name: "An access token is received", + sequence: [{ + task: "{accessRequester}.getAccessToken", + args: [gpii.tests.accessRequester.gpiiKey], + resolve: "jqUnit.assertDeepEq", + resolveArgs: ["The response is expected with an access token", gpii.tests.accessRequester.success.nockConfig.response, "{arguments}.0"] + }] + }] + }] +}); + +fluid.defaults("gpii.tests.accessRequesterTests.success", { + gradeNames: ["gpii.tests.accessRequesterTests", "gpii.test.testWithNock"], + clientCredentialFilePath: gpii.tests.accessRequester.success.clientCredentialFilePath, + testCaseHolderGrade: "gpii.tests.accessRequester.testCaseHolder.success", + invokers: { + setUpNock: { + funcName: "gpii.tests.accessRequester.setUpNock", + args: gpii.tests.accessRequester.success.nockConfig + } + } +}); + +// 2. Error occurs due to the wrong client credentials +gpii.tests.accessRequester.error_wrongClientCredential = { + clientCredentialFilePath: "%gpii-universal/gpii/node_modules/accessRequester/test/data/clientCredential-wrong.json", + nockConfig: { + url: gpii.tests.accessRequester.path, + type: "post", + request: { + "grant_type": "password", + "password": "dummy", + "client_id": "wrong-client-id", + "client_secret": "wrong-client-secret", + "username": gpii.tests.accessRequester.gpiiKey + }, + status: 401, + response: { + isError: true, + message: "Unauthorized" + } + }, + expected: { + statusCode: 401, + isError: true + } +}; + +fluid.defaults("gpii.tests.accessRequester.testCaseHolder.error.wrongClientCredential", { + gradeNames: "fluid.test.testCaseHolder", + modules: [{ + name: "The access requester module tests - a failed workflow due to the wrong client credential", + expect: 1, + tests: [{ + name: "The request for an access token is rejected due to the wrong client credential", + sequence: [{ + task: "{accessRequester}.getAccessToken", + args: [gpii.tests.accessRequester.gpiiKey], + reject: "jqUnit.assertLeftHand", + rejectArgs: ["The error is received", gpii.tests.accessRequester.error_wrongClientCredential.expected, "{arguments}.0"] + }] + }] + }] +}); + +fluid.defaults("gpii.tests.accessRequesterTests.error.wrongClientCredential", { + gradeNames: ["gpii.tests.accessRequesterTests", "gpii.test.testWithNock"], + clientCredentialFilePath: gpii.tests.accessRequester.error_wrongClientCredential.clientCredentialFilePath, + testCaseHolderGrade: "gpii.tests.accessRequester.testCaseHolder.error.wrongClientCredential", + accessRequesterGrade: "gpii.tests.accessRequester.configNock", + invokers: { + setUpNock: { + funcName: "gpii.tests.accessRequester.setUpNock", + args: gpii.tests.accessRequester.error_wrongClientCredential.nockConfig + } + } +}); + +// 3. Error occurs due to the missing client credential file +gpii.tests.accessRequester.error_missingClientCredentialFile = { + clientCredentialFilePath: "non-existing.json", + expected: { + statusCode: 404, + isError: true + } +}; + +fluid.defaults("gpii.tests.accessRequester.testCaseHolder.error.missingClientCredentialFile", { + gradeNames: "fluid.test.testCaseHolder", + modules: [{ + name: "The access requester module tests - a failed workflow due to the missing client credential file", + expect: 1, + tests: [{ + name: "The request for an access token is rejected due to the missing client credential file", + sequence: [{ + task: "{accessRequester}.getAccessToken", + args: [gpii.tests.accessRequester.gpiiKey], + reject: "jqUnit.assertLeftHand", + rejectArgs: ["The response is expected with an access token", gpii.tests.accessRequester.error_missingClientCredentialFile.expected, "{arguments}.0"] + }] + }] + }] +}); + +fluid.defaults("gpii.tests.accessRequesterTests.error.missingClientCredentialFile", { + gradeNames: ["gpii.tests.accessRequesterTests"], + clientCredentialFilePath: gpii.tests.accessRequester.error_missingClientCredentialFile.clientCredentialFilePath, + testCaseHolderGrade: "gpii.tests.accessRequester.testCaseHolder.error.missingClientCredentialFile" +}); + +// Run all tests +fluid.test.runTests([ + "gpii.tests.accessRequesterTests.success", + "gpii.tests.accessRequesterTests.error.wrongClientCredential", + "gpii.tests.accessRequesterTests.error.missingClientCredentialFile" +]); diff --git a/gpii/node_modules/accessRequester/test/data/clientCredential-correct.json b/gpii/node_modules/accessRequester/test/data/clientCredential-correct.json new file mode 100644 index 000000000..c8a964a69 --- /dev/null +++ b/gpii/node_modules/accessRequester/test/data/clientCredential-correct.json @@ -0,0 +1,4 @@ +{ + "client_id": "pilot-computer", + "client_secret": "pilot-computer-secret" +} diff --git a/gpii/node_modules/accessRequester/test/data/clientCredential-wrong.json b/gpii/node_modules/accessRequester/test/data/clientCredential-wrong.json new file mode 100644 index 000000000..00aafe7ba --- /dev/null +++ b/gpii/node_modules/accessRequester/test/data/clientCredential-wrong.json @@ -0,0 +1,4 @@ +{ + "client_id": "wrong-client-id", + "client_secret": "wrong-client-secret" +} diff --git a/gpii/node_modules/deviceReporter/README.md b/gpii/node_modules/deviceReporter/README.md index 68a2e55ea..3b6dc5b2f 100644 --- a/gpii/node_modules/deviceReporter/README.md +++ b/gpii/node_modules/deviceReporter/README.md @@ -1,26 +1,25 @@ -Device Reporter -=== +# Device Reporter A server app built to be deployed with node.js server that returns device information as well as the list of installed solutions. -### Device Reporter API +## Device Reporter API The app currently supports the following url: {urlToDeviceReporterServer}/device // GET ONLY -### Dependencies +## Dependencies [infusion](https://github.com/fluid-project/infusion) framework. -### Installation +## Installation Run the following command in your newly checked out universal repository. This will pull all dependencies that are required by universal. npm install -### Run +## Run To run device reporter app simply type: @@ -33,4 +32,4 @@ For example: node node_modules/kettle/lib/init.js gpii/node_modules/deviceReporter/configs/ node node_modules/kettle/lib/init.js /Users/{userName}/universal/gpii/node_modules/deviceReporter/configs/ - NODE_ENV=production node node_modules/kettle/lib/init.js gpii/node_modules/deviceReporter/configs/ \ No newline at end of file + NODE_ENV=production node node_modules/kettle/lib/init.js gpii/node_modules/deviceReporter/configs/ diff --git a/gpii/node_modules/deviceReporter/configs/base.json b/gpii/node_modules/deviceReporter/configs/base.json deleted file mode 100644 index 2395d3d9e..000000000 --- a/gpii/node_modules/deviceReporter/configs/base.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "modules": [ - "deviceReporter" - ] -} diff --git a/gpii/node_modules/deviceReporter/configs/development.json b/gpii/node_modules/deviceReporter/configs/development.json deleted file mode 100644 index 46144eea0..000000000 --- a/gpii/node_modules/deviceReporter/configs/development.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "typeName": "deviceReporter.development", - "options": { - "gradeNames": ["autoInit", "fluid.littleComponent"], - "components": { - "server": { - "type": "kettle.server", - "options": { - "logging": true, - "components": { - "deviceReporter": { - "type": "gpii.deviceReporter", - "options": { - "gradeNames": ["gpii.deviceReporter.dev"], - "installedSolutionsUrl": "file://%root/../../../testData/deviceReporter/installedSolutions.json" - } - } - } - } - } - } - }, - "includes": [ - "./base.json" - ] -} diff --git a/gpii/node_modules/deviceReporter/configs/gpii.deviceReporter.config.base.json5 b/gpii/node_modules/deviceReporter/configs/gpii.deviceReporter.config.base.json5 new file mode 100644 index 000000000..66944fde2 --- /dev/null +++ b/gpii/node_modules/deviceReporter/configs/gpii.deviceReporter.config.base.json5 @@ -0,0 +1,19 @@ +{ + "type": "gpii.deviceReporter.config.base", + "options": { + "gradeNames": ["fluid.component"], + "components": { + "server": { + "type": "kettle.server", + "options": { + "components": { + "deviceReporter": { + "type": "gpii.deviceReporter" + } + } + } + } + } + }, + "require": "deviceReporter" +} diff --git a/gpii/node_modules/deviceReporter/configs/gpii.deviceReporter.config.dynamic.json5 b/gpii/node_modules/deviceReporter/configs/gpii.deviceReporter.config.dynamic.json5 new file mode 100644 index 000000000..caae7d5b2 --- /dev/null +++ b/gpii/node_modules/deviceReporter/configs/gpii.deviceReporter.config.dynamic.json5 @@ -0,0 +1,17 @@ +/** + * Running the device reporter in production mode + * + * **WARNING: This config this is untrusted and most likely broken** + */ +{ + "type": "gpii.deviceReporter.config.dynamic", + "options": { + "distributeOptions": { + "deviceReporterType": { + "record": "gpii.deviceReporter.live", + "target": "{that deviceReporter}.type" + } + } + }, + "mergeConfigs": "gpii.deviceReporter.config.base.json5" +} diff --git a/gpii/node_modules/deviceReporter/configs/gpii.deviceReporter.config.static.json5 b/gpii/node_modules/deviceReporter/configs/gpii.deviceReporter.config.static.json5 new file mode 100644 index 000000000..6f1a54d98 --- /dev/null +++ b/gpii/node_modules/deviceReporter/configs/gpii.deviceReporter.config.static.json5 @@ -0,0 +1,20 @@ +/** + * This configuration runs the device reporter in development mode, meaning that the list of + * installed solutions is read from a file in the filesystem. + */ +{ + "type": "gpii.deviceReporter.config.static", + "options": { + "distributeOptions": { + "development.installedSolutionsPath": { + "record": "%gpii-universal/testData/deviceReporter/installedSolutions.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path" + }, + "deviceReporterType": { + "record": "gpii.deviceReporter.static", + "target": "{that gpii.deviceReporter}.options.gradeNames" + } + } + }, + "mergeConfigs": "gpii.deviceReporter.config.base.json5" +} diff --git a/gpii/node_modules/deviceReporter/configs/kettleModuleLoader.js b/gpii/node_modules/deviceReporter/configs/kettleModuleLoader.js deleted file mode 100644 index 20c0170a7..000000000 --- a/gpii/node_modules/deviceReporter/configs/kettleModuleLoader.js +++ /dev/null @@ -1,16 +0,0 @@ -/*! -Kettle Module Loader. - -Copyright 2012-2013 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/kettle/LICENSE.txt -*/ - -// The purpose of this file is to be copied to a location for which the -// require function is needed. It allows to find node modules relative -// to that location that are otherwise non-resolvable. -module.exports = require; \ No newline at end of file diff --git a/gpii/node_modules/deviceReporter/configs/production.json b/gpii/node_modules/deviceReporter/configs/production.json deleted file mode 100644 index ffc3ac22c..000000000 --- a/gpii/node_modules/deviceReporter/configs/production.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "typeName": "deviceReporter.production", - "options": { - "gradeNames": ["autoInit", "fluid.littleComponent"], - "components": { - "server": { - "type": "kettle.server", - "options": { - "logging": false, - "components": { - "deviceReporter": { - "type": "gpii.deviceReporter", - "options": { - "gradeNames": ["gpii.deviceReporter.dev"], - "installedSolutionsUrl": "file://%root/../../../testData/deviceReporter/installedSolutions.json" - } - } - } - } - } - } - }, - "includes": [ - "./base.json" - ] -} diff --git a/gpii/node_modules/deviceReporter/index.js b/gpii/node_modules/deviceReporter/index.js index b2daafab2..521ce7c90 100644 --- a/gpii/node_modules/deviceReporter/index.js +++ b/gpii/node_modules/deviceReporter/index.js @@ -1,5 +1,7 @@ +"use strict"; + var fluid = require("infusion"); -var loader = fluid.getLoader(__dirname); +fluid.module.register("deviceReporter", __dirname, require); -loader.require("./src/DeviceReporter.js"); \ No newline at end of file +require("./src/DeviceReporter.js"); diff --git a/gpii/node_modules/deviceReporter/package.json b/gpii/node_modules/deviceReporter/package.json index 34775752f..f87581ca1 100644 --- a/gpii/node_modules/deviceReporter/package.json +++ b/gpii/node_modules/deviceReporter/package.json @@ -1,19 +1,13 @@ { - "name": "deviceReporter", - "description": "The Device Reporter module provides information about the platform and its installed software in order to guide the choice to a solution fitting the user's profile", - "version": "0.1.0", - "author": "GPII", - "bugs": "http://wiki.gpii.net/index.php/Main_Page", - "homepage": "http://gpii.net/", - "dependencies": {}, - "licenses": [ - { - "type": "BSD-3-Clause", - "url": "http://www.opensource.org/licenses/BSD-3-Clause" - } - ], - "keywords": ["gpii", "accessibility", "settings", "fluid", "IoC", "Inversion of Control", "configuration", "evented"], - "repository": "git://github.com/GPII/universal.git", - "main": "./index.js", - "engines": { "node" : ">=0.1.9" } -} \ No newline at end of file + "name": "deviceReporter", + "description": "The Device Reporter module provides information about the platform and its installed software in order to guide the choice to a solution fitting the user's profile", + "version": "0.3.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } +} diff --git a/gpii/node_modules/deviceReporter/src/DeviceGet.js b/gpii/node_modules/deviceReporter/src/DeviceGet.js deleted file mode 100644 index 9fbfbf91b..000000000 --- a/gpii/node_modules/deviceReporter/src/DeviceGet.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * GPII Preferences Server GET Handler - * - * Copyright 2012 OCAD University - * - * Licensed under the New BSD license. You may not use this file except in - * compliance with this License. - * - * You may obtain a copy of the License at - * https://github.com/gpii/universal/LICENSE.txt - */ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - os = require("os"), - gpii = fluid.registerNamespace("gpii"), - kettle = fluid.require("kettle", require); - - fluid.defaults("kettle.requests.request.handler.deviceGet", { - gradeNames: ["fluid.littleComponent", "autoInit"], - invokers: { - handle: { - funcName: "gpii.handleDeviceGet", - args: ["{requestProxy}", "{deviceReporter}.installedSolutionsDataSource"] - } - } - }); - - gpii.handleDeviceGet = function (requestProxy, installedSolutionsDataSource) { - installedSolutionsDataSource.get(null, function onSuccess(solutions) { - requestProxy.events.onSuccess.fire({ - solutions: solutions, - OS: { - //TODO: need to report more details - windowmanager, etc. - id: os.platform(), - // TODO: Need a better strategy - Node semver fails horribly - // in the face of the benign underscore (eg. x86_64). - version: os.release().replace("_", "-") - } - }); - }); - }; - -})(); diff --git a/gpii/node_modules/deviceReporter/src/DeviceReporter.js b/gpii/node_modules/deviceReporter/src/DeviceReporter.js index 4abc9ae85..c4fe7d9d2 100644 --- a/gpii/node_modules/deviceReporter/src/DeviceReporter.js +++ b/gpii/node_modules/deviceReporter/src/DeviceReporter.js @@ -1,54 +1,169 @@ /*! -Device Reporter + * Device Reporter + * + * Copyright 2012 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ -Copyright 2012 Raising the Floor - International +"use strict"; -Licensed under the New BSD license. You may not use this file except in -compliance with this License. +var fluid = require("infusion"), + os = require("os"), + gpii = fluid.registerNamespace("gpii"); -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ +require("kettle"); +require("./DeviceReporterUtilities.js"); -(function () { +fluid.defaults("gpii.deviceReporter", { + gradeNames: ["fluid.component"], + components: { + platformReporter: { + type: "gpii.platformReporter.native" + } + }, + invokers: { + get: "fluid.notImplemented", + getFinalPayload: { + funcName: "gpii.deviceReporter.getFinalPayload", + args: ["{arguments}.0", "{platformReporter}"] + // solutions + } + } +}); - "use strict"; +gpii.deviceReporter.getFinalPayload = function (installedSolutions, platformReporter) { + return { + solutions: installedSolutions, + OS: platformReporter.reportPlatform() + }; +}; - var fluid = require("infusion"), - path = require("path"), - gpii = fluid.registerNamespace("gpii"); +// Static Device Reporter +fluid.defaults("gpii.deviceReporter.static", { + gradeNames: ["gpii.deviceReporter"], + components: { + installedSolutionsDataSource: { + type: "kettle.dataSource.file", + options: { + gradeNames: "kettle.dataSource.file.moduleTerms" + } + } + }, + invokers: { + get: { + funcName: "gpii.deviceReporter.static.get", + args: ["{deviceReporter}"] + } + } +}); - fluid.require("kettle", require); - fluid.require("./DeviceGet.js", require); +gpii.deviceReporter["static"].get = function (deviceReporter) { + var promiseTogo = fluid.promise(); + var solutionsPromise = deviceReporter.installedSolutionsDataSource.get(); + solutionsPromise.then(function (solutions) { + if (!fluid.isDestroyed(deviceReporter)) { // Fix failure for GPII-1227 tests which can receive this response after the deviceReporter is gone + promiseTogo.resolve(deviceReporter.getFinalPayload(solutions)); // TODO: Remove such checks once FLUID-5790 is resolved + } + }, function (err) { + var error = fluid.extend(err, { + statusCode: 500, + message: "Failed to read deviceReporter source: " + err.message, + isError: true + }); + promiseTogo.reject(error); + }); + return promiseTogo; +}; - fluid.defaults("gpii.deviceReporter", { - gradeNames: ["kettle.app", "autoInit"], - handlers: { - deviceGet: { - route: "/device", - type: "get" - } +// Dynamic Device Reporter +// TODO: Dynamic Device Reporter has *NO* unit tests + +fluid.defaults("gpii.deviceReporter.live.nameResolver", { + gradeNames: ["fluid.component"], + invokers: { + resolveName: { + funcName: "fluid.identity" + } + } +}); + +fluid.defaults("gpii.deviceReporter.live", { + gradeNames: ["gpii.deviceReporter"], + components: { + // currently in "gpii.config.development.dynamicDR.local" as {flowManager}.solutionsRegistryDataSource + // solutionsRegistryDataSource: // should be supplied by integrator + nameResolver: { + type: "gpii.deviceReporter.live.nameResolver" + } + }, + invokers: { + get: { + funcName: "gpii.deviceReporter.live.get", + args: ["{deviceReporter}", "{solutionsRegistryDataSource}"] }, - installedSolutionsUrl: "", - root: path.join(__dirname, ".."), - components: { - installedSolutionsDataSource: { - type: "kettle.dataSource.URL", - options: { - gradeNames: ["kettle.dataSource.simpleCallbackWrapper"], - url: "{gpii.deviceReporter}.options.installedSolutionsUrl" + filterByInstalledSolutions: { + funcName: "gpii.deviceReporter.filterByInstalledSolutions", + args: [ "{arguments}.0", "{deviceReporter}" ] + } + } +}); + +gpii.deviceReporter.live.get = function (deviceReporter, solutionsRegistryDataSource) { + var promiseTogo = fluid.promise(); + var solutionsPromise = solutionsRegistryDataSource.get({ + os: deviceReporter.platformReporter.reportPlatform().id + }); + + solutionsPromise.then(function (entries) { + var filteredSolutions = deviceReporter.filterByInstalledSolutions(entries); + promiseTogo.resolve(deviceReporter.getFinalPayload(filteredSolutions)); + }, promiseTogo.reject); + + return promiseTogo; +}; + +gpii.deviceReporter.filterByInstalledSolutions = function (entries, deviceReporter) { + var installedSolutions = []; + fluid.each(entries, function (entry, entryId) { + if (!installedSolutions.some(function (s) { return s.id === entryId; })) { + var foundEntryId = fluid.find(entry.isInstalled, function (installedSolutionsReporter) { + var resolvedName = deviceReporter.nameResolver.resolveName(installedSolutionsReporter.type, "deviceReporter"); + if (fluid.invokeGradedFunction(resolvedName, installedSolutionsReporter)) { + return entryId; } + }, null); + if (foundEntryId !== null) { + installedSolutions.push({ "id": foundEntryId }); } } }); + return installedSolutions; +}; - fluid.defaults("gpii.deviceReporter.dev", { - gradeNames: ["autoInit", "fluid.littleComponent"], - distributeOptions: { - source: "{that}.options.urlExpanderGradeNames", - target: "{that urlExpander}.options.gradeNames" - }, - urlExpanderGradeNames: ["kettle.urlExpander.development"] - }); +fluid.defaults("gpii.platformReporter.native", { + gradeNames: ["fluid.component"], + invokers: { + reportPlatform: { + funcName: "gpii.platformReporter.native.reportPlatform" + } + } +}); -})(); +gpii.platformReporter["native"].reportPlatform = function () { // "native" is a reserved word + return { + // TODO: need to report more details - windowmanager, etc. + id: os.platform(), + // TODO: Need a better strategy - Node semver fails horribly + // in the face of the benign underscore (eg. x86_64). + version: os.release().replace("_", "-") + }; +}; diff --git a/gpii/node_modules/deviceReporter/src/DeviceReporterUtilities.js b/gpii/node_modules/deviceReporter/src/DeviceReporterUtilities.js new file mode 100644 index 000000000..b7b276f3f --- /dev/null +++ b/gpii/node_modules/deviceReporter/src/DeviceReporterUtilities.js @@ -0,0 +1,37 @@ +/** + * GPII Device Reporter Utilities. + * + * Copyright 2015 Emergya + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.deviceReporter"); + +/* Marker function for use in isInstalled sections of Solutions Registry + * to identify in a meaningful way that the solution is always installed or + * available to the system. + * This is meant to be used by any OS built-in solution that needs to report + * to the dynamic device reporter that they are always installed. + */ +fluid.defaults("gpii.deviceReporter.alwaysInstalled", { + gradeNames: "fluid.function", + argumentMap: {} +}); + +gpii.deviceReporter.alwaysInstalled = function () { + return true; +}; diff --git a/gpii/node_modules/deviceReporter/test/StaticDeviceReporterTests.js b/gpii/node_modules/deviceReporter/test/StaticDeviceReporterTests.js new file mode 100644 index 000000000..511a397e5 --- /dev/null +++ b/gpii/node_modules/deviceReporter/test/StaticDeviceReporterTests.js @@ -0,0 +1,111 @@ +/*! + * Static Device Reporter Tests + * + * Copyright 2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.registerNamespace("jqUnit"), + kettle = require("kettle"); + +kettle.loadTestingSupport(); + +require("../src/DeviceReporter.js"); + +fluid.registerNamespace("gpii.tests.deviceReporter"); + +// The base testEnvironment grade to be inherited by all tests +fluid.defaults("gpii.tests.deviceReporterTestEnv", { + gradeNames: ["fluid.test.testEnvironment"], + installedSolutionsPath: null, // supplied by individual tests + testCaseHolderGrade: null, // supplied by individual tests + distributeOptions: { + installedSolutionsPath: { + source: "{that}.options.installedSolutionsPath", + target: "{that deviceReporter installedSolutionsDataSource}.options.path" + }, + testCaseHolderGrade: { + source: "{that}.options.testCaseHolderGrade", + target: "{that > testCaseHolder}.type" + } + }, + components: { + deviceReporter: { + type: "gpii.deviceReporter.static" + }, + testCaseHolder: { + type: "fluid.test.testCaseHolder" + } + } +}); + +// Successful test cases +fluid.defaults("gpii.tests.deviceReporter.testCaseHolder.success", { + gradeNames: "fluid.test.testCaseHolder", + modules: [{ + name: "The device reporter test - a successful workflow", + expect: 2, + tests: [{ + name: "The payload returned by the device reporter is correct", + sequence: [{ + task: "{deviceReporter}.get", + resolve: "gpii.tests.deviceReporter.verifyPayload", + resolveArgs: ["{arguments}.0"] + }] + }] + }] +}); + +fluid.defaults("gpii.tests.deviceReporter.success", { + gradeNames: ["gpii.tests.deviceReporterTestEnv"], + installedSolutionsPath: "%gpii-universal/testData/deviceReporter/installedSolutions.json", + testCaseHolderGrade: "gpii.tests.deviceReporter.testCaseHolder.success" +}); + +gpii.tests.deviceReporter.verifyPayload = function (response) { + jqUnit.assertNotUndefined("OS id has been received", response.OS.id); + jqUnit.assertTrue("Solutions have been received", response.solutions.length > 0); +}; + +// Error test cases +fluid.defaults("gpii.tests.deviceReporter.testCaseHolder.error", { + gradeNames: "fluid.test.testCaseHolder", + modules: [{ + name: "Device Reporter fails on corrupt JSON file", + expect: 2, + tests: [{ + name: "The payload returned by the device reporter is correct", + sequence: [{ + task: "{deviceReporter}.get", + reject: "gpii.tests.deviceReporter.assertError", + rejectArgs: ["{arguments}.0", ["Failed to read deviceReporter", "Parse error"]] + }] + }] + }] +}); + +fluid.defaults("gpii.tests.deviceReporter.error", { + gradeNames: ["gpii.tests.deviceReporterTestEnv"], + installedSolutionsPath: "%gpii-universal/tests/data/faultyDeviceReport.jsonx", + testCaseHolderGrade: "gpii.tests.deviceReporter.testCaseHolder.error" +}); + +gpii.tests.deviceReporter.assertError = function (response, expectedErrorMsg) { + fluid.each(expectedErrorMsg, function (msg) { + jqUnit.assertTrue("The error message is expected", response.message.includes(msg)); + }); +}; + +fluid.test.runTests([ + "gpii.tests.deviceReporter.success", + "gpii.tests.deviceReporter.error" +]); diff --git a/gpii/node_modules/eventLog/README.md b/gpii/node_modules/eventLog/README.md new file mode 100644 index 000000000..2456699c7 --- /dev/null +++ b/gpii/node_modules/eventLog/README.md @@ -0,0 +1,164 @@ +# GPII Event Log + +Produces logs that can be read by a backend server for analysis and auditing. The log is in JSON format, and is sent +to an external log server via tcp. + +Rather than transforming the fluid.log output, this module was produced so the debug log is still usable by humans and +it serves as a way of opting-in to sending only useful (and non-private) information. + +## Log Destination + +GPII sends the log data to the Filebeat service on the localhost, via tcp. This can be overridden by setting the +`logDestination` option of the `gpii.eventLog` grade, or the environment variable `GPII_EVENT_LOG`, to either the path +to a file or a tcp URI (`tcp://127.0.0.1:51481`). + +If GPII is unable to connect to the log server, then the log data is buffered in memory until the connection can be +made. + +### Filebeat + +Filebeat is usually automatically installed with GPII. The Filebeat configuration and installer module is in +[github.com/stegru/gpii-metrics/.../filebeat-installer](https://github.com/stegru/gpii-metrics/tree/master/filebeat-installer) + +## Logged Events + +This module records GPII related events, listed below. For the additional events, related to the OS, see +[gpii-windows/gpii/node_modules/windowsMetrics/README.md](https://github.com/GPII/windows/blob/master/gpii/node_modules/windowsMetrics/README.md). + +All logged events contain the following data: + +```json5 +{ + "module": "gpii", // The area of GPII that caused this event + "event": "start", // The event name + "data": { + // Additional fields specific to the event + }, + "installID": "hK/Dvo1GG8", // Unique identifier of the installation. + "sequence": 60689584, // Incrementing number, unique per installation. + "timestamp": "2018-10-31T06:45:21.971-07:00", // When the event occurred + "version": "0.4.0", // The version of this module (or the windows metrics, if higher). + + // The following are optional, depending on the current state: + "sessionID": "4acr8sks-4069", // unique session identifier + "subSessionID": "4acr8sks-4069-5", // unique sub-session identifier + "logon": "in", // "in" or "out" if currently logging in/out - "in-after"/"out-after" 10 seconds after log in/out. + // for any other time, this field does not exist. + "gpiiKey": "d90eda4e-d1cd-415b-9605-dd9ce8be6359", // gpiiKey currently in use, + // from the UI: + "app": "active", // The active window belongs to Morphic. + "focus": "openUSB", // The button that currently has focus (combine with "app" to determine if it has system focus) + "hover": "launch-documorph", // The button that the mouse is currently hovering over. + "widget-hover": "openUSB", // The QSS pop-up that the mouse is over. + "field-hover": "ejectUsbButton", // The field within the pop-up that the mouse is over. + "field-focus": "regular-contrast", // The field within the pop-up that has key focus. +} +``` + +### Start and stop + +When GPII starts + +```json5 +{ + "module": "gpii", + "event": "start", +} +``` + +```json5 +{ + "module": "gpii", + "event": "stop", +} +``` + +### Session start and end + +When a user session starts. + +```json5 +{ + "module": "lifecycle", + "event": "SessionStart" +} +``` + +```json5 +{ + "module": "lifecycle", + "event": "SessionStop", + "data": { + "duration": 481 // number of seconds the session lasted. + } +} +``` + +### Solutions + +When a solution has been applied or failed. + +```json5 +{ + "module": "metrics", + "event": "solution-applied", + "data": { + "solutionID": "com.microsoft.windows.screenDPI" + } +} +``` + +```json5 +{ + "module": "metrics", + "event": "solution-failed", + "data": { + "solutionID": "com.microsoft.windows.screenResolution" + } +} +``` + +### Preference + +A preference has been set + +```json5 +{ + "module": "metrics", + "event": "preference", + "data": { + "name": "http://registry.gpii.net/common/highContrastTheme", + "value": "black-white" + } +} +``` + +### Errors + +Two types of errors are recorded - those raised by `fluid.fail`, and uncaught exceptions. + +```json5 +{ + "event": "Error.Fail", + "module": "GPII", + "data": { + "error": { + "message": "Received an invalid screen resolution: [object Object]", + "stack": "..." + } + } +} +``` + +```json5 +{ + "event": "Error.Exception", + "module": "GPII", + "data": { + "error": { + "message": "Cannot read property 'inferredConfiguration' of undefined", + "stack": "..." + } + } +} +``` diff --git a/gpii/node_modules/eventLog/index.js b/gpii/node_modules/eventLog/index.js new file mode 100644 index 000000000..62c464543 --- /dev/null +++ b/gpii/node_modules/eventLog/index.js @@ -0,0 +1,27 @@ +/* + * Event logging. + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"); + +fluid.module.register("eventLog", __dirname, require); + +require("./src/installID.js"); +require("./src/eventLog.js"); +require("./src/metrics.js"); diff --git a/gpii/node_modules/eventLog/package.json b/gpii/node_modules/eventLog/package.json new file mode 100644 index 000000000..b8c2270ce --- /dev/null +++ b/gpii/node_modules/eventLog/package.json @@ -0,0 +1,13 @@ +{ + "name": "eventLog", + "description": "Event logging.", + "version": "0.5.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } +} diff --git a/gpii/node_modules/eventLog/src/eventLog.js b/gpii/node_modules/eventLog/src/eventLog.js new file mode 100644 index 000000000..8b1a59f51 --- /dev/null +++ b/gpii/node_modules/eventLog/src/eventLog.js @@ -0,0 +1,439 @@ +/* + * Event logging. + * Handles logging of events that are deemed interesting enough to be recorded and sent to a log server for analysis. + * The log produced here is in JSON and sent to a different output than the usual fluid.log. + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"); +var fs = require("fs"), + moment = require("moment"), + net = require("net"); + +var gpii = fluid.registerNamespace("gpii"), + $ = fluid.registerNamespace("jQuery"); + +fluid.registerNamespace("gpii.eventLog"); + +fluid.defaults("gpii.eventLog", { + gradeNames: ["fluid.component", "fluid.contextAware"], + components: { + installID: { + type: "gpii.installID" + }, + settingsDir: { + type: "gpii.settingsDir" + } + }, + invokers: { + logEvent: { + funcName: "gpii.eventLog.log", // moduleName, event, data, level + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "{arguments}.3"] + }, + logError: "gpii.eventLog.logError", + getGpiiSettingsDir: "{settingsDir}.getGpiiSettingsDir", + getVersion: "gpii.eventLog.getVersion", + setState: { + funcName: "gpii.eventLog.setState", + args: ["{eventLog}", "{arguments}.0", "{arguments}.1"] // name, value + } + }, + members: { + sequence: 0, + // Buffered log lines while there's not log server connection + logBuffer: [], + logLevel: fluid.logLevel.INFO, + // Maximum number of lines to buffer. + maxBufferlength: 0xfff, + // A TCP socket to send the log entries to (filebeat service). Taken from options.logDestination, if it's a url. + logServer: { + host: null, + port: null + }, + // A file to write the log entries to. Taken from options.logDestination, if it's a local file. + logPath: null, + // Data to include in every log entry. + eventData: { + // The installation ID + installID: "@expand:{that}.installID.getInstallID()", + version: "@expand:{that}.getVersion()", + sessionID: undefined, + gpiiKey: undefined + }, + // The start times for events which require durations to be recorded. + eventTimes: {} + }, + listeners: { + "onCreate.logFile": { + func: "gpii.eventLog.initLogDestination", + args: ["{that}", "{that}.options.logDestination"] + }, + "onCreate.log": { + func: "gpii.eventLog.logStartStop", + args: ["{that}", "start"] + }, + "onDestroy.log": { + func: "gpii.eventLog.logStartStop", + args: ["{that}", "stop"] + } + }, + // File path, or tcp://host:port + logDestination: null +}); + +/** + * A log event + * @typedef {Object} LogEvent + * @property {String} module The area of GPII the event is from. + * @property {String} event The name of the event. + * @property {String} data [optional] Extra information about the event. + * @property {Object} level The severity of the event (from fluid.logLevelsSpec, default: fluid.INFO). + * @property {String} version The eventLog module version. (automatically added) + * @property {String} timestamp Current time of the event. (automatically added) + * @property {String} sequence Ordered unique identifier to the event. (automatically added) + */ + +/** + * Returns the actual date and time, as a string, in ISO 8601 format with the localtime + offset from UTC. + * eg: '2018-07-13T13:03:03.863+01:00' + * @return {String} The current date, in ISO-8601 format with localtime and UTC offset. + */ +gpii.eventLog.getTimestamp = function () { + return moment().toISOString(true); +}; + +/** + * Gets the version of the logger. This is logged so the log processing can adjust for any updates to this module. + * + * @return {String} The version of this module, from package.json. + */ +gpii.eventLog.getVersion = function () { + var packageJson = fluid.require("%eventLog/package.json"); + return packageJson.version; +}; + +/** + * Logs the start or stop of GPII, without duplication. + * @param {Component} that - The gpii.eventLog instance. + * @param {String} state - "start" or "stop". + */ +gpii.eventLog.logStartStop = function (that, state) { + if (state !== gpii.eventLog.logStartStop.currentState) { + gpii.eventLog.logStartStop.currentState = state; + if (state === "start") { + // Start the event numbering based on the current timestamp to ensure it's unique (per instance). + that.sequence = Date.now() - 15e11; + } + that.logEvent("gpii", state); + } +}; + +/** + * Creates an object for the log. Everything in this object is what will be logged, and the "time" field will be added + * later when it is actually logged. + * + * @param {String} moduleName - The part of GPII causing this event. + * @param {String} event - Name of the event. + * @param {Any} [data] - [optional] Event specific data. + * @param {Object} level -[optional] Level of the log, see fluid.logLevelsSpec [FATAL,FAIL,WARN,IMPORTANT,INFO,TRACE]. + * @return {LogEvent} The log object. + */ +gpii.eventLog.createLogObject = function (moduleName, event, data, level) { + var eventObject = { + module: moduleName || "GPII", + event: event, + level: level + }; + + var hasValue = (data !== null && data !== undefined); + if (hasValue && fluid.isPlainObject(data)) { + hasValue = !$.isEmptyObject(data); + } + + if (hasValue) { + eventObject.data = fluid.copy(data); + // Replace any components in the data object with their id. + fluid.each(eventObject.data, function (value, key) { + if (fluid.isComponent(value)) { + eventObject.data[key] = value.id; + } + }); + } + return eventObject; +}; + +/** + * Logs an event. + * + * @param {Component} that - The gpii.eventLog instance. + * @param {String} moduleName - The part of GPII causing this event. + * @param {String} event - The event name. + * @param {Object} [data] - [optional] Event specific data. Can (shallowly) contain components, in which case just the ID is + * logged. + * @param {Object} [level] - [optional] Level of the log, see fluid.logLevelsSpec [FATAL,FAIL,WARN,IMPORTANT,INFO,TRACE]. + */ +gpii.eventLog.log = function (that, moduleName, event, data, level) { + var eventObject = gpii.eventLog.createLogObject(moduleName, event, data, level); + gpii.eventLog.writeLog(that, level, eventObject); +}; + +/** + * Logs an error. + * + * @param {Component} that - The gpii.eventLog instance. + * @param {String} moduleName - The part of GPII causing this error. + * @param {String} errType - Type of error. + * @param {Object} err - The error. + * @param {Object} [level] - [optional] Level of the log. default: fluid.logLevel.FAIL. + */ +gpii.eventLog.logError = function (that, moduleName, errType, err, level) { + if (!level) { + level = fluid.logLevel.FAIL; + } + + var data = {}; + if (err instanceof Error) { + // Error doesn't serialise + data.error = {}; + fluid.each(Object.getOwnPropertyNames(err), function (source) { + // Ensure the first character of the field is lowercase - "Message" vs "message" was causing a duplicate + // field during the analysis. + var dest = source.charAt(0).toLowerCase() + source.slice(1); + data.error[dest] = err[source]; + }); + } else if (fluid.isPlainObject(err, true)) { + data.error = Object.assign({}, err); + } else { + data.error = { + message: err + }; + } + + if (!data.error.stack) { + data.error.stack = new Error().stack; + } + + var eventObject = gpii.eventLog.createLogObject(moduleName, "Error." + errType, data); + gpii.eventLog.writeLog(that, fluid.logLevel.FAIL, eventObject); +}; + +// Log fluid.fail. +fluid.failureEvent.addListener(function (args) { + var err = Array.isArray(args) ? args.join(" ") : args; + gpii.eventLog.gotError(err, "Fail"); +}, "gpii-eventLog", "before:fail"); + +/** + * Logs an error caught by onUncaughtException or failureEvent. + * + * @param {String} err - The error. + * @param {String} errType - The error type (defaults to "Exception"). + */ +gpii.eventLog.gotError = function (err, errType) { + var eventLog = fluid.queryIoCSelector(fluid.rootComponent, "gpii.eventLog"); + if (eventLog.length > 0) { + fluid.each(eventLog, function (inst) { + inst.logError(inst, null, errType || "Exception", err); + }); + } +}; + +// Log uncaught exceptions. +fluid.onUncaughtException.addListener(gpii.eventLog.gotError, "gpii-eventLog"); + +/** + * Parses the log path (which can be overridden by the GPII_EVENT_LOG environment variable), and sets either the + * logServer or logPath member. + * + * @param {Component} that - The gpii.eventLog instance. + * @param {String} logDestination - Where the log is sent (a local file path, or a tcp socket in the format of + * `tcp://:`). + */ +gpii.eventLog.initLogDestination = function (that, logDestination) { + logDestination = process.env.GPII_EVENT_LOG || logDestination; + + var match = logDestination && /tcp:\/*([^:]+):([0-9]+)/.exec(logDestination); + if (match) { + that.logServer.host = match[1]; + that.logServer.port = match[2]; + } else { + that.logPath = logDestination; + } + + var dest = that.logServer.host ? ("tcp://" + that.logServer.host + ":" + that.logServer.port) : that.logPath; + fluid.log(fluid.logLevel.IMPORTANT, "Writing event log to " + dest); +}; + +/** + * Writes an event to the log file. + * + * @param {Component} that - The gpii.eventLog instance. + * @param {Object} level - Level of the log, see fluid.logLevelsSpec [FATAL,FAIL,WARN,IMPORTANT,INFO,TRACE]. + * @param {LogEvent} event - The object. This will be modified to what has been sent to the log, adding the installID and + * timestamp fields. + */ +gpii.eventLog.writeLog = function (that, level, event) { + var eventLevel = gpii.eventLog.checkLevel(level); + event.level = eventLevel.value; + + gpii.eventLog.recordDuration(that, event); + + Object.assign(event, that.eventData); + event.timestamp = gpii.eventLog.getTimestamp(); + event.sequence = that.sequence++; + + if (eventLevel.priority <= that.logLevel.priority) { + var logLine = JSON.stringify(event) + "\n"; + + // Logging to both the server and file is allowed (gpii-windows tests depend on this feature) + if (that.logServer.host && that.logServer.port) { + gpii.eventLog.writeLogTcp(that, logLine); + } + if (that.logPath) { + fs.appendFileSync(that.logPath, logLine); + } + } +}; + +/** + * Handles the timing of a pair of duration events. + * + * Some events can be paired into having a duration. For example, tooltip-shown and tooltip-hidden. The timestamp for + * the start event is recorded, so when the end event is seen the duration is calculated and added to the event data. + * + * @param {Component} that The gpii.eventLog instance. + * @param {LogEvent} event The event. + */ +gpii.eventLog.recordDuration = function (that, event) { + var endEvent = that.options.durationEvents[event.event]; + if (endEvent) { + // This is the start of a pair of duration events. + if (!that.eventTimes[endEvent]) { + that.eventTimes[endEvent] = []; + } + that.eventTimes[endEvent].push(process.hrtime()); + } else { + var startTimes = that.eventTimes[event.event]; + var startTime = startTimes && startTimes.pop(); + if (startTime) { + // This is the ending of a pair of duration events. + if (!event.data) { + event.data = {}; + } + var memberName = event.data.hasOwnProperty("duration") ? "duration_auto" : "duration"; + event.data[memberName] = process.hrtime(startTime)[0]; + } + } +}; + +/** + * Sends a log line to the configured log server. + * + * @param {Component} that - The gpii.eventLog instance. + * @param {String} logLine - The log line, including the newline. + */ +gpii.eventLog.writeLogTcp = function (that, logLine) { + if (!that.logSocket) { + gpii.eventLog.connectLog(that); + } + + if (that.logSocket.connecting) { + // Buffer the output until the connection is made. Socket.write already buffers, however if the connection + // fails the data will be lost. + if (that.logBuffer.length > that.maxBufferlength) { + fluid.log("Dropping metrics data - exceeded initial buffer size of " + that.maxBufferlength); + } else { + that.logBuffer.push(logLine); + } + } else { + that.logSocket.write(logLine); + } + +}; + +/** + * Connect to a TCP port which receives the log data. + * @param {Component} that - The gpii.eventLog instance. + */ +gpii.eventLog.connectLog = function (that) { + if (that.logSocket) { + fluid.fail("Already connecting to log server"); + } + + // Connect to the server. + that.logSocket = new net.Socket(); + that.logSocket.connect(that.logServer.port, that.logServer.host, function () { + gpii.eventLog.connectLog.lastError = null; + fluid.log("Connected to log server"); + // Send the initial data. + if (that.logBuffer.length > 0) { + that.logSocket.write(that.logBuffer.join("")); + that.logBuffer = []; + } + }); + + that.logSocket.on("close", function () { + if (that.logSocket) { + that.logSocket.destroy(); + that.logSocket = null; + } + }); + + that.logSocket.on("error", function (err) { + if (gpii.eventLog.connectLog.lastError !== err.code) { + fluid.log("Log server socket error:", err); + if (that.logSocket) { + that.logSocket.destroy(); + } + // Don't repeat the same error again. + gpii.eventLog.connectLog.lastError = err.code; + } + }); +}; + +/** + * Ensure that the loglevel has a valid value. The levels are defined in the fluid.logLevelsSpec + * Sets INFO as default loglevel + * + * @param {Object} level - Level to check, can be a string that represents the value or a property of fluid.logLevel. + * @return {Object} A valid fluid.logLevel, with INFO as default. + */ +gpii.eventLog.checkLevel = function (level) { + var togo; + if (typeof level === "string" && level in fluid.logLevelsSpec) { + togo = fluid.logLevel[level]; + } else { + togo = fluid.isLogLevel(level) && level; + } + return togo || fluid.logLevel.INFO; +}; + +/** + * Specifies a field which gets logged with every subsequent event. + * + * @param {Component} that The gpii.eventLog instance. + * @param {String} name The name of the field. + * @param {Object} value The field value. undefined or null will remove the field. + */ +gpii.eventLog.setState = function (that, name, value) { + if (value === undefined || value === null) { + delete that.eventData[name]; + } else { + that.eventData[name] = value; + } +}; diff --git a/gpii/node_modules/eventLog/src/installID.js b/gpii/node_modules/eventLog/src/installID.js new file mode 100644 index 000000000..356bf604c --- /dev/null +++ b/gpii/node_modules/eventLog/src/installID.js @@ -0,0 +1,98 @@ +/* + * Retrieves an "installation ID", which is something that uniquely identifies a particular machine. + * + * The installation ID is a string that's based on the OS specific "machine ID". + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + fs = require("fs"), + crypto = require("crypto"); + +fluid.defaults("gpii.installID", { + gradeNames: ["fluid.component", "fluid.contextAware"], + contextAwareness: { + platform: { + checks: { + windows: { + contextValue: "{gpii.contexts.windows}", + gradeNames: "gpii.installID.windows" + }, + linux: { + contextValue: "{gpii.contexts.linux}", + gradeNames: "gpii.installID.standard" + } + } + } + }, + invokers: { + getInstallID: "gpii.installID.get({that}.getMachineID)", + getMachineID: "fluid.identity" + } +}); + +fluid.defaults("gpii.installID.standard", { + invokers: { + getMachineID: "gpii.installID.machineID.standard" + } +}); + +/** + * Gets the installation ID, using the given machine ID. + * + * @param {Function} getMachineID - The OS specific function to get the machine ID. + * @return {String} The installation ID. + */ +gpii.installID.get = function (getMachineID) { + var machineID = getMachineID(); + var installID; + + if (machineID) { + installID = crypto.createHash("sha1").update(machineID).digest("base64").substr(0, 10); + fluid.log(fluid.logLevel.IMPORTANT, "Installation ID: ", installID); + } else { + fluid.log(fluid.logLevel.WARN, "Unable to get installation ID"); + installID = "none"; + } + + return installID; +}; + +fluid.registerNamespace("gpii.installID.machineID"); + +/** + * Retrieve the machine ID from /etc/machine-id. + * + * @return {String} The machine ID. + */ +gpii.installID.machineID.standard = function () { + var togo; + var files = [ "/etc/machine-id", "/var/lib/dbus/machine-id" ]; + + for (var f in files) { + try { + togo = fs.readFileSync(files[f], "utf8"); + break; + } catch (e) { + // Continue. + } + } + + return togo; +}; diff --git a/gpii/node_modules/eventLog/src/metrics.js b/gpii/node_modules/eventLog/src/metrics.js new file mode 100644 index 000000000..e2dc8e1ab --- /dev/null +++ b/gpii/node_modules/eventLog/src/metrics.js @@ -0,0 +1,336 @@ +/* + * Metrics logging. + * This acts upon events that are deemed interesting enough to be recorded and sent to a log server for analysis. + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"); + +var gpii = fluid.registerNamespace("gpii"); + +fluid.defaults("gpii.metrics", { + gradeNames: ["fluid.modelComponent", "fluid.contextAware"], + contextAwareness: { + platform: { + checks: { + test: { + contextValue: "{gpii.contexts.test}", + gradeNames: "gpii.metrics.test" + }, + windows: { + contextValue: "{gpii.contexts.windows}", + gradeNames: "gpii.windowsMetrics" + } + } + } + }, + invokers: { + logMetric: { + func: "{eventLog}.logEvent", + args: ["metrics", "{arguments}.0", "{arguments}.1"] // event, data + }, + startSubSession: { + funcName: "gpii.metrics.startSubSession", + args: ["{that}", "{eventLog}"] + }, + stopSubSession: { + funcName: "gpii.metrics.stopSubSession", + args: ["{that}", "{eventLog}"] + } + }, + members: { + sessionSolutions: {}, + // Incrementing sub-session id. + subSessionIncrementer: 0 + }, + events: { + "onStartMetrics": null, + "onStopMetrics": null, + // The user has become active (called on the first input after onInactive) + "onActive": null, // args: duration-inactive + // The user has become inactive (no input was received after {that}.config.input.inactiveTime) + "onInactive": null // args: {sleep:true} + }, + listeners: { + "onStartMetrics.log": { + funcName: "fluid.log", + args: "Metrics started" + }, + "onActive.metrics": { + func: "{that}.logMetric", + args: ["inactive-stop"] + }, + "onActive.subsession": { + func: "{that}.startSubSession" + }, + "onInactive.metrics": { + func: "{that}.logMetric", + args: ["inactive-begin", "{arguments}.0"] + }, + "onInactive.subsession": { + func: "{that}.stopSubSession" + } + }, + durationEvents: { + "start": "stop", + "inactive-begin": "inactive-stop", + "SessionStart": "SessionStop", + "subsession-begin": "subsession-end" + }, + siteConfig: {} +}); + +// Mixin grade for gpii.metrics to log lifecycle manager things +fluid.defaults("gpii.metrics.lifecycle", { + modelListeners: { + "{lifecycleManager}.model.logonChange": { + funcName: "gpii.metrics.logonStateChanged", + args: ["{that}", "{eventLog}", "{lifecycleManager}", "{change}.oldValue", "{change}.value"] + } + }, + invokers: { + sessionStopped: { + funcName: "gpii.metrics.sessionStopped", + args: [ "{that}", "{eventLog}"] + } + }, + listeners: { + "{lifecycleManager}.events.onCreate": { + namespace: "trackPrefsSetChange", + listener: "gpii.metrics.trackPrefsSetChange", + args: ["{that}", "{lifecycleManager}"] + }, + "{lifecycleManager}.events.onSessionStart": [ + { + namespace: "metrics.session", + funcName: "gpii.metrics.sessionStarted", + args: ["{that}", "{eventLog}", "{arguments}.1"] + }, { + + func: "{that}.events.onStartMetrics" + }], + "{lifecycleManager}.events.onSessionStop": [{ + namespace: "metrics.session", + func: "{that}.sessionStopped", + args: ["{that}", "{eventLog}", "{arguments}.1.id"] + }, { + "func": "{that}.events.onStopMetrics", + "priority": "before:eventLog" + }], + "{lifecycleManager}.events.onSessionSnapshotUpdate": { + namespace: "metrics", + funcName: "gpii.metrics.snapshotUpdate", + args: ["{that}", "{arguments}.2"] + }, + "onDestroy.session": { + func: "{that}.sessionStopped" + } + } +}); + +fluid.defaults("gpii.metrics.standalone", { + listeners: { + "onCreate": "{that}.events.onStartMetrics.fire", + "onDestroy": "{that}.events.onStopMetrics.fire" + } +}); + +/** + * Attached as the lifecycleManager.onCreate event. + * + * Adds a model change listener to the user session's model to listen for changes to the preferences. + * + * @param {Component} that - The gpii.metrics instance. + * @param {Component} lifecycleManager - The lifecycleManager instance. + */ +gpii.metrics.trackPrefsSetChange = function (that, lifecycleManager) { + var userSession = lifecycleManager.getSession(); + userSession.applier.modelChanged.addListener({ + path: "preferences.contexts", + excludeSource: "SessionCleanup" + }, function (newValue, oldValue) { + var current = newValue && newValue[userSession.model.activePrefsSetName]; + current = current && current.preferences; + var previous = oldValue && oldValue[userSession.model.activePrefsSetName]; + previous = previous && previous.preferences; + + gpii.metrics.preferenceChanged(that, current, previous); + }); +}; + +/** + * Log the solutions as they're applied. + * + * @param {Component} that - The gpii.metrics instance. + * @param {Object} originalSettings - The original settings (only interested in the keys). + */ +gpii.metrics.snapshotUpdate = function (that, originalSettings) { + var ids = fluid.keys(originalSettings); + if (!that.sessionSolutions) { + that.sessionSolutions = {}; + } + // Log the solution IDs that haven't been logged. + fluid.each(ids, function (id) { + if (!that.sessionSolutions[id]) { + that.logMetric("solution-applied", { + solutionID: id + }); + that.sessionSolutions[id] = true; + } + }); +}; + +/** + * Called when a preference has changed. (a change in the preferences field of the session's model.) + * + * @param {Comment} that - The gpii.metrics instance. + * @param {Object} current - The current preferences map for the active preferences set. + * @param {Object} previous - The previous preferences map for the active preferences set. + */ +gpii.metrics.preferenceChanged = function (that, current, previous) { + var diff = { changeMap: {}, changes: 0, unchanged: 0}; + var same = fluid.model.diff(previous, current, diff); + if (!same) { + var changedPreferences; + if (fluid.isPlainObject(diff.changeMap)) { + changedPreferences = {}; + fluid.each(diff.changeMap, function (value, key) { + if (value === "ADD") { + changedPreferences[key] = current[key]; + } + }); + } else if (diff.changeMap === "ADD") { + // Everything is new + changedPreferences = current; + } + + fluid.each(changedPreferences, function (value, name) { + that.logMetric("preference", { + name: name, + newValue: value.toString() + }); + }); + } +}; + +/** + * Updates the logged gpiiKey + * @param {Component} that - The gpii.metrics instance. + * @param {Component} eventLog - The gpii.eventLog instance. + * @param {String} gpiiKey - The gpiiKey used for this session. + */ +gpii.metrics.sessionStarted = function (that, eventLog) { + eventLog.eventData.sessionID = fluid.allocateGuid(); + eventLog.logEvent("lifecycle", "SessionStart"); + that.startSubSession(); +}; + +/** + * Removes the logged solution IDs for the session, and the current gpii key. + * @param {Component} that - The gpii.metrics instance. + * @param {Component} eventLog - The gpii.eventLog instance. + * @param {String} sessionID - Session ID. + */ +gpii.metrics.sessionStopped = function (that, eventLog) { + if (eventLog.eventData.sessionID) { + eventLog.logEvent("lifecycle", "SessionStop"); + delete eventLog.eventData.sessionID; + that.sessionSolutions = {}; + that.startSubSession(); + } +}; + +/** + * A model state listener for {lifecycleManager}.model.logonChange. + * When the login state changes from "login" to "logout", log the solutions that did not get applied for that session. + * + * @param {Component} that - The gpii.metrics instance. + * @param {Component} eventLog - The eventLog instance. + * @param {Component} lifecycleManager - The lifecycleManager instance. + * @param {Object} oldValue - The old value. + * @param {Object} newValue - The new value. + */ +gpii.metrics.logonStateChanged = function (that, eventLog, lifecycleManager, oldValue, newValue) { + if (newValue.type === "login") { + if (newValue.inProgress) { + // Start marking all events with the gpiiKey. + if (newValue.gpiiKey === "noUser") { + eventLog.eventData.gpiiKeyBefore = eventLog.eventData.gpiiKey; + delete eventLog.eventData.gpiiKey; + } else { + eventLog.eventData.gpiiKey = newValue.gpiiKey; + } + // So metrics knows the next set of events is due to someone keying in + eventLog.eventData.logon = "in"; + } else { + // Login is complete, so all further events are unrelated to key-in. There's a 15 second grace period for + // events that may occur shortly after, however. + eventLog.eventData.logon = "in-after"; + setTimeout(function () { + delete eventLog.eventData.gpiiKeyBefore; + delete eventLog.eventData.logon; + }, 15000); + } + } else if (newValue.type === "logout" && !newValue.inProgress) { + // Stop marking events with the gpiiKey when they've completely logged out + delete eventLog.eventData.gpiiKey; + + if (newValue.inProgress) { + eventLog.eventData.gpiiKeyBefore = eventLog.eventData.gpiiKey; + eventLog.eventData.logon = "out"; + } else { + eventLog.eventData.logon = "out-after"; + } + setTimeout(function () { + delete eventLog.eventData.gpiiKeyBefore; + delete eventLog.eventData.logon; + }, 15000); + } + + if (oldValue && oldValue.type === "login" && (newValue.type === "logout" || !newValue.inProgress)) { + var session = lifecycleManager.getSession(oldValue.gpiiKey); + // The reason for this null checker is that the lifecyclaManager's "session" may be null after + // having logged on with a bogus token which terminated the request in error before the + // "session" could be created. + if (session && session.model.currentSettings) { + // Log the solution IDs that hadn't been applied. + var expectedSolutions = fluid.keys(session.model.activeConfiguration.lifecycleInstructions); + fluid.each(expectedSolutions, function (id) { + if (!session.model.currentSettings[id]) { + that.logMetric("solution-failed", { + solutionID: id + }); + } + }); + } + } +}; + +gpii.metrics.startSubSession = function (that, eventLog) { + eventLog.eventData.subSessionID = eventLog.eventData.sessionID + "-" + that.subSessionIncrementer++; + that.logMetric("subsession-begin", { subSessionID: eventLog.eventData.subSessionID}); +}; + +gpii.metrics.stopSubSession = function (that, eventLog) { + if (eventLog.eventData.subSessionID) { + that.logMetric("subsession-end", { + subSessionID: eventLog.eventData.subSessionID + }); + } + delete eventLog.eventData.subSessionID; +}; diff --git a/gpii/node_modules/eventLog/test/EventLogTests.js b/gpii/node_modules/eventLog/test/EventLogTests.js new file mode 100644 index 000000000..b3bb1038a --- /dev/null +++ b/gpii/node_modules/eventLog/test/EventLogTests.js @@ -0,0 +1,865 @@ +/* + * eventLog Tests + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + fs = require("fs"), + os = require("os"), + net = require("net"), + readline = require("readline"); + +var jqUnit = fluid.require("node-jqunit"); +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.tests.eventLog"); + +require("../index.js"); + +require("../../lifecycleManager/"); + +var teardowns = []; + +jqUnit.module("gpii.tests.eventLog", { + teardown: function () { + while (teardowns.length) { + teardowns.pop()(); + } + } +}); + +fluid.defaults("gpii.installID.test", { + invokers: { + getMachineID: "gpii.tests.eventLog.getMachineID" + } +}); + +gpii.tests.eventLog.getMachineID = function () { + return "TEST-MACHINE-ID"; +}; + +/** + * Creates a testable gpii.eventLog component. + * + * @param {String} logFile - The log file. + * @return {Component} An instance of gpii.eventLog component. + */ +gpii.tests.eventLog.createEventLogComponent = function (logFile) { + var eventLog = gpii.eventLog({ + gradeNames: ["gpii.lifecycleManager", "gpii.metrics"], + distributeOptions: { + installID: { + record: "gpii.installID.test", + target: "{that installID}.options.gradeNames" + } + }, + durationEvents: { + "test-start1": "test-end1", + "test-start2": "test-end2", + "test-start3": "test-end3", + "test-start4": "test-end4" + }, + members: { + eventData: { + installID: gpii.tests.eventLog.installID, + sessionID: gpii.tests.eventLog.sessionID + } + }, + logDestination: logFile + }); + teardowns.push(eventLog.destroy); + return eventLog; +}; + +gpii.tests.eventLog.installID = "installation-id"; +gpii.tests.eventLog.timestamp = "2000-00-00T00:00:00.000Z"; +gpii.tests.eventLog.version = require("../package.json").version; +gpii.tests.eventLog.sessionID = "session-id"; + +gpii.tests.eventLog.testDefs = fluid.freezeRecursive(fluid.transform([ + { + // generated by the component + testData: null, + expected: { + "level": "INFO", + "module": "gpii", + "event": "start" + } + }, + { + // data object + testData: { + func: "log", + args: ["{that}", "module0", "event0", {"data0": "value0"}] + }, + expected: { + "level": "INFO", + "module": "module0", + "event": "event0", + "data": {"data0": "value0"} + } + }, + { + // no data + testData: { + func: "log", + args: ["{that}", "module1", "event1"] + }, + expected: { + "level": "INFO", + "module": "module1", + "event": "event1" + } + }, + { + // data string + testData: { + func: "log", + args: ["{that}", "module2", "event2", "value2"] + }, + expected: { + "level": "INFO", + "module": "module2", + "event": "event2", + "data": "value2" + } + }, + { + // data number + testData: { + func: "log", + args: ["{that}", "module3", "event3", 1234] + }, + expected: { + "level": "INFO", + "module": "module3", + "event": "event3", + "data": 1234 + } + }, + { + // empty data + testData: { + func: "log", + args: ["{that}", "module4", "event4", {}] + }, + expected: { + "level": "INFO", + "module": "module4", + "event": "event4" + } + }, + { + // null data + testData: { + func: "log", + args: ["{that}", "module5", "event5", null] + }, + expected: { + "level": "INFO", + "module": "module5", + "event": "event5" + } + }, + { + // test log level + testData: { + func: "log", + args: ["{that}", "module14", "event14", {"data0": "value0"}, fluid.logLevel.WARN] + }, + expected: { + "level": "WARN", + "module": "module14", + "event": "event14", + "data": {"data0": "value0"} + } + }, + { + // test log level (dropped) + testData: { + func: "log", + args: ["{that}", "module14b", "event14b", {"data0": "value0"}, fluid.logLevel.TRACE] + }, + expected: null + }, + { + // logError + testData: { + func: "logError", + args: ["{that}", "module15", "event15", {"error15": "value15"}] + }, + expected: { + "level": "FAIL", + "module": "module15", + "event": "Error.event15", + "data": { + "error": {"error15": "value15", "stack": ""} + } + } + }, + // Duration events + { + // start + testData: { + func: "log", + args: ["{that}", "module", "test-start1", {"value": "yes"}] + }, + expected: { + "module": "module", + "event": "test-start1", + "level": "INFO", + "data": { + "value": "yes" + } + } + }, + { + // end with data + testData: { + func: "log", + args: ["{that}", "module", "test-end1", {"value": "yes"}] + }, + expected: { + "module": "module", + "event": "test-end1", + "level": "INFO", + "data": { + "value": "yes", + "duration": 2 + } + } + }, + { + // start with no data + testData: { + func: "log", + args: ["{that}", "module", "test-start1" ] + }, + expected: { + "module": "module", + "event": "test-start1", + "level": "INFO" + } + }, + { + // end with no data + testData: { + func: "log", + args: ["{that}", "module", "test-end1" ] + }, + expected: { + "module": "module", + "event": "test-end1", + "level": "INFO", + "data": { + "duration": 2 + } + } + }, + // multiple pairs stacked + { + // start 1 + testData: { + func: "log", + args: ["{that}", "module", "test-start1" ] + }, + expected: { + "module": "module", + "event": "test-start1", + "level": "INFO" + } + }, + { + // start 2 + testData: { + func: "log", + args: ["{that}", "module", "test-start2" ] + }, + expected: { + "module": "module", + "event": "test-start2", + "level": "INFO" + } + }, + { + // end 2 + testData: { + func: "log", + args: ["{that}", "module", "test-end2" ] + }, + expected: { + "module": "module", + "event": "test-end2", + "level": "INFO", + "data": { + "duration": 2 + } + } + }, + { + // end 1 + testData: { + func: "log", + args: ["{that}", "module", "test-end1" ] + }, + expected: { + "module": "module", + "event": "test-end1", + "level": "INFO", + "data": { + "duration": 2 + } + } + }, + // multiple pairs intersected + { + // start 1 + testData: { + func: "log", + args: ["{that}", "module", "test-start1" ] + }, + expected: { + "module": "module", + "event": "test-start1", + "level": "INFO" + } + }, + { + // start 2 + testData: { + func: "log", + args: ["{that}", "module", "test-start2" ] + }, + expected: { + "module": "module", + "event": "test-start2", + "level": "INFO" + } + }, + { + // end 1 + testData: { + func: "log", + args: ["{that}", "module", "test-end1" ] + }, + expected: { + "module": "module", + "event": "test-end1", + "level": "INFO", + "data": { + "duration": 2 + } + } + }, + { + // end 2 + testData: { + func: "log", + args: ["{that}", "module", "test-end2" ] + }, + expected: { + "module": "module", + "event": "test-end2", + "level": "INFO", + "data": { + "duration": 2 + } + } + }, + // same pairs stacked + { + // start 1 + testData: { + func: "log", + args: ["{that}", "module", "test-start1" ] + }, + expected: { + "module": "module", + "event": "test-start1", + "level": "INFO" + } + }, + { + // start 1 (again) + testData: { + func: "log", + args: ["{that}", "module", "test-start1" ] + }, + expected: { + "module": "module", + "event": "test-start1", + "level": "INFO" + } + }, + { + // end 1 + testData: { + func: "log", + args: ["{that}", "module", "test-end1" ] + }, + expected: { + "module": "module", + "event": "test-end1", + "level": "INFO", + "data": { + "duration": 2 + } + } + }, + { + // end 1 (again) + testData: { + func: "log", + args: ["{that}", "module", "test-end1" ] + }, + expected: { + "module": "module", + "event": "test-end1", + "level": "INFO", + "data": { + "duration": 2 + } + } + }, + // End without start + { + // end 3 (no start) + testData: { + func: "log", + args: ["{that}", "module", "test-end3" ] + }, + expected: { + "module": "module", + "event": "test-end3", + "level": "INFO" + } + }, + { + // start 3 (prove the test previous is correct) + testData: { + func: "log", + args: ["{that}", "module", "test-start3" ] + }, + expected: { + "module": "module", + "event": "test-start3", + "level": "INFO" + } + }, + { + // end 3 + testData: { + func: "log", + args: ["{that}", "module", "test-end3" ] + }, + expected: { + "module": "module", + "event": "test-end3", + "level": "INFO", + "data": { + "duration": 2 + } + } + }, + // Already has duration data + { + // start 4 + testData: { + func: "log", + args: ["{that}", "module", "test-start3" ] + }, + expected: { + "module": "module", + "event": "test-start3", + "level": "INFO" + } + }, + { + // end 3 + testData: { + func: "log", + args: ["{that}", "module", "test-end3", {"duration": "hello"} ] + }, + expected: { + "module": "module", + "event": "test-end3", + "level": "INFO", + "data": { + "duration": "hello", + "duration_auto": 2 + } + } + } +], function (item) { + fluid.each(fluid.makeArray(item.expected), function (expected) { + expected.timestamp = gpii.tests.eventLog.timestamp; + expected.installID = gpii.tests.eventLog.installID; + expected.version = gpii.tests.eventLog.version; + expected.sessionID = gpii.tests.eventLog.sessionID; + }); + return item; +})); + +/** + * Sets up the logging so it's predictable and can be examined easily. + * + * @return {String} The log file. + */ +gpii.tests.eventLog.prepareLogFile = function () { + var logFile = os.tmpdir() + "/gpii-test-eventLog-" + Math.random().toString(36); + teardowns.push(function () { + fs.unlinkSync(logFile); + }); + + // Mock getTimestamp function + var oldGetTimestamp = gpii.eventLog.getTimestamp; + teardowns.push(function () { + // Restore getTimestamp. + gpii.eventLog.getTimestamp = oldGetTimestamp; + }); + gpii.eventLog.getTimestamp = function () { + return gpii.tests.eventLog.timestamp; + }; + + return logFile; +}; + +/** + * Checks a log line. + * + * @param {String} line - The log lone. + * @param {Object} expected - What it should parse into. + */ +gpii.tests.eventLog.checkLogLine = function (line, expected) { + jqUnit.expect(5); + jqUnit.assertTrue("Line should look like a JSON object", !!line.match(/^{.*}$/)); + var obj = JSON.parse(line); + jqUnit.assertEquals("JSON should be an object", "object", typeof(obj)); + + if (obj.data) { + // The stack property of an error is unpredictable. If it's found, just make sure it's a string. + if (obj.data.error && obj.data.error.stack) { + jqUnit.expect(1); + jqUnit.assertEquals("data.error.stack property should a string", "string", typeof(obj.data.error.stack)); + obj.data.error.stack = ""; + } + } + + // The sequence is unpredictable - just make sure it exists, and is larger than the last one. + var seq = obj.sequence || null; + jqUnit.assertFalse("Log line must have a numeric sequence field.", isNaN(seq)); + + var prev = gpii.tests.eventLog.checkLogLine.lastSequence || 0; + jqUnit.assertTrue("log sequence should be greater than the previous", seq > prev); + gpii.tests.eventLog.checkLogLine.lastSequence = seq; + delete obj.sequence; + + + jqUnit.assertDeepEq("Parsed log should match expected", expected, obj); +}; + +/** + * Checks an entire log file. + * + * @param {String} logFile - The log file. + * @param {Object[]} expected - Array of objects expected for each line. + * @return {Promise} A promise that resolves when the file is read. + */ +gpii.tests.eventLog.checkLogFile = function (logFile, expected) { + var promise = fluid.promise(); + var reader = readline.createInterface({ + input: fs.createReadStream(logFile) + }); + + // Inspect each line of the log, making sure each one parses into the expected object. + var lineNumber = 0; + reader.on("line", function (line) { + console.log("Log line", lineNumber, line); + + if (lineNumber >= expected.length) { + jqUnit.fail("Log file should not contain more data."); + promise.reject(); + reader.close(); + return; + } + gpii.tests.eventLog.checkLogLine(line, expected[lineNumber]); + lineNumber++; + }); + + reader.on("close", function () { + if (!promise.disposition) { + promise.resolve(); + } + }); + + return promise; +}; + +jqUnit.asyncTest("Uncaught exception test", function () { + jqUnit.expect(1); + + var logFile = gpii.tests.eventLog.prepareLogFile(); + gpii.tests.eventLog.createEventLogComponent(logFile); + // Clear the log file + try { + fs.unlinkSync(logFile); + } catch (e) { + // ignore + } + + jqUnit.assertNotUndefined("Uncaught exception listener added", + fluid.onUncaughtException.listeners["gpii-eventLog"]); + + var errorText = "eventLog uncaught exception logging test"; + var expected = [ + { + "installID": gpii.tests.eventLog.installID, + "timestamp": gpii.tests.eventLog.timestamp, + "version": gpii.tests.eventLog.version, + "sessionID": gpii.tests.eventLog.sessionID, + "level": "FAIL", + "module": "GPII", + "event": "Error.Exception", + "data": { + "error": { + "testError": true, + "message": errorText, + "stack": "" + } + } + } + ]; + + // Disable jqUnit and fluid's handlers, to allow the test to continue when the exception is thrown. + fluid.failureEvent.addListener(fluid.identity, "jqUnit", "before:fail"); + fluid.onUncaughtException.addListener(fluid.identity, "fail", "first"); + + var promise = fluid.promise(); + process.nextTick(function () { + try { + throw { + testError: true, + message: errorText + }; + } finally { + // Resolve after the next tick - so the uncaught exception handler can be called. + setTimeout(promise.resolve, 1); + } + }); + + promise.then(function () { + fluid.failureEvent.removeListener("jqUnit"); + fluid.onUncaughtException.removeListener("fail"); + + gpii.tests.eventLog.checkLogFile(logFile, expected) + .then(jqUnit.start); + }); +}); + +jqUnit.asyncTest("fluid.fail test", function () { + var logFile = gpii.tests.eventLog.prepareLogFile(); + gpii.tests.eventLog.createEventLogComponent(logFile); + // Clear the log file + fs.unlinkSync(logFile); + + var errorText = "eventLog failure logging test"; + var expected = [ + { + "installID": gpii.tests.eventLog.installID, + "timestamp": gpii.tests.eventLog.timestamp, + "version": gpii.tests.eventLog.version, + "sessionID": gpii.tests.eventLog.sessionID, + "level": "FAIL", + "module": "GPII", + "event": "Error.Fail", + "data": { + "error": { + "message": errorText, + "stack": "" + } + } + } + ]; + + // Test fluid.fail gets logged. + jqUnit.expectFrameworkDiagnostic("testing fluid.fail gets logged", function () { + fluid.fail(errorText); + }, [errorText]); + + gpii.tests.eventLog.checkLogFile(logFile, expected) + .then(jqUnit.start); +}); + +jqUnit.asyncTest("eventLog tests #1", function () { + var tests = gpii.tests.eventLog.testDefs; + var logFile = gpii.tests.eventLog.prepareLogFile(); + var expected = []; + + var that = gpii.tests.eventLog.createEventLogComponent(logFile); + + // mock hrtime so it return something expected + var hrtime = process.hrtime; + process.hrtime = function (arg) { + return arg ? [2, 0] : [3, 0]; + }; + + + // Log the test data + for (var n = 0; n < tests.length; n++) { + var test = tests[n]; + var calls = fluid.makeArray(test.testData); + while (calls.length) { + var call = calls.shift(); + var args = fluid.transform(call.args, function (c) { + return (c === "{that}") ? that : c; + }); + gpii.eventLog[call.func].apply(null, args); + } + + expected.push.apply(expected, fluid.makeArray(test.expected)); + } + + process.hrtime = hrtime; + + gpii.tests.eventLog.checkLogFile(logFile, expected) + .then(jqUnit.start); +}); + +/** + * Creates a TCP server, calling a callback for every line received. + * @param {Number} port The TCP port to listen on. + * @param {Function} gotLine The function to call for each line. + * @return {net.Server} The TCP server. + */ +gpii.tests.eventLog.createLogServer = function (port, gotLine) { + var buffer = ""; + var logServer = net.createServer(); + + logServer.listen(port, "127.0.0.1"); + + logServer.on("listening", function () { + fluid.log("[test] log server listening"); + }); + logServer.on("connection", function (socket) { + fluid.log("[test] log server connection"); + socket.setEncoding("utf8"); + + socket.on("data", function (chunk) { + buffer += chunk; + if (buffer.indexOf("\n") >= 0) { + var lines = buffer.split("\n"); + buffer = lines.pop(); + fluid.each(lines, gotLine); + } + }); + }); + logServer.on("close", function () { + }); + logServer.on("error", function (err) { + jqUnit.fail(err); + }); + + return logServer; +}; + +/** + * Resolve after a timeout + * @param {Number} timeout The millisecond timeout. + * @return {Promise} Resolves after the timeout. + */ +gpii.tests.eventLog.promiseTimeout = function (timeout) { + var promise = fluid.promise(); + setTimeout(promise.resolve, timeout); + return promise; +}; + +/** + * Tests writing logs to a log server. + */ +jqUnit.asyncTest("eventLog tests - writeLogTcp", function () { + + var port = 50000 + Math.floor(Math.random() * 5000); + var logFile = "tcp://127.0.0.1:" + port; + + var that = gpii.tests.eventLog.createEventLogComponent(logFile); + + var linesWrote = []; + var lineCount = 0; + var stopTest = false; + + var firstLine = true; + // Check each line as it comes in. + var gotLine = function (line) { + if (firstLine) { + // Ignore the first line - it's the gpii.start event + firstLine = false; + } else { + // All lines written are expected to be received. + var expected = linesWrote.shift(); + jqUnit.assertEquals("Line received must match the one sent in sequence", expected, line); + } + }; + + // Continuously write log lines. + var writeLine = function () { + if (!stopTest) { + var line = "message" + lineCount++; + linesWrote.push(line); + gpii.eventLog.writeLogTcp(that, line + "\n"); + setTimeout(writeLine, 100); + } + }; + + // Start writing logs before the server is started. Log lines should be buffered until the connection is made. + writeLine(); + + // Start the log server, resolving some time after a client connection is received + var startServer = function (state) { + fluid.log("[test] Starting log server"); + state.logServer = gpii.tests.eventLog.createLogServer(port, gotLine); + var p = fluid.promise(); + state.logServer.on("connection", function (socket) { + state.logServer.close(); + state.socket = socket; + setTimeout(p.resolve, 500); + }); + return p; + }; + + var actions = [ + gpii.tests.eventLog.promiseTimeout(500), + // Start the log server. + startServer, + // Stop the log server. Outgoing lines should be buffered until reconnected. + function (state) { + fluid.log("[test] Stopping log server"); + state.socket.end(); + return gpii.tests.eventLog.promiseTimeout(700); + }, + // Restart the log server. eventLog should reconnect. + startServer, + // End the test + function (state) { + stopTest = true; + state.socket.end(); + jqUnit.expect(lineCount); + return linesWrote.length > 0 && gpii.tests.eventLog.promiseTimeout(500); + } + ]; + + fluid.promise.sequence(actions, {}).then(jqUnit.start); +}); diff --git a/gpii/node_modules/eventLog/test/all-tests.js b/gpii/node_modules/eventLog/test/all-tests.js new file mode 100644 index 000000000..8f276e0ce --- /dev/null +++ b/gpii/node_modules/eventLog/test/all-tests.js @@ -0,0 +1,22 @@ +/* + * Event log and metrics tests. + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +require("./EventLogTests.js"); +require("./metricsTests.js"); diff --git a/gpii/node_modules/eventLog/test/metricsTests.js b/gpii/node_modules/eventLog/test/metricsTests.js new file mode 100644 index 000000000..b118d94dd --- /dev/null +++ b/gpii/node_modules/eventLog/test/metricsTests.js @@ -0,0 +1,213 @@ +/* + * Metrics Tests + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"); + +var jqUnit = fluid.require("node-jqunit"); +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.tests.metrics"); + +require("../../lifecycleManager/"); + +require("../index.js"); + +var teardowns = []; + +jqUnit.module("gpii.tests.metrics", { + teardown: function () { + while (teardowns.length) { + teardowns.pop()(); + } + } +}); + +// Tests ensure preferenceChanged logs added and changed preferences. +gpii.tests.metrics.preferenceChangedTestData = fluid.freezeRecursive({ + "unchanged 1/1": { + previous: { + pref1: "value1" + }, + current: { + pref1: "value1" + }, + expect: [] + }, + "unchanged 2/2": { + previous: { + pref1: "value1", + pref2: "value2" + }, + current: { + pref1: "value1", + pref2: "value2" + }, + expect: [] + }, + "changed 1/1": { + previous: { + pref1: "value1" + }, + current: { + pref1: "changed value1" + }, + expect: { + name: "pref1", + newValue: "changed value1" + } + }, + "changed 2/2": { + previous: { + pref1: "value1", + pref2: "value2" + }, + current: { + pref1: "changed value1", + pref2: "changed value2" + }, + expect: [{ + name: "pref1", + newValue: "changed value1" + }, { + name: "pref2", + newValue: "changed value2" + }] + }, + "changed 2/3": { + previous: { + pref1: "value1", + pref2: "value2", + pref3: "value3" + }, + current: { + pref1: "changed value1", + pref2: "changed value2", + pref3: "value3" + }, + expect: [{ + name: "pref1", + newValue: "changed value1" + }, { + name: "pref2", + newValue: "changed value2" + }] + }, + "add 1+1": { + previous: { + pref1: "value1" + }, + current: { + pref1: "value1", + pref2: "new value2" + }, + expect: [{ + name: "pref2", + newValue: "new value2" + }] + }, + "add+change": { + previous: { + pref1: "value1" + }, + current: { + pref1: "changed value1", + pref2: "new value2" + }, + expect: [{ + name: "pref1", + newValue: "changed value1" + }, { + name: "pref2", + newValue: "new value2" + }] + }, + "remove 1-1": { + previous: { + pref1: "value1" + }, + current: { + }, + expect: [] + }, + "remove 2-1": { + previous: { + pref1: "value1", + pref2: "value2" + }, + current: { + pref2: "value2" + }, + expect: [] + }, + "remove+change+static+add": { + previous: { + pref1: "value1", + pref2: "value2", + pref3: "value3" + }, + current: { + pref2: "changed value2", + pref3: "value3", + pref4: "new value4" + }, + expect: [{ + name: "pref2", + newValue: "changed value2" + }, { + name: "pref4", + newValue: "new value4" + }] + } +}); + +fluid.defaults("gpii.tests.metricsWrapper", { + gradeNames: ["fluid.component", "gpii.metrics", "gpii.eventLog", "gpii.lifecycleManager"], + listeners: { + "onStartMetrics.application": null, + "onStartMetrics.input": null, + "onStopMetrics.application": null, + "onStopMetrics.input": null + } +}); + + +jqUnit.test("preferenceChanged", function () { + + var tests = gpii.tests.metrics.preferenceChangedTestData; + + var logValues; + + var metrics = gpii.tests.metricsWrapper({ + invokers: { + "logMetric": function (metric, data) { + logValues.push(data); + } + } + }); + + fluid.each(tests, function (test) { + logValues = []; + + gpii.metrics.preferenceChanged(metrics, test.current, test.previous); + + jqUnit.assertDeepEq("preferenceChanged should have logged the expected value.", + fluid.makeArray(test.expect), logValues); + }); + +}); diff --git a/gpii/node_modules/flatMatchMaker/configs/gpii.flatMatchMaker.config.base.json5 b/gpii/node_modules/flatMatchMaker/configs/gpii.flatMatchMaker.config.base.json5 new file mode 100644 index 000000000..753a12200 --- /dev/null +++ b/gpii/node_modules/flatMatchMaker/configs/gpii.flatMatchMaker.config.base.json5 @@ -0,0 +1,26 @@ +/** + * This configuration runs the flat MatchMaker in development mode + */ +{ + "type": "gpii.flatMatchMaker.config.base", + "options": { + "gradeNames": ["fluid.component"], + "distributeOptions": { + "flatMatchMaker.ontologyHandler": { + "record": "{ontologyHandler}", + "target": "{that gpii.flatMatchMaker}.options.components.ontologyHandler" + } + }, + "components": { + "server": { + "options": { + "components": { + "matchMaker": { + "type": "gpii.flatMatchMaker" + } + } + } + } + } + } +} diff --git a/gpii/node_modules/flatMatchMaker/index.js b/gpii/node_modules/flatMatchMaker/index.js new file mode 100644 index 000000000..adf92560a --- /dev/null +++ b/gpii/node_modules/flatMatchMaker/index.js @@ -0,0 +1,7 @@ +"use strict"; + +var fluid = require("infusion"); + +fluid.module.register("flatMatchMaker", __dirname, require); + +require("./src/FlatMatchMaker.js"); diff --git a/gpii/node_modules/flatMatchMaker/package.json b/gpii/node_modules/flatMatchMaker/package.json new file mode 100644 index 000000000..3d750950a --- /dev/null +++ b/gpii/node_modules/flatMatchMaker/package.json @@ -0,0 +1,13 @@ +{ + "name": "flatMatchMaker", + "description": "The Flat MatchMaker is a simple lightweight Matchmaker", + "version": "0.3.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } +} diff --git a/gpii/node_modules/flatMatchMaker/src/FlatMatchMaker.js b/gpii/node_modules/flatMatchMaker/src/FlatMatchMaker.js new file mode 100644 index 000000000..0645d5120 --- /dev/null +++ b/gpii/node_modules/flatMatchMaker/src/FlatMatchMaker.js @@ -0,0 +1,136 @@ +/* +* Flat MatchMaker +* +* Copyright 2014 Raising the Floor - International +* Copyright 2018 OCAD University +* +* Licensed under the New BSD license. You may not use this file except in +* compliance with this License. +* +* The research leading to these results has received funding from the European Union's +* Seventh Framework Programme (FP7/2007-2013) +* under grant agreement no. 289016. +* +* You may obtain a copy of the License at +* https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +/* eslint-env browser */ +/* eslint strict: ["error", "function"] */ + +(function () { + "use strict"; + + var fluid = fluid || require("infusion"), + gpii = fluid.registerNamespace("gpii"); + + require("kettle"); + + fluid.defaults("gpii.flatMatchMaker", { + gradeNames: ["fluid.component"], + components: { + ontologyHandler: { + "type": "gpii.ontologyHandler" + } + }, + invokers: { + match: { + funcName: "gpii.flatMatchMaker.match", + args: [ "{ontologyHandler}", "{arguments}.0", "{flatMatchMaker}.disposeStrategy"] + }, + disposeStrategy: { + funcName: "gpii.flatMatchMaker.disposeStrategy", + args: [ "{arguments}.0", "{arguments}.1", "{arguments}.2"] + // leaves, solrecs, solutionTypeMapping + } + } + }); + + /* + * Main function of the flat matchmaker. Ensures that the inferred common terms are taken + * into account and that the preferences are stored in a hierarchical format, needed by + * the flat MM, and then runs the framework's 'disposeSolutions' function with the + * flatMatchMaker.disposeStrategy strategy. The correct output format is ensured by the + * buildReturnPayload function of the MM framework. + */ + gpii.flatMatchMaker.match = function (ontologyHandler, payload, disposeStrategy) { + // augment payload with information about the solution types + var appTransformSpec = gpii.ontologyHandler.getTransformSpec(ontologyHandler.ontologyTransformSpecs, "flat", "apptology"); + gpii.matchMakerFramework.utils.addSolutionTypeInformation(payload, appTransformSpec); + + payload.hierarchicalPrefs = ontologyHandler.prefsToOntology(payload.preferences, "flat", "ISO24751"); + + var transformSpec = gpii.ontologyHandler.getTransformSpec(ontologyHandler.ontologyTransformSpecs, "flat", "ISO24751"); + var disposed = gpii.matchMakerFramework.utils.disposeSolutions(payload, disposeStrategy, transformSpec); + return gpii.matchMakerFramework.utils.buildReturnPayload(payload, disposed); + }; + + /* + * Very simple strategy that accepts any solution for which any preference el-path matches + * a one of the solutions capabilities (inferred from the capabilities transformations and + * capabilities block of that solution). + * + * We consider that any accepted solution will be activated because of the simple-minded + * policy adopted by the flat matchmaker + */ + gpii.flatMatchMaker.disposeStrategy = function (leaves, solrecs, solutionTypeMapping) { + fluid.each(solrecs, function (solrec) { + var accepted = fluid.find(leaves, function (leaf) { + return fluid.get(solrec.skeleton, leaf, fluid.model.escapedGetConfig); + }); + if (solrec.priority !== undefined) { + accepted = true; + } + fluid.extend(solrec, { + disposition: accepted ? "accept" : "reject", + active: accepted ? true : false // always true if accepted + }); + }); + + // Apply Apptology + fluid.each(solutionTypeMapping, function (solutions) { + var numOfSolutions = fluid.keys(solutions).length; + // If there's only one solution for a solution type, the filtering by apptology is not needed. + if (numOfSolutions > 1) { + gpii.flatMatchMaker.applyApptology(solrecs, solutions); + } + }); + + return solrecs; + }; + + // Apply "Apptology" to only select one solution from all that perform the same functionality. For example, only one + // screen reader will be selected when there are multiple matched screen readers. + gpii.flatMatchMaker.applyApptology = function (solrecs, solutions) { + var bestSolutionId, highestPriority; + + // Find the best solution: + // 1. the one with the highest priority value; + // 2. if all solutions have the same priority value, select the first one. + fluid.each(solutions, function (value, solutionId) { + var thisPriority = solrecs[solutionId].priority ? solrecs[solutionId].priority : 0; + + if (solrecs[solutionId].active) { + // Select the first active solution to be the best match in case all solutions have the same priority. + if (!bestSolutionId) { + bestSolutionId = solutionId; + highestPriority = thisPriority; + } + // find the solution with the highest priority + if (thisPriority > highestPriority) { + bestSolutionId = solutionId; + highestPriority = thisPriority; + } + } + }); + + // Reject all non-best solutions + fluid.each(solutions, function (value, solutionId) { + // Reject solutions from 2nd place onwards + if (solrecs[solutionId].active && solutionId !== bestSolutionId) { + fluid.set(solrecs, [solutionId, "disposition"], "reject"); + fluid.set(solrecs, [solutionId, "active"], false); + } + }); + }; +})(); diff --git a/gpii/node_modules/flatMatchMaker/test/FlatMatchMakerTests.js b/gpii/node_modules/flatMatchMaker/test/FlatMatchMakerTests.js new file mode 100644 index 000000000..5f6bb4fd4 --- /dev/null +++ b/gpii/node_modules/flatMatchMaker/test/FlatMatchMakerTests.js @@ -0,0 +1,412 @@ +/* +* Flat MatchMaker Tests +* +* Copyright 2014 Raising the Floor - International +* +* Licensed under the New BSD license. You may not use this file except in +* compliance with this License. +* +* The research leading to these results has received funding from the European Union's +* Seventh Framework Programme (FP7/2007-2013) +* under grant agreement no. 289016. +* +* You may obtain a copy of the License at +* https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +/* eslint strict: ["error", "function"] */ + +// TODO: This should be converted to a set of browser tests + +(function () { + "use strict"; + + var fluid = require("infusion"), + jqUnit = fluid.registerNamespace("jqUnit"), + kettle = require("kettle"), + gpii = fluid.registerNamespace("gpii"); + + kettle.loadTestingSupport(); + + require("flatMatchMaker"); + require("ontologyHandler"); // TODO: We don't require to depend on the entire ontologyHandler + require("matchMakerFramework"); + + fluid.defaults("gpii.flatMatchMaker.tests", { + gradeNames: ["fluid.test.testEnvironment"], + components: { + tester: { + type: "gpii.flatMatchMaker.tests.testCaseHolder" + } + } + }); + + var disposeStrategyFixtures = [ + { + description: "Basic test for disposeStrategy function", + leavesArg: [ + "display.screenEnhancement.fontSize", + "display.screenEnhancement.magnification", + "display.screenEnhancement.-provisional-magnification/enabled", + "display.screenReader.-provisional-screenReaderTTS/enabled" + ], + solRecsArg: { + "test.dummy.matched1": { + "solution": { + "name": "Test dummy - matched1", + "contexts": {}, + "settingsHandlers": { + "myconf": { + "type": "gpii.settingsHandlers.JSONSettingsHandler.set", + "options": { + "path": "/some/path/to/file.json" + }, + "capabilitiesTransformations": { + "setting1": "http://registry\\.gpii\\.net/common/fontSize", + "setting2.path1": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled", + "setting2.path2": "http://registry\\.gpii\\.net/common/magnification/enabled", + "setting2.path3.hello": "http://registry\\.gpii\\.net/common/magnification" + } + } + } + }, + "skeleton": { + "applications": { + "test.dummy.matched1": { + "id": {} + } + }, + "display": { + "screenEnhancement": { + "fontSize": {}, + "magnification": {}, + "-provisional-magnification/enabled": {} + }, + "screenReader": { + "-provisional-screenReaderTTS/enabled": {} + } + } + }, + "index": "test.dummy.matched1" + }, + "test.dummy.unmatched": { + "solution": { + "name": "Test dummy - unmatched", + "contexts": {}, + "settingsHandlers": { + "myconf": { + "type": "gpii.settingsHandlers.noSettings" + } + } + }, + "skeleton": { + "applications": { + "test.dummy.unmatched": { + "id": {} + } + } + }, + "index": "test.dummy.unmatched" + } + }, + solutionTypeMapping: {}, + expect: { + "test.dummy.matched1": { + disposition: "accept", + active: true + }, + "test.dummy.unmatched": { + disposition: "reject", + active: false + } + } + }, + { + description: "Test for disposeStrategy function with the solution type mapping", + leavesArg: [ + "display.screenEnhancement.fontSize", + "display.screenEnhancement.magnification", + "display.screenEnhancement.-provisional-magnification/enabled", + "display.screenReader.-provisional-screenReaderTTS/enabled" + ], + solRecsArg: { + "test.dummy.matched1": { + "solution": { + "name": "Test dummy - matched1", + "contexts": {}, + "settingsHandlers": { + "myconf": { + "type": "gpii.settingsHandlers.JSONSettingsHandler.set", + "options": { + "path": "/some/path/to/file.json" + }, + "capabilitiesTransformations": { + "setting1": "http://registry\\.gpii\\.net/common/fontSize", + "setting2.path1": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled", + "setting2.path2": "http://registry\\.gpii\\.net/common/magnification/enabled", + "setting2.path3.hello": "http://registry\\.gpii\\.net/common/magnification" + } + } + } + }, + "skeleton": { + "applications": { + "test.dummy.matched1": { + "id": {} + } + }, + "display": { + "screenEnhancement": { + "fontSize": {}, + "magnification": {}, + "-provisional-magnification/enabled": {} + }, + "screenReader": { + "-provisional-screenReaderTTS/enabled": {} + } + } + }, + "index": "test.dummy.matched1" + }, + "test.dummy.matched2": { + "solution": { + "name": "Test dummy - matched2", + "contexts": {}, + "settingsHandlers": { + "myconf": { + "type": "gpii.settingsHandlers.JSONSettingsHandler.set", + "options": { + "path": "/some/path/to/file.json" + }, + "capabilitiesTransformations": { + "setting1": "http://registry\\.gpii\\.net/common/fontSize", + "setting2.path1": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled", + "setting2.path2": "http://registry\\.gpii\\.net/common/magnification/enabled", + "setting2.path3.hello": "http://registry\\.gpii\\.net/common/magnification" + } + } + } + }, + "skeleton": { + "applications": { + "test.dummy.matched1": { + "id": {} + } + }, + "display": { + "screenEnhancement": { + "fontSize": {}, + "magnification": {}, + "-provisional-magnification/enabled": {} + }, + "screenReader": { + "-provisional-screenReaderTTS/enabled": {} + } + } + }, + "index": "test.dummy.matched1" + }, + "test.dummy.unmatched": { + "solution": { + "name": "Test dummy - unmatched", + "contexts": {}, + "settingsHandlers": { + "myconf": { + "type": "gpii.settingsHandlers.noSettings" + } + } + }, + "skeleton": { + "applications": { + "test.dummy.unmatched": { + "id": {} + } + } + }, + "index": "test.dummy.unmatched" + } + }, + solutionTypeMapping: { + "http://registry\\.gpii\\.net/common/fontSize": { + "test.dummy.matched1": true, + "test.dummy.matched2": true + }, + "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled": { + "test.dummy.matched1": true, + "test.dummy.matched2": true + }, + "http://registry\\.gpii\\.net/common/magnification/enabled": { + "test.dummy.matched1": true, + "test.dummy.matched2": true + }, + "http://registry\\.gpii\\.net/common/magnification": { + "test.dummy.matched1": true, + "test.dummy.matched2": true + } + }, + expect: { + "test.dummy.matched1": { + disposition: "accept", + active: true + }, + "test.dummy.matched2": { + disposition: "reject", + active: false + }, + "test.dummy.unmatched": { + disposition: "reject", + active: false + } + } + } + ]; + + gpii.flatMatchMaker.tests.disposeStrategy = function () { + fluid.each(disposeStrategyFixtures, function (fixture) { + var result = gpii.flatMatchMaker.disposeStrategy(fixture.leavesArg, fixture.solRecsArg, fixture.solutionTypeMapping); + var expected = fluid.extend(true, {}, fixture.solRecsArg, fixture.expect); + + jqUnit.assertDeepEq(fixture.description, expected, result); + }); + }; + + var matchFixtures = [ + { + description: "Full match call test", + input: require("./data/pre_mm_payload1.json"), + expect: { + "inferredConfiguration": { + "gpii-default": { + "applications": { + "mac.dummy": { + "active": true, + "settings": { + "http://registry.gpii.net/common/fontSize": 16, + "http://registry.gpii.net/common/screenReaderTTS/enabled": false, + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + } + } + } + }, { + description: "GPII-3369: the top level high contrast theme is dispatched to both windows and UIO+", + input: require("./data/pre_mm_payload2.json"), + expect: { + "inferredConfiguration": { + "gpii-default": { + "applications": { + "com.microsoft.windows.highContrast": { + "active": true, + "settings": { + "http://registry.gpii.net/common/highContrast/enabled": true, + "http://registry.gpii.net/common/highContrastTheme": "white-black" + } + }, + "net.gpii.uioPlus": { + "active": true, + "settings": { + "http://registry.gpii.net/common/highContrast/enabled": true, + "http://registry.gpii.net/common/highContrastTheme": "white-black" + } + } + } + } + } + } + }, { + description: "GPII-3388: Apptology: when there are multiple matched solutions for one solution type, only one is selected", + input: require("./data/pre_mm_payload3.json"), + expect: { + "inferredConfiguration": { + "gpii-default": { + "applications": { + "com.freedomscientific.jaws": { + "active": true, + "settings": { + "http://registry.gpii.net/common/keyEcho": false, + "http://registry.gpii.net/common/wordEcho": true, + "http://registry.gpii.net/common/announceCapitals": true, + "http://registry.gpii.net/common/readingUnit": "word", + "http://registry.gpii.net/common/punctuationVerbosity": "all", + "http://registry.gpii.net/common/screenReaderTTS/enabled": true, + "http://registry.gpii.net/common/speechRate": 400, + "http://registry.gpii.net/common/auditoryOutLanguage": "en-BS", + "http://registry.gpii.net/common/pitch": 0.1 + } + } + } + } + } + } + }, { + description: "GPII-3388: Apptology: the specified application is selected for an application specific preference", + input: require("./data/pre_mm_payload4.json"), + expect: { + "inferredConfiguration": { + "gpii-default": { + "applications": { + "org.nvda-project": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/org.nvda-project": { + "speech.synth": "espeak", + "speech.outputDevice": "Microsoft Sound Mapper", + "speech.symbolLevel": 300, + "speech.espeak.rate": 40, + "speech.espeak.voice": "en-us", + "speech.espeak.pitch": 60, + "speech.espeak.volume": 80, + "reviewCursor.followFocus": "False", + "reviewCursor.followCaret": "True", + "reviewCursor.followMouse": "True", + "keyboard.speakTypedWords": "True", + "speech.espeak.rateBoost": "True", + "keyboard.speakTypedCharacters": "False", + "presentation.reportHelpBalloons": "False", + "speech.espeak.sayCapForCapitals": "True", + "virtualBuffers.autoSayAllOnPageLoad": "False" + } + } + } + } + } + } + } + } + ]; + + gpii.flatMatchMaker.tests.match = function (ontologyHandler) { + fluid.each(matchFixtures, function (fixture) { + fixture.input = gpii.matchMakerFramework.utils.preProcess(fixture.input); + var result = gpii.flatMatchMaker.match(ontologyHandler, fixture.input, gpii.flatMatchMaker.disposeStrategy); + jqUnit.assertDeepEq(fixture.description, fixture.expect, result); + }); + }; + + fluid.defaults("gpii.flatMatchMaker.tests.testCaseHolder", { + gradeNames: ["fluid.test.testCaseHolder"], + components: { + "ontologyHandler": { + type: "gpii.ontologyHandler" + } + }, + modules: [{ + name: "flatMatchMakerTests", + tests: [{ + expect: 2, + name: "gpii.flatMatchMaker.disposeStrategy tests", + func: "gpii.flatMatchMaker.tests.disposeStrategy" + }, { + expect: 4, + name: "gpii.flatMatchMaker.match tests", + func: "gpii.flatMatchMaker.tests.match", + args: "{ontologyHandler}" + }] + }] + }); + + module.exports = kettle.test.bootstrap("gpii.flatMatchMaker.tests"); +})(); diff --git a/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload1.json b/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload1.json new file mode 100644 index 000000000..3d66f6369 --- /dev/null +++ b/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload1.json @@ -0,0 +1,79 @@ +{ + "gpiiKey": "mac", + "preferences": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 16, + "http://registry.gpii.net/common/screenReaderTTS/enabled": false, + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + }, + "deviceContext": { + "solutions": [ + { + "id": "mac.dummy" + }, + { + "id": "mac.dummy_empty" + } + ], + "OS": { + "id": "darwin", + "version": "12.5.0" + } + }, + "solutionsRegistryEntries": { + "mac.dummy": { + "name": "Mac dummy", + "contexts": { + "OS": [ + { + "id": "darwin" + } + ] + }, + "settingsHandlers": { + "myconf": { + "type": "gpii.settingsHandlers.JSONSettingsHandler.set", + "options": { + "path": "/path/to/some/file.json" + }, + "capabilitiesTransformations": { + "setting1": "http://registry\\.gpii\\.net/common/fontSize", + "setting2.path1": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled", + "setting2.path2": "http://registry\\.gpii\\.net/common/magnification/enabled", + "setting2.path3.hello": "http://registry\\.gpii\\.net/common/magnification" + } + } + }, + "configure": [ + "settings.myconf" + ], + "restore": [ + "settings.myconf" + ] + }, + "mac.dummy_empty": { + "name": "Mac dummy empty", + "contexts": { + "OS": [ + { + "id": "darwin" + } + ] + }, + "settingsHandlers": { + "otherconf": { + "type": "gpii.settingsHandlers.noSettings" + } + }, + "start": [], + "stop": [] + } + } +} diff --git a/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload2.json b/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload2.json new file mode 100644 index 000000000..e6553194d --- /dev/null +++ b/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload2.json @@ -0,0 +1,281 @@ +{ + "gpiiKey": "highContrast", + "preferences": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/highContrast/enabled": true, + "http://registry.gpii.net/common/highContrastTheme": "white-black" + } + } + } + }, + "deviceContext": { + "solutions": [ + { + "id": "com.microsoft.windows.highContrast" + }, { + "id": "net.gpii.uioPlus" + } + ], + "OS": { + "id": "win32", + "version": "6.1.7601" + } + }, + "solutionsRegistryEntries": { + "com.microsoft.windows.highContrast": { + "name": "Windows High Contrast", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure-spi": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETHIGHCONTRAST", + "setAction": "SPI_SETHIGHCONTRAST", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "HIGHCONTRAST" + }, + "verifySettings": true + }, + "supportedSettings": { + "HighContrastOn": { + "schema": { + "title": "High Contrast", + "description": "Whether to enable/disable High Contrast.", + "type": "boolean", + "default": false + } + } + }, + "capabilitiesTransformations": { + "HighContrastOn": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/highContrast/enabled", + "left": false, + "operator": "||", + "rightPath": "http://registry\\.gpii\\.net/common/highContrastTheme", + "right": false + } + }, + "operator": "&&", + "right": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": "", + "leftPath": "http://registry\\.gpii\\.net/common/highContrastTheme", + "operator": "!==", + "right": "regular-contrast" + } + }, + "outputPath": "value" + }, + "path": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": "pvParam.dwFlags.HCF_HIGHCONTRASTON" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/highContrast/enabled": "HighContrastOn.value" + } + }, + "configure-registry": { + "type": "gpii.windows.registrySettingsHandler", + "liveness": "live", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes", + "dataTypes": { + "LastHighContrastTheme": "REG_SZ" + } + }, + "supportedSettings": { + "LastHighContrastTheme": { + "schema": { + "title": "High Contrast theme", + "description": "High Contrast Theme.", + "type": "string", + "default": "%SystemRoot%\\resources\\Ease of Access Themes\\hcwhite.theme", + "enum": [ + "%SystemRoot%\\resources\\Ease of Access Themes\\hcwhite.theme", + "%SystemRoot%\\resources\\Ease of Access Themes\\hcblack.theme", + "%SystemRoot%\\resources\\Ease of Access Themes\\hc1.theme"] + } + } + }, + "capabilitiesTransformations": { + "LastHighContrastTheme": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/highContrastTheme", + "match": { + "black-white": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\hcwhite.theme", + "white-black": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\hcblack.theme", + "yellow-black": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\hc1.theme", + "black-yellow": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\yellowOnBlack.theme", + "lime-black": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\hc2.theme", + "black-brown": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\blackOnBrown.theme", + "gray-black": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\grayOnDark.theme", + "grey-black": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\grayOnDark.theme", + "gray-white": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\grayOnWhite.theme", + "grey-white": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\grayOnWhite.theme" + } + } + } + } + } + }, + "configure": [ + "settings.configure-registry", + { + "type": "gpii.windows.spiSettingsHandler.setHighContrastTheme", + "filename": "${{registry}HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\LastHighContrastTheme}" + }, + "settings.configure-spi" + ], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + + "net.gpii.uioPlus": { + "name": "UIO+", + "contexts": { + "OS": [{ + "id": "win32", + "version": ">=5.0" + }] + }, + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.webSockets", + "liveness": "live", + "options": { + "path": "net.gpii.uioPlus" + }, + "supportedSettings": { + "lineSpace": {}, + "fontSize": {}, + "characterSpace": {}, + "inputsLargerEnabled": {}, + "contrastTheme": {}, + "selfVoicingEnabled": {}, + "selectionTheme": {}, + "tableOfContentsEnabled": {}, + "simplifiedUiEnabled": {}, + "syllabificationEnabled": {} + }, + "capabilitiesTransformations": { + "lineSpace": "http://registry\\.gpii\\.net/common/lineSpace", + "fontSize": { + "transform": { + "type": "fluid.transforms.round", + "scale": 1, + "input": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/fontSize", + "right": 12, + "operator": "/" + } + } + } + }, + "characterSpace": { + "transform": { + "type": "fluid.transforms.round", + "scale": 1, + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "offset": 1, + "inputPath": "http://registry\\.gpii\\.net/common/characterSpace" + } + } + } + }, + "inputsLargerEnabled": "http://registry\\.gpii\\.net/common/inputsLarger/enabled", + "contrastTheme": { + "transform": { + "type": "fluid.transforms.condition", + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/highContrast/enabled", + "left": false, + "operator": "||", + "rightPath": "http://registry\\.gpii\\.net/common/highContrastTheme", + "right": false + } + }, + "operator": "&&", + "right": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": "", + "leftPath": "http://registry\\.gpii\\.net/common/highContrastTheme", + "operator": "!==", + "right": "regular-contrast" + } + } + } + }, + "true": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/highContrastTheme", + "defaultOutputValue": "default", + "match": { + "black-white": "bw", + "white-black": "wb", + "black-yellow": "by", + "yellow-black": "yb", + "gray-dark": "gd", + "gray-white": "gw", + "black-brown": "bbr" + } + } + }, + "false": "default" + } + }, + "selfVoicingEnabled": "http://registry\\.gpii\\.net/common/selfVoicing/enabled", + "selectionTheme": "http://registry\\.gpii\\.net/common/highlightColor", + "tableOfContentsEnabled": "http://registry\\.gpii\\.net/common/tableOfContents", + "simplifiedUiEnabled": "http://registry\\.gpii\\.net/common/simplifiedUi/enabled", + "syllabificationEnabled": "http://registry\\.gpii\\.net/common/syllabification/enabled" + }, + "inverseCapabilitiesTransformations": {} + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + } + } +} diff --git a/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload3.json b/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload3.json new file mode 100644 index 000000000..ec7b46efe --- /dev/null +++ b/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload3.json @@ -0,0 +1,3686 @@ +{ + "gpiiKey": "screenreader", + "preferences": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/speakTutorialMessages": false , + "http://registry.gpii.net/common/keyEcho": false , + "http://registry.gpii.net/common/wordEcho": true , + "http://registry.gpii.net/common/announceCapitals": true , + "http://registry.gpii.net/common/readingUnit": "word" , + "http://registry.gpii.net/common/punctuationVerbosity": "all" , + "http://registry.gpii.net/common/screenReaderTTS/enabled": true , + "http://registry.gpii.net/common/trackingTTS": ["mouse", "caret"], + "http://registry.gpii.net/common/speechRate": 400 , + "http://registry.gpii.net/common/auditoryOutLanguage": "en-BS", + "http://registry.gpii.net/common/pitch": 0.1 , + "http://registry.gpii.net/common/volumeTTS": 0.75 + } + } + } + }, + "deviceContext": { + "solutions": [ + { + "id": "com.freedomscientific.jaws" + }, { + "id": "org.nvda-project" + } + ], + "OS": { + "id": "win32", + "version": "6.1.7601" + } + }, + "solutionsRegistryEntries": { + "com.freedomscientific.jaws": { + "name": "JAWS", + "contexts": { + "OS": [ + { + "id": "win32" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled" + ], + "settingsHandlers": { + "configuration1": { + "type": "gpii.settingsHandlers.INISettingsHandler", + "liveness": "manualRestart", + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF" + }, + "supportedSettings": { + "Braille.AllDotsBrailleCursor": { + "title": "All Dots Braille Cursor", + "description": "Whether to display the braille cursor as all dots raised instead of showing it using only dots seven and eight.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + }, + "Braille.AttributeRotationDelay": { + "schema": { + "title": "Attribute Rotation Delay", + "description": "When in Attribute Mode and characters have multiple attributes, determines how long each attribute should be shown for. This value is in Milliseconds.", + "type": "integer", + "default": 1000 + } + }, + "Braille.AutoPanMode": { + "schema": { + "title": "Auto Pan Mode", + "description": "The \"Auto Pan\" algorithm to use.", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 255 + ], + "enumLabels": [ + "off", + "move display minimally to include whole word containing cursor", + "match user pan", + "move display to show whole word at cursor nearest the middle of the display area", + "maximize text after cursor", + "maximize text before cursor", + "autopan to default structured segment", + "Smart Autopan, autopan using the best algorithm for the current situation" + ], + "default": 255 + } + }, + "Braille.BrailleAutoRouteToCursor": { + "title": "Braille Auto Route To Cursor", + "description": "Whether to automatically route the Braille display to the active cursor whenever the active cursor moves or a key on the keyboard is pressed.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + }, + "Braille.BrailleCursorBlinkRate": { + "title": "Braille Cursor Blink Rate", + "description": "Speed, in milliseconds, at which the cursor on the braille display should blink.", + "type": "integer", + "default": 500 + }, + "Braille.BrailleKeyInterruptSpeech": { + "title": "Braille Key Interrupt Speech", + "description": "Whether to Interrupt speech if a keystroke in the Braille display is pressed.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + }, + "Braille.BrailleMessages": { + "schema": { + "title": "Enable Flash Messages", + "description": "Whether or not to show Flash Messages on a braille display.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Braille.BrailleMode": { + "schema": { + "title": "Braille Mode", + "description": "Use this option to control the format of the information sent to the braille display. When Line mode is selected, JAWS sends the line of text at the current cursor position to the braille display. When Structured mode is selected, JAWS sends information to the display that is relevant to the current cursor position. The information sent includes things such as control type, dialog name, or number of items in a list. When Speech Output mode is selected, JAWS sends the same text to the display that it sends to the synthesizer. The default setting for this option is Structured.", + "type": "number", + "default": 1, + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "line", + "structured", + "speech output", + "attribute indicators" + ] + } + }, + "Braille.BrailleMoveActiveCursor": { + "title": "Braille Move Active Cursor", + "description": "Whether to move the active cursor when the Braille cursor moves.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + }, + "Braille.BrailleSleepMode": { + "schema": { + "title": "Enable Braille Sleep Mode", + "description": "Whether to turn off the braille driver, typically used to disable braille for a specific application.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Braille.BrailleVerbosity": { + "schema": { + "title": "Braille Verbosity", + "description": "Sets braille verbosity for Flash messages.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "beginner", + "intermediate", + "advanced" + ] + } + }, + "Braille.ContractedBrailleInput": { + "title": "Contracted Braille Input", + "description": "Enable contracted braille to be entered on the braille display's keyboard.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + }, + "Braille.EightDotBraille": { + "schema": { + "title": "Braille Dots", + "description": "The number of braille dots (six or eight) to use.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "6 dot braille", + "8 dot braille" + ], + "default": 1 + } + }, + "Braille.FilterControlCharacters": { + "title": "Filter Control Characters", + "description": "Whether or not to filter certain control characters (currently newlines, linefeeds and tabs)", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + }, + "Braille.GeneralizeBullets": { + "title": "Generalize Bullets", + "description": "Whether to treat all bullets the same.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + }, + "Braille.Grade2SuppressCapitalSigns": { + "title": "Grade2 Suppress Capital Signs", + "description": "When Grade 2 is on, whether or not to show capital indicators", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + }, + "Braille.MessagePrefixes": { + "schema": { + "title": "Enable Flash Message Prefixes", + "description": "If a display has no Status Cells, whether or not to prepend the Message Prefix to the Flash Message", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Braille.MessageStatusText": { + "title": "Message Status Text", + "description": "The text to show in the status area of the display when a Flash message is being shown if the display has no status cells, the text will be prepended to the Flash message.", + "type": "string", + "default": "msg" + }, + "Braille.MessageTime": { + "schema": { + "title": "Flash Message Timeout", + "description": "When a Flash message is sent to the display via a script, how long in milliseconds should it be shown if not specified by the script.", + "type": "integer", + "default": 5000 + } + }, + "Braille.ReversePanningButtons": { + "schema": { + "title": "Reverse Panning Buttons", + "description": "Whether or not panning buttons are reversed.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Braille.StructuredModeReverseOrder": { + "schema": { + "title": "Reverse Order of Structured Data", + "description": "Whether to reverse the order of the structured data to show the focused control's info first, followed by its grouping information, and then the dialog box information. By default, items are read in the opposite order.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Braille.UseHowManyStatusCells": { + "title": "Use How Many Status Cells", + "description": "How many cells to use for status information if the display defines none of its own.", + "type": "integer", + "default": 4 + }, + "Braille.UseScreenModelForBrailleInRichEdits": { + "title": "Use Screen Model for Braille in Rich Edits", + "description": "Whether to use the screen model when providing field prompts and text in Braille with rich edits.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + }, + "Braille.WordWrap": { + "schema": { + "title": "Enable Word Wrap", + "description": "Whether or not to allow words to be split in order to maximize the use of the braille display. Ignored if panning is set to fixed increment", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "FSCasts.EnableNotifications": { + "schema": { + "title": "FSCast Notifications", + "description": "Whether to have JAWS display a dialog when new podcasts from Freedom Scientific are available.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "HTML.Abbreviations": { + "schema": { + "title": "Abbreviations", + "description": "Whether to expand abbreviations. If this is enabled, abbreviations with a title attribute will have their title read instead of the on screen text.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "HTML.AccessKeys": { + "schema": { + "title": "Speak Access Keys Within Web Page", + "description": "Whether to announce HTML element access keys when reading a web page.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "HTML.Acronyms": { + "schema": { + "title": "Acronyms", + "description": "If this is enabled, acronyms with a title attribute will have their title read instead of the on screen text.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "HTML.BlockQuoteIndication": { + "title": "Block Quote Indication", + "description": "Whether to indicate HTML block quotations.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + }, + "HTML.EmbeddedActiveXSupport": { + "title": "Embedded ActiveX Support", + "description": "Whether or not to support embedded ActiveX controls such as Macromedia Flash Movies. If enabled, only objects defined in JActiveX.ini are affected by this setting.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + }, + "HTML.ExpandAbbreviations": { + "schema": { + "title": "Expand Abbreviations", + "description": "Whether or not to expand abbreviations (using the abbr tag).", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "HTML.ExpandAcronyms": { + "schema": { + "title": "Expand Acronyms", + "description": "Whether or not to expand acronyms (using the acronym tag).", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "HTML.FilterConsecutiveDuplicateLinks": { + "schema": { + "title": "Filter Consecutive Duplicate Links", + "description": "Whether to skip (not announce) consecutive duplicate links.", + "type": "number", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "HTML.FormFieldPromptOptions": { + "schema": { + "title": "Form Field Prompt Options", + "description": "Which attributes to prefer when describing form fields.", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "enumLabels": [ + "favor label tag", + "favor title attribute", + "favor alt attribute", + "favor longest", + "use both label and title if different", + "use both label and alt if different" + ], + "default": 0 + } + }, + "HTML.FormsModeAutoOff": { + "schema": { + "title": "Forms Mode Auto Off", + "description": "Whether or not Forms Mode should be automatically turned off when the current page is updated.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Leave Forms Mode on", + "Turn Forms Mode off automatically" + ], + "default": 1 + } + }, + "HTML.GraphicalLinkLastResort": { + "title": "Graphical Link Last Resort", + "description": "What to announce when a graphical link contains no title or alt text for its enclosing image and the enclosing anchor has no title.", + "enum": [0,1], + "enumLabels": "Read the image's src, Read the image's enclosing anchor's href.", + "default": 0 + }, + "HTML.IdentifyLinkType": { + "title": "Identify Link Type", + "description": "Whether to distinguish the various types of links e.g. \"FTP link\", \"mailto link\" versus simply announcing \"link\" for all types of links.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + }, + "HTML.IdentifySamePageLinks": { + "title": "Identify Same Page Links", + "description": "Whether to identify links that point to other places on the current page by saying \"same page link\".", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + }, + "HTML.IgnoreInlineFrames": { + "schema": { + "title": "Ignore Inline Frames", + "description": "Whether to ignore inline frames, such as those used for advertising.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "HTML.IncludeGraphics": { + "schema": { + "title": "Include Graphics", + "description": "Whether/when to include graphics in web pages.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "never include graphics", + "include graphics that have labels, i.e. alt=attributes", + "include all graphics" + ], + "default": 1 + } + }, + "HTML.IndicateColSpan": { + "schema": { + "title": "Indicate ColSpan for Braille", + "description": "Whether to announce column spans when reading table data in web pages.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + + "HTML.IndicateElementAttributes": { + "title": "Indicate Element Attributes", + "description": "Whether to announce any HTML attributes defined in the \"HTML Attributes Behavior\" map of the current scheme.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + }, + "HTML.ListIndication": { + "title": "List Indication", + "description": "Whether to announce lists with the item count and nesting level.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + }, + "HTML.MaxLineLength": { + "title": "Max Line Length", + "description": "The maximum number of characters which can appear on a line of a paragraph.", + "type": "integer", + "default": 150 + }, + "HTML.PageRefreshFilter": { + "title": "Page Refresh Filter", + "description": "How often, in milliseconds, to allow all refreshes. Set to 0 to allow all refreshes without any delay.", + "type": "integer", + "default": 0 + }, + "HTML.SayAllOnDocumentLoad": { + "schema": { + "title": "Read Web Pages Automatically When Loaded", + "description": "Whether to automatically read web pages when they're first loaded.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "HTML.ScreenFollowsVCursor": { + "title": "Screen Follows Virtual Cursor", + "description": "Whether the screen should automatically scroll to display the line on which the Virtual Cursor is positioned.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + }, + "HTML.SkipPastRepeatedText": { + "title": "Skip Past Repeated Text", + "description": "Whether to position the Virtual Cursor on the first line which is different than the previous page whenever a new page is displayed.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + }, + "HTML.SmartNavigation": { + "schema": { + "title": "Smart Navigation", + "description": "Whether to enable \"smart navigation\" when reading web pages.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "HTML.TableDetection": { + "title": "Table Detection", + "description": "Whether to announce all tables, or only data tables.", + "enum": ["0","1"], + "enumLabels": "Indicate all tables, Only indicate data tables", + "default": "1" + }, + "HTML.TblMaxCellTextLength": { + "title": "Table Maximum Cell Text Length", + "description": "A cell is considered a valid data cell if it contains upto this number of characters", + "type": "integer", + "default": 250 + }, + "HTML.TblMinCellTextLength": { + "title": "Table Minimum Cell Text Length", + "description": "A cell is considered a valid data cell if it contains at least this number of characters", + "type": "integer", + "default": 1 + }, + "HTML.TblMinTextColumns": { + "title": "Table Minimum Text Columns", + "description": "This option specifies the minimum number of columns which must contain text in order for the table to be considered a data table.", + "type": "integer", + "default": 2 + }, + "HTML.TblMinTextRows": { + "title": "Table Minimum Text Rows", + "description": "This option specifies the minimum number of rows which must contain text in order for the table to be considered a data table.", + "type": "integer", + "default": 2 + }, + "HTML.TblMinValidDataRows": { + "title": "Table Minimum Valid Data Rows", + "description": "Next options used for configuring how data tables are detected This option specifies the minimum number of valid data rows that a table must contain in order for it to be considered a data table.", + "type": "integer", + "default": 2 + }, + "HTML.TblValidRowThreshold": { + "title": "Table Valid Row Threshold", + "description": "This option specifies the minimum number of cells that a row must contain in order for it to be considered a valid data row.", + "type": "integer", + "default": 2 + }, + "HTML.TextBlockLength": { + "schema": { + "title": "Text Block Length", + "description": "The number of consecutive characters that must appear in a web page for JAWS to treat this as a block of text.", + "type": "integer", + "default": 25 + } + }, + "HTML.UseLegacyIESupport": { + "title": "Use Legacy IE Support", + "description": "Whether or not to use legacy IE support instead of the default FS Dom Server support.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + }, + "HTML.WrapNavigation": { + "title": "Wrap Navigation", + "description": "Whether or not to allow navigation keystrokes such as tab and shift tab to wrap to the top or bottom of the document.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + }, + "Options.AllCapsIndicator": { + "schema": { + "title": "AllCapsIndicator", + "description": "The text to read before text that appears in all capital letters.", + "type": "string", + "default": "all cap" + } + }, + "Options.AllowMouseEchoWhenMuted": { + "schema": { + "title": "Allow Mouse Echo", + "description": "Whether to echo mouse movement when otherwise muted.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Options.AllowSpeechOnDemandWhenMuted": { + "schema": { + "title": "Allow Reading Commands", + "description": "Whether to allow speech on demand when otherwise muted.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.AllowTypingEchoWhenMuted": { + "schema": { + "title": "Allow Typing Echo", + "description": "Whether to echo typed keys when otherwise muted.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Options.CapIndicator": { + "schema": { + "title": "Cap Indicator", + "description": "The text to read before a capital letter.", + "type": "string", + "default": "cap" + } + }, + "Options.CaretBlinkRate": { + "schema": { + "title": "Caret Blink Rate", + "description": "How fast the caret blinks, in milliseconds.", + "type": "integer", + "default": 53, + "minimum": 1 + } + }, + "Options.CaretDetect": { + "schema": { + "title": "Caret Detect", + "description": "How many cursor blinks are used to locate the caret.", + "type": "integer", + "default": 1, + "minimum": 1 + } + }, + "Options.CaretDetectTimeOut": { + "schema": { + "title": "Caret Detect Timeout", + "description": "The number of milliseconds before JAWS for Windows will give up looking for the caret", + "type": "integer", + "default": 250, + "minimum": 0 + } + }, + "OSM.CaretTimeOut": { + "title": "Caret Time Out", + "description": "The time in milliseconds to wait before redrawing the caret.", + "type": "integer", + "default": 1000 + }, + "Options.Case": { + "schema": { + "title": "Case", + "description": "Whether to indicate the case of spoken text with a change of inflection.", + "type": "number", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.CustomPageSummary": { + "schema": { + "title": "Custom Page Summary", + "description": "What to do when a virtual document loads for which a custom page summary has been defined.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "do nothing (\"Say All\" will be started as normal)", + "The summary will be spoken and focus left on the page at the place defined by the page", + "the summary will be presented in the virtual viewer for the user to immediately read" + ] + } + }, + "Options.DetectKeyboardInputLanguage": { + "schema": { + "title": "Detect Keyboard Languages", + "description": "Whether to detect the language used by the keyboard.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.Dictionary": { + "schema": { + "title": "Dictionary", + "description": "Whether words, phrases, abbreviations, or symbols should be processed through a global or application specific dictionary to determine proper pronunciation. This is enabled by default.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.Filter": { + "schema": { + "title": "Filter", + "description": "Determines how repeated characters are handled.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "enumLabels": [ + "Say First 3 Repeated Characters", + "Say First 4 Repeated Characters", + "Say First 5 Repeated Characters", + "Say First 6 Repeated Characters", + "Say All Repeated Characters", + "Count Repeated Characters" + ] + } + }, + "Options.GeneralizeDialect": { + "schema": { + "title": "Generalize Dialect", + "description": "Whether to switch languages when encountering content with the same underlying base language.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.Indentation": { + "schema": { + "title": "Indentation", + "description": "Whether or not to announce indentation.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.IndicateAttributesInDialogsAndMenus": { + "schema": { + "title": "Indicate Attributes on Dialogs and Menus", + "description": "Whether to announce attributes in dialogs and menus if the scheme allows for it.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.IndicateCaps": { + "schema": { + "title": "Indicate Caps", + "description": "When to indicate the presence of capital letters.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "Off", + "indicate caps when spelling or when navigating by character", + "also indicate caps when reading by words", + "also indicate caps when reading by lines" + ] + } + }, + "Options.IndicateNewlinesAndParagraphs": { + "schema": { + "title": "Indicate Newlines and Paragraphs", + "description": "Whether/how to indicate the presence of new lines and paragraphs.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "enumLabels": [ + "off", + "indicate when typing and the editor or wordprocessor wraps to a new line", + "indicate when arrowing left/right or using left/right with modifiers", + "indicate when typing and also when arrowing. the method of indication is controlled by the Speech and Sounds scheme in use", + "indicate when reading text containing newline characters bitwise or the values together to indicate in multiple contexts eg a value of" + ] + } + }, + "Options.IndicateSelected": { + "schema": { + "title": "List Item", + "description": "Use this list to determine how JAWS describes list box items. Select \"Say None\" to silence reading of list box descriptions. When you select \"Say Selected\", JAWS only tells you when list box items are selected. When you select the \"Say Not Selected\" item, JAWS only tells you when list box items are not selected. This is the default setting. If you select \"Say Both\", JAWS tells you when list box items are selected, and when they are not selected.", + "default": 2, + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "None", + "Selected", + "Not Selected", + "Both" + ] + } + }, + "Options.InitialNumlockState": { + "schema": { + "title": "Initial State of NumLock Key", + "description": "The initial state of the NumLock key.", + "type": "number", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "off at startup", + "on at startup", + "leave unmodified" + ], + "default": 0 + } + }, + "Options.InsertKeyMode": { + "schema": { + "title": "Insert Key Mode", + "description": "The operating mode of the insert key.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "JAWS", + "Sticky" + ] + } + }, + "Options.JAWSInsertKey": { + "schema": { + "title": "JAWS Insert Key", + "description": "Whether/which insert keys can be used as a JAWS Insert.", + "type": "number", + "default": 3, + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "none", + "Numpad Insert", + "Extended Insert", + "both Numpad and Extended" + ] + } + }, + "Options.JAWSPunctuationEnabled": { + "schema": { + "title": "JAWS Punctuation Enabled", + "description": "Which punctuation marks to speak.", + "type": "number", + "default": 2, + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "None", + "Some", + "Most", + "All" + ] + } + }, + "Options.KeyRepeat": { + "schema": { + "title": "Key Repeat", + "description": "Whether or not to allow repeated keys. Repeated keys are allowed by default.", + "type": "number", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.LanguageDetection": { + "schema": { + "title": "Language Detection", + "description": "Whether to enable automatic Language detection.", + "type": "number", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.LeftShiftSkipBack": { + "schema": { + "title": "Left Shift Skip Back", + "description": "How the shift keys should behave in \"Say All\" mode.", + "type": "number", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "during SayAll, right shift will skip back and left shift will skip forward", + "during SayAll, left shift will skip back and right shift will skip forward" + ] + } + }, + "Options.LessSpeechMode": { + "schema": { + "title": "Less Speech Options", + "description": "How to behave when in \"less speech mode\".", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Speech On Demand", + "Mute Speech" + ] + } + }, + "Options.LinePauses": { + "schema": { + "title": "Line Pauses", + "description": "Whether to pause at end of a line.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.LowerOtherAppsVolumeWhileJAWSIsRunning": { + "schema": { + "title": "Lower Audio Volume of Programs while JAWS Speaks", + "description": "Whether to lower the volume of programs other than JAWS when speaking.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Options.MigrationWizardDisplayed": { + "schema": { + "title": "Migration Wizard Displayed", + "description": "Whether or not to display the migration wizard on startup.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Options.MixedCase": { + "schema": { + "title": "Mixed Case", + "description": "Whether to announce words with embedded capital letters (such as \"MixedCase\") as separate words.", + "type": "number", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.MouseEchoSpeaksControlTypeAndState": { + "schema": { + "title": "Speak Control Type and State of Item", + "description": "Whether to speak the type of control and state of item when mousing over a control.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.MouseEchoSpeaksHelpAndDescription": { + "schema": { + "title": "Speak Description of Item", + "description": "Whether to speak the description of an item on mouseover.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Options.MouseMovementStopsSpeech": { + "schema": { + "title": "Mouse Echo Interrupt", + "description": "Whether to stop speaking when the mouse is moved.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.MouseSpeechDelay": { + "schema": { + "title": "Mouse Echo Delay", + "description": "How long to delay announcing content under the mouse, in milliseconds. Defaults to 0 (no delay).", + "type": "integer", + "default": 0 + } + }, + "Options.MouseSpeechEchoUnit": { + "schema": { + "title": "Mouse Echo Unit", + "description": "The unit of text to announce when the mouse pauses over text in a control. Defaults to reading the whole line.", + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "character", + "word", + "line", + "paragraph" + ], + "default": 2 + } + }, + "Options.MouseSpeechEnabled": { + "schema": { + "title": "Enable Mouse Echo", + "description": "Whether or not to enable the mouse echo, i.e. reading content under the mouse pointer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Options.Numbers": { + "schema": { + "title": "Numbers", + "description": "How to announce numbers.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "off (handled by synth)", + "digits", + "pairs", + "full numbers" + ] + } + }, + "Options.OnScreenKeyboard": { + "schema": { + "title": "Allow On Screen Keyboards", + "description": "Whether to allow on screen keyboards.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Options.ProcessToolTipEvent": { + "schema": { + "title": "Process Tooltip Event", + "description": "Whether or not to process tooltip events.", + "type": "number", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "on", + "off" + ] + } + }, + "Options.ProgressBarUpdateInterval": { + "schema": { + "title": "Progress Bar Update Interval", + "description": "How often to announce progress bar updates, in milliseconds. Set to 0 to disable update announcements.", + "type": "integer", + "default": 5000, + "minimum": 0 + } + }, + "Options.QuickKeyNavigationMode": { + "schema": { + "title": "Quick Key Navigation Mode", + "description": "Navigation Quick Keys let you move through Web pages with easy to remember commands, such as T for table, F for form field, N for non link text, and V for visited link. These commands are only available when the Virtual Cursor is active. Use these radio buttons to set Navigation Quick Keys off, on, or on only during Say All reading. The default is on.", + "type": "number", + "default": 1, + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "off", + "on", + "only on during \"Say All\"" + ] + } + }, + "Options.ReadingInterrupt": { + "schema": { + "title": "Reading Interrupt", + "description": "Whether to interrupt speech when a key that is bound to a macro is pressed.", + "type": "number", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.Repetitions": { + "schema": { + "title": "Repetitions", + "description": "Whether or not to indicate repeated characters.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.SayAllIgnoreShiftKeys": { + "schema": { + "title": "\"Say All\" Ignores Shift Keys", + "description": "Allows sticky keys to be used, i.e.: ignores shift keys during SayAll. User can use left/right arrow to do same functions.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "on", + "off" + ] + } + }, + "Options.SayAllIndicateCaps": { + "schema": { + "title": "\"Say All\" Announces Capitals", + "description": "In \"Say All\" mode, announce an initial capital letter or capitalized word.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "on", + "off" + ] + } + }, + "Options.SayAllMode": { + "schema": { + "title": "\"Say All\" Mode", + "description": "Sets the amount of text which \"Say All\" sends to the synthesizer as a single unit.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "line", + "sentence", + "paragraph" + ] + } + }, + "Options.SayBlankLineCount": { + "schema": { + "title": "Say Blank Line Count", + "description": "In \"Say All\" mode, whether to announce the number of blank lines.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.SayCursorShapeChange": { + "schema": { + "title": "Say Cursor Shape Change", + "description": "Whether to announce a change in the shape of the cursor.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.SayDollars": { + "schema": { + "title": "Say Dollars", + "description": "Whether to announce currency symbols.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.SayNumericDates": { + "schema": { + "title": "Say Numeric Dates", + "description": "How to announce numeric dates. With no translation, dates are read as numbers. With some translation, dd-mm-yy values are read as text. With extended translation, both dd-mm-yy and dd-mm values are read as text.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "no translation", + "some translation", + "extended translation" + ] + } + }, + "Options.SaySelectedFirst": { + "schema": { + "title": "Say Selected First", + "description": "Whether to announce the selected text first.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.SayStateFirst": { + "schema": { + "title": "Say State First", + "description": "Whether to announce the window state before title/text.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.SayWindowTypeFirst": { + "schema": { + "title": "Say Window Type First", + "description": "Whether to announce the type of window before announcing the window title/text.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.Scheme": { + "schema": { + "title": "Scheme", + "description": "Whether or not to intercept CreateDIBSection Note that this value is only read once per JAWS session at startup thus changing this value while JAWS is running has no effect.", + "type": "string", + "default": "Classic" + } + }, + "Options.ScreenEcho": { + "schema": { + "title": "Screen Echo", + "description": "Use the radio buttons in this group to determine how much text is read when information on the screen changes. This includes highlighted text, all text as it appears, or no speech echo as text appears or changes on screen. The default setting is Echo Highlighted Text. Changing the Screen Echo setting may affect the reading of menus and other items. For this reason, it is recommended that you only make Screen Echo changes in application-specific settings files.", + "type": "number", + "default": 1, + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "off", + "highlight", + "all" + ] + } + }, + "Options.SimultaneousSynthAndWave": { + "schema": { + "title": "Simultaneous Synth and Wave", + "description": "Whether to enable smoother playing of WAV files when using the DEC Talk Access 32 software synthesizer. This option is only available in Default.jcf. Changes to this setting take effect the next time you start JAWS. Disabled by default.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.SingleDigitThreshold": { + "schema": { + "title": "Single Digit Threshold", + "description": "The number of digits a number must contain before it is read as single digits (like a phone number). Defaults to five.", + "type": "integer", + "default": 5 + } + }, + "Options.SkimReadingIndication": { + "schema": { + "title": "Skim Reading Indication", + "description": "Whether or not to indicate (via a beep) when skimming over text units for which the regular expression returns FALSE.", + "type": "number", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "beep every 20 units skimmed over" + ] + } + }, + "Options.SkipILM": { + "schema": { + "title": "Skip ILM", + "description": "Whether to show the licensing manager on startup when running in 40-minute evaluation mode.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "display the license manager on startup", + "do not display the license manager on startup" + ] + } + }, + "Options.SmartWordReading": { + "schema": { + "title": "Smart Word Reading", + "description": "Whether or not to enable \"smart reading\" when using the \"Say Word\" command.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.SpeakANSIChars": { + "schema": { + "title": "Speak ANSI Chars", + "description": "Whether or not to announce ANSI characters.", + "type": "number", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.SpeakCharacterValueAsMultibyteSequence": { + "schema": { + "title": "Speak Character Value As Sequence of Multibyte Values", + "description": "Whether to read unicode characters as a series of multi-byte values.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Options.SpeakCharacterValueInHex": { + "schema": { + "title": "Speak Character Value In Hex", + "description": "Whether to announce the hex value of a character when numpad 5 is pressed three times quickly.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Options.SpeakNumbersSepByDashesAsDigits": { + "schema": { + "title": "Numbers Containing Dashes as Digits", + "description": "Whether to read numbers that contain dashes (such as phone numbers) as a series of digits.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Options.SpeechHistory": { + "schema": { + "title": "Enable Speech History", + "description": "Whether to read through the 50-item speech history when \"insert+spacebar\" and then \"h\" are pressed.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.SpeechMode": { + "schema": { + "title": "Speech Mode", + "description": "The \"speech mode\" to use, either \"full\" or \"less\".", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "full speech", + "less speech" + ], + "default": 0 + } + }, + "Options.SpellAlphanumericData": { + "schema": { + "title": "Spell Alphanumeric Data", + "description": "How to read alphanumeric data.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "read as appears", + "spell", + "spell phonetically" + ], + "default": 0 + } + }, + "Options.SpellPhonetic": { + "schema": { + "title": "Spell Phonetic", + "description": "How to announce word spellings.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "spell normal", + "spell phonetic" + ] + } + }, + "Options.Synthesizer": { + "schema": { + "title": "Synthesizer", + "description": "The synthesizer to use/ Available synthesizers are listed in JfW.INI", + "type": "string", + "default": "Synth1" + } + }, + "Options.SynthesizerResetFrequency": { + "schema": { + "title": "Synthesizer Reset Frequency", + "description": "How often to reset communication with the synthesizer, in milliseconds.", + "type": "integer", + "default": 2000, + "minimum": 0 + } + }, + "Options.TetherJawsToPC": { + "schema": { + "title": "Tether Jaws to PC", + "description": "Whether or not to tether the JAWS cursor to the PC cursor. If this is set to 'on', whenever the Pc cursor moves, the Jaws cursor will follow it. If this is set to 'off', the JAWS cursor is indifferent to PC cursor movements (this is the default).", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.TextAnalyser": { + "schema": { + "title": "Text Analyzer", + "description": "The type of alert to use when announcing formatting errors.", + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "turn off", + "indicate with sound", + "speak count", + "describe inconsistencies" + ], + "default": 0 + } + }, + "Options.TouchKeyboardChildPanelNotification": { + "schema": { + "title": "Touch Alternative Character Panel Popup Notification", + "description": "How to announce the appearance of a panel of alternate keys in the touch keyboard.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "by both message and sound", + "by sound only" + ], + "default": 0 + } + }, + "Options.TouchKeyboardNotification": { + "schema": { + "title": "Touch Keyboard Notification", + "description": "Whether/how to notify when the touch keyboard appears/disappears.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "by message", + "by sound", + "by both message and sound" + ], + "default": 2 + } + }, + "Options.TouchTypingEcho": { + "schema": { + "title": "Touch Typing Echo", + "description": "Whether/how to echo text entered using the touch keyboard.", + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "none", + "characters", + "words", + "both characters and words" + ], + "default": 3 + } + }, + "Options.TouchTypingMode": { + "schema": { + "title": "Touch Typing Mode", + "description": "Which keyboard mode to use for the touch keyboard.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "standard", + "touch typing" + ], + "default": 1 + } + }, + "Options.TypingEcho": { + "schema": { + "title": "Typing echo", + "description": "How to announce typed words and/or characters.", + "type": "number", + "default": 1, + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "don't announce characters or words", + "announce characters", + "announce words", + "announce characters and words" + ] + } + }, + "Options.TypingInterrupt": { + "schema": { + "title": "Typing Interrupt", + "description": "Whether to stop speaking when the user types.", + "type": "number", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.UseExtendedKeys": { + "schema": { + "title": "Use Extended Keys", + "description": "Whether to distinguish between extended and number pad keys.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Treat Extended and Numpad keys the same", + "Differentiate between Extended and Numpad keys" + ] + } + }, + "Options.UseVirtualInfoInFormsMode": { + "schema": { + "title": "Use Virtual Info in Forms Mode", + "description": "Whether to use information from the virtual HTML area for prompts in Forms mode. Warning: this will not always result in accurate information, particularly on pages where there are errors in the HTML coding which result in forms mode and virtual mode being out of synchronization.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.Verbosity": { + "schema": { + "title": "Verbosity", + "description": "How verbose announcements should be.", + "type": "number", + "default": 0, + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "most speech", + "intermediate", + "least speech" + ] + } + }, + "Options.VIRTUALMSAAREFRESH": { + "schema": { + "title": "Virtual MSAA Refresh", + "description": "How often (in milliseconds) should an embedded ActiveX control cause the Virtual HTML area to be updated when receiving rapid ValueChange events from MSAA. Set to -1 to disable refreshes.", + "type": "integer", + "default": 0, + "minimum": -1 + } + }, + "Options.VTcolor": { + "schema": { + "title": "Visual Tracking Color", + "description": "The colour to use when highlighting items selected using the touch cursor.", + "type": "integer" + } + }, + "Options.VTspacing": { + "schema": { + "title": "Visual Tracking Spacing", + "description": "The amount of spacing between the highlight box and the highlighted item.", + "type": "integer" + } + }, + "Options.VTstyle": { + "schema": { + "title": "Visual Tracking Style", + "description": "The style of highlighting to use.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "box", + "block", + "underline" + ], + "default": 0 + } + }, + "Options.VTthickness": { + "schema": { + "title": "Visual Tracking Thickness", + "description": "How thick to make the \"box\" and \"underline\" highlighting styles.", + "type": "integer" + } + }, + "Options.VTtransparency": { + "schema": { + "title": "Visual Tracking Transparency", + "description": "How transparent to make the visual highlighting of selected items.", + "type": "integer" + } + }, + "Options.VTUseTouchCursor": { + "schema": { + "title": "Highlight Touch Cursor", + "description": "Whether or not to highlight selected items when using the touch cursor or touch gestures.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Options.VTUseVirtualPCCursor": { + "schema": { + "title": "Use Virtual PC Cursor", + "description": "Whether to use the virtual PC cursor when reading HTML documents.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.VTUseVirtualRibbon": { + "schema": { + "title": "Use Virtual Ribbon Menu", + "description": "Whether to turn on the virtual ribbon menu.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "OSM.GraphicsMode": { + "title": "Graphics Mode", + "description": "How to handle graphic CRC values.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Store graphic CRC values when the graphics are being drawn to the screen.", + "Compute them from the screen image at the time they are requested to be spoken." + ], + "default": 0 + }, + "OSM.IgnoreSetSysModal": { + "title": "Ignore Set Sys Modal", + "description": "Whether to disallow Windows calling the SetSysModalWindow function. In most cases this is desirable, because System Modal Windows cannot be spoken.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + }, + "OSM.IncludeGraphics": { + "title": "Include Graphics", + "description": "Which graphics to announce.", + "enum": [ 0, 1, 2], + "enumLabels": ["Ignore all graphics", "Announce labeled graphics only", "Announce all graphics"], + "default": 1 + }, + "OSM.IncludeLines": { + "title": "Include Lines", + "description": "Whether to include line segments in the Off Screen Model. This is required for Excel and other programs which draw a focus rectangle.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + }, + "OSM.MouseClickFudgeX": { + "title": "Mouse Click Fudge X", + "description": "number of pixels to the right of the current JAWS cursor at which a mouse click should be simulated", + "type": "integer", + "default": 1 + }, + "OSM.MouseClickFudgeY": { + "title": "Mouse Click Fudge Y", + "description": "number of pixels below the current JAWS cursor at which a mouse click should be simulated", + "type": "integer", + "default": -2 + }, + "OSM.PixelsPerBlankLine": { + "title": "Pixels Per Blank Line", + "description": "how many pixels (high) per blank line", + "type": "integer", + "default": 13 + }, + "OSM.PixelsPerSpace": { + "schema": { + "title": "Pixels Per Space", + "description": "When simulating spaces in blank portions of a line, the number of pixels used to represent each space.", + "type": "integer", + "default": 8 + } + }, + "OSM.PixelsPerTab": { + "schema": { + "title": "Pixels Per Tab", + "description": "The number of pixels per tab, used to estimate the number of tab spaces contained within the blank portion of a line.", + "type": "integer", + "default": 10 + } + }, + "OSM.SpeakSysModalMessageBox": { + "title": "Speak Sys Modal Message Box", + "description": "Whether to announce System Modal Message Boxes.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + }, + "OSM.TableDetection": { + "schema": { + "title": "Table Presentation Information", + "description": "Which tables to read as tables (data tables, or all tables).", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "data tables only", + "all tables" + ], + "default": 0 + } + }, + "OSM.TextOutDelay": { + "title": "Text Out Delay", + "description": "The number of milliseconds to wait before announcing newly written text. Setting this to a non-zero value may cause terminal programs to respond in a less choppy fashion.", + "type": "integer", + "default": 0 + }, + "OSM.TrackCommandBars": { + "title": "Track Command Bars", + "description": "Whether to track the menu bar in Office applications.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + }, + "OSM.TrackFocusRect": { + "schema": { + "title": "Track Focus Rectangle", + "description": "Set to one causes MAGic to track the focus rectangle in standard controls that utilize focus rectangles.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "OSM.TrimBrailleGraphicLabels": { + "title": "Trim Braille Graphic Labels", + "description": "Whether to trim the size of a braille graphic. Useful to preserve the virtical positioning of braille text.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + }, + "OSM.UnderlineProximity": { + "schema": { + "title": "Underline Proximity", + "description": "How far away in pixels can an underline be before being considered an actual underline.", + "type": "integer", + "default": 0 + } + }, + "Touch.FlickVelocityMin": { + "schema": { + "title": "Flick Velocity", + "description": "The minimum finger velocity to treat as a \"flick\".", + "type": "integer" + } + }, + "Touch.TapEventDurationMax": { + "schema": { + "title": "Explore Delay", + "description": "The delay between touching a touch screen and displaying the explore menu, in milliseconds.", + "type": "integer" + } + }, + "Touch.TapInterEventDurationMax": { + "schema": { + "title": "Double Tap Speed", + "description": "The threshold for treating two taps as a double-tap, expressed in milliseconds between taps.", + "type": "integer" + } + }, + "Touch.TapTranslationMax": { + "schema": { + "title": "Tap Radius", + "description": "The size of the finger contact spot on the touch screen.", + "type": "integer" + } + }, + "Voice Profiles.ActiveVoiceProfileName": { + "schema": { + "title": "Active Voice Profile Name", + "description": "The active voice profile.", + "type": "string", + "default": "GPII" + } + } + }, + "capabilitiesTransformations": { + "Voice Profiles\\.ActiveVoiceProfileName": { + "literalValue": "GPII" + }, + "Options\\.SayAllIndicateCaps": { + "transform": { + "type": "valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/announceCapitals", + "match": [ + { + "inputValue": true, + "outputValue": 1 + }, + { + "inputValue": false, + "outputValue": 0 + } + ], + "noMatch": { + "outputValue": 0 + } + } + }, + "Options\\.TypingEcho": { + "transform": { + "type": "fluid.transforms.condition", + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/keyEcho", + "rightPath": "http://registry\\.gpii\\.net/common/wordEcho", + "operator": "&&" + } + }, + "true": 3, + "false": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/common/keyEcho", + "true": 1, + "false": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/common/wordEcho", + "true": 2 + } + } + } + } + } + }, + "Options\\.SayAllMode": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/readingUnit", + "match": { + "line": 0, + "sentence": 1, + "paragraph": 2 + } + } + }, + "Braille\\.BrailleMode": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/brailleMode", + "match": { + "line": 0, + "structured": 1, + "speechHistory": 2 + } + } + }, + "Options\\.SayAllIgnoreShiftKeys": { + "transform": { + "type": "valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/stickyKeys", + "match": [ + { + "inputValue": true, + "outputValue": 1 + }, + { + "inputValue": false, + "outputValue": 0 + } + ], + "noMatch": { + "outputValue": 0 + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/announceCapitals": { + "transform": { + "type": "valueMapper", + "defaultInputPath": "Options\\.SayAllIndicateCaps", + "match": [ + { + "inputValue": 0, + "outputValue": false + }, + { + "inputValue": 1, + "outputValue": true + } + ], + "noMatch": { + "outputValue": false + } + } + }, + "http://registry\\.gpii\\.net/common/keyEcho": { + "transform": { + "type": "valueMapper", + "defaultInputPath": "Options\\.TypingEcho", + "match": [ + { + "inputValue": 1, + "outputValue": true + }, + { + "inputValue": 3, + "outputValue": true + } + ], + "noMatch": { + "outputValue": false + } + } + }, + "http://registry\\.gpii\\.net/common/wordEcho": { + "transform": { + "type": "valueMapper", + "defaultInputPath": "Options\\.TypingEcho", + "match": [ + { + "inputValue": 2, + "outputValue": true + }, + { + "inputValue": 3, + "outputValue": true + } + ], + "noMatch": { + "outputValue": false + } + } + }, + "http://registry\\.gpii\\.net/common/readingUnit": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "Options\\.SayAllMode", + "match": [ + { + "inputValue": 0, + "outputValue": "line" + }, + { + "inputValue": 1, + "outputValue": "sentence" + }, + { + "inputValue": 2, + "outputValue": "paragraph" + } + ] + } + }, + "http://registry\\.gpii\\.net/common/brailleMode": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "Braille\\.BrailleMode", + "match": [ + { + "inputValue": 0, + "outputValue": "line" + }, + { + "inputValue": 1, + "outputValue": "structured" + }, + { + "inputValue": 2, + "outputValue": "speechHistory" + } + ] + } + } + } + }, + "configuration2": { + "type": "gpii.settingsHandlers.INISettingsHandler", + "liveness": "manualRestart", + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\VoiceProfiles\\GPII.VPF" + }, + "supportedSettings": { + "Options.PrimarySynthesizer": {}, + "ENU-Global.Pitch": { + "schema": { + "title": "Pitch", + "description": "The pitch to use for all types of announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ENU-Global.Punctuation": { + "schema": { + "title": "Punctuation", + "description": "How much punctuation to announce for all types of announcements.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 2 + } + }, + "ENU-Global.Rate": { + "schema": { + "title": "Rate", + "description": "Speech rate for all announcements (scale varies by voice).", + "type": "integer" + } + }, + "ENU-Global.SynthLangString": { + "schema": { + "title": "Synth Language", + "description": "The language to use for all announcements.", + "type": "string" + } + }, + "ENU-Global.Volume": { + "schema": { + "title": "Global Volume", + "description": "The volume for all announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ENU-JAWSCursor.Pitch": { + "schema": { + "title": "JAWS Cursor Pitch", + "description": "The pitch to use for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ENU-JAWSCursor.Punctuation": { + "schema": { + "title": "JAWS Cursor Punctuation", + "description": "How much punctuation to announce for the JAWS cursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 2 + } + }, + "ENU-JAWSCursor.Rate": { + "schema": { + "title": "JAWS Cursor Rate", + "description": "Speech rate for announcements related to the JAWS cursor (scale varies by voice).", + "type": "integer" + } + }, + "ENU-JAWSCursor.SynthLangString": { + "schema": { + "title": "JAWS Cursor Language", + "description": "The language to use for announcements related to the JAWS cursor.", + "type": "string" + } + }, + "ENU-JAWSCursor.Volume": { + "schema": { + "title": "JAWS Cursor Volume", + "description": "The volume for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ENU-Keyboard.Pitch": { + "schema": { + "title": "Keyboard Pitch", + "description": "The pitch to use for announcements related to the keyboard.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ENU-Keyboard.Punctuation": { + "schema": { + "title": "Keyboard Punctuation", + "description": "How much punctuation to announce for announcements related to the keyboard.", + "enum": [0,1,2,3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 2 + } + }, + "ENU-Keyboard.Rate": { + "schema": { + "title": "Keyboard Rate", + "description": "Speech rate for announcements related to the keyboard (scale varies by voice).", + "type": "integer" + } + }, + "ENU-Keyboard.SynthLangString": { + "schema": { + "title": "Keyboard Language", + "description": "The language to use for announcements related to the keyboard.", + "type": "string" + } + }, + "ENU-Keyboard.Volume": { + "schema": { + "title": "Keyboard Volume", + "description": "The volume for announcements related to the keyboard.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ENU-MenuAndDialog.Pitch": { + "schema": { + "title": "Menu and Dialog Pitch", + "description": "The pitch to use for announcements related to menus and dialogs.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ENU-MenuAndDialog.Punctuation": { + "schema": { + "title": "Menu and Dialog Punctuation", + "description": "How much punctuation to announce for menus and dialogs.", + "enum": [0,1,2,3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 2 + } + }, + "ENU-MenuAndDialog.Rate": { + "schema": { + "title": "Menu and Dialog Rate", + "description": "Speech rate for announcements related to menus and dialogs (scale varies by voice).", + "type": "integer" + } + }, + "ENU-MenuAndDialog.SynthLangString": { + "schema": { + "title": "Menu and Dialog Language", + "description": "The language to use for announcements related to menus and dialogs.", + "type": "string" + } + }, + "ENU-MenuAndDialog.Volume": { + "schema": { + "title": "Menu and Dialog Volume", + "description": "The volume for announcements related to menus and dialogs.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ENU-Message.Pitch": { + "schema": { + "title": "Message Pitch", + "description": "The pitch to use for message announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ENU-Message.Punctuation": { + "schema": { + "title": "Message Punctuation", + "description": "How much punctuation to announce for messages.", + "enum": [0,1,2,3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 2 + } + }, + "ENU-Message.Rate": { + "schema": { + "title": "Message Rate", + "description": "Speech rate for message announcements.", + "type": "integer" + } + }, + "ENU-Message.SynthLangString": { + "schema": { + "title": "Message Language", + "description": "The language to use for message announcements.", + "type": "string" + } + }, + "ENU-Message.Volume": { + "schema": { + "title": "Message Volume", + "description": "The volume for message announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ENU-PCCursor.Pitch": { + "schema": { + "title": "PC Cursor Pitch", + "description": "The pitch to use for announcements related to the PC cursor.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ENU-PCCursor.Punctuation": { + "schema": { + "title": "PC Cursor Punctuation", + "description": "How much punctuation to announce for the PC cursor.", + "enum": [0,1,2,3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 2 + } + }, + "ENU-PCCursor.Rate": { + "schema": { + "title": "PC Cursor Rate", + "description": "Speech rate for announcements related to the PC cursor (scale varies by voice).", + "type": "integer" + } + }, + "ENU-PCCursor.SynthLangString": { + "schema": { + "title": "PC Cursor Language", + "description": "The language to use for announcements related to the PC cursor.", + "type": "string" + } + }, + "ENU-PCCursor.Volume": { + "schema": { + "title": "PC Cursor Volume", + "description": "The volume for announcements related to the PC cursor.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + } + }, + "capabilitiesTransformations": { + "Options\\.PrimarySynthesizer": "http://registry\\.gpii\\.net/common/speechSynthesizer", + "ENU-Global\\.Rate": "http://registry\\.gpii\\.net/common/speechRate", + "ENU-JAWSCursor\\.Rate": "http://registry\\.gpii\\.net/common/speechRate", + "ENU-Keyboard\\.Rate": "http://registry\\.gpii\\.net/common/speechRate", + "ENU-MenuAndDialog\\.Rate": "http://registry\\.gpii\\.net/common/speechRate", + "ENU-Message\\.Rate": "http://registry\\.gpii\\.net/common/speechRate", + "ENU-PCCursor\\.Rate": "http://registry\\.gpii\\.net/common/speechRate", + "ENU-Global\\.Pitch": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 99, + "offset": 1 + } + } + } + }, + "ENU-JAWSCursor\\.Pitch": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 99, + "offset": 1 + } + } + } + }, + "ENU-Keyboard\\.Pitch": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 99, + "offset": 1 + } + } + } + }, + "ENU-MenuAndDialog\\.Pitch": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 99, + "offset": 1 + } + } + } + }, + "ENU-Message\\.Pitch": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 99, + "offset": 1 + } + } + } + }, + "ENU-PCCursor\\.Pitch": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 99, + "offset": 1 + } + } + } + }, + "ENU-Global\\.Volume": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volume", + "factor": 100 + } + }, + "ENU-JAWSCursor\\.Volume": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volume", + "factor": 100 + } + }, + "ENU-Keyboard\\.Volume": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volume", + "factor": 100 + } + }, + "ENU-MenuAndDialog\\.Volume": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volume", + "factor": 100 + } + }, + "ENU-Message\\.Volume": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volume", + "factor": 100 + } + }, + "ENU-PCCursor\\.Volume": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volume", + "factor": 100 + } + }, + "ENU-Global\\.Punctuation": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "match": { + "none": 0, + "some": 1, + "most": 2, + "all": 3 + } + } + }, + "ENU-JAWSCursor\\.Punctuation": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "match": { + "none": 0, + "some": 1, + "most": 2, + "all": 3 + } + } + }, + "ENU-Keyboard\\.Punctuation": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "match": { + "none": 0, + "some": 1, + "most": 2, + "all": 3 + } + } + }, + "ENU-MenuAndDialog\\.Punctuation": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "match": { + "none": 0, + "some": 1, + "most": 2, + "all": 3 + } + } + }, + "ENU-Message\\.Punctuation": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "match": { + "none": 0, + "some": 1, + "most": 2, + "all": 3 + } + } + }, + "ENU-PCCursor\\.Punctuation": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "match": { + "none": 0, + "some": 1, + "most": 2, + "all": 3 + } + } + }, + "ENU-Global\\.SynthLangString": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "match": { + "en-US": "American English", + "en-GB": "British English", + "fr": "French", + "fr-CA": "French", + "it": "Italian", + "pt-BR": "Brazilian Portuguese", + "de": "German", + "es-US": "Latin American Spanish", + "es-ES": "Castilian Spanish", + "fi": "Finnish" + } + } + }, + "ENU-JAWSCursor\\.SynthLangString": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "match": { + "en-US": "American English", + "en-GB": "British English", + "fr": "French", + "fr-CA": "French", + "it": "Italian", + "pt-BR": "Brazilian Portuguese", + "de": "German", + "es-US": "Latin American Spanish", + "es-ES": "Castilian Spanish", + "fi": "Finnish" + } + } + }, + "ENU-Keyboard\\.SynthLangString": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "match": { + "en-US": "American English", + "en-GB": "British English", + "fr": "French", + "fr-CA": "French", + "it": "Italian", + "pt-BR": "Brazilian Portuguese", + "de": "German", + "es-US": "Latin American Spanish", + "es-ES": "Castilian Spanish", + "fi": "Finnish" + } + } + }, + "ENU-MenuAndDialog\\.SynthLangString": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "match": { + "en-US": "American English", + "en-GB": "British English", + "fr": "French", + "fr-CA": "French", + "it": "Italian", + "pt-BR": "Brazilian Portuguese", + "de": "German", + "es-US": "Latin American Spanish", + "es-ES": "Castilian Spanish", + "fi": "Finnish" + } + } + }, + "ENU-Message\\.SynthLangString": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "match": { + "en-US": "American English", + "en-GB": "British English", + "fr": "French", + "fr-CA": "French", + "it": "Italian", + "pt-BR": "Brazilian Portuguese", + "de": "German", + "es-US": "Latin American Spanish", + "es-ES": "Castilian Spanish", + "fi": "Finnish" + } + } + }, + "ENU-PCCursor\\.SynthLangString": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "match": { + "en-US": "American English", + "en-GB": "British English", + "fr": "French", + "fr-CA": "French", + "it": "Italian", + "pt-BR": "Brazilian Portuguese", + "de": "German", + "es-US": "Latin American Spanish", + "es-ES": "Castilian Spanish", + "fi": "Finnish" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/speechSynthesizer": "Options\\.PrimarySynthesizer", + "http://registry\\.gpii\\.net/common/speechRate": "ENU-Global\\.Rate", + "http://registry\\.gpii\\.net/common/pitch": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "ENU-Global\\.Pitch", + "factor": 0.010101, + "offset": -0.010101 + } + }, + "http://registry\\.gpii\\.net/common/volume": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "ENU-Global\\.Volume", + "factor": 0.01 + } + }, + "http://registry\\.gpii\\.net/common/punctuationVerbosity": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "ENU-Global\\.Punctuation", + "match": [ + { + "inputValue": 0, + "outputValue": "none" + }, + { + "inputValue": 1, + "outputValue": "some" + }, + { + "inputValue": 2, + "outputValue": "most" + }, + { + "inputValue": 3, + "outputValue": "all" + } + ] + } + }, + "http://registry\\.gpii\\.net/common/auditoryOutLanguage": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "ENU-Global\\.SynthLangString", + "match": { + "American English": "en-US", + "British English": "en-GB", + "French": "fr", + "Italian": "it", + "Brazilian Portuguese": "pt-BR", + "German": "de", + "Latin American Spanish": "es-US", + "Castilian Spanish": "es-ES", + "Finnish": "fi" + } + } + } + } + } + }, + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "verifySettings": true, + "retryOptions": { + "rewriteEvery": 0, + "numRetries": 20 + }, + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "jfw.exe" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\JAWS2018.exe\\}\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "jfw.exe" + }, + { + "type": "gpii.windows.closeProcessByName", + "filename": "fsSynth32.exe" + }, + { + "type": "gpii.windows.closeProcessByName", + "filename": "jhookldr.exe" + }, + { + "type": "gpii.windows.closeProcessByName", + "filename": "ScannerHandler.exe" + } + ] + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.registryKeyExists", + "hKey": "HKEY_LOCAL_MACHINE", + "path": "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\JAWS2018.exe", + "subPath": "", + "dataType": "REG_SZ" + } + ] + }, + + "org.nvda-project": { + "name": "NVDA Screen Reader", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configs": { + "type": "gpii.settingsHandlers.INISettingsHandler", + "liveness": "manualRestart", + "options": { + "filename": "${{environment}.APPDATA}\\nvda\\nvda.ini" + }, + "supportedSettings": { + "speech.espeak.pitch": {}, + "speech.espeak.volume": {}, + "presentation.reportHelpBalloons": {}, + "keyboard.speakTypedCharacters": {}, + "keyboard.speakTypedWords": {}, + "speech.espeak.sayCapForCapitals": {}, + "reviewCursor.followFocus": {}, + "reviewCursor.followCaret": {}, + "reviewCursor.followMouse": {}, + "speech.synth": {}, + "speech.outputDevice": {}, + "speech.espeak.rate": {}, + "speech.espeak.rateBoost": {}, + "speech.symbolLevel": {}, + "speech.espeak.voice": {} + }, + "capabilitiesTransformations": { + "speech\\.espeak\\.pitch": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 100 + } + }, + "speech\\.espeak\\.volume": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volumeTTS", + "factor": 100 + } + }, + "presentation\\.reportHelpBalloons": "http://registry\\.gpii\\.net/common/speakTutorialMessages", + "keyboard\\.speakTypedCharacters": "http://registry\\.gpii\\.net/common/keyEcho", + "keyboard\\.speakTypedWords": "http://registry\\.gpii\\.net/common/wordEcho", + "speech\\.espeak\\.sayCapForCapitals": "http://registry\\.gpii\\.net/common/announceCapitals", + "transform": [ + { + "type": "fluid.transforms.arrayToSetMembership", + "inputPath": "http://registry\\.gpii\\.net/common/trackingTTS", + "outputPath": "", + "presentValue": true, + "missingValue": false, + "options": { + "focus": "reviewCursor\\.followFocus", + "caret": "reviewCursor\\.followCaret", + "mouse": "reviewCursor\\.followMouse" + } + }, + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled", + "match": { + "false": { + "outputValue": { + "transform": [ + { + "type": "fluid.transforms.literalValue", + "input": "silence", + "outputPath": "speech\\.synth" + }, + { + "type": "fluid.transforms.literalValue", + "input": "Microsoft Sound Mapper", + "outputPath": "speech\\.outputDevice" + } + ] + } + } + } + } + ], + "speech\\.espeak\\.rate": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.binaryOp", + "right": 3.10, + "operator": "/", + "left": { + "transform": { + "type": "fluid.transforms.binaryOp", + "right": 80, + "operator": "-", + "left": { + "transform": { + "type": "fluid.transforms.condition", + "truePath": "http://registry\\.gpii\\.net/common/speechRate", + "false": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/speechRate", + "operator": "/", + "right": 3 + } + }, + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/speechRate", + "operator": "<=", + "right": 390 + } + } + } + } + } + } + } + } + } + }, + "speech\\.espeak\\.rateBoost": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/speechRate", + "operator": ">", + "right": 390 + } + }, + "speech\\.symbolLevel": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "match": { + "none": 0, + "some": 100, + "most": 200, + "all": 300 + } + } + }, + "speech\\.espeak\\.voice": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "match": { + "en": "en-us", + "en-GB": "en-gb", + "en-US": "en-us", + "en-scotland": "en-gb-scotland", + "af": "af", + "bg": "bg", + "bs": "bs", + "ca": "ca", + "cs": "cs", + "cy": "cy", + "da": "da", + "de": "de", + "el": "el", + "eo": "eo", + "es": "es", + "es-419": "es-la", + "et": "et", + "fi": "fi", + "fr": "fr", + "fr-BE": "fr-be", + "hi": "hi", + "hr": "hr", + "hu": "hu", + "hy": "hy", + "hy-arevmda": "hy-west", + "id": "id", + "is": "is", + "it": "it", + "ka": "ka", + "kn": "kn", + "ku": "ku", + "la": "la", + "lv": "lv", + "mk": "mk", + "ml": "ml", + "nl": "nl", + "no": "no", + "pl": "pl", + "pt-BR": "pt", + "pt-PT": "pt-pt", + "ro": "ro", + "ru": "ru", + "sk": "sk", + "sq": "sq", + "sr": "sr", + "sv": "sv", + "sw": "sw", + "ta": "ta", + "tr": "tr", + "vi": "vi", + "zh-cmn": "zh", + "cmn": "zh", + "zh-yue": "zh-yue" + }, + "noMatch": { + "outputValue": "en-us" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/speakTutorialMessages": "presentation\\.reportHelpBalloons", + "http://registry\\.gpii\\.net/common/keyEcho": "keyboard\\.speakTypedCharacters", + "http://registry\\.gpii\\.net/common/wordEcho": "keyboard\\.speakTypedWords", + "http://registry\\.gpii\\.net/common/announceCapitals": "speech\\.espeak\\.sayCapForCapitals", + "transform": [ + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "virtualBuffers\\.autoSayAllOnPageLoad.value", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/readingUnit", + "match": { + "true": "all", + "false": "sentence" + } + }, + { + "type": "fluid.transforms.setMembershipToArray", + "inputPath": "", + "outputPath": "http://registry\\.gpii\\.net/common/trackingTTS", + "presentValue": true, + "missingValue": false, + "options": { + "reviewCursor\\.followFocus": "focus", + "reviewCursor\\.followCaret": "caret", + "reviewCursor\\.followMouse": "mouse" + } + }, + { + "type": "fluid.transforms.valueMapper", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "defaultInputPath": "speech\\.symbolLevel", + "match": { + "0": "none", + "100": "some", + "200": "most", + "300": "all" + } + }, + { + "type": "fluid.transforms.condition", + "outputPath": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled", + "false": true, + "true": false, + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "speech\\.synth", + "operator": "===", + "right": "silence" + } + }, + "right": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "speech\\.outputDevice", + "operator": "===", + "right": "Microsoft Sound Mapper" + } + }, + "operator": "&&" + } + } + }, + { + "type": "fluid.transforms.condition", + "outputPath": "http://registry\\.gpii\\.net/common/speechRate", + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "right": true, + "operator": "===", + "leftPath": "speech\\.espeak\\.rateBoost" + } + }, + "false": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "speech\\.espeak\\.rate", + "factor": 3.1, + "offset": 80 + } + }, + "true": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "speech\\.espeak\\.rate", + "factor": 3.1, + "offset": 80 + } + }, + "operator": "*", + "right": 3 + } + } + }, + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "speech\\.espeak\\.voice", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "match": { + "en\\en": "en", + "en\\en-us": "en-US", + "en\\en-sc": "en-scotland", + "en\\en-wi": "en-BZ", + "af": "af", + "bg": "bg", + "bs": "bs", + "ca": "ca", + "cs": "cs", + "cy": "cy", + "da": "da", + "de": "de", + "el": "el", + "test\\grc": "grc", + "eo": "eo", + "es": "es", + "es-la": "es-419", + "et": "et", + "fi": "fi", + "fr": "fr", + "fr-be": "fr-BE", + "hi": "hi", + "hr": "hr", + "hu": "hu", + "hy": "hy", + "hy-west": "hy-arevmda", + "id": "id", + "is": "is", + "it": "it", + "test\\jbo": "jbo", + "ka": "ka", + "kn": "kn", + "ku": "ku", + "la": "la", + "lv": "lv", + "mk": "mk", + "ml": "ml", + "test\\nci": "nci", + "nl": "nl", + "no": "no", + "test\\pap": "pap", + "pl": "pl", + "pt": "pt-BR", + "pt-pt": "pt-PT", + "ro": "ro", + "ru": "ru", + "sk": "sk", + "sq": "sq", + "sr": "sr", + "sv": "sv", + "sw": "sw", + "ta": "ta", + "tr": "tr", + "vi": "vi", + "zh": "zh-cmn", + "zh-yue": "zh-yue" + } + } + ] + } + } + }, + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "verifySettings": true, + "retryOptions": { + "rewriteEvery": 0, + "numRetries": 20 + }, + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "nvda.exe" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe\\}\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "nvda_service.exe" + }, + { + "type": "gpii.windows.closeProcessByName", + "filename": "nvda.exe" + } + ] + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.registryKeyExists", + "hKey": "HKEY_LOCAL_MACHINE", + "path": "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe", + "subPath": "", + "dataType": "REG_SZ" + } + ] + } + } +} diff --git a/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload4.json b/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload4.json new file mode 100644 index 000000000..ffc139def --- /dev/null +++ b/gpii/node_modules/flatMatchMaker/test/data/pre_mm_payload4.json @@ -0,0 +1,773 @@ +{ + "gpiiKey": "screenreader", + "preferences": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/org.nvda-project": { + "speech.synth": "espeak", + "speech.outputDevice": "Microsoft Sound Mapper", + "speech.symbolLevel": 300, + "speech.espeak.rate": 40, + "speech.espeak.voice": "en-us", + "speech.espeak.pitch": 60, + "speech.espeak.volume": 80, + "reviewCursor.followFocus": "False", + "reviewCursor.followCaret": "True", + "reviewCursor.followMouse": "True", + "keyboard.speakTypedWords": "True", + "speech.espeak.rateBoost": "True", + "keyboard.speakTypedCharacters": "False", + "presentation.reportHelpBalloons": "False", + "speech.espeak.sayCapForCapitals": "True", + "virtualBuffers.autoSayAllOnPageLoad": "False" + } + } + } + } + }, + "deviceContext": { + "solutions": [ + { + "id": "com.microsoft.windows.narrator" + }, { + "id": "org.nvda-project" + }, { + "id": "webinsight.webAnywhere.windows" + } + ], + "OS": { + "id": "win32", + "version": "6.1.7601" + } + }, + "solutionsRegistryEntries": { + "com.microsoft.windows.narrator": { + "name": "Windows Built-in Narrator", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled" + ], + "settingsHandlers": { + "configure": { + "type": "gpii.windows.registrySettingsHandler", + "liveness": "liveRestart", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\Narrator", + "dataTypes": { + "SpeechSpeed": "REG_DWORD", + "SpeechPitch": "REG_DWORD", + "InteractionMouse": "REG_DWORD", + "CoupleNarratorCursorKeyboard": "REG_DWORD", + "FollowInsertion": "REG_DWORD", + "EchoChars": "REG_DWORD", + "EchoWords": "REG_DWORD" + } + }, + "supportedSettings": { + "SpeechSpeed": {}, + "SpeechPitch": {}, + "CoupleNarratorCursorKeyboard": {}, + "FollowInsertion": {}, + "InteractionMouse": {}, + "EchoChars": {}, + "EchoWords": {} + }, + "capabilitiesTransformations": { + "SpeechSpeed": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.quantize", + "inputPath": "http://registry\\.gpii\\.net/common/speechRate", + "ranges": [ + { + "upperBound": 50, + "output": 0 + }, + { + "upperBound": 150, + "output": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/speechRate", + "factor": 0.1, + "offset": -5 + } + } + }, + { + "upperBound": 450, + "output": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/speechRate", + "factor": 0.033333333333, + "offset": -5 + } + } + }, + { + "output": 20 + } + ] + } + } + } + }, + "SpeechPitch": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 20, + "offset": 0 + } + }, + "transform": [ + { + "type": "fluid.transforms.arrayToSetMembership", + "inputPath": "http://registry\\.gpii\\.net/common/trackingTTS", + "outputPath": "", + "presentValue": 1, + "missingValue": 0, + "options": { + "focus": "CoupleNarratorCursorKeyboard", + "caret": "FollowInsertion", + "mouse": "InteractionMouse" + } + } + ], + "EchoChars": { + "transform": { + "type": "fluid.transforms.condition", + "false": 0, + "true": 1, + "conditionPath": "http://registry\\.gpii\\.net/common/keyEcho" + } + }, + "EchoWords": { + "transform": { + "type": "fluid.transforms.condition", + "false": 0, + "true": 1, + "conditionPath": "http://registry\\.gpii\\.net/common/wordEcho" + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/speechRate": { + "transform": { + "type": "fluid.transforms.condition", + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "SpeechSpeed", + "right": 10, + "operator": "<" + } + }, + "true": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "SpeechSpeed", + "factor": 10, + "offset": 50 + } + }, + "false": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "SpeechSpeed", + "factor": 30, + "offset": -150 + } + } + } + }, + "http://registry\\.gpii\\.net/common/pitch": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "SpeechPitch", + "factor": 0.05, + "offset": 0 + } + }, + "transform": [ + { + "type": "fluid.transforms.setMembershipToArray", + "inputPath": "", + "outputPath": "http://registry\\.gpii\\.net/common/trackingTTS", + "presentValue": 1, + "missingValue": 0, + "options": { + "CoupleNarratorCursorKeyboard": "focus", + "FollowInsertion": "caret", + "InteractionMouse": "mouse" + } + } + ], + "http://registry\\.gpii\\.net/common/keyEcho": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "EchoChars", + "right": 0, + "operator": "!==" + } + }, + "http://registry\\.gpii\\.net/common/wordEcho": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "EchoWords", + "right": 0, + "operator": "!==" + } + } + } + }, + "configureNoRoam": { + "type": "gpii.windows.registrySettingsHandler", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\Narrator\\NoRoam", + "dataTypes": { + "SpeechVolume": "REG_DWORD", + "ShowKeyboardIntroduction": "REG_DWORD", + "ShowBrowserSelection": "REG_DWORD" + } + }, + "supportedSettings": { + "SpeechVolume": {}, + "ShowKeyboardIntroduction": {}, + "ShowBrowserSelection": {} + }, + "capabilitiesTransformations": { + "SpeechVolume": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volumeTTS", + "factor": 100, + "offset": 0 + } + }, + "ShowKeyboardIntroduction": { + "transform": { + "type": "fluid.transforms.condition", + "false": 0, + "true": 1, + "conditionPath": "http://registry\\.gpii\\.net/common/speakTutorialMessages" + } + }, + "ShowBrowserSelection": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": 0 + } + } + } + } + }, + "launchHandlers": { + "launcher": { + "type": "gpii.windows.enableRegisteredAT", + "options": { + "registryName": "Narrator", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "Narrator.exe" + } + ] + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + + "webinsight.webAnywhere.windows": { + "name": "Web Anywhere", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled" + ], + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "setTrue": { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\firefox.exe\\}\" \"http://webanywhere.cs.washington.edu/beta/?starting_url=http%3A%2F%2Fcloud4all.info\"" + }, + "setFalse": { + "type": "gpii.windows.closeProcessByName", + "filename": "firefox.exe" + }, + "getState": [ + { + "type": "gpii.processReporter.neverRunning" + } + ] + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + + "org.nvda-project": { + "name": "NVDA Screen Reader", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configs": { + "type": "gpii.settingsHandlers.INISettingsHandler", + "liveness": "manualRestart", + "options": { + "filename": "${{environment}.APPDATA}\\nvda\\nvda.ini" + }, + "supportedSettings": { + "speech.espeak.pitch": {}, + "speech.espeak.volume": {}, + "presentation.reportHelpBalloons": {}, + "keyboard.speakTypedCharacters": {}, + "keyboard.speakTypedWords": {}, + "speech.espeak.sayCapForCapitals": {}, + "reviewCursor.followFocus": {}, + "reviewCursor.followCaret": {}, + "reviewCursor.followMouse": {}, + "speech.synth": {}, + "speech.outputDevice": {}, + "speech.espeak.rate": {}, + "speech.espeak.rateBoost": {}, + "speech.symbolLevel": {}, + "speech.espeak.voice": {} + }, + "capabilitiesTransformations": { + "speech\\.espeak\\.pitch": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 100 + } + }, + "speech\\.espeak\\.volume": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volumeTTS", + "factor": 100 + } + }, + "presentation\\.reportHelpBalloons": "http://registry\\.gpii\\.net/common/speakTutorialMessages", + "keyboard\\.speakTypedCharacters": "http://registry\\.gpii\\.net/common/keyEcho", + "keyboard\\.speakTypedWords": "http://registry\\.gpii\\.net/common/wordEcho", + "speech\\.espeak\\.sayCapForCapitals": "http://registry\\.gpii\\.net/common/announceCapitals", + "transform": [ + { + "type": "fluid.transforms.arrayToSetMembership", + "inputPath": "http://registry\\.gpii\\.net/common/trackingTTS", + "outputPath": "", + "presentValue": true, + "missingValue": false, + "options": { + "focus": "reviewCursor\\.followFocus", + "caret": "reviewCursor\\.followCaret", + "mouse": "reviewCursor\\.followMouse" + } + }, + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled", + "match": { + "false": { + "outputValue": { + "transform": [ + { + "type": "fluid.transforms.literalValue", + "input": "silence", + "outputPath": "speech\\.synth" + }, + { + "type": "fluid.transforms.literalValue", + "input": "Microsoft Sound Mapper", + "outputPath": "speech\\.outputDevice" + } + ] + } + } + } + } + ], + "speech\\.espeak\\.rate": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.binaryOp", + "right": 3.10, + "operator": "/", + "left": { + "transform": { + "type": "fluid.transforms.binaryOp", + "right": 80, + "operator": "-", + "left": { + "transform": { + "type": "fluid.transforms.condition", + "truePath": "http://registry\\.gpii\\.net/common/speechRate", + "false": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/speechRate", + "operator": "/", + "right": 3 + } + }, + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/speechRate", + "operator": "<=", + "right": 390 + } + } + } + } + } + } + } + } + } + }, + "speech\\.espeak\\.rateBoost": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/speechRate", + "operator": ">", + "right": 390 + } + }, + "speech\\.symbolLevel": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "match": { + "none": 0, + "some": 100, + "most": 200, + "all": 300 + } + } + }, + "speech\\.espeak\\.voice": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "match": { + "en": "en\\en", + "en-GB": "en\\en", + "en-US": "en\\en-us", + "en-scotland": "en\\en-sc", + "en-BZ": "en\\en-wi", + "en-BS": "en\\en-wi", + "en-AG": "en\\en-wi", + "en-AI": "en\\en-wi", + "af": "af", + "bg": "bg", + "bs": "bs", + "ca": "ca", + "cs": "cs", + "cy": "cy", + "da": "da", + "de": "de", + "el": "el", + "grc": "test\\grc", + "eo": "eo", + "es": "es", + "es-419": "es-la", + "et": "et", + "fi": "fi", + "fr": "fr", + "fr-BE": "fr-be", + "hi": "hi", + "hr": "hr", + "hu": "hu", + "hy": "hy", + "hy-arevmda": "hy-west", + "id": "id", + "is": "is", + "it": "it", + "jbo": "test\\jbo", + "ka": "ka", + "kn": "kn", + "ku": "ku", + "la": "la", + "lv": "lv", + "mk": "mk", + "ml": "ml", + "nci": "test\\nci", + "nl": "nl", + "no": "no", + "pap": "test\\pap", + "pl": "pl", + "pt-BR": "pt", + "pt-PT": "pt-pt", + "ro": "ro", + "ru": "ru", + "sk": "sk", + "sq": "sq", + "sr": "sr", + "sv": "sv", + "sw": "sw", + "ta": "ta", + "tr": "tr", + "vi": "vi", + "zh-cmn": "zh", + "cmn": "zh", + "zh-yue": "zh-yue" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/speakTutorialMessages": "presentation\\.reportHelpBalloons", + "http://registry\\.gpii\\.net/common/keyEcho": "keyboard\\.speakTypedCharacters", + "http://registry\\.gpii\\.net/common/wordEcho": "keyboard\\.speakTypedWords", + "http://registry\\.gpii\\.net/common/announceCapitals": "speech\\.espeak\\.sayCapForCapitals", + "transform": [ + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "virtualBuffers\\.autoSayAllOnPageLoad.value", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/readingUnit", + "match": { + "true": "all", + "false": "sentence" + } + }, + { + "type": "fluid.transforms.setMembershipToArray", + "inputPath": "", + "outputPath": "http://registry\\.gpii\\.net/common/trackingTTS", + "presentValue": true, + "missingValue": false, + "options": { + "reviewCursor\\.followFocus": "focus", + "reviewCursor\\.followCaret": "caret", + "reviewCursor\\.followMouse": "mouse" + } + }, + { + "type": "fluid.transforms.valueMapper", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "defaultInputPath": "speech\\.symbolLevel", + "match": { + "0": "none", + "100": "some", + "200": "most", + "300": "all" + } + }, + { + "type": "fluid.transforms.condition", + "outputPath": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled", + "false": true, + "true": false, + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "speech\\.synth", + "operator": "===", + "right": "silence" + } + }, + "right": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "speech\\.outputDevice", + "operator": "===", + "right": "Microsoft Sound Mapper" + } + }, + "operator": "&&" + } + } + }, + { + "type": "fluid.transforms.condition", + "outputPath": "http://registry\\.gpii\\.net/common/speechRate", + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "right": true, + "operator": "===", + "leftPath": "speech\\.espeak\\.rateBoost" + } + }, + "false": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "speech\\.espeak\\.rate", + "factor": 3.1, + "offset": 80 + } + }, + "true": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "speech\\.espeak\\.rate", + "factor": 3.1, + "offset": 80 + } + }, + "operator": "*", + "right": 3 + } + } + }, + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "speech\\.espeak\\.voice", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "match": { + "en\\en": "en", + "en\\en-us": "en-US", + "en\\en-sc": "en-scotland", + "en\\en-wi": "en-BZ", + "af": "af", + "bg": "bg", + "bs": "bs", + "ca": "ca", + "cs": "cs", + "cy": "cy", + "da": "da", + "de": "de", + "el": "el", + "test\\grc": "grc", + "eo": "eo", + "es": "es", + "es-la": "es-419", + "et": "et", + "fi": "fi", + "fr": "fr", + "fr-be": "fr-BE", + "hi": "hi", + "hr": "hr", + "hu": "hu", + "hy": "hy", + "hy-west": "hy-arevmda", + "id": "id", + "is": "is", + "it": "it", + "test\\jbo": "jbo", + "ka": "ka", + "kn": "kn", + "ku": "ku", + "la": "la", + "lv": "lv", + "mk": "mk", + "ml": "ml", + "test\\nci": "nci", + "nl": "nl", + "no": "no", + "test\\pap": "pap", + "pl": "pl", + "pt": "pt-BR", + "pt-pt": "pt-PT", + "ro": "ro", + "ru": "ru", + "sk": "sk", + "sq": "sq", + "sr": "sr", + "sv": "sv", + "sw": "sw", + "ta": "ta", + "tr": "tr", + "vi": "vi", + "zh": "zh-cmn", + "zh-yue": "zh-yue" + } + } + ] + } + } + }, + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "verifySettings": true, + "retryOptions": { + "rewriteEvery": 0, + "numRetries": 20 + }, + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "nvda.exe" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe\\}\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "nvda_service.exe" + }, + { + "type": "gpii.windows.closeProcessByName", + "filename": "nvda.exe" + } + ] + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.registryKeyExists", + "hKey": "HKEY_LOCAL_MACHINE", + "path": "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe", + "subPath": "", + "dataType": "REG_SZ" + } + ] + } + } +} diff --git a/gpii/node_modules/flowManager/README.md b/gpii/node_modules/flowManager/README.md index 9d6a91a34..467546303 100644 --- a/gpii/node_modules/flowManager/README.md +++ b/gpii/node_modules/flowManager/README.md @@ -1,7 +1,7 @@ -Flow Manager -=== +# Flow Manager -The flow manager is responsible for the overall orchestration of the various subcomponents which comprise the GPII personalization architecture. It: +The flow manager is responsible for the overall orchestration of the various subcomponents which comprise the GPII +personalization architecture. It: - is able to be notified by the user listener. - queries the preferences server for user preferences. @@ -9,59 +9,73 @@ The flow manager is responsible for the overall orchestration of the various sub - communicates with the matchmaker. - communicates with the launch manager. -### Dependencies +## Dependencies - [infusion](https://github.com/fluid-project/infusion) framework. -### Installation +## Installation -Run the following command in your newly checked out universal repository. This -will pull all dependencies that are required by universal. +Run the following command in your newly checked out universal repository. This will pull all dependencies that are +required by universal. npm install -### Flow Manager API +## Flow Manager API The app currently supports the following urls: // LOCAL DEPLOYMENT - {urlToFlowManager}/user/{userName}/login // GET - {urlToFlowManager}/user/{uesrName}/logout // GET + {urlToFlowManager}/user/{gpiiKey}/login // GET + {urlToFlowManager}/user/{gpiiKey}/logout // GET // CLOUD BASED DEPLOYMENT - {urlToFlowManager}/{userName}/settings/:device // GET + {urlToFlowManager}/health // GET + {urlToFlowManager}/ready // GET + {urlToFlowManager}/revision // GET + {urlToFlowManager}/access_token // POST + {urlToFlowManager}/{gpiiKey}/settings/:device // GET // where :device is an url encoded {device: "..."} block. + {urlToFlowManager}/{gpiiKey}/settings/ // PUT + +Refer to [GPII OAuth2 Guide](https://wiki.gpii.net/w/GPII_OAuth_2_Guide) for how to use endpoints provided by the cloud +based deployment. For example: {urlToFlowManager}/user/andrei/login // GET - {urlToFlowManager}/andrei/settings/%7BOS%3A%20%7B%7D%7D // GET with {OS: {}} - {urlToFlowManager}/user/nisha/logout // GET + {urlToFlowManager}/user/andrei/logout // GET -### Cloud based flow manager +## Cloud based flow manager -The cloud based configuration of the flow manager enables web apps to communicate to the GPII architecture components and -to retrieve the relevant settings filtered/mapped based on the user profile, platform and application specification. This -configuration of the flow manager is intended for the apps on platforms without a locally running core framework (like web apps). +The cloud based configuration of the flow manager enables GPII installations on local computers to communicate to the +GPII architecture components and to retrieve the relevant lifecycle instructions filtered/mapped based on the user +preferences, platform and application specification. This configuration of the flow manager is intended for the GPII +apps on platforms without a locally running core framework. -The expected payload contains the settings for the particular solution keyed by the solution id. Those settings are -produced by the match maker that matches the supplied device/platform information along with the solutions installed with the -current preferences set. These settings are transformed to be consumed by the actual application. For example: +The expected payload contains the lifecycle instructions for the particular solution keyed by the solution id. Those +instructions are produced by the MatchMaker that matches the supplied device/platform information along with the +solutions installed with the current preference set. These settings are transformed to be consumed by the actual +application. For example: { - "org.gnome.desktop.a11y.magnifier": { - "mag-factor":2, - "mouse-tracking":"mouse" + "http://registry.gpii.net/applications/com.texthelp.readWriteGold": { + "ApplicationSettings.AppBar.Width.$t": 788, + "ApplicationSettings.AppBar.ShowText.$t": true, + "ApplicationSettings.AppBar.optToolbarShowText.$t": true, + "ApplicationSettings.AppBar.LargeIcons.$t": true, + "ApplicationSettings.AppBar.optToolbarLargeIcons.$t": true, + "ApplicationSettings.Speech.optSAPI5Speed.$t": 50, + "ApplicationSettings.Speech.optAutoUseScreenReading.$t": false }, ... } -### Run +## Run To run flow manager app simply type: [NODE_ENV={environment}] node node_modules/kettle/lib/init.js path/to/flowManager/configs/folder - [NODE_ENV={environment}] node node_modules/kettle/lib/init.js path/to/gpii/configs/folder // That folder contains configs for other configurations that inlcude Flow Manager. + [NODE_ENV={environment}] node node_modules/kettle/lib/init.js path/to/gpii/configs/folder // That folder contains configs for other configurations that include Flow Manager. - Default environment is development. - Path to configs folder can be absolute or relative to the current user directory. @@ -69,5 +83,5 @@ To run flow manager app simply type: For example: node node_modules/kettle/lib/init.js gpii/node_modules/flowManager/configs/ - NODE_ENV=production node node_modules/kettle/lib/init.js /Users/{userName}/universal/gpii/node_modules/flowManager/configs/ - NODE_ENV=fm.ps.sr.dr.mm.development node node_modules/kettle/lib/init.js gpii/configs/ + NODE_ENV=production node node_modules/kettle/lib/init.js /Users/{gpiiKey}/universal/gpii/node_modules/flowManager/configs/ + NODE_ENV=gpii.config.cloudBased.flowManager.production node node_modules/kettle/lib/init.js gpii/configs/ diff --git a/gpii/node_modules/flowManager/configs/base.json b/gpii/node_modules/flowManager/configs/base.json deleted file mode 100644 index 30bcc0494..000000000 --- a/gpii/node_modules/flowManager/configs/base.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "modules": [ - "flowManager" - ] -} diff --git a/gpii/node_modules/flowManager/configs/cloudBased.json b/gpii/node_modules/flowManager/configs/cloudBased.json deleted file mode 100644 index 165f62ba3..000000000 --- a/gpii/node_modules/flowManager/configs/cloudBased.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "typeName": "flowManager.cloudBased", - "options": { - "components": { - "server": { - "options": { - "logging": true, - "components": { - "flowManager": { - "type": "gpii.flowManager.cloudBased", - "options": { - "typeName": "flowManager.cloudBased", - "urls": { - "preferences": "http://preferences.gpii.net/user/%token", - "lifecycleManagerServer": "", - "deviceReporter": "", - "matchMaker": "http://localhost:8080/match" - } - } - } - } - } - } - } - }, - "includes": [ - "./production.json" - ] -} diff --git a/gpii/node_modules/flowManager/configs/development.json b/gpii/node_modules/flowManager/configs/development.json deleted file mode 100644 index 3e61fe5d9..000000000 --- a/gpii/node_modules/flowManager/configs/development.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "typeName": "flowManager.development", - "options": { - "gradeNames": ["autoInit", "fluid.littleComponent"], - "components": { - "server": { - "type": "kettle.server", - "options": { - "logging": true, - "port": 8081, - "components": { - "flowManager": { - "type": "gpii.flowManager", - "options": { - "gradeNames": ["gpii.flowManager.dev"], - "typeName": "flowManager.development", - "urls": { - "preferences": "http://localhost:%port/user/%token", - "lifecycleManagerServer": "http://localhost:%port/%operation/%token", - "deviceReporter": "http://localhost:%port/device", - "matchMaker": "http://localhost:%port/match" - } - } - } - } - } - } - } - }, - "includes": [ - "./base.json" - ] -} \ No newline at end of file diff --git a/gpii/node_modules/flowManager/configs/gpii.flowManager.config.base.json5 b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.base.json5 new file mode 100644 index 000000000..30f65ca07 --- /dev/null +++ b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.base.json5 @@ -0,0 +1,40 @@ +// This config contains common flow manager configurations that are shared by both Local Flow Manager and +// Cloud Based Flow Manager. +{ + "type": "gpii.flowManager.config.base", + "options": { + "mainServerPort": 8081, + "distributeOptions": { + "flowManager.solutions": { + "record": { + "type": "gpii.flowManager.solutionsRegistry.dataSource", + "options": { + "gradeNames": "gpii.flowManager.solutionsRegistry.dataSource.moduleTerms", + "path": "%gpii-universal/testData/solutions/" + } + }, + "target": "{that flowManager solutionsRegistryDataSource}" + } + }, + "components": { + "server": { + "type": "kettle.server", + "options": { + "gradeNames": ["kettle.server.ws"], + "port": "{kettle.config}.options.mainServerPort", + "components": { + "flowManager": { + "type": "gpii.flowManager" + }, + "ontologyHandler": { + "type": "gpii.ontologyHandler" + } + } + } + } + } + }, + "require": [ + "flowManager" + ] +} diff --git a/gpii/node_modules/flowManager/configs/gpii.flowManager.config.cloud.base.json5 b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.cloud.base.json5 new file mode 100644 index 000000000..41c4663ec --- /dev/null +++ b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.cloud.base.json5 @@ -0,0 +1,41 @@ +{ + "type": "gpii.flowManager.config.cloud.base", + "options": { + "dbServerPort": 25984, + "dataSourceConfig": { + "baseUrl": "http://localhost", + "port": "{kettle.config}.options.dbServerPort", + "dbName": "gpii" + }, + "distributeOptions": { + "flowManager.base.dataStore": { + "source": "{that}.options.dataSourceConfig", + "target": "{that flowManager oauth2DataStore}.options.dataSourceConfig" + }, + "flowManager.cloudGrades": { + "record": ["gpii.flowManager.cloudBased"], + "target": "{that flowManager}.options.gradeNames" + }, + "flowManager.gpiiRevisionPath": { + "record": "%gpii-universal/gpii-revision.json", + "target": "{that flowManager}.options.gpiiRevisionPath" + }, + "flowManager.envPort": { + "record": "@expand:kettle.resolvers.env(GPII_FLOWMANAGER_LISTEN_PORT)", + "target": "{that kettle.server}.options.port" + }, + "flowManager.prefsServerDataSource.default": { + "record": "http://localhost:8081", + "target": "{that flowManager prefsServerDataSource}.options.prefsServerURL" + }, + "flowManager.prefsServerDataSource.envUrl": { + "record": "@expand:kettle.resolvers.env(GPII_FLOWMANAGER_TO_PREFERENCESSERVER_URL)", + "target": "{that flowManager prefsServerDataSource}.options.prefsServerURL", + "priority": "after:flowManager.prefsServerDataSource.default" + } + } + }, + "mergeConfigs": [ + "./gpii.flowManager.config.base.json5" + ] +} diff --git a/gpii/node_modules/flowManager/configs/gpii.flowManager.config.local.base.json5 b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.local.base.json5 new file mode 100644 index 000000000..b7814ffb0 --- /dev/null +++ b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.local.base.json5 @@ -0,0 +1,14 @@ +{ + "type": "gpii.flowManager.config.local.base", + "options": { + "distributeOptions": { + "flowManager.local": { + "record": ["gpii.flowManager.local"], + "target": "{that flowManager}.options.gradeNames" + } + } + }, + "mergeConfigs": [ + "./gpii.flowManager.config.base.json5" + ] +} diff --git a/gpii/node_modules/flowManager/configs/gpii.flowManager.config.remoteDefaultSettings.base.json5 b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.remoteDefaultSettings.base.json5 new file mode 100644 index 000000000..2fb91267a --- /dev/null +++ b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.remoteDefaultSettings.base.json5 @@ -0,0 +1,13 @@ +// This production config that fetches default settings for reset from a remote url +// when GPII runs in production config +{ + "type": "gpii.flowManager.config.remoteDefaultSettings.base", + "options": { + "distributeOptions": { + "flowManager.remoteDefaultSettings": { + "record": "https://raw.githubusercontent.com/GPII/universal/master/testData/defaultSettings/defaultSettings.win32.json5", + "target": "{that gpii.flowManager.local defaultSettingsLoader}.options.defaultSettingsUrl" + } + } + } +} diff --git a/gpii/node_modules/flowManager/configs/gpii.flowManager.config.resetAtStart.base.json5 b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.resetAtStart.base.json5 new file mode 100644 index 000000000..3ea68bcb0 --- /dev/null +++ b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.resetAtStart.base.json5 @@ -0,0 +1,12 @@ +// This config turns on the local flowManager flag "resetAtStart" to reset the system to default settings on system startup. +{ + "type": "gpii.flowManager.config.resetAtStart.base", + "options": { + "distributeOptions": { + "flowManager.resetAtStart": { + "record": true, + "target": "{that gpii.flowManager.local}.options.resetAtStart" + } + } + } +} diff --git a/gpii/node_modules/flowManager/configs/gpii.flowManager.config.suppressHttpEndpoints.base.json5 b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.suppressHttpEndpoints.base.json5 new file mode 100644 index 000000000..5e58cd071 --- /dev/null +++ b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.suppressHttpEndpoints.base.json5 @@ -0,0 +1,13 @@ +// This production config that hides /login, /logout, /proximityTriggered http endpoints +// when GPII runs in production config +{ + "type": "gpii.flowManager.config.suppressHttpEndpoints.base", + "options": { + "distributeOptions": { + "flowManager.suppressHttpEndpoints": { + "record": true, + "target": "{that gpii.flowManager.local}.options.suppressHttpEndpoints" + } + } + } +} diff --git a/gpii/node_modules/flowManager/configs/gpii.flowManager.config.untrusted.base.json5 b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.untrusted.base.json5 new file mode 100644 index 000000000..5ae7cfa1b --- /dev/null +++ b/gpii/node_modules/flowManager/configs/gpii.flowManager.config.untrusted.base.json5 @@ -0,0 +1,28 @@ +{ + "type": "gpii.flowManager.config.untrusted.base", + "options": { + "gradeNames": "fluid.component", + "distributeOptions": { + "flowManager.untrustedGrades": { + "record": "gpii.flowManager.untrusted", + "target": "{that flowManager}.options.gradeNames" + }, + "flowManager.defaultCloudURL": { + "record": "http://localhost:8084", + "target": "{that flowManager settingsDataSource}.options.cloudURL" + }, + "flowManager.envCloudURL": { + "record": "@expand:kettle.resolvers.env(GPII_CLOUD_URL)", + "target": "{that flowManager settingsDataSource}.options.cloudURL", + "priority": "after:flowManager.defaultCloudURL" + }, + "flowManager.clientCredentialFilePath": { + "record": "%gpii-universal/testData/clientCredentials/pilot.json", + "target": "{that flowManager settingsDataSource}.options.clientCredentialFilePath" + } + } + }, + "mergeConfigs": [ + "./gpii.flowManager.config.local.base.json5" + ] +} diff --git a/gpii/node_modules/flowManager/configs/kettleModuleLoader.js b/gpii/node_modules/flowManager/configs/kettleModuleLoader.js deleted file mode 100644 index 20c0170a7..000000000 --- a/gpii/node_modules/flowManager/configs/kettleModuleLoader.js +++ /dev/null @@ -1,16 +0,0 @@ -/*! -Kettle Module Loader. - -Copyright 2012-2013 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/kettle/LICENSE.txt -*/ - -// The purpose of this file is to be copied to a location for which the -// require function is needed. It allows to find node modules relative -// to that location that are otherwise non-resolvable. -module.exports = require; \ No newline at end of file diff --git a/gpii/node_modules/flowManager/configs/production.json b/gpii/node_modules/flowManager/configs/production.json deleted file mode 100644 index b846cdc3d..000000000 --- a/gpii/node_modules/flowManager/configs/production.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "typeName": "flowManager.production", - "options": { - "gradeNames": ["autoInit", "fluid.littleComponent"], - "components": { - "server": { - "type": "kettle.server", - "options": { - "logging": false, - "port": 8081, - "components": { - "flowManager": { - "type": "gpii.flowManager", - "options": { - "typeName": "flowManager.production", - "urls": { - "preferences": "http://preferences.gpii.net/user/%token", - "lifecycleManagerServer": "http://localhost:%port/%operation/%token", - "deviceReporter": "http://localhost:%port/device", - "matchMaker": "http://localhost:%port/match" - } - } - } - } - } - } - } - }, - "includes": [ - "./base.json" - ] -} \ No newline at end of file diff --git a/gpii/node_modules/flowManager/index.js b/gpii/node_modules/flowManager/index.js index 84eb96187..b5dc91184 100644 --- a/gpii/node_modules/flowManager/index.js +++ b/gpii/node_modules/flowManager/index.js @@ -1,6 +1,12 @@ +"use strict"; + var fluid = require("infusion"); -var loader = fluid.getLoader(__dirname); +fluid.module.register("flowManager", __dirname, require); -loader.require("./src/FlowManager.js"); -loader.require("./src/FlowManagerUtilities.js"); +require("./src/SystemUtils.js"); +require("./src/MatchMaking.js"); +require("./src/SessionAware.js"); +require("./src/FlowManager.js"); +require("./src/CloudBasedFlowManager.js"); +require("./src/SolutionsRegistryDataSource.js"); diff --git a/gpii/node_modules/flowManager/package.json b/gpii/node_modules/flowManager/package.json index 751c701d5..d889e622a 100644 --- a/gpii/node_modules/flowManager/package.json +++ b/gpii/node_modules/flowManager/package.json @@ -1,19 +1,13 @@ { - "name": "flowManager", - "description": "Flow Manager handles communication between various data sources: user preferences, device reporter, configuration manager, etc.", - "version": "0.1", - "author": "GPII", - "bugs": "http://wiki.gpii.net/index.php/Main_Page", - "homepage": "http://gpii.net/", - "dependencies": {}, - "licenses": [ - { - "type": "BSD-3-Clause", - "url": "http://www.opensource.org/licenses/BSD-3-Clause" - } - ], - "keywords": ["gpii", "accessibility", "settings", "fluid", "IoC", "Inversion of Control", "configuration", "evented"], - "repository": "git://github.com/GPII/universal.git", - "main": "./index.js", - "engines": { "node" : ">=0.1.9" } -} \ No newline at end of file + "name": "flowManager", + "description": "The GPII Flow Manager handles communication between various data sources: user preferences, device reporter, configuration manager, etc.", + "version": "0.3.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } +} diff --git a/gpii/node_modules/flowManager/src/BrowserChannel.js b/gpii/node_modules/flowManager/src/BrowserChannel.js new file mode 100644 index 000000000..728739ed8 --- /dev/null +++ b/gpii/node_modules/flowManager/src/BrowserChannel.js @@ -0,0 +1,115 @@ +/*! + GPII BrowserChannel Handler + + Copyright 2014, 2015 Emergya + Copyright 2015-2018 Raising the Floor - International + Copyright 2020 OCAD University + + Licensed under the New BSD license. You may not use this file except in + compliance with this License. + + You may obtain a copy of the License at + https://github.com/gpii/universal/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.flowManager"); + +fluid.defaults("gpii.flowManager.browserChannel", { + gradeNames: ["fluid.modelComponent"] +}); + +fluid.defaults("gpii.flowManager.browserChannel.handler", { + gradeNames: ["kettle.request.ws", "gpii.flowManager.sessionAware"], + listeners: { + onReceiveMessage: { + funcName: "gpii.flowManager.browserChannel.receiveMessage", + args: [ + "{that}", + "{arguments}.1", + "{flowManager}.solutionsRegistryDataSource", + "{deviceReporter}.platformReporter" + ] + } + } +}); + +/** + * Send an error response for the request. + * @param {Component} request - An instance of gpii.flowManager.browserChannel.handler. + * @param {String} message - Error message text to include in the response. + */ +gpii.flowManager.browserChannel.sendError = function (request, message) { + fluid.log("Sending browserChannel error ", message); + var error = { + isError: true, + message: message + }; + request.sendMessage(error); + // These codes bizarrely are not HTTP codes, but listed here: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent + request.ws.close(1008, "Solution id not authorized"); +}; + +/** + * Handler for all message types: + * - an initial "connect" message type establishes the connection and + * initializes the channel and its relationship with the WebSockets settings + * handler. This includes dynamically attaching the + * gpii.flowManager.browserChannel.receiveChangeSettingsMsg() listener to + * handle "changeSettings" message types after the connection is established. + * - any subsequent "connect" message types are silently ignored, + * - all other message types cause an error response and close the connection + * The one exception is the "changeSettings" message type (see first point). + * @param {Component} that - An instance of gpii.flowManager.browserChannel.handler. + * @param {Object} message - Object containing the message type and its payload. + * @param {Component} solutionsRegistryDataSource - Used to match the solution + * given in the payload. + * @param {Component} platformReporter - Used to determine the platform this + * is running on. + */ +gpii.flowManager.browserChannel.receiveMessage = function (that, message, solutionsRegistryDataSource, platformReporter) { + if (message.type !== "connect") { + return; + } + if (that.established) { + gpii.flowManager.browserChannel.sendError(that, "Connection already established - cannot send a second connect message"); + } + var solutionId = message.payload.solutionId; + solutionsRegistryDataSource.get({os: platformReporter.reportPlatform().id}, function onSuccess(entries) { + if (!(solutionId in entries)) { + gpii.flowManager.browserChannel.sendError(that, "Rejecting a connection request from '" + solutionId + + "'. The solution id was not found in the solutions registry"); + } else { + gpii.settingsHandlers.webSockets.instance.addClient(solutionId, that); + that.established = true; + that.solutionId = solutionId; + } + }, function (error) { + gpii.flowManager.browserChannel.sendError(that, error.message); + }); +}; + +/** + * Listener for the "changeSettings" message type. This is added as a listener + * after the connection has been established. That is, this will not function + * without a previous "connect" message type. + * @param {Component} that - An instance of gpii.flowManager.browserChannel.handler. + * @param {Object} message - Object containing the message type and its payload. + */ +gpii.flowManager.browserChannel.receiveChangeSettingsMsg = function (that, message) { + if (message.type === "changeSettings" && that.established) { + var wsPayload = {}; + wsPayload[that.solutionId] = [{ + options: { + path: that.solutionId, + source: that + }, + settings: message.payload.settings + }]; + gpii.settingsHandlers.webSockets.set(wsPayload); + } +}; diff --git a/gpii/node_modules/flowManager/src/Capture.js b/gpii/node_modules/flowManager/src/Capture.js new file mode 100755 index 000000000..f87f2068f --- /dev/null +++ b/gpii/node_modules/flowManager/src/Capture.js @@ -0,0 +1,203 @@ +/** + * GPII Capture Component + * + * Copyright 2020 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/gpii/universal/LICENSE.txt + */ +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.defaults("gpii.flowManager.capture", { + gradeNames: ["fluid.component"], + events: { + // Pseudoevents for transforming promise chains to fetch the solutions on the current device + // and capture the settings for the device. + onSolutionsForCurrentDevice: null, + onCaptureSettingsForCurrentDevice: null + }, + listeners: { + // Begin declaration of Promise Chain for onSolutionsForCurrentDevice + "onSolutionsForCurrentDevice.getDeviceContextPromise": { + funcName: "gpii.lifecycleManager.getDeviceContextPromise", + args: ["{flowManager}.deviceReporter"], + priority: "first" + }, + "onSolutionsForCurrentDevice.getSolutions": { + funcName: "gpii.flowManager.getSolutions", + args: [ "{flowManager}.solutionsRegistryDataSource", "{arguments}.0"], + priority: "after:getDeviceContextPromise" + }, + "onSolutionsForCurrentDevice.solutionsRegistryEntriesToPromise": { + funcName: "fluid.toPromise", + args: ["{arguments}.0.solutionsRegistryEntries"], + priority: "after:getSolutions" + }, + // Begin declaration of Promise Chain for onCaptureSettingsForCurrentDevice + "onCaptureSettingsForCurrentDevice.getInstalledSolutionsForCurrentDevice": { + func: "{that}.getInstalledSolutionsForCurrentDevice", + priority: "first" + }, + "onCaptureSettingsForCurrentDevice.captureSystemSettings": { + funcName: "gpii.flowManager.capture.captureSystemSettings", + args: ["{lifecycleManager}.read", "{lifecycleManager}.getSession", "{arguments}.0", "{arguments}.1"], // solutionsRegistryEntries, options + priority: "after:getInstalledSolutionsForCurrentDevice" + }, + "onCaptureSettingsForCurrentDevice.formatRawCapturedSettings": { + func: "gpii.flowManager.capture.formatRawCapturedSettings", + args: ["{arguments}.0"], + priority: "after:captureSystemSettings" + } + }, + invokers: { + getInstalledSolutionsForCurrentDevice: { + funcName: "fluid.promise.fireTransformEvent", + args: ["{that}.events.onSolutionsForCurrentDevice"] + }, + getSystemSettingsCapture: { + funcName: "fluid.promise.fireTransformEvent", + args: ["{that}.events.onCaptureSettingsForCurrentDevice", null, "{arguments}.0"] // options + } + } +}); + +/** + * Invoker `{gpii.flowManager.capture}.getInstalledSolutionsForCurrentDevice` + * + * @method + * @name {gpii.flowManager.capture}.getInstalledSolutionsForCurrentDevice + * + * This invoker method will return the solution registry entries, in their usual json format, + * that are available on the current device. + * + * @return {Promise} A promise resolved with an object of solutions registry entries available on the + * current device. As with the solutions registry itself, these are keyed by the solution ID. + */ + +/** + * Invoker `{gpii.flowManager.capture}.getSystemSettingsCapture` + * + * @method + * @name {gpii.flowManager.capture}.getSystemSettingsCapture + * + * This main API entry point for capturing settings from a system or computer. This captures + * the actual settings on the device, so it assumed to be running in a local untrusted flow + * manager. + * + * @param {Object} options - Options for this chain. + * @param {Array} options.solutionsList - An array of solution IDs to filter by when + * retreiving settings. If this option is not included, all available settings will be + * returned. ex: `["com.microsoft.windows.mouseSettings", "com.freedomscientific.jaws"]`. + * @return {Promise} A promise resolved with the payload of captured system settings. + */ + +/** + * Runs through all the solutions currently available on the system, pulls the current + * setting for each supportedSetting and returns them in an object. Primary use case + * is for backing Capture tools that would allow a user to set up their GPII profile + * starting with the current settings for their applications on the local machine. + * + * @param {Function|gpii.lifecycleManager.read} readSettingsFunc - lifecycleManager.read (or suitable implementation), + * that takes solution registry entries, reads their current values on the device, and returns a promise resolved to + * them. + * @param {Function|gpii.lifecycleManager.getSession} getSession - Function to return the current GPII session which + * has a `localResolver` we can use for expanding material. + * @param {Object} solutions - Solutions registry entries for solutions available on the current machine. + * @param {Object} options - Extra options for processing. + * @param {Array} options.solutionsList - If provided, only solutions in this list of `solutionsID`s will + * be captured. Example: + * + * '''json + * ["com.microsoft.windows.cursors", "com.freedomscientific.jaws"] + * ''' + * @return {fluid.promise} Returns a promise resolving with the entire system settings capture. + */ +gpii.flowManager.capture.captureSystemSettings = function (readSettingsFunc, getSession, solutions, options) { + var solutionsToFetch = fluid.copy(solutions); + if (options.solutionsList) { + fluid.remove_if(solutionsToFetch, function (solution, solutionID) { + return !options.solutionsList.includes(solutionID); + }); + } + + var expandedSolutions = getSession().localResolver(solutionsToFetch); + return readSettingsFunc(expandedSolutions); +}; + +/** + * The raw return payload from the capture promise sequence looks like: + * '''json + * [ + * { + * "fakemag1": [ + * { + * "settings": { + * "magnification": 2 + * } + * } + * ] + * }, + * { + * "fakemag1": [ + * { + * "settings": { + * "invert": true + * } + * } + * ] + * }, + * { + * "fakemag2": [ + * { + * "settings": { + * "magnification": 2, + * "invert": true + * } + * } + * ] + * } + * ] + * ''' + * + * and we want: + * '''json + * { + * "fakemag1": { + * "magnification": 2, + * "invert": true + * }, + * "fakemag2": { + * "magnification": 2, + * "invert": true + * } + * } + * ''' + * + * @param {Object} data - The raw captured data. + * @return {Object} Returns a new payload with collapsed data, and multiple settings handler + * results for the same solution merged together. + */ +gpii.flowManager.capture.formatRawCapturedSettings = function (data) { + var togo = {}; + fluid.each(data, function (sequenceItem) { + if (sequenceItem.isError) { + fluid.log("Error capturing settings for: ", sequenceItem); + return; + } + fluid.each(sequenceItem, function (item, key) { + if (!togo[key]) { + togo[key] = {}; + } + fluid.each(fluid.get(item, [0, "settings"]), function (value, settingId) { + togo[key][settingId] = value; + }); + }); + }); + return togo; +}; diff --git a/gpii/node_modules/flowManager/src/CloudBasedFlowManager.js b/gpii/node_modules/flowManager/src/CloudBasedFlowManager.js new file mode 100644 index 000000000..de64b0517 --- /dev/null +++ b/gpii/node_modules/flowManager/src/CloudBasedFlowManager.js @@ -0,0 +1,136 @@ +/** + * GPII Cloud-Based FlowManager + * + * Copyright 2013 OCAD University + * Copyright 2017-2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"); + +require("./PrefsServerDataSource.js"); +require("./HealthGetHandler.js"); +require("./ReadyGetHandler.js"); +require("./SettingsGetHandler.js"); +require("./SettingsPutHandler.js"); +require("./RevisionGetHandler.js"); + +require("gpii-oauth2"); + +/** + * OAUTH2 SECURED CLOUD-BASED FLOW MANAGER + */ + +fluid.defaults("gpii.flowManager.cloudBased", { + gradeNames: ["fluid.contextAware"], + contextAwareness: { + cloudStatus: { + checks: { + addCloudStatusOption: { + contextValue: "{preferencesServer}", + gradeNames: null + } + }, + defaultGradeNames: "gpii.flowManager.cloudBased.cloudStatus" + } + }, + // JSON file containing the full sha256 of the revision of the repository for + // this CBFM, e.g., { "sha256": "86a83d2f93a6f8f954a4fef618ca6aea1399c980" } + // When CI is triggered to build a Docker image of gpii-universal, the + // scripts and Dockerfile determine the sha256 and write it to this file. + // CI names it "gpii-revision.json" and puts it in the root directory. + // Developers have the option of putting it elsewhere -- the + // "gpiiRevisionPath" is distributed down from a configuration such as + // gpii.flowManager.config.cloud.base + gpiiRevisionPath: null, + components: { + prefsServerDataSource: { + type: "gpii.flowManager.prefsServerDataSource" + }, + oauth2DataStore: { + type: "gpii.dbOperation.dbDataStore" + }, + authServer: { + // note that this subcomponent directly attaches express routes to our underlying express app via its direct API - + // e.g. at the paths /access_token + type: "gpii.oauth2.authServer", + options: { + components: { + dataStore: "{gpii.dbOperation.dataStore}" + }, + members: { + expressApp: "{kettle.server}.expressApp" + }, + events: { + onContributeMiddleware: "{kettle.server}.events.onContributeMiddleware", + onContributeRouteHandlers: "{kettle.server}.events.onContributeRouteHandlers" + } + } + }, + authGrantFinder: { + type: "gpii.oauth2.authGrantFinder" + } + }, + distributeOptions: { + setDataStoreOnAuthGrantFinder: { + record: "{gpii.dbOperation.dataStore}", + target: "{that gpii.oauth2.authorizationService}.options.components.dataStore" + } + }, + requestHandlers: { + // The endpoint to request user settings + settingsGet: { + route: "/:gpiiKey/settings/:device", + method: "get", + type: "gpii.flowManager.cloudBased.settings.get.handler" + }, + // The endpoint to update user settings + settingsPut: { + route: "/:gpiiKey/settings", + method: "put", + type: "gpii.flowManager.cloudBased.settings.put.handler" + }, + // The endpoint to request the revision of the repository used for this + // CPFM + revisionGet: { + route: "/revision", + method: "get", + type: "gpii.flowManager.cloudBased.gpiiRevision.handler" + } + } +}); + +// The addon grade that adds the liveness (/ready) and the readiness (/health) endpoints to the cloud based flow manager only +// when it runs as a separate server from the preferences server. /ready endpoint on CBFM will ping the /ready endpoint +// on the preferences server to ensure the preferences server is ready to handle requests. When the cloud based flow manager +// and preferences server are running as one server, this addon grade will NOT be added to CBFM, instead, the /ready and /health +// endpoints from the preferenes server are used, where the /ready endpoint of the preferences server will check the db +// connection. +fluid.defaults("gpii.flowManager.cloudBased.cloudStatus", { + gradeNames: ["fluid.component"], + requestHandlers: { + // The endpoint to check the readiness of the cloud based flow manager: the server is ready but does not check the database connection. + healthGet: { + route: "/health", + method: "get", + type: "gpii.health.handler" + }, + // The endpoint to check the liveness of the cloud based flow manager: both the server and the database connection is ok + readyGet: { + route: "/ready", + method: "get", + type: "gpii.flowManager.cloudBased.ready.handler" + } + } +}); diff --git a/gpii/node_modules/flowManager/src/DefaultSettingsLoader.js b/gpii/node_modules/flowManager/src/DefaultSettingsLoader.js new file mode 100644 index 000000000..8abee922d --- /dev/null +++ b/gpii/node_modules/flowManager/src/DefaultSettingsLoader.js @@ -0,0 +1,157 @@ +/*! + * Default Settings Loader + * + * Copyright 2018 OCAD University + * Copyright 2020 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = fluid || require("infusion"), + gpii = fluid.registerNamespace("gpii"), + fs = require("fs"), + path = require("path"), + JSON5 = require("json5"); + +// The default settings loader reads default settings thru these steps: +// 1. Fetch default settings from the remote URL; +// 2. If the remote fetch fails, read default settings from the local system settings directory. In the case that the +// default settings file doesn't exists in the system settings directory, copy it from the code base to the settings +// directory. +// If the default settings file is not found at both places, return undefined. +fluid.defaults("gpii.defaultSettingsLoader", { + gradeNames: ["fluid.component"], + // The path to the file that has all QSS supported settings. + defaultSettingsInCodeBase: "%gpii-universal/testData/defaultSettings/defaultSettings.json5", + // The url to the remote default settings file. Must be provided by integrators. + defaultSettingsUrl: null, + members: { + gpiiSettingsDir: "@expand:{settingsDir}.getGpiiSettingsDir()", + // Populated by "onCreate.prepareSettingsFile" listener. It points to the default settings file in + // the settings directory rather than defaultSettingsInCodeBase, the default settings file in the code base. + defaultSettingsFile: null + }, + components: { + settingsDir: { + type: "gpii.settingsDir" + }, + remoteDefaultSettingsDataSource: { + type: "kettle.dataSource.URL", + options: { + url: "{defaultSettingsLoader}.options.defaultSettingsUrl", + components: { + encoding: { + type: "kettle.dataSource.encoding.JSON5" + } + } + } + } + }, + events: { + onGet: null + }, + listeners: { + "onCreate.prepareSettingsFile": { + listener: "fluid.set", + args: ["{that}", ["defaultSettingsFile"], "@expand:gpii.defaultSettingsLoader.prepareSettingsFile({that}.options.defaultSettingsInCodeBase,{that}.gpiiSettingsDir)"] + }, + + // The promise chain to fetch default settings: + // 1. If defaultSettingsUrl is provided, the first attempt is to fetch from the remote url; + // 2. If the fetch remote fails, perform the first fallback to read from the local system settings directory; + // 3. If the read from the local fails, return undefined. + "onGet.getRemoteDefaultSettings": { + listener: "gpii.defaultSettingsLoader.getRemoteDefaultSettings", + args: ["{that}.options.defaultSettingsUrl", "{that}.remoteDefaultSettingsDataSource"], + priority: "first" + }, + "onGet.getLocalDefaultSettings": { + listener: "gpii.defaultSettingsLoader.getLocalDefaultSettings", + args: ["{arguments}.0", "{that}.defaultSettingsFile"], + priority: "after:getRemoteDefaultSettings" + } + }, + invokers: { + get: { + funcName: "fluid.promise.fireTransformEvent", + args: ["{that}.events.onGet"] + } + } +}); + +/** + * Return the path to the default settings file in the settings directory. If it doesn't yet exist, + * copy the default settings file from the code base to the setting dir then return the path. + * + * @param {String} defaultSettingsInCodeBase - The path to the default settings file in the universal code base. + * @param {String} gpiiSettingsDir - GPII settings directory. + * @return {String} Return the path to the default settings file in the settings dir. + */ +gpii.defaultSettingsLoader.prepareSettingsFile = function (defaultSettingsInCodeBase, gpiiSettingsDir) { + var defaultSettingsInCodeBaseFullPath = fluid.module.resolvePath(defaultSettingsInCodeBase); + + // Return undefined if the default settings file does not exist in the code base + if (!fs.existsSync(defaultSettingsInCodeBaseFullPath)) { + return undefined; + } + + var gpiiSettingsAbsolutePath = fluid.module.resolvePath(gpiiSettingsDir); + + var defaultSettingsFileName = path.basename(defaultSettingsInCodeBaseFullPath); + var defaultSettingsFile = path.join(gpiiSettingsAbsolutePath, defaultSettingsFileName); + + // Copy the default settings file to the settings dir if it doesn't already in there + if (!fs.existsSync(defaultSettingsFile)) { + fs.writeFileSync(defaultSettingsFile, fs.readFileSync(defaultSettingsInCodeBaseFullPath, "utf-8")); + } + + return defaultSettingsFile; +}; + +/** + * Read default settings from the remote URL + * + * @param {String} url - The url pointing to the remote default settings file. + * @param {Object} remoteDefaultSettingsDataSource - The content fetched from the remote default settings url. + * @return {Object} The content of default settings fetched from the remote URL. If the fetch fails, + * return undefined. + */ +gpii.defaultSettingsLoader.getRemoteDefaultSettings = function (url, remoteDefaultSettingsDataSource) { + if (!url) { + return undefined; + } else { + var promiseTogo = fluid.promise(); + var remoteContentPromise = remoteDefaultSettingsDataSource.get(); + + remoteContentPromise.then(function (remoteDefaultSettings) { + promiseTogo.resolve(remoteDefaultSettings); + }, function () { + // If the fetch fails, return undefined. + promiseTogo.resolve(undefined); + }); + return promiseTogo; + } +}; + +/** + * The fall back: read default settings from the local file system + * + * @param {Object} remoteDefaultSettings - The content fetched from the remote default settings url. + * @param {String} defaultSettingsFile - The path to the local default settings file. + * @return {Object} Default settings. + */ +gpii.defaultSettingsLoader.getLocalDefaultSettings = function (remoteDefaultSettings, defaultSettingsFile) { + var promiseTogo = fluid.promise(); + if (remoteDefaultSettings) { + promiseTogo.resolve(remoteDefaultSettings); + } else { + promiseTogo.resolve(defaultSettingsFile ? JSON5.parse(fs.readFileSync(defaultSettingsFile, "utf-8")) : undefined); + } + return promiseTogo; +}; diff --git a/gpii/node_modules/flowManager/src/FlowManager.js b/gpii/node_modules/flowManager/src/FlowManager.js index 6bd3d9158..61fc345d3 100644 --- a/gpii/node_modules/flowManager/src/FlowManager.js +++ b/gpii/node_modules/flowManager/src/FlowManager.js @@ -1,125 +1,390 @@ -/*! -GPII Flow Manager - -Copyright 2012 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - path = require("path"), - gpii = fluid.registerNamespace("gpii"); - - fluid.require("kettle", require); - fluid.require("./UserLogin.js", require); - fluid.require("./Settings.js", require); - fluid.require("./UserLogout.js", require); - - fluid.require("lifecycleManager", require); - - fluid.defaults("gpii.flowManager", { - gradeNames: ["kettle.app", "autoInit"], - urls: { - preferences: "", - deviceReporter: "", - matchMaker: "", - solutionsReporter: "", - lifecycleManagerServer: "" - }, - root: path.join(__dirname, ".."), - components: { - preferencesDataSource: { - type: "kettle.dataSource.URL", - options: { - gradeNames: ["kettle.dataSource.simpleCallbackWrapper"], - url: "{gpii.flowManager}.options.urls.preferences", - termMap: { - token: "%token" +/* + * GPII Flow Manager + * + * Copyright 2012 OCAD University + * Copyright 2017-2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +require("kettle"); + +require("./UserLogonHandlers.js"); +require("./BrowserChannel.js"); +require("./DefaultSettingsLoader.js"); +require("./PSPChannel.js"); +require("./SettingsDataSource.js"); +require("./UntrustedFlowManager.js"); +require("./Capture.js"); + +require("preferencesServer"); +require("lifecycleManager"); +require("deviceReporter"); +require("transformer"); +require("journal"); +require("singleInstance"); +require("eventLog"); +require("gpii-user-errors"); + +fluid.defaults("gpii.flowManager", { + gradeNames: ["kettle.app"], + components: { + // TODO: Make this use the solutions registry data source by default? + solutionsRegistryDataSource: { + type: "kettle.dataSource", + options: { + termMap: { + "os": "%os", + "version": "%version" + }, + components: { + encoding: { + type: "kettle.dataSource.encoding.none" } } - }, - deviceReporterDataSource: { - type: "kettle.dataSource.URL", - options: { - gradeNames: ["kettle.dataSource.simpleCallbackWrapper"], - url: "{gpii.flowManager}.options.urls.deviceReporter" + } + }, + matchMakerFramework: { + type: "gpii.matchMakerFramework" + }, + browserChannel: { + type: "gpii.flowManager.browserChannel" + }, + // persistent validation component for settings. + prefsValidator: { + type: "gpii.universal.solutionsRegistry.validators.preferences" + } + } +}); + + +// Mixin grades for the FlowManager + +fluid.defaults("gpii.flowManager.local", { + gradeNames: ["fluid.contextAware"], + // The "resetAtStart" flag is to indicate whether to reset the system to the default settings on system startup. + // It is turned on in production configs. + resetAtStart: false, + // The "suppressHttpEndpoints" flag is to indicate whether to hide un-needed localhost http endpoints. + // It is turned on in production configs. + suppressHttpEndpoints: false, + members: { + /* The resolved value is an object containing data for performing reset actions: + { + defaultSettings: {Object}, // The content of the reset to default file + defaultLifecycleInstructions: {Object}, // For the reset action at key-in + defaultSnapshot: {Object} // For reset actions at system startup and explicit reset request + } + "defaultSnapshot" and "defaultLifecycleInstructions" structures are generated for performing + "reset to system default settings" functionality that is applied when: + 1. reset to default settings on system startup using lifecycleManager.restoreSnapshot(); + 2. at receiving request "/user/reset/login" to logout the current key and reset to default settings. + This reset also uses lifecycleManager.restoreSnapshot(); + 3. at any keyin. The settings for the keyin will be combined with default settings, then apply using + lifecycleManager.start(lifecycleInstructions). + + "defaultSnapshot" is used at case 1 & 2 while "defaultLifecycleInstructions" is used at case 3. + + The difference between "defaultLifecycleInstructions" and "defaultSnapshot" is the "settings" block for each + solution: + * "defaultSnapshot": the snapshot structure expected by lifecycleManager.restoreSnapshot(): + { + "settings": { + "masterVolume": { + "type": "ADD", + "value": 75 + } + } + } + * "defaultLifecycleInstructions": the lifecycle instruction structure expected by lifecycleManager.start(): + { + "settings": { + "masterVolume": 75 + } + } + */ + defaultSettingsDataPromise: "@expand:fluid.promise()" + }, + contextAwareness: { + suppressHttpEndpoints: { + checks: { + addHttpEndpointsOption: { + contextValue: "{gpii.flowManager.local}.options.suppressHttpEndpoints", + gradeNames: null } }, - matchMakerDataSource: { - type: "kettle.dataSource.URL", - options: { - gradeNames: ["kettle.dataSource.simpleCallbackWrapper"], - url: "{gpii.flowManager}.options.urls.matchMaker", - writable: true - } + defaultGradeNames: "gpii.flowManager.local.requestHandlers" + } + }, + distributeOptions: { + // Distribute the grade to handle user logon requests. + // In all-in-local config, the handler is "gpii.lifecycleManager.userLogonHandling.matchMakingStateChangeHandler". + // In untrusted config, the handler should be "gpii.lifecycleManager.untrusted.stateChangeHandler". + "lifecycleManager.loginRequestHandler": { + record: "gpii.lifecycleManager.userLogonHandling.matchMakingStateChangeHandler", + target: "{that gpii.lifecycleManager.loginRequest}.options.gradeNames" + }, + "userLogonHandling.defaultSettingsDataPromise": { + record: "{gpii.flowManager.local}.defaultSettingsDataPromise", + target: "{that gpii.lifecycleManager.userLogonHandling.stateChangeHandler}.options.members.defaultSettingsDataPromise" + }, + "lifecycleManager.privateMatchMaker": { + record: { + privateMatchMaker: "{gpii.flowManager.local}.privateMatchMaker" }, - lifecycleManagerServerDataSource: { - type: "kettle.dataSource.URL", - options: { - gradeNames: ["kettle.dataSource.simpleCallbackWrapper"], - url: "{gpii.flowManager}.options.urls.lifecycleManagerServer", - writable: true, - termMap: { - operation: "%operation", - token: "%token" - } + target: "{that lifecycleManager}.options.components" + }, + "userListeners.lifecycleManager": { + record: "{gpii.flowManager.local}.lifecycleManager", + target: "{that gpii.userListener}.options.components.lifecycleManager" + }, + "lifecycleManager.pspChannel.sessionBinder": { + record: "gpii.pspChannel.sessionBinder", + target: "{that lifecycleManager gpii.lifecycleManager.userSession}.options.gradeNames" + } + }, + components: { + lifecycleManager: { + type: "gpii.lifecycleManager", + options: { + components: { + userErrors: "{gpii.flowManager.local}.userErrors" } } }, - handlers: { - userLogin: { - route: "/user/:token/login", - type: "get" - }, - userLogout: { - route: "/user/:token/logout", - type: "get" + privateMatchMaker: { + type: "gpii.lifecycleManager.privateMatchMaker" + }, + deviceReporter: { + type: "gpii.deviceReporter" + }, + journal: { + type: "gpii.journal", + options: { + gradeNames: ["gpii.journalLifecycleManager", "gpii.journalApp"] } + }, + eventLog: { + type: "gpii.eventLog", + options: { + gradeNames: ["gpii.metrics", "gpii.metrics.lifecycle"] + } + }, + capture: { + type: "gpii.flowManager.capture" + }, + userListeners: { + type: "gpii.userListeners" + }, + webSocketsSettingsHandlerComponent: { + type: "gpii.settingsHandlers.webSockets.component" + }, + pspChannel: { + type: "gpii.pspChannel" + }, + defaultSettingsLoader: { + type: "gpii.defaultSettingsLoader" + }, + userErrors: { + type: "gpii.userErrors" } - }); - - fluid.defaults("gpii.flowManager.dev", { - gradeNames: ["autoInit", "fluid.littleComponent"], - distributeOptions: { - source: "{that}.options.urlExpanderGradeNames", - target: "{that urlExpander}.options.gradeNames" + }, + requestHandlers: { + browserChannel: { + route: "/browserChannel", + type: "gpii.flowManager.browserChannel.handler" + }, + pspChannel: { + route: "/pspChannel", + type: "gpii.pspChannel.handler" + } + }, + invokers: { + savePreferences: { + funcName: "gpii.flowManager.local.savePreferences", + args: ["{that}", "{arguments}.0", "{arguments}.1"] + } + }, + events: { + preferencesSavedSuccess: null, + preferencesSavedError: null, + noUserLoggedIn: null, + getDefaultSettingsData: null, + defaultSettingsDataLoaded: null, + flowManagerReady: { + events: { + "defaultSettingsDataLoaded": "defaultSettingsDataLoaded", + "kettleReady": "{kettle.server}.events.onListen" + } + }, + // Fired only when: + // 1. The flag for resetting to default settings on system start, that.options.resetAtStart, is turned on; + // 2. The default settings are defined. See documentation/ResetComputer.md. + resetAtStartSuccess: null, + resetAtStartError: null + }, + listeners: { + "onCreate.registerInstance": "gpii.singleInstance.registerInstance", + "onCreate.mountWebSocketsSettingsHandler": { + funcName: "gpii.flowManager.local.mountWebSocketsSettingsHandler", + args: ["{webSocketsSettingsHandlerComponent}"] }, - urlExpanderGradeNames: ["kettle.urlExpander.development"] - }); - fluid.defaults("gpii.flowManager.cloudBased", { - gradeNames: ["autoInit", "gpii.flowManager"], - mergePolicy: { - handleUnsupported: "noexpand" + // Fire "getDefaultSettingsData" event to trigger promise transform chain that does: + // 1. Read default settings from the reset to default file; + // 2. Calculate defaultLifecycleInstructions and defaultSnapshot based on the default settings; + // 3. All default settings data are held in flowManager.defaultSettingsDataPromise. + // Note: all of this machinery should be removed once we can update to the FLUID-6148 framework and we can + // put all of this I/O into the "resources" block of a fluid.resourceLoader. + "onCreate.calculateDefaultSettingsData": { + listener: "fluid.promise.fireTransformEvent", + args: ["{that}.events.getDefaultSettingsData"] }, - components: { - deviceReporterDataSource: { - type: "fluid.emptySubcomponent" - }, - lifecycleManagerServerDataSource: { - type: "fluid.emptySubcomponent" - } + "getDefaultSettingsData.loadDefaultSettngs": { + listener: "{defaultSettingsLoader}.get", + priority: "first" }, - typeName: "flowManager.cloudBased", - urls: { - deviceReporter: "", + "getDefaultSettingsData.convertDefaultSettings": { + listener: "gpii.flowManager.convertDefaultSettingsToSnapshot", + args: ["{that}", "{arguments}.0", "{that}.events.defaultSettingsDataLoaded"], + priority: "after:loadDefaultSettngs" }, - handlers: { - settings: { - route: "/:token/settings/:device", - type: "get" - }, - userLogin: null, - userLogout: null + + "flowManagerReady.loginWithNoUser": { + listener: "gpii.flowManager.local.noUserLoggedIn", + args: ["{lifecycleManager}", "{that}.events.noUserLoggedIn"] + }, + "noUserLoggedIn.resetAtStart": { + listener: "gpii.flowManager.local.resetAtStart", + args: ["{lifecycleManager}", "{that}.options.resetAtStart", "{that}.defaultSettingsDataPromise", "{that}.events.resetAtStartSuccess", "{that}.events.resetAtStartError"] + }, + "afterDestroy.deregisterInstance": "gpii.singleInstance.deregisterInstance", + "{lifecycleManager}.events.onAutoSaveRequired": { + namespace: "savePreferences", + listener: "{that}.savePreferences" + } + } +}); + +gpii.flowManager.local.mountWebSocketsSettingsHandler = function (webSocketsSettingsHandlerComponent) { + gpii.settingsHandlers.webSockets.instance = webSocketsSettingsHandlerComponent; +}; + +gpii.flowManager.local.noUserLoggedIn = function (lifecycleManager, noUserLoggedInEvent) { + var promise = lifecycleManager.addToUserLogonRequestQueue({ + gpiiKey: "noUser", + logonState: "login" + }); + promise.then(noUserLoggedInEvent.fire); +}; + +gpii.flowManager.convertDefaultSettingsToSnapshot = function (that, defaultSettings, defaultSettingsDataCreatedEvent) { + var promiseTogo = fluid.promise(); + if (defaultSettings) { + var matchMakingPromise = that.privateMatchMaker.doMatch("noUser", defaultSettings); + matchMakingPromise.then(function (payload) { + var activePrefsSetName = payload.activePrefsSetName || "gpii-default"; + // convert the matchmaker output to lifecycle instructions + var defaultLifecycleInstructions = gpii.transformer.configurationToSettings(payload.matchMakerOutput.inferredConfiguration[activePrefsSetName], payload.solutionsRegistryEntries); + // convert lifecycle instructions to snapshots + var defaultSnapshot = fluid.transform(defaultLifecycleInstructions, function (handlerResponse) { + return gpii.settingsHandlers.settingsPayloadToChanges(handlerResponse); + }); + promiseTogo.resolve({ + defaultSettings: defaultSettings, + defaultLifecycleInstructions: defaultLifecycleInstructions, + defaultSnapshot: defaultSnapshot + }); + defaultSettingsDataCreatedEvent.fire(); + }); + } else { + promiseTogo.resolve({}); + defaultSettingsDataCreatedEvent.fire(); + } + fluid.promise.follow(promiseTogo, that.defaultSettingsDataPromise); +}; + +gpii.flowManager.local.resetAtStart = function (lifecycleManager, resetAtStart, defaultSettingsDataPromise, resetSuccessEvent, resetErrorEvent) { + defaultSettingsDataPromise.then(function (defaultSettingsData) { + var defaultSnapshot = defaultSettingsData.defaultSnapshot; + if (resetAtStart && defaultSnapshot) { + fluid.log("Resetting on system startup to default settings: ", defaultSnapshot); + var resetPromise = lifecycleManager.restoreSnapshot(defaultSnapshot); + resetPromise.then(function () { + fluid.log("Resetting on system startup completes successfully."); + resetSuccessEvent.fire(); + }, function (error) { + fluid.log("Resetting on system startup fails with the error: ", error); + resetErrorEvent.fire(error); + }); + } else { + resetSuccessEvent.fire(); } }); +}; + +/** + * Auto save updated preferences received from PSP or QSS. + * @param {Component} that - An instance of gpii.flowManager.local. + * @param {String} [gpiiKey] - [optional] The GPII key that the auto-saved preferences associates with. + * @param {Object} [preferences] - [optional] The preferences to be auto saved to the cloud. + * Note: when there isn't any preference to be auto saved, gpiiKey and preferences should not be provided. + * In this case, preferencesSavedSuccess event will still be fired, which will trigger the firing of its parent + * aggregate event preferencesApplied for the auto save process to proceed. + */ +gpii.flowManager.local.savePreferences = function (that, gpiiKey, preferences) { + // Don't support the save when the active user is "noUser" + if (gpiiKey && preferences && gpiiKey !== "noUser") { + if (that.setSettings) { + // With untrusted config where that.setSettings() is defined, auto save updated preferences that are allowed + // to be auto saved. + var savePromise = that.setSettings(gpiiKey, preferences); + savePromise.then(that.events.preferencesSavedSuccess.fire, that.events.preferencesSavedError.fire); + } + else { + // With trusted config where that.setSettings() is not defined, fire preferencesSavedSuccess() for the + // workflow to proceed. + that.events.preferencesSavedSuccess.fire(); + } + } else { + // When the updated preferences are not allowed to be auto saved, fire success event automatically + that.events.preferencesSavedSuccess.fire(); + } +}; -})(); \ No newline at end of file +/** + * The add-on grade containing http endpoints that will be attached to the local flow manager when the + * "suppressHttpEndpoints" flag is turned on. + */ +fluid.defaults("gpii.flowManager.local.requestHandlers", { + requestHandlers: { + userLogin: { + route: "/user/:gpiiKey/login", + method: "get", + type: "gpii.flowManager.userLogin.handler" + }, + userLogout: { + route: "/user/:gpiiKey/logout", + method: "get", + type: "gpii.flowManager.userLogout.handler" + }, + proximityTriggered: { + route: "/user/:gpiiKey/proximityTriggered", + method: "get", + type: "gpii.flowManager.proximityTrigger.handler" + } + } +}); diff --git a/gpii/node_modules/flowManager/src/FlowManagerUtilities.js b/gpii/node_modules/flowManager/src/FlowManagerUtilities.js deleted file mode 100644 index 6e15daa61..000000000 --- a/gpii/node_modules/flowManager/src/FlowManagerUtilities.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - GPII Flow Manager Utilities. - - Copyright 2012 OCAD University - - Licensed under the New BSD license. You may not use this file except in - compliance with this License. - - You may obtain a copy of the License at - https://github.com/gpii/universal/LICENSE.txt -*/ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - request = fluid.registerNamespace("gpii.request.flowManager"); - - request.logAndNotify = function (msg, event, callback) { - return function (data) { - fluid.log(msg, data); - event.fire(callback ? callback(data) : data); - }; - }; - - request.getPreferences = function (preferencesDataSource, event, token) { - preferencesDataSource.get({ - token: token - }, request.logAndNotify("Fetched user preferences: ", event)); - }; - - request.getDevice = function (deviceReporterDataSource, event) { - deviceReporterDataSource.get(null, request.logAndNotify( - "Fetched device reporter data: ", event)); - }; - - request.getMatch = function (matchMakerDataSource, event, preferences, device) { - matchMakerDataSource.set(null, { - preferences: preferences, - device: device - }, request.logAndNotify( - "Matched preferences and device reporter data: ", event)); - }; - - fluid.defaults("gpii.request.flowManager.token", { - gradeNames: ["fluid.eventedComponent", "autoInit"], - invokers: { - getPreferences: { - funcName: "gpii.request.flowManager.getPreferences", - args: ["{flowManager}.preferencesDataSource", "{that}.events.onPreferences", "{that}.userToken"] - }, - getMatch: { - funcName: "gpii.request.flowManager.getMatch", - args: ["{flowManager}.matchMakerDataSource", "{that}.events.onMatch", "{arguments}.0", "{arguments}.1"] - } - }, - events: { - onUserListener: null, - onPreferences: null, - onReadyToMatch: null, - onMatch: null - }, - listeners: { - onUserListener: "{that}.getPreferences", - onReadyToMatch: "{that}.getMatch" - } - }); - -})(); diff --git a/gpii/node_modules/flowManager/src/HealthGetHandler.js b/gpii/node_modules/flowManager/src/HealthGetHandler.js new file mode 100644 index 000000000..337b5739b --- /dev/null +++ b/gpii/node_modules/flowManager/src/HealthGetHandler.js @@ -0,0 +1,30 @@ +/*! +GPII Health Check Handler shared by Cloud Based Flow Manager and Preferences Server + +Copyright 2018 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.defaults("gpii.health.handler", { + gradeNames: ["kettle.request.http"], + invokers: { + handleRequest: { + funcName: "gpii.health.handleRequest", + args: ["{that}"] + } + } +}); + +gpii.health.handleRequest = function (that) { + that.events.onSuccess.fire({isHealthy: true}); +}; diff --git a/gpii/node_modules/flowManager/src/MatchMaking.js b/gpii/node_modules/flowManager/src/MatchMaking.js new file mode 100644 index 000000000..8bad672e8 --- /dev/null +++ b/gpii/node_modules/flowManager/src/MatchMaking.js @@ -0,0 +1,225 @@ +/** + * GPII Flow Manager Requests + * + * Copyright 2012 OCAD University + * Copyright 2015 Raising the Floor - International + * Copyright 2018 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +/* eslint-env browser */ +/* eslint strict: ["error", "function"] */ + +(function () { + + "use strict"; + + var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + + fluid.registerNamespace("gpii.flowManager"); + + gpii.logFully = function () { + if (!process.env.GITHUB_ACTIONS) { + var oldRenderChars = fluid.logObjectRenderChars; + fluid.logObjectRenderChars = 2 << 20; + fluid.log.apply(null, arguments); + fluid.logObjectRenderChars = oldRenderChars; + } else { + fluid.log.apply(null, arguments); + } + }; + + /** Render an instance of the "megapayload" which circulates amongst the FlowManager, MatchMaker, LifecycleManager etc. methods. + * This builds up incrementally starting with the GPII key during logon until we reach the "finalPayload" dispatched to the + * lifecycleManager. This method transforms the payload into a form appropriate for logging to the console as an aid + * to diagnosing and debugging issues encountered in the field. Currently it only performs the action of suppressing the + * body of any entries of `solutionsRegistryEntries` since these are bulky and can easily be recovered from the + * solutions registry itself. + * @param {Object} megapayload - A megapayload record. + * @return {Object} The megapayload transformed to a more appropriate form for logging. + */ + gpii.renderMegapayload = function (megapayload) { + var solutionsRegistryEntries = fluid.transform(megapayload.solutionsRegistryEntries, function () { + return ""; + }); + return fluid.extend({}, megapayload, { + solutionsRegistryEntries: solutionsRegistryEntries + }); + }; + + gpii.flowManager.getPreferences = function (that, prefsServerDataSource) { + var gpiiKey = that.gpiiKey; + if (gpiiKey !== "noUser") { + fluid.log("gpii.flowManager.getPreferences called - fetching preferences from URL " + prefsServerDataSource.options.prefsServerURL); + } + + var noPrefs = {}; + var promise = gpiiKey === "noUser" ? fluid.promise().resolve(noPrefs) : prefsServerDataSource.get(gpiiKey); + promise.then(function (data) { + that.events.onPreferences.fire(data.preferences || data); + }, function (err) { + // GPII-3721: Return an empty payload for nonexistent GPII keys + if (err.errorCode === "GPII_ERR_NO_GPIIKEY") { + that.events.onPreferences.fire(noPrefs); + } else { + that.events.onError.fire({ + isError: true, + message: "Error when retrieving preferences: " + err.message, + statusCode: 404 + }); + } + }); + }; + + /** + * Asynchronous/Promise returning function which makes a get call to the solutions registry (1st parameter) to + * retrieve the solutions registry matching what is passed in the `device` parameter. + * This is appended to the matchmaker payload (mmpayload) parameter, which in turn is passed + * as parameter in the event fired. + * + * This function can be used with either (or both) asyncronously with an `event` and `onError` handlers + * passed in, or with the returned `fluid.promise`. + * + * @param {Object} solutionsRegistryDataSource - a solutions registry data source + * @param {Object} deviceContext - output from a device reporter. Used to filter solutions registry entries + * @param {Object} onSuccessEvent - Optional: Event to be fired when the solutionsRegistry entry has been retrieved + * @param {Object} onErrorEvent - Optional: Event to be fired when an error occurs + * @return {fluid.promise} - Returns a promise resolving with the mmpayload. Optionally if provided, the events + * are also fired with the modified mmpayload. + */ + gpii.flowManager.getSolutions = function (solutionsRegistryDataSource, deviceContext, onSuccessEvent, onErrorEvent) { + var promiseTogo = fluid.promise(); + + var os = fluid.get(deviceContext, "OS.id"); + var promise = solutionsRegistryDataSource.get({}); + promise.then(function (solutions) { + var solutionsRegistryEntries = gpii.matchMakerFramework.filterSolutions(solutions[os], deviceContext); + fluid.log("Fetched filtered solutions registry entries: ", gpii.renderMegapayload({solutionsRegistryEntries: solutionsRegistryEntries})); + promiseTogo.resolve({ + solutionsRegistryEntries: solutionsRegistryEntries, + solutions: solutions + }); + if (onSuccessEvent) { + onSuccessEvent.fire(solutionsRegistryEntries, solutions); + } + }, function (error) { + promiseTogo.reject(error); + if (onErrorEvent) { + onErrorEvent.fire(error); + } + }); + + return promiseTogo; + }; + + // initialPayload contains fields + // gpiiKey, preferences, deviceContext, solutionsRegistryEntry + // resulting from the initial fetch process + gpii.flowManager.processMatch = function (that, initialPayload) { + var promise = fluid.promise.fireTransformEvent(that.events.processMatch, initialPayload, {}); + promise.then(function (finalPayload) { + that.events.onMatchDone.fire(finalPayload); + }, function (error) { // TODO: This rejection handler is untested + that.handlerPromise.reject(error); + }); + }; + + // This is a table of priorities for handlers in the "processMatch" filter chain governing the MatchMaking process. + // This will be removed in favour of a relative constraints system once we have FLUID-5506 completed + // Higher priority numbers are handled earlier than lower ones + // This process is kicked off by the "onReadyToMatch" event defined in the "gpii.flowManager.matchMaking" grade + gpii.flowManager.processMatch.priorities = { + // onReadyToMatch event signals start of process + preProcess: 100, + matchMakerDispatcher: 90 + // onMatchDone event fired to lifecycleManager or cloud-based settings endpoint + }; + + /* This component orchestrates the lifecycle for a component which assembles the raw materials required for the matchmaking + * process, invokes it, and distributes the results. These raw materials are the gpiiKey (of the user whose preferences are to + * be fetched), the device information for the current platform, and the registry of solutions which are available to be + * invoked. It is a request-scoped grade and intended as a mixin for a matchmaking request such as gpii.lifecycleManager.loginRequest + * and gpii.flowManager.cloudBased.settings.get.handler. + * This grade is used in the following places: + * UserLogonStateChange.js - where it coordinates the standard lifecycle for a user logging on to a local FlowManager + * CloudBasedFlowManager.js - where it coordinates the lifecycle for a user requesting settings from a cloud-based FlowManager + */ + fluid.defaults("gpii.flowManager.matchMaking", { + invokers: { + getPreferences: { + funcName: "gpii.flowManager.getPreferences", + args: ["{that}", "{flowManager}.prefsServerDataSource"] + }, + processMatch: { + funcName: "gpii.flowManager.processMatch", + args: [ "{that}", "{arguments}.0"] + // initial payload + }, + getSolutions: { + funcName: "gpii.flowManager.getSolutions", + args: [ "{flowManager}.solutionsRegistryDataSource", "{arguments}.0", "{that}.events.onSolutions", "{that}.events.onError"] + } + }, + events: { + // Four pre-requisites for the match process to begin + onGpiiKey: null, + onPreferences: null, + onDeviceContext: null, + onSolutions: null, + // The "pseudo-event" whose handlers govern the match processing chain + processMatch: null, + // Output of the matching process - listeners in derived grades + onMatchDone: null, + // Boiled event which initiates the match process + onReadyToMatch: { + events: { + preferences: "onPreferences", + deviceContext: "onDeviceContext", + solutions: "onSolutions" + }, + args: [{ + gpiiKey: "{that}.gpiiKey", + preferences: "{arguments}.preferences.0", + deviceContext: "{arguments}.deviceContext.0", + solutionsRegistryEntries: "{arguments}.solutions.0", + fullSolutionsRegistry: "{arguments}.solutions.1" + }] + } + }, + listeners: { + "onGpiiKey.setGpiiKey": { + listener: "gpii.flowManager.setGpiiKey", + args: ["{that}", "{arguments}.0"] + }, + "onGpiiKey.getPreferences": { + func: "{that}.getPreferences", + priority: "after:setGpiiKey" + }, + "onDeviceContext.getSolutions": "{that}.getSolutions", + processMatch: [{ // Definition of the MatchMaking processing chain + priority: gpii.flowManager.processMatch.priorities.preProcess, + namespace: "preProcess", + listener: "gpii.matchMakerFramework.utils.preProcess" + }, { + priority: gpii.flowManager.processMatch.priorities.matchMakerDispatcher, + namespace: "matchMakerDispatcher", + listener: "{flowManager}.matchMakerFramework.matchMakerDispatcher" + }], + "onReadyToMatch.processMatch": "{that}.processMatch" + } + }); + + gpii.flowManager.setGpiiKey = function (that, gpiiKey) { + that.gpiiKey = gpiiKey; + }; +})(); diff --git a/gpii/node_modules/flowManager/src/PSPChannel.js b/gpii/node_modules/flowManager/src/PSPChannel.js new file mode 100644 index 000000000..ba5c9a6e9 --- /dev/null +++ b/gpii/node_modules/flowManager/src/PSPChannel.js @@ -0,0 +1,442 @@ +"use strict"; + + +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.flowManager"); + +// A mixin grade applied to the lifecycleManager's session by the pspChannel +fluid.defaults("gpii.pspChannel.sessionBinder", { + modelRelay: { + pspChannel: { + source: "", + target: "{flowManager}.pspChannel.model", + singleTransform: { + type: "gpii.pspChannel.sessionToPSP", + pspChannel: "{pspChannel}" + }, + // compensate for FLUID-6194 + backward: "never", + forward: { + // avoid notifying the client for the init of LifecycleManagerSession, simplifies test and clients + excludeSource: "init" + } + } + }, + modelListeners: { + // When PSP channel receives a setting change request from PSP clients, this model listener informs flowManager + // to apply this new setting. + updatePreferences: { + path: "{flowManager}.pspChannel.model.settingControls", + includeSource: "PSP", + funcName: "{flowManager}.lifecycleManager.applyPreferences", + args: ["{change}.value", "{pspChannel}.events.preferencesAppliedLocal"] + }, + updatePrefsSetName: { + path: "{flowManager}.pspChannel.model.activePrefsSetName", + includeSource: "PSP", + funcName: "gpii.pspChannel.updatePrefsSetName", + args: ["{flowManager}.lifecycleManager", "{pspChannel}", "{change}.value"] + }, + savePreferences: { // explicit save when the save button is clicked + path: "{flowManager}.pspChannel.model.saveButtonClickCount", + includeSource: "PSP", + listener: "gpii.pspChannel.savePreferences", + args: ["{flowManager}", "{lifecycleManager}", "{pspChannel}.events.preferencesAppliedLocal"] + } + } +}); + +/** + * The PSP channel maintains its own model of the PSP state, it contains the following data: + * + * "gpiiKey": "", // currently logged in GPII key - { type: "string" } + * "activePrefsSetName": "", // currently applied preferences set - { type: "string" } + * "settingControls": { // any number of preferences with values and schemas + * "": { // settings URI, note that '.'s are interpreted as paths unless escaped + * "value": , // the value that the given preference has + * "schema": { ... } // schema for the preference + * }, + * "": { .. }, + * "": { .. } + * }, + * "preferences": { // Used for listing available preference sets and name of preference set + * "name": "My preference set", + * "contexts": { + * "": { // the name/id of the preferences set + * "name": "" // name to be displayed in the PSP + * } + * "": {...}, + * "": {...} + * } + * }, + * saveButtonClickCount // increments by 1 every time the save button is clicked + */ +fluid.defaults("gpii.pspChannel", { + gradeNames: ["fluid.modelComponent"], + settingsSchemaPath: "%gpii-universal/build/schemas/solution-schema-codex.json", + settingsSchema: "@expand:fluid.require({that}.options.settingsSchemaPath)", + members: { + outputBlocked: null, + // Holds schemas containing default preferences with values defined in the reset to default file. + // This static value is populated the first time handling a key in request. + defaultSettingControls: null + }, + listeners: { + "{lifecycleManager}.events.onSessionStop": { + namespace: "pspChannel", + funcName: "gpii.pspChannel.sessionStop", + args: "{pspChannel}" + } + }, + events: { + // Fired when preferences are applied to the local computer. Its main current purpose is to compose the aggregate event "preferencesApplied" + preferencesAppliedLocal: null, + // Fired when preferences are applied to the local computer and saved to the cloud. + preferencesApplied: { + events: { + preferencesAppliedLocal: "preferencesAppliedLocal", + preferencesSavedSuccess: "{gpii.flowManager}.events.preferencesSavedSuccess" + } + }, + // Fired when the read of a preference completes successfully + preferenceReadSuccess: null, + // Fired when the read of a preference fails + preferenceReadFail: null + }, + invokers: { + generateDefaultSettingControls: { + funcName: "gpii.pspChannel.generateDefaultSettingControls", + args: ["{flowManager}.defaultSettingsDataPromise", "{that}.options.settingsSchema"] + } + } +}); + +/** + * Calculate the schemas containing preferences with their default values from the reset to default file. + * @param {Object} defaultSettingsDataPromise - The value of flowManager.defaultSettingsDataPromise. + * @param {Object} schemas - The content of build/schemas/solution-schema-codex.json. + * @return {Object} - A collection of schemas for each preference defined in the reset to default file. + * Each default preference value defined in the reset to default file are populated into the corresponding "default" + * field in the output schema. An example: + * { + * "http://registry\\.gpii\\.net/common/cursorSize": { + * "schema": { + * "title": "Cursor Size", + * "description": "Cursor size", + * "type": "number", + * "default": 0.8, // This default value is from the reset to default file + * "minimum": 0, + * "maximum": 1, + * "multipleOf": 0.1 + * }, + * "liveness": "live" + * }, + * ... + * } + */ +gpii.pspChannel.generateDefaultSettingControls = function (defaultSettingsDataPromise, schemas) { + var defaultSettingControls; + + defaultSettingsDataPromise.then(function (defaultSettingsData) { + var defaultPreferences = fluid.get(defaultSettingsData.defaultSettings, ["contexts", "gpii-default", "preferences"]); + defaultSettingControls = {}; + + // Set the setting default values from reset to standard file to schema.default field for each setting in + // "settingControls" block. + fluid.each(defaultPreferences, function (defaultPrefsVal, defaultPrefsKey) { + var prefsInfo = gpii.pspChannel.getPreferenceInfo(schemas, defaultPrefsKey, defaultPrefsVal); + var schema = prefsInfo.schema; + if (schema) { + fluid.set(schema, ["default"], prefsInfo.prefsValue); + gpii.pspChannel.emitSettingControl(defaultSettingControls, schema, undefined, prefsInfo.prefsKeySegs, "live", prefsInfo.solutionName); + } + }); + }); + + // Although defaultSettingControls is populated asynchronously in the promise callback function above, in practice, + // we know that if the architecture is organised properly, the interlock that prevents a PSP request before + // defaultSettingsDataPromise is resolved should ensure that this return is actually synchronous. The check here + // is to ensure that defaultSettingControls has been initialised before the function returns. + if (defaultSettingControls) { + return defaultSettingControls; + } else { + fluid.fail("PSPChannel: defaultSettingControls is not populated in time."); + } +}; + +/** + * Get the preference information required for generating the output for PSPChannel clients. + * @param {Object} solutionSchemas - The content of "build/schemas/solution-schema-codex.json" + * @param {String} prefsKey - A common term or an application term. + * @param {Primitive|Object} prefsValue - The value of the prefsKey. It could be the actual preference value + * or an object containing a nested preference path with the value. + * @return {Object} - The preference information. + * + * @typedef {Object} returnObject + * @property {Object} schema - The schema of the input preference. + * @property {String} solutionName - The solution name for the input preference. + * @property {String} presentedCommonTerm - The common term that is presented as the prefsKey or in the prefsValue. + * Returns undefined if no common term is presented. + * @property {Primitive|Object} prefsValue - The actual preference value. + * @property {Boolean} prefsKeySegs - The segments of preference keys. It can contain 1 or 2 elements depending on + * if there's a nested preference path. + */ +gpii.pspChannel.getPreferenceInfo = function (solutionSchemas, prefsKey, prefsValue) { + if (!solutionSchemas || !prefsKey || prefsValue === undefined) { + return undefined; + }; + + var thisSolutionSchema = fluid.get(solutionSchemas, [prefsKey]); + + if (!thisSolutionSchema) { + return {}; + } + + // Actual handling when the schema for the input preference is found. + var togo = {}; + if (gpii.matchMakerFramework.utils.isApplicationTerm(prefsKey)) { + var solutionName = fluid.get(thisSolutionSchema, ["title"]); + // Handle application terms + fluid.each(prefsValue, function (val, innerPrefsKey) { + var schema, presentedCommonTerm; + if (gpii.matchMakerFramework.utils.isCommonTerm(innerPrefsKey)) { + // When the inner pref key is a common term, use the schema of this common term. + // TODO: This if block should be removed at fixing https://issues.gpii.net/browse/GPII-4063 + // when the support for "common terms scoped within applications" discontinues. + schema = fluid.get(solutionSchemas, [innerPrefsKey]); + delete schema.$schema; + presentedCommonTerm = innerPrefsKey; + } else { + // When the inner pref key is not a common term, find the schema of the corresponding inner key + // within th application schema. + schema = fluid.get(thisSolutionSchema, ["properties", innerPrefsKey]); + presentedCommonTerm = undefined; + } + togo = { + schema: schema, + solutionName: solutionName, + presentedCommonTerm: presentedCommonTerm, + prefsValue: val, + prefsKeySegs: [prefsKey, innerPrefsKey] + }; + }); + } else { + // Handle common terms + var schema = fluid.get(solutionSchemas, [prefsKey]); + delete schema.$schema; + togo = { + schema: schema, + solutionName: fluid.get(solutionSchemas, [prefsKey, "title"]), + presentedCommonTerm: prefsKey, + prefsValue: prefsValue, + prefsKeySegs: [prefsKey] + }; + } + return togo; +}; + +gpii.pspChannel.updatePrefsSetName = function (lifecycleManager, pspChannel, newPrefsSetName) { + fluid.log("Received prefsSet update from PSP UI of ", newPrefsSetName); + // Abominable hack to avoid confusing client with numerous update messages. The ChangeApplier should really support + // "manifest transactions" / "vertical transactions" + pspChannel.outputBlocked = fluid.promise(); + var clearBlock = function () { + delete pspChannel.outputBlocked; + }; + pspChannel.outputBlocked.then(clearBlock, clearBlock); + pspChannel.applier.change([], null, "DELETE"); + var promise = lifecycleManager.prefsSetNameChanged(newPrefsSetName); + if (promise) { + fluid.promise.follow(promise, pspChannel.outputBlocked); + } else { + pspChannel.outputBlocked.resolve(); + } +}; + +gpii.pspChannel.filterSolution = function (solution) { + return { + name: solution.name, + settingsHandlers: fluid.transform(solution.settingsHandlers, function (oneHandler) { + return fluid.filterKeys(oneHandler, ["supportedSettings"]); + }) + }; +}; + +// Explicit save when the save button is clicked +gpii.pspChannel.savePreferences = function (flowManager, lifecycleManager, preferencesAppliedLocalEvent) { + // Grab current active lifecycle manager session + var userSession = lifecycleManager.getSession(); + var gpiiKey = userSession.model.gpiiKey; + var preferences = userSession.model.preferences; + fluid.log("PSPChannel: explicit save for gpiiKey (", gpiiKey, "), with preferences: ", preferences); + flowManager.savePreferences(gpiiKey, preferences); + + // At explicit save, preferencesAppliedLocal event will not be triggered. This event is only triggered when + // there is setting change that needs to be applied to the local computer. Directly firing this event is to + // trigger its parent aggregate event "preferencesApplied" to be fired. + preferencesAppliedLocalEvent.fire(); +}; + +/** Emit an entry in the `settingControls` block for a single setting. + * + * @param {Object} settingControls - *This object will be modified by the function's action* One top-level member + * will be added to this object, with a key given by composing the argument `keySegs`. The value of the + * member will be a structure {SettingControl} consisting of: + * @member {Any} value - The actual value of the corresponding setting. + * @member {JSONSchema} schema - A JSON schema structure describing the value space of the setting. + * @member {String} [solutionName] - [optional] The solution name to which this setting is allocated in the preferences + * document, if there is one. If it is defined as a top-level common term, this member will be omitted. + * @member {String} liveness - The liveness value of the setting. + */ + +gpii.pspChannel.emitSettingControl = function (settingControls, schema, prefVal, keySegs, liveness, solutionName) { + var fullKey = fluid.pathUtil.composeSegments.apply(null, keySegs); + if (schema) { + settingControls[fullKey] = { + schema: schema, + liveness: liveness + }; + } + if (solutionName) { + fluid.set(settingControls, [fullKey, "solutionName"], solutionName); + } + if (prefVal !== null && prefVal !== undefined) { + fluid.set(settingControls, [fullKey, "value"], prefVal); + } +}; + +/** Transduces the session model held for the currently logged-on user in the LifecycleManager's session into the model + * structure which is suitable for shipping to the PSP over its bus. Each setting control output in the section + * `settingControls` will be dumped using the utility `gpii.pspChannel.emitSettingControl`. This is a model relay + * function which is run continuously as the session's model is updated. + * + * @param {Object} model - The LifecycleManager's session model. + * @param {ModelTransformSpec} transformSpec - The model transformation spec. + * @return {Object} The output model suitable for shipping to the PSP, including top-level members: + * - {Object} settingControls - A hash keyed by preference path, whose values are {SettingControl} objects. + * - {Object} preferences - A filtered skeleton of the user's preferences document, just containing the names of preferences sets (`contexts`). + */ +gpii.pspChannel.sessionToPSP = function (model, transformSpec) { + var that = transformSpec.pspChannel; + var schemas = that.options.settingsSchema; + + // that.defaultSettingControls is generated at the system startup when "noUser" logs in. At then, all default + // settings data are ready. + if (!that.defaultSettingControls) { + that.defaultSettingControls = that.generateDefaultSettingControls(); + } + + var outModel = fluid.filterKeys(model, ["gpiiKey", "activePrefsSetName"]); + var settingControls = {}; + var activePreferences = fluid.get(model, ["currentPreferences"]); + var activeSolutionIds, activeSolutions; + var applications = fluid.get(model, "activeConfiguration.inferredConfiguration.applications"); + if (applications) { + activeSolutionIds = Object.keys(applications); + activeSolutions = fluid.filterKeys(model.solutionsRegistryEntries, activeSolutionIds); + } + + fluid.each(activePreferences, function (prefsVal, prefsKey) { + var prefsInfo = gpii.pspChannel.getPreferenceInfo(schemas, prefsKey, prefsVal); + var liveness = gpii.matchMakerFramework.utils.getLeastLiveness(activeSolutions, prefsInfo.presentedCommonTerm); + gpii.pspChannel.emitSettingControl(settingControls, prefsInfo.schema, prefsInfo.prefsValue, prefsInfo.prefsKeySegs, liveness, prefsInfo.solutionName); + }); + outModel.settingControls = fluid.extend(true, {}, settingControls, that.defaultSettingControls || {}); + outModel.preferences = { + name: fluid.get(model, "preferences.name"), + contexts: fluid.transform(fluid.get(model, "preferences.contexts"), function (contextVal) { + return fluid.filterKeys(contextVal, ["name"]); + }) + }; + return outModel; +}; + + +gpii.pspChannel.modelChangeListener = function (handler, pspChannel, value, oldValue, path, transaction) { + fluid.log("PSPChannel's PSP-facing modelChangeListener, sources are ", fluid.keys(transaction.sources)); + if (!transaction.sources.PSP && !transaction.sources.SessionCleanup) { + fluid.log("Model change source is not PSP - candidate for update message"); + if (pspChannel.outputBlocked) { + // Ensure that we queue just a single outgoing message for when the channel unblocks + if (!pspChannel.outputBlocked.queued) { + pspChannel.outputBlocked.queued = true; + pspChannel.outputBlocked.then(function () { + fluid.log("PSPChannel sending unblocked full update message", JSON.stringify(pspChannel.model, null, 2)); + handler.sendTypedMessage("modelChanged", {path: [], type: "ADD", value: pspChannel.model}); + }); + } + } else { + var changes = fluid.modelPairToChanges(value, oldValue); + var hasDeletion = fluid.find(changes, function (change) { + return change.type === "DELETE"; + }); + if (hasDeletion) { + changes.forEach(function (change) { + handler.sendTypedMessage("modelChanged", change); + }); + } else { + handler.sendTypedMessage("modelChanged", {path: [], type: "ADD", value: value}); + } + } + } +}; + +gpii.pspChannel.sessionStop = function (pspChannel) { + pspChannel.applier.change("", null, "DELETE"); +}; + +fluid.defaults("gpii.pspChannel.handler", { + gradeNames: ["kettle.request.ws"], + invokers: { + modelChangeListener: { + funcName: "gpii.pspChannel.modelChangeListener", + args: ["{that}", "{pspChannel}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "{arguments}.4"] + // value, oldValue, pathSegs, transaction: http://docs.fluidproject.org/infusion/development/ChangeApplierAPI.html#programmatic-style-for-listening-to-changes + } + }, + listeners: { + onBindWs: { + funcName: "gpii.pspChannel.bindWs", + args: ["{that}", "{pspChannel}"] + }, + "{pspChannel}.events.preferencesApplied": { + funcName: "{that}.sendTypedMessage", + args: ["preferencesApplied"] + }, + "{pspChannel}.events.preferenceReadSuccess": { + funcName: "{that}.sendTypedMessage", + args: ["preferenceReadSuccess"] + }, + "{pspChannel}.events.preferenceReadFail": { + funcName: "{that}.sendTypedMessage", + args: ["preferenceReadFail"] + }, + onReceiveMessage: { + funcName: "gpii.pspChannel.receiveMessage", + args: ["{arguments}.1", "{pspChannel}", "{lifecycleManager}"] + }, + "onDestroy.unbindModel": { + func: "{pspChannel}.applier.modelChanged.removeListener", + args: ["{that}.id"] + } + } +}); + +gpii.pspChannel.bindWs = function (handler, pspChannel) { + pspChannel.applier.modelChanged.addListener("", handler.modelChangeListener, handler.id); + // Note that this is inconsistent with the Nexus' protocol, but is more correct - for example if the model consists + // purely of a primitive or is undefined, the initial Nexus message will break + handler.sendTypedMessage("modelChanged", {path: [], type: "ADD", value: pspChannel.model}); +}; + +gpii.pspChannel.receiveMessage = function (message, pspChannel, lifecycleManager) { + fluid.log("pspChannel received a message: ", message); + if (message.type === "modelChanged") { + pspChannel.applier.change("", message.value, "ADD", "PSP"); + } + if (message.type === "pullModel") { + lifecycleManager.readPreferences(message.value.settingControls, pspChannel.events.preferenceReadSuccess, pspChannel.events.preferenceReadFail); + } +}; diff --git a/gpii/node_modules/flowManager/src/PrefsServerDataSource.js b/gpii/node_modules/flowManager/src/PrefsServerDataSource.js new file mode 100644 index 000000000..bfe06573b --- /dev/null +++ b/gpii/node_modules/flowManager/src/PrefsServerDataSource.js @@ -0,0 +1,180 @@ +/** + * GPII Preferences Server Data Source + * + * Copyright 2018 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.flowManager.prefsServerDataSource"); + +// "gpii.flowManager.prefsServerDataSource" is used by the cloud based flow manager to communicate with the +// preferences server. +fluid.defaults("gpii.flowManager.prefsServerDataSource", { + gradeNames: ["fluid.component"], + + // This option should be distributed down from, e.g., gpii.flowManager.config.cloud.base + prefsServerURL: null, + + preferencesUrlTemplate: "%prefsServerURL/preferences/%gpiiKey?merge=%merge", + prefsServerReadyUrlTemplate: "%prefsServerURL/ready", + + // The URLs below are calculated from the flow manager base URL and their templates. + preferencesUrl: { + expander: { + funcName: "fluid.stringTemplate", + args: ["{that}.options.preferencesUrlTemplate", { + prefsServerURL: "{that}.options.prefsServerURL" + }] + } + }, + prefsServerHealthUrl: { + expander: { + funcName: "fluid.stringTemplate", + args: ["{that}.options.prefsServerReadyUrlTemplate", { + prefsServerURL: "{that}.options.prefsServerURL" + }] + } + }, + + distributeOptions: { + preferencesUrl: { + source: "{that}.options.preferencesUrl", + target: "{that > preferencesDataSourceImpl}.options.url" + }, + prefsServerHealthUrl: { + source: "{that}.options.prefsServerHealthUrl", + target: "{that > prefsServerHealthImpl}.options.url" + } + }, + + components: { + preferencesDataSourceImpl: { + type: "kettle.dataSource.URL", + options: { + termMap: { + gpiiKey: "%gpiiKey" + }, + writable: true, + writeMethod: "PUT" + } + }, + prefsServerHealthImpl: { + type: "kettle.dataSource.URL" + } + }, + invokers: { + // Retrieve preferences + get: { + funcName: "gpii.flowManager.prefsServerDataSource.get", + args: ["{that}", "{arguments}.0"] + // gpiiKey + }, + // Update preferences + update: { + funcName: "gpii.flowManager.prefsServerDataSource.update", + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] + // gpiiKey, merge, preferences + }, + // Create GPII key and associated preferences + create: { + funcName: "gpii.flowManager.prefsServerDataSource.create", + args: ["{that}", "{arguments}.0", "{arguments}.1"] + // gpiiKey, preferences + }, + // Check the liveness of the preferences server: running with proper database connection + isLive: { + funcName: "gpii.flowManager.prefsServerDataSource.isLive", + args: ["{prefsServerHealthImpl}"] + } + } +}); + +/** + * Retrieve user preferences from the preferences server for the given GPII key. + * @param {Component} that - An instance of gpii.flowManager.prefsServerDataSource. + * @param {String} gpiiKey - A GPII key. + * @return {Promise} A promise whose resolved value is the user preferences. + */ +gpii.flowManager.prefsServerDataSource.get = function (that, gpiiKey) { + return that.preferencesDataSourceImpl.get({ + gpiiKey: gpiiKey + }); +}; + +/** + * Update user preferences on the preferences server for the given GPII key. + * @param {Component} that - An instance of gpii.flowManager.prefsServerDataSource. + * @param {String} gpiiKey A GPII key. + * @param {Boolean} merge The flag of whether to merge the given preferences with the existing preferences saved + * in the preferences server. + * @param {Object} preferences The to-be-updated preferences. + * @return {Promise} A promise whose resolved value is the status of the update. + */ +gpii.flowManager.prefsServerDataSource.update = function (that, gpiiKey, merge, preferences) { + fluid.log("prefsServerDataSource, Merging preferences for GPII key (", gpiiKey , ") with preferences: ", preferences); + merge = merge || true; + var mergeStr = merge.toString(); + + var directModel = { + gpiiKey: gpiiKey, + merge: mergeStr + }; + + return that.preferencesDataSourceImpl.set(directModel, preferences, { + writeMethod: "PUT", + termMap: { + gpiiKey: "%gpiiKey", + merge: "%merge" + } + }); +}; + +/** + * Create the given GPII key and associated preferences on the preferences server. + * @param {Component} that - An instance of gpii.flowManager.prefsServerDataSource. + * @param {String} gpiiKey A GPII key. + * @param {Object} preferences The preferences associated with the GPII key. + * @return {Promise} A promise whose resolved value is the status of the create. + */ +gpii.flowManager.prefsServerDataSource.create = function (that, gpiiKey, preferences) { + fluid.log("prefsServerDataSource, Creating GPII key (", gpiiKey , ") and its preferences: ", preferences); + var directModel = { + gpiiKey: gpiiKey + }; + + return that.preferencesDataSourceImpl.set(directModel, preferences, { + writeMethod: "PUT", + termMap: { + gpiiKey: "%gpiiKey" + } + }); +}; + +/** + * Check the liveness of the preferences server: running with proper database connection. + * @param {Component} prefsServerHealthImpl - An instance of {gpii.flowManager.prefsServerDataSource}.prefsServerHealthImpl. + * @return {Promise} A promise whose resolved value is the liveness status of the preferences server. The resolved value is + * true if the server is alive. Otherwise, false. + */ +gpii.flowManager.prefsServerDataSource.isLive = function (prefsServerHealthImpl) { + var promiseTogo = fluid.promise(); + var isLivePromise = prefsServerHealthImpl.get(); + + isLivePromise.then(function (value) { // onResolve + promiseTogo.resolve(value); + }, function (value) { // onReject + promiseTogo.resolve(value); + }); + + return promiseTogo; +}; diff --git a/gpii/node_modules/flowManager/src/ReadyGetHandler.js b/gpii/node_modules/flowManager/src/ReadyGetHandler.js new file mode 100644 index 000000000..59171532f --- /dev/null +++ b/gpii/node_modules/flowManager/src/ReadyGetHandler.js @@ -0,0 +1,70 @@ +/*! +GPII Cloud Based Flow Manager Ready Check Handler + +Copyright 2018 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +// TODO: Discuss how/whether to test this. +fluid.defaults("gpii.flowManager.cloudBased.ready.handler", { + gradeNames: ["kettle.request.http"], + invokers: { + handleRequest: { + funcName: "gpii.flowManager.cloudBased.ready.handler.isLive", + args: ["{gpii.flowManager.cloudBased}.prefsServerDataSource", "{that}"] + } + } +}); + +gpii.flowManager.cloudBased.ready.handler.reportError = function (that, prefsServerURL) { + fluid.log ("Error connecting to Preferences Sever: '" + prefsServerURL + "'"); + that.events.onError.fire({ + isError: true, + message: "Error connecting to Preferences Server", + statusCode: 503 + }); +}; + +gpii.flowManager.cloudBased.ready.handler.isSameHost = function (flowManagerHost, prefsServerHost) { + // Convert "http://localhost:8081" to "127.0.0.1:8081" + prefsServerHost = prefsServerHost.replace(/http:\/\/|https:\/\//, ""); + prefsServerHost = prefsServerHost.replace("localhost", "127.0.0.1"); + + // Remove the trailing slash if any + prefsServerHost = prefsServerHost.replace(/\/$/, ""); + flowManagerHost = flowManagerHost.replace(/\/$/, ""); + return flowManagerHost === prefsServerHost; +}; + +gpii.flowManager.cloudBased.ready.handler.isLive = function (prefsServerDataSource, that) { + var isSameHost = gpii.flowManager.cloudBased.ready.handler.isSameHost(that.req.headers.host, prefsServerDataSource.options.prefsServerURL); + + // Reaching code here means the cloud based flow manager and the preferences server are running as separate kettle servers. + // If the given preferences server host is same as the host of the cloud based flow manager, sending the liveness check to + // the preferences server will result in the infinite loop. This case occurs when the preferences server host defined in the + // config file is wrong, so report "preferences server is not alive" error. + if (isSameHost) { + gpii.flowManager.cloudBased.ready.handler.reportError(that, prefsServerDataSource.options.prefsServerURL); + } else { + var isLivePromise = prefsServerDataSource.isLive(); + isLivePromise.then(function (result) { + if (result.isReady) { + that.events.onSuccess.fire(result); + } else { + gpii.flowManager.cloudBased.ready.handler.reportError(that, prefsServerDataSource.options.prefsServerURL); + } + }, function () { + gpii.flowManager.cloudBased.ready.handler.reportError(that, prefsServerDataSource.options.prefsServerURL); + }); + } +}; diff --git a/gpii/node_modules/flowManager/src/RevisionGetHandler.js b/gpii/node_modules/flowManager/src/RevisionGetHandler.js new file mode 100644 index 000000000..0cfbd57d7 --- /dev/null +++ b/gpii/node_modules/flowManager/src/RevisionGetHandler.js @@ -0,0 +1,55 @@ +/*! +GPII GET /revision handler used by the Cloud Based Flowmanager + +Copyright 2020 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fs = require("fs"), + fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.defaults("gpii.flowManager.cloudBased.gpiiRevision.handler", { + gradeNames: ["kettle.request.http"], + invokers: { + handleRequest: { + funcName: "gpii.flowManager.cloudBased.gpiiRevision.handler.handleRequest", + args: ["{gpii.flowManager.cloudBased}.options.gpiiRevisionPath", "{that}"] + } + } +}); + +gpii.flowManager.cloudBased.gpiiRevision.handler.reportError = function (that, filePath, errMsg) { + fluid.log ("Error retrieving full git revision: '" + filePath + "'"); + that.events.onError.fire({ + isError: true, + message: "Error retrieving full git revision: " + errMsg, + statusCode: 404 + }); +}; + +gpii.flowManager.cloudBased.gpiiRevision.handler.handleRequest = function (gpiiRevisionPath, that) { + var errMsg; + var fullPath = fluid.module.resolvePath(gpiiRevisionPath); + var result = {sha256: ""}; + try { + result = JSON.parse(fs.readFileSync(fullPath, "utf-8")); + if (result.sha256.length === 0) { + errMsg = "Missing revision value"; + } + } catch (e) { + errMsg = e.message; + }; + if (!errMsg) { + that.events.onSuccess.fire(result); + } else { + gpii.flowManager.cloudBased.gpiiRevision.handler.reportError(that, fullPath, errMsg); + } +}; diff --git a/gpii/node_modules/flowManager/src/SessionAware.js b/gpii/node_modules/flowManager/src/SessionAware.js new file mode 100644 index 000000000..9b0b2f10c --- /dev/null +++ b/gpii/node_modules/flowManager/src/SessionAware.js @@ -0,0 +1,58 @@ +/** + * GPII Session Aware + * + * Copyright 2012 OCAD University + * Copyright 2015 Raising the Floor - International + * Copyright 2018 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +/* eslint-env browser */ +/* eslint strict: ["error", "function"] */ + +(function () { + + "use strict"; + + var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + + fluid.registerNamespace("gpii.lifecycleManager"); + + /** A mixin request grade for requests which require an active user session to do their work. Exposes a method + * withSession which will acquire the session tokens and supply them as an argument or else fail if none is available. + */ + fluid.defaults("gpii.flowManager.sessionAware", { + invokers: { + withSession: { + funcName: "gpii.flowManager.sessionAware.withSession", + args: ["{that}", "{lifecycleManager}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] + } + } + }); + + gpii.flowManager.sessionAware.withSession = function (that, lifecycleManager, onSuccess, failMessage, onError) { + onError = onError || that.events.onError.fire; + var userSession = lifecycleManager.getSession(); + if (!userSession.model.gpiiKey) { + failMessage = failMessage || "Error handling request which required active session, but none was active"; + onError({ + isError: true, + statusCode: 401, + message: failMessage + }); + } else { + onSuccess(userSession, userSession.model.gpiiKey); + } + }; + +})(); diff --git a/gpii/node_modules/flowManager/src/Settings.js b/gpii/node_modules/flowManager/src/Settings.js deleted file mode 100644 index c07f7cb22..000000000 --- a/gpii/node_modules/flowManager/src/Settings.js +++ /dev/null @@ -1,86 +0,0 @@ -/** - GPII Settings Component - - Copyright 2013 OCAD University - - Licensed under the New BSD license. You may not use this file except in - compliance with this License. - - You may obtain a copy of the License at - https://github.com/gpii/universal/LICENSE.txt -*/ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - $ = fluid.registerNamespace("jQuery"), - request = fluid.registerNamespace("gpii.request.flowManager"); - - request.onSettings = function (token, handler, device) { - try { - device = JSON.parse(device); - } catch (ex) { - handler.requestProxy.events.onError.fire({ - isError: true, - message: "Cloud based flow manager requires device information." - }); - return; - } finally { - handler.device = device; - handler.userToken = token; - handler.events.onUserListener.fire(token); - } - }; - - request.processMatch = function (match) { - var settings = {}; - fluid.each(match, function processSolution(solution) { - if (!solution.settingsHandlers) { - return; - } - fluid.each(solution.settingsHandlers, - function findSettings(settingsHandler) { - if (!settingsHandler.settings) { - return; - } - settings[solution.id] = settingsHandler.settings; - } - ); - }); - return settings; - }; - - request.getSettings = function (event, match) { - request.logAndNotify( - "Flow manager matched the following settings/solutions: ", event, - request.processMatch)(match); - }; - - fluid.defaults("kettle.requests.request.handler.settings", { - gradeNames: ["fluid.littleComponent", "gpii.request.flowManager.token", "autoInit"], - invokers: { - onMatchHandler: { - funcName: "gpii.request.flowManager.getSettings", - args: ["{requestProxy}.events.onSuccess", "{arguments}.0"] - }, - handle: { - funcName: "gpii.request.flowManager.onSettings", - args: ["{request}.req.params.token", "{that}", "{request}.req.params.device"], - dynamic: true - } - }, - listeners: { - onPreferences: { - listener: "{that}.events.onReadyToMatch.fire", - args: [ - "{arguments}.0", - "{that}.device" - ] - }, - onMatch: "{that}.onMatchHandler" - } - }); - -})(); diff --git a/gpii/node_modules/flowManager/src/SettingsDataSource.js b/gpii/node_modules/flowManager/src/SettingsDataSource.js new file mode 100644 index 000000000..496b4a8ef --- /dev/null +++ b/gpii/node_modules/flowManager/src/SettingsDataSource.js @@ -0,0 +1,243 @@ +/** + * GPII Untrusted Settings Data Source + * + * Copyright 2017 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +require("accessRequester"); + +// To reuse gpii.oauth2.getExpiresIn() and gpii.oauth2.getTimestampExpires() +fluid.require("%gpii-universal/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Utilities.js"); + +// gpii.flowManager.settingsDataSource provides a get() API that returns a promise +// whose resolved value is user settings. The internal steps performed by this API: +// 1. check the access token requested last time. If it's still valid, use it to request then return user settings; +// 2. If the access token has not been requested or it has expired, request an access token via accessRequester subcomponent; +// 3. Save the received access token as member options; +// 4. Use the access token to request then return user settings. +fluid.defaults("gpii.flowManager.settingsDataSource", { + gradeNames: ["fluid.component"], + + // This option should be distributed down from, e.g., gpii.flowManager.config.untrusted.base + cloudURL: null, + + settingsGetUrlTemplate: "%cloudURL/%gpiiKey/settings/%device", + settingsPutUrlTemplate: "%cloudURL/%gpiiKey/settings", + accessTokenUrlTemplate: "%cloudURL/access_token", + + // The URLs below are calculated from the flow manager base URL and their templates. + settingsGetUrl: { + expander: { + funcName: "fluid.stringTemplate", + args: ["{that}.options.settingsGetUrlTemplate", { + cloudURL: "{that}.options.cloudURL" + }] + } + }, + settingsPutUrl: { + expander: { + funcName: "fluid.stringTemplate", + args: ["{that}.options.settingsPutUrlTemplate", { + cloudURL: "{that}.options.cloudURL" + }] + } + }, + accessTokenUrl: { + expander: { + funcName: "fluid.stringTemplate", + args: ["{that}.options.accessTokenUrlTemplate", { + cloudURL: "{that}.options.cloudURL" + }] + } + }, + + // TODO: Reading the client credential from the file system is a temporary solution. This option + // should be removed once a proper access requester is in place (https://issues.gpii.net/browse/GPII-2436). + clientCredentialFilePath: null, + + // The minimum number of seconds of the lfe time of an access token for it to continuing to be used. + minAccessTokenLifeTimeInSecond: 10, + distributeOptions: { + settingsGetUrl: { + source: "{that}.options.settingsGetUrl", + target: "{that > settingsDataSourceGetImpl}.options.url" + }, + settingsPutUrl: { + source: "{that}.options.settingsPutUrl", + target: "{that > settingsDataSourcePutImpl}.options.url" + }, + accessTokenUrl: { + source: "{that}.options.accessTokenUrl", + target: "{that > accessRequester}.options.url" + }, + clientCredentialFilePath: { + source: "{that}.options.clientCredentialFilePath", + target: "{that clientCredentialDataSource}.options.path" + } + }, + + members: { + accessTokens: { + // To keep track of the most recent access token and its expiresIn timestamp for each GPII key. The structure looks like: + // "gpiiKey1": { + // accessToken: "a-accessToken-value", + // timestampExpires: "an-timestamp-the-accessToken-expires" + // } + // ... + } + }, + components: { + settingsDataSourceGetImpl: { + type: "kettle.dataSource.URL", + options: { + // url: distributed down from the parent component gpii.flowManager.settingsDataSource + termMap: { + "gpiiKey": "%gpiiKey", + "device": "%device" + } + } + }, + settingsDataSourcePutImpl: { + type: "kettle.dataSource.URL", + options: { + // url: distributed down from the parent component gpii.flowManager.settingsDataSource + termMap: { + "gpiiKey": "%gpiiKey" + }, + writable: true, + writeMethod: "PUT" + } + }, + accessRequester: { + type: "gpii.accessRequester", + options: { + clientCredentialDataSourceGrade: "gpii.accessRequester.clientCredentialDataSource.file" + } + } + }, + invokers: { + get: { + funcName: "gpii.flowManager.settingsDataSource.get", + args: ["{that}", "{arguments}.0", "{arguments}.1"] + // gpiiKey, device + }, + set: { + funcName: "gpii.flowManager.settingsDataSource.set", + args: ["{that}", "{arguments}.0", "{arguments}.1"] + // gpiiKey, preferences + }, + save: { + funcName: "gpii.flowManager.settingsDataSource.save", + args: ["{that}.accessTokens", "{arguments}.0", "{arguments}.1", "{arguments}.2"] + // gpiiKey, accessToken, timestampExpires + } + } +}); + +/** + * Retrieve user settings from the cloud using the access token requested for the keyed in GPII key. + * @param {Component} that - An instance of gpii.flowManager.settingsDataSource. + * @param {String} gpiiKey - A GPII key. + * @param {Object} device - The device information provided by the device reporter. + * @return {Promise} A promise whose resolved value is the user settings. + */ +gpii.flowManager.settingsDataSource.get = function (that, gpiiKey, device) { + var accessTokenPromise = gpii.flowManager.settingsDataSource.findValidAccessToken(that, gpiiKey); + var promiseTogo = fluid.promise(); + + accessTokenPromise.then(function (accessToken) { + var settingsPromise = that.settingsDataSourceGetImpl.get({ + gpiiKey: gpiiKey, + device: JSON.stringify(device) + }, { + headers: { + "Authorization": "Bearer " + accessToken + } + }); + fluid.promise.follow(settingsPromise, promiseTogo); + }, function (err) { + promiseTogo.reject(err); + }); + + return promiseTogo; +}; + +/** + * Update user preferences to the cloud using the access token requested for the keyed in GPII key. + * @param {Component} that - An instance of gpii.flowManager.settingsDataSource. + * @param {String} gpiiKey A GPII key. + * @param {Object} preferences The to-be-updated preferences. + * @return {Promise} A promise whose resolved value is the status of the update. + */ +gpii.flowManager.settingsDataSource.set = function (that, gpiiKey, preferences) { + var accessTokenPromise = gpii.flowManager.settingsDataSource.findValidAccessToken(that, gpiiKey); + var promiseTogo = fluid.promise(); + + accessTokenPromise.then(function (accessToken) { + var updatePromise = that.settingsDataSourcePutImpl.set({ + gpiiKey: gpiiKey + }, preferences, { + headers: { + "Authorization": "Bearer " + accessToken + } + }); + fluid.promise.follow(updatePromise, promiseTogo); + }, function (err) { + promiseTogo.reject(err); + }); + + return promiseTogo; +}; + +/** + * Find a valid access token. It first checks the saved access token for the keyed in GPII key, + * If it has expired, request and return a new one from the cloud, otherwise, return the saved access token. + * @param {Component} that - An instance of gpii.flowManager.settingsDataSource. + * @param {String} gpiiKey - A GPII key. + * @return {Promise} A promise whose resolved value is a valid access token. + */ +gpii.flowManager.settingsDataSource.findValidAccessToken = function (that, gpiiKey) { + var allAccessTokens = that.accessTokens; + + var accessToken = fluid.get(allAccessTokens, [gpiiKey, "accessToken"]); + var expiresIn = gpii.oauth2.getExpiresIn(new Date(), fluid.get(allAccessTokens, [gpiiKey, "timestampExpires"])); + + // If the locally saved access token exists and is still valid, return it. + // Otherwise, request an new access token from the cloud and return. + // The new access token is saved locally for the continuing use. + if (!accessToken || !expiresIn || expiresIn < that.options.minAccessTokenLifeTimeInSecond) { + var accessTokenPromise = that.accessRequester.getAccessToken(gpiiKey); + var mapper = function (accessTokenObj) { + var accessTokenFromCloud = accessTokenObj.access_token; + var timestampExpiresFromCloud = gpii.oauth2.getTimestampExpires(new Date(), accessTokenObj.expiresIn); + that.save(gpiiKey, accessTokenFromCloud, timestampExpiresFromCloud); + return accessTokenFromCloud; + }; + return fluid.promise.map(accessTokenPromise, mapper); + } else { + return fluid.promise().resolve(accessToken); + } +}; + +/** + * Save the access token and its timestampExpires in the index of the GPII key for the next use. + * @param {Component} allAccessTokens - All saved access tokens + * @param {String} gpiiKey - The GPII key that the access token associates with. + * @param {String} accessToken - The access token to be saved. + * @param {String} timestampExpires - A timestampExpires to be saved. + */ +gpii.flowManager.settingsDataSource.save = function (allAccessTokens, gpiiKey, accessToken, timestampExpires) { + fluid.set(allAccessTokens, [gpiiKey, "accessToken"], accessToken); + fluid.set(allAccessTokens, [gpiiKey, "timestampExpires"], timestampExpires); +}; diff --git a/gpii/node_modules/flowManager/src/SettingsGetHandler.js b/gpii/node_modules/flowManager/src/SettingsGetHandler.js new file mode 100644 index 000000000..c0c0139c7 --- /dev/null +++ b/gpii/node_modules/flowManager/src/SettingsGetHandler.js @@ -0,0 +1,102 @@ +/* + * GPII Untrusted Settings Get Handler + * + * Copyright 2017 OCAD University + * Copyright 2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + kettle = require("kettle"), + gpii = fluid.registerNamespace("gpii"); + +// Get settings in the ontology of preferences from the cloud based flow manager. +// These settings are untransformed lifecycle instructions. +// See [an example of the return payload of this endpoint](https://github.com/GPII/gpii-payloads/blob/master/CloudBasedFlowManagerUntrustedSettings.md#user-content-return-payload). +fluid.defaults("gpii.flowManager.cloudBased.settings.get.handler", { + gradeNames: ["kettle.request.http", "gpii.flowManager.matchMaking"], + invokers: { + handleRequest: { + funcName: "gpii.flowManager.cloudBased.settings.get.handleRequest", + args: [ + "{that}", + "{gpii.flowManager.cloudBased}.authGrantFinder" + ] + }, + matchToSettings: { + funcName: "gpii.flowManager.cloudBased.matchToSettings", + args: ["{arguments}.0", "{that}.events.onSuccess", "CloudBased FlowManager"] + } + }, + listeners: { + onMatchDone: "{that}.matchToSettings" + } +}); + +gpii.flowManager.cloudBased.settings.get.handleRequest = function (that, authGrantFinder) { + // Verify the access token + var accessToken = gpii.oauth2.parseAccessTokenFromRequest(that.req); + var authorizationPromise = gpii.oauth2.getAuthorization(accessToken, authGrantFinder); + var gpiiKey = that.req.params.gpiiKey; + var deviceString = that.req.params.device; + + authorizationPromise.then(function (authorization) { + if (authorization && authorization.gpiiKey === gpiiKey && authorization.allowSettingsGet) { + var deviceContext; + try { + deviceContext = kettle.JSON.parse(deviceString); + } catch (ex) { + that.events.onError.fire({ + isError: true, + message: "Cloud based flow manager requires device information - failed to parse " + ex.message + }); + return; + } + that.events.onGpiiKey.fire(gpiiKey); + that.events.onDeviceContext.fire(deviceContext); + } else { + fluid.log("CloudBased flowManager: unauthorized GET request at /settings due to one of these reasons: 1. authorization record is missing; 2. gpiiKey associated with the authorization does not match the in-used GPII key " + gpiiKey + "; 3. the access token is unauthorized for using GET method at /settings endpoint."); + that.events.onError.fire(gpii.dbOperation.errors.unauthorized); + return; + } + }, function (error) { + fluid.log("CloudBased flowManager: GET request at /settings for the GPII key (" + gpiiKey + ") failed with error: ", error); + that.events.onError.fire(gpii.dbOperation.errors.unauthorized); + return; + }); +}; + +/** + * This should be used as the last step of the matchmaking process in a cloudbased/Untrusted flowmanager setup. + * It modifies the final payload by filtering out the irrelevant data before passing it on to the + * given event. + * + * Note that we modify (filter) the users preference set before passing it on in the payload. This is done because + * this function is meant for a configuration of the GPII where the local flowmanager is untrusted - therefore we do not + * want to pass the full preference set back to it. On the other hand, the PSP need access to the users preferences to show and + * modify them. Therefore the preference set is filtered to only contain the preferences and settings that are + * relevant to the configuration of the device (i.e. in the inferredConfiguration), any preferences not present there + * are removed. This means a compromised local flowmanager would not gain any further information from looking at + * the preference set than it could have got by looking at the inferred configuration. + * @param {Object} finalPayload - The MatchMaker final payload result. + * @param {Event} event - Fired when with the filtered output payload. + * @param {String} sourceName - The name of the source component that calls this function. The source could be the + * cloud based flow manager or the private matchmaker. + */ +gpii.flowManager.cloudBased.matchToSettings = function (finalPayload, event, sourceName) { + // Note that we send the filtered preferences to assist a PSP - we may do more filtering here at some point + var settings = fluid.filterKeys(finalPayload, [ + "gpiiKey", "activePrefsSetName", "preferences", + "activeConfiguration", "solutionsRegistryEntries", "matchMakerOutput" + ]); + settings.preferences = gpii.matchMakerFramework.utils.filterPreferencesFromInferredConfig(settings.preferences, settings.matchMakerOutput.inferredConfiguration); + fluid.log(sourceName, ": /settings endpoint responding settings ", settings); + event.fire(settings); +}; diff --git a/gpii/node_modules/flowManager/src/SettingsPutHandler.js b/gpii/node_modules/flowManager/src/SettingsPutHandler.js new file mode 100644 index 000000000..c55d86195 --- /dev/null +++ b/gpii/node_modules/flowManager/src/SettingsPutHandler.js @@ -0,0 +1,117 @@ +/* + * GPII Untrusted Settings Put Handler + * + * Copyright 2017 OCAD University + * Copyright 2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +require("../../solutionsRegistry/src/js/sr-validation-middleware"); + +// Update preferences by first ensuring the client that requests the update action does have the privilege +// to update, by verifying the access token embedded in the request "Authorization" header. +fluid.defaults("gpii.flowManager.cloudBased.settings.put.handler", { + gradeNames: ["gpii.universal.solutionsRegistry.requestHandlers.preferences"], + members: { + prefsServerDataSource: "{flowManager}.prefsServerDataSource", + authGrantFinder: "{gpii.flowManager.cloudBased}.authGrantFinder", + dataStore: "{gpii.flowManager.cloudBased}.oauth2DataStore" + }, + invokers: { + handleRequest: { + funcName: "gpii.flowManager.cloudBased.settings.put.handleRequest", + args: [ + "{that}", + "{that}.req.params.gpiiKey", + "{that}.req.body" + ] + }, + reject: { + funcName: "gpii.flowManager.cloudBased.settings.put.reject", + args: ["{that}", "{arguments}.0"] + } + } +}); + +gpii.flowManager.cloudBased.settings.put.messages = { + success: "Successfully updated." +}; + +gpii.flowManager.cloudBased.settings.put.handleRequest = function (that, gpiiKey, preferences) { + var accessToken = gpii.oauth2.parseAccessTokenFromRequest(that.req); + var authorizationPromise = gpii.oauth2.getAuthorization(accessToken, that.authGrantFinder); + + authorizationPromise.then(function (authorization) { + if (authorization && authorization.gpiiKey === gpiiKey && authorization.allowSettingsPut) { + var prefs = fluid.get(preferences, ["contexts", "gpii-default", "preferences"]); + // GPII-3717: Verify incoming preferences are in the allowed list when "allowedPrefsToWrite" array is + // provided. If allowedPrefsToWrite === null, skip this verification and save preferences. + if (authorization.allowedPrefsToWrite) { + if (Array.isArray(authorization.allowedPrefsToWrite) && gpii.flowManager.cloudBased.settings.isPrefsAllowedToWrite(prefs, authorization.allowedPrefsToWrite)) { + gpii.flowManager.cloudBased.settings.put.savePrefs(that, gpiiKey, preferences); + } else { + that.reject("unauthorized request due to one of these reasons: 1. allowedPrefsToWrite field holds a non-array value; 2. one or more preference keys in the preferences - ", preferences, " - are not in the allowed list: ", authorization.allowedPrefsToWrite); + return; + } + } else { + gpii.flowManager.cloudBased.settings.put.savePrefs(that, gpiiKey, preferences); + } + } else { + that.reject("unauthorized request due to one of these reasons: 1. authorization record is missing; 2. gpiiKey associated with the authorization does not match the in-used token " + gpiiKey + "; 3. the access token is unauthorized for using PUT method at /settings endpoint."); + return; + } + }, function (error) { + that.reject(error); + return; + }); +}; + +gpii.flowManager.cloudBased.settings.put.savePrefs = function (that, gpiiKey, preferences) { + var handlerPromise; + var gpiiKeyPromise = that.dataStore.findGpiiKey(gpiiKey); + gpiiKeyPromise.then(function (data) { + // GPII-3721: If the GPII key already exists, update its prefs safe. Otherwise, create the GPII key and its prefs safe. + if (data) { + handlerPromise = that.prefsServerDataSource.update(gpiiKey, true, preferences); + } else { + handlerPromise = that.prefsServerDataSource.create(gpiiKey, preferences); + } + + fluid.log("CloudBased flowManager: SettingsPutHandler - Save the GPII key (", gpiiKey ,") with a preference set: ", preferences); + handlerPromise.then(function (response) { + // The default success response from the preference server contains the updated preferences, + that.events.onSuccess.fire({ + gpiiKey: response.gpiiKey, + message: gpii.flowManager.cloudBased.settings.put.messages.success + }); + }, that.events.onError.fire); + }, that.events.onError.fire); +}; + +gpii.flowManager.cloudBased.settings.put.reject = function (that, errorMsg) { + fluid.log("CloudBased flowManager: PUT request at /settings failed with error: ", errorMsg); + that.events.onError.fire(gpii.dbOperation.errors.unauthorized); +}; + +/** + * This function verifies if all preference keys are in the allowed list. + * + * @param {Object} prefs - The object that is in the path of "contexts.gpii-defaults.preferences" of a prefs safe. + * @param {Array} allowedPrefs - An array of preference keys that are allowed. + * @return {Boolean} - If all preference keys are in the allowed list, return true. Otherwise, return false. + */ +gpii.flowManager.cloudBased.settings.isPrefsAllowedToWrite = function (prefs, allowedPrefs) { + var actualPrefKeys = fluid.keys(prefs); + gpii.arrayDifference(actualPrefKeys, allowedPrefs); + return actualPrefKeys.length === 0; +}; diff --git a/gpii/node_modules/flowManager/src/SolutionsRegistryDataSource.js b/gpii/node_modules/flowManager/src/SolutionsRegistryDataSource.js new file mode 100644 index 000000000..1f81a4c72 --- /dev/null +++ b/gpii/node_modules/flowManager/src/SolutionsRegistryDataSource.js @@ -0,0 +1,96 @@ +/* + * GPII Solutions Registry Datasource + * + * Copyright 2016 RtF-I + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + fs = require("fs"); + + +require("kettle"); + +fluid.registerNamespace("gpii.flowManager.solutionsRegistry"); + +fluid.defaults("gpii.flowManager.solutionsRegistry.dataSource", { + gradeNames: ["kettle.dataSource"], + components: { + encoding: { + type: "kettle.dataSource.encoding.none" + } + }, + members: { + fullSolutionsRegistry: null + }, + readOnlyGrade: "gpii.flowManager.solutionsRegistry.dataSource", + invokers: { + getImpl: { + funcName: "gpii.flowManager.solutionsRegistry.dataSource.handle", + args: ["{that}", "{arguments}.1", "{arguments}.2"] + // options, directModel + } + }, + listeners: { + onCreate: "gpii.flowManager.solutionsRegistry.dataSource.loadSolutionsRegistry" + } +}); + +// TODO: Add an invoker to reload once we are "more live". +gpii.flowManager.solutionsRegistry.dataSource.loadSolutionsRegistry = function (that) { + if (!that.options.path) { + fluid.fail("The solutionsRegistry datasource ", that, " needs a \"path\" option pointing to the solution entries folder"); + } + var url = fluid.module.resolvePath(that.options.path); + if (!fs.existsSync(url)) { + fluid.fail("The path provided to the solutionsRegistry datasource (", url, ") has not been found on the file system"); + } + + that.fullSolutionsRegistry = fluid.freezeRecursive(require(url)); +}; + +/** + * Handler for get requests of solutions registry. It will return either a full solution registry, + * or if an 'os' is provided in the requestOptions, only the entries for that os will be returned + * + * @param {Object} that - The gpii.flowManager.solutionsRegistry.dataSource. + * @param {Object} requestOptions - Currently the only request option supported is "os". If provided, + * the returned solutions registry will be filtered by OS version. + * @return {Promise} A promise that will be resolved with results (see above) or rejected on error. + */ +gpii.flowManager.solutionsRegistry.dataSource.handle = function (that, requestOptions) { + var promise = fluid.promise(); + if (requestOptions.os) { // if "os" is defined, return only solution registry entries for that OS + if (requestOptions.os in that.fullSolutionsRegistry) { + promise.resolve(that.fullSolutionsRegistry[requestOptions.os]); + } else { + promise.reject({ + isError: true, + message: "The requested OS (" + requestOptions.os + ") was not present in the solutions registry", + statusCode: 404 + }); + } + } else { // if no "os" is requested, return the full solutions registry + promise.resolve(that.fullSolutionsRegistry); + } + return promise; +}; + +/** A mixin grade which automatically expands any %terms corresponding to module names registered in Infusion's module database */ +// TODO: This is a duplicate of kettle.dataSource.file.moduleTerms - this should be rewritten after KETTLE-50 is resolved +fluid.defaults("gpii.flowManager.solutionsRegistry.dataSource.moduleTerms", { + gradeNames: "gpii.flowManager.solutionsRegistry.dataSource", + termMap: "@expand:fluid.module.terms()" +}); diff --git a/gpii/node_modules/flowManager/src/SystemUtils.js b/gpii/node_modules/flowManager/src/SystemUtils.js new file mode 100644 index 000000000..cc11b84c7 --- /dev/null +++ b/gpii/node_modules/flowManager/src/SystemUtils.js @@ -0,0 +1,88 @@ +/* + * GPII Utilities + * + * Copyright 2018 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = fluid || require("infusion"), + util = util || require("util"), + gpii = fluid.registerNamespace("gpii"); + +gpii.reservedGpiiKeys = ["noUser", "restore", "reset", "readSetting"]; + +/** + * Check if the given GPII key is a system reserved key. + * @param {String} gpiiKey - The GPII key. + * @return {Boolean} Return true if the given GPII key is a system reserved key. Otherwise, return false. + */ +gpii.isReservedGpiiKey = function (gpiiKey) { + return gpii.reservedGpiiKeys.indexOf(gpiiKey) !== -1; +}; + +/** + * Modify an array not to include string(s) in the other given array (or a string). + * + * @param {Array} array - An array to have given values removed - MODIFIED IN PLACE. + * @param {Primitive|Array} valuesToRemove - A primitive value, or an array of primitive values, to be removed. + */ + +gpii.arrayDifference = function (array, valuesToRemove) { + if (!array || !valuesToRemove) { + return; + } + var arrayToRemove = fluid.makeArray(valuesToRemove); + fluid.remove_if(array, function (i) { + return arrayToRemove.includes(i); + }); +}; + +/** + * Get inverse capability tranformation rule. If the settings handler has "inverseCapabilitiesTransformations" + * block defined, return it. Otherwise, attempt to calculate the inverse rule based on "capabilitiesTransformations" + * definition. + * + * @param {Object} settingsHandler - A settings handler object from the solution registry. + * @return {Object} - The inverse capability transformation rule. + */ +gpii.getInverseRules = function (settingsHandler) { + return settingsHandler.inverseCapabilitiesTransformations ? settingsHandler.inverseCapabilitiesTransformations : + settingsHandler.capabilitiesTransformations ? fluid.model.transform.invertConfiguration(settingsHandler.capabilitiesTransformations) : {}; +}; + +// Solve GPII-3310 by ensuring fluid.log output in the cloud lies on a single line +gpii.renderCloudLoggingArgs = function (args) { + fluid.each(args, function (arg, i) { + var togo = fluid.isPrimitive(arg) ? arg : util.inspect(arg, { + breakLength: Infinity, + depth: 3, + compact: true + }); + if (typeof(togo) === "string" && togo.length > fluid.logObjectRenderChars) { + togo = togo.substring(0, fluid.logObjectRenderChars) + " .... [output suppressed at " + fluid.logObjectRenderChars + " chars - for more output, increase fluid.logObjectRenderChars]"; + } + args[i] = togo; + }); +}; + +gpii.applyCloudLogging = function () { + fluid.loggingEvent.addListener(gpii.renderCloudLoggingArgs, "renderNodeLoggingArgs", "before:log"); +}; + +gpii.removeCloudLogging = function () { + fluid.loggingEvent.removeListener("renderNodeLoggingArgs"); +}; + +fluid.defaults("gpii.withCloudLogging", { + listeners: { + "onCreate.applyCloudLogging": "gpii.applyCloudLogging", + "onDestroy.removeCloudLogging": "gpii.removeCloudLogging" + } +}); diff --git a/gpii/node_modules/flowManager/src/UntrustedFlowManager.js b/gpii/node_modules/flowManager/src/UntrustedFlowManager.js new file mode 100644 index 000000000..9957994c8 --- /dev/null +++ b/gpii/node_modules/flowManager/src/UntrustedFlowManager.js @@ -0,0 +1,71 @@ +/** + * GPII Untrusted Local FlowManager + * + * Copyright 2015-2018 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +require("accessRequester"); +fluid.require("%gpii-universal/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Utilities.js"); + +// The Untrusted or Hybrid (Local) FlowManager is a Local FlowManager which defers to the cloud for the matchMaking +// and Preferences fetch process. The architectural goal of the untrusted FlowManager is that the unfiltered +// user preferences never reach the local device. Therefore the userLogon stages of the "gpii.flowManager.matchMaking" +// are all abridged, and instead we simply receive a final settings payload from a cloudBased flowManager which are +// then directly applied to the local device. + +// Described at https://issues.gpii.net/browse/GPII-1224 + +// Overrides request handlers in "gpii.flowManager.local" + +fluid.defaults("gpii.flowManager.untrusted", { + // Distribute the grade to handle user logon requests. + distributeOptions: { + "lifecycleManager.loginRequestHandler": { + record: "gpii.lifecycleManager.untrusted.stateChangeHandler", + target: "{that gpii.lifecycleManager.loginRequest}.options.gradeNames" + } + }, + components: { + settingsDataSource: { + type: "gpii.flowManager.settingsDataSource" + // cloudURL: distributed down from, e.g., gpii.flowManager.untrusted.config.development + } + }, + invokers: { + setSettings: { + func: "{that}.settingsDataSource.set", + args: ["{arguments}.0", "{arguments}.1"] + } + }, + listeners: { + "preferencesSavedSuccess.log": { + listener: "fluid.log", + args: ["FlowManager: updated preferences have been saved to the cloud"] + }, + "preferencesSavedError.reportSaveError": { + listener: "gpii.flowManager.untrusted.reportSaveError", + args: ["{that}.userErrors.events.userError", "{arguments}.0"] + } + } +}); + +gpii.flowManager.untrusted.reportSaveError = function (userErrorEvent, error) { + fluid.log("The save of preferences to the cloud fails with the error: ", error); + + userErrorEvent.fire({ + isError: true, + messageKey: gpii.userErrors.isConnectionError(error.code) ? "NoConnection" : "SaveToCloudFail", + originalError: error + }); +}; diff --git a/gpii/node_modules/flowManager/src/UserLogin.js b/gpii/node_modules/flowManager/src/UserLogin.js deleted file mode 100644 index d25a36349..000000000 --- a/gpii/node_modules/flowManager/src/UserLogin.js +++ /dev/null @@ -1,70 +0,0 @@ -/*! -GPII User Login Component - -Copyright 2012 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - request = fluid.registerNamespace("gpii.request.flowManager"); - - request.onUserLogin = function (token, handler) { - handler.userToken = token; - handler.events.onUserListener.fire(token); - }; - - request.onMatchHandler = function (lifecycleManagerServerDataSource, event, token, match) { - lifecycleManagerServerDataSource.set({ - operation: "start", - token: token - }, match, request.logAndNotify("Lifecycle manager returned: ", event, - function () { - return "User with token " + token + - " was successfully logged in."; - } - )); - }; - - fluid.defaults("kettle.requests.request.handler.userLogin", { - gradeNames: ["fluid.littleComponent", "gpii.request.flowManager.token", "autoInit"], - invokers: { - onMatchHandler: { - funcName: "gpii.request.flowManager.onMatchHandler", - args: ["{flowManager}.lifecycleManagerServerDataSource", "{requestProxy}.events.onSuccess", "{that}.userToken", "{arguments}.0"] - }, - getDevice: { - funcName: "gpii.request.flowManager.getDevice", - args: ["{flowManager}.deviceReporterDataSource", "{that}.events.onDevice"] - }, - handle: { - funcName: "gpii.request.flowManager.onUserLogin", - args: ["{request}.req.params.token", "{that}"], - dynamic: true - } - }, - events: { - onDevice: null, - onReadyToMatch: { - events: { - preferences: "onPreferences", - device: "onDevice" - }, - args: ["{arguments}.preferences.0", "{arguments}.device.0"] - } - }, - listeners: { - onUserListener: "{that}.getDevice", - onMatch: "{that}.onMatchHandler" - } - }); - -})(); diff --git a/gpii/node_modules/flowManager/src/UserLogonHandlers.js b/gpii/node_modules/flowManager/src/UserLogonHandlers.js new file mode 100644 index 000000000..89ebd812b --- /dev/null +++ b/gpii/node_modules/flowManager/src/UserLogonHandlers.js @@ -0,0 +1,72 @@ +/* + * GPII User Logon State Change + * + * Copyright 2012 OCAD University + * Copyright 2015, 2017 Raising the Floor - International + * Copyright 2018 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +// The base handler component containing common functions required by all http request handlers for these endpoints: +// * /user/:gpiiKey/proximityTriggered +// * /user/:gpiiKey/login +// * /user/:gpiiKey/logout +fluid.defaults("gpii.flowManager.baseHandler", { + gradeNames: ["kettle.request.http"], + logonState: "proximityTriggered", + invokers: { + handleRequest: { + funcName: "gpii.flowManager.baseHandler.handleRequest", + args: ["{flowManager}.lifecycleManager", "{that}", "{that}.options.logonState", "{that}.req.params.gpiiKey"] + // logonFunc + } + } +}); + +/** + * Capitalize the first letter of a string. + * @param {String} inputStr - The string to have the first letter capitalized. + * @return {String} - The input string having the first letter capitalized. + */ + +gpii.flowManager.capitalizeFirstLetter = function (inputStr) { + return inputStr.charAt(0).toUpperCase() + inputStr.substr(1); +}; + +gpii.flowManager.baseHandler.handleRequest = function (lifecycleManager, request, logonState, gpiiKey) { + var logonFunc = "perform" + gpii.flowManager.capitalizeFirstLetter(logonState); + var logonPromise = lifecycleManager[logonFunc](gpiiKey); + logonPromise.then(request.events.onSuccess.fire, request.events.onError.fire); +}; + +// Request handler for /user/:gpiiKey/proximityTriggered +fluid.defaults("gpii.flowManager.proximityTrigger.handler", { + gradeNames: ["gpii.flowManager.baseHandler"], + logonState: "proximityTriggered" +}); + +// Request handler for /user//login +fluid.defaults("gpii.flowManager.userLogin.handler", { + gradeNames: ["gpii.flowManager.baseHandler"], + logonState: "login" +}); + +// Request handler for /user//logout +fluid.defaults("gpii.flowManager.userLogout.handler", { + gradeNames: ["gpii.flowManager.baseHandler"], + logonState: "logout" +}); diff --git a/gpii/node_modules/flowManager/src/UserLogout.js b/gpii/node_modules/flowManager/src/UserLogout.js deleted file mode 100644 index 4333467f7..000000000 --- a/gpii/node_modules/flowManager/src/UserLogout.js +++ /dev/null @@ -1,43 +0,0 @@ -/*! -GPII User Logout Component - -Copyright 2012 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - request = fluid.registerNamespace("gpii.request.flowManager"); - - fluid.defaults("kettle.requests.request.handler.userLogout", { - gradeNames: ["fluid.littleComponent", "autoInit"], - invokers: { - handle: { - funcName: "gpii.request.flowManager.onUserLogout", - args: ["{requestProxy}", "{request}.req.params.token", "{flowManager}.lifecycleManagerServerDataSource"], - dynamic: true - } - } - }); - - request.onUserLogout = function (requestProxy, token, lifecycleManagerServerDataSource) { - // TODO: clarify semantics of multiple active start/stop cycles. - lifecycleManagerServerDataSource.set({ - operation: "stop", - token: token - }, {}, function onSuccess(response) { - fluid.log("Lifecycle manager returned: ", response); - requestProxy.events.onSuccess.fire("User with token " + token + - " was successfully logged out."); - }); - }; - -})(); diff --git a/gpii/node_modules/flowManager/test/BrowserChannelTests.js b/gpii/node_modules/flowManager/test/BrowserChannelTests.js new file mode 100644 index 000000000..291842352 --- /dev/null +++ b/gpii/node_modules/flowManager/test/BrowserChannelTests.js @@ -0,0 +1,25 @@ +/** + * GPII Flow Manager Browser Channel Tests + * + * Copyright 2013 OCAD University + * Copyright 2015 Emergya + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/kettle/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +require("./shared/BrowserChannelTestDefs.js"); + +gpii.test.runCouchTestDefs(gpii.tests.flowManager.browserChannel.testDefs); diff --git a/gpii/node_modules/flowManager/test/CaptureTests.js b/gpii/node_modules/flowManager/test/CaptureTests.js new file mode 100644 index 000000000..981fafa96 --- /dev/null +++ b/gpii/node_modules/flowManager/test/CaptureTests.js @@ -0,0 +1,238 @@ +/** + * GPII Capture Tests + * + * Copyright 2019 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + kettle = require("kettle"); + +var jqUnit = require("node-jqunit"); + +fluid.require("%gpii-universal"); +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.flowManager.capture"); + +kettle.config.createDefaults({ + configName: "gpii.flowManager.tests.capture.fakeData.config", + configPath: "%gpii-universal/gpii/node_modules/flowManager/test/configs" +}); + +gpii.tests.flowManager.capture.checkForFakeMags = function (payload) { + jqUnit.assertDeepEq("FakeMag1 with multiple settings handlers", { + "magnification": 2, + "invert": true + }, payload.fakemag1); + + jqUnit.assertDeepEq("FakeMag2 with single settings handler", { + "invert": 1, + "magnification": 4.5, + "location": "topRight" + }, payload.fakemag2); +}; + +gpii.tests.flowManager.capture.platformReporter = function () { + return { + id: "darwin" + }; +}; + +gpii.tests.flowManager.capture.checkForInstalledMags = function (payload) { + jqUnit.assertEquals("There should be 2 solutions installed", 2, Object.keys(payload).length); + jqUnit.assertEquals("Check for Fake Mag 1", "Fake Magnifier 1", payload.fakemag1.name); + jqUnit.assertEquals("Check for Fake Mag 2", "Fake Magnifier 2 - fully featured", payload.fakemag2.name); +}; + +/* + * A simple resolver config for testing resolver substituion inside of solutions for + * capture. This only needs to support a single varialbe in test `PWD` that will be + * used to lookup json settings files for the fake magnifier configurations. + */ +fluid.defaults("gpii.tests.flowManager.capture.standardResolverConfig", { + gradeNames: "fluid.component", + resolvers: { + test: "gpii.tests.flowManager.capture.testResolver" + } +}); + +gpii.tests.flowManager.capture.testResolver = function (name) { + if (name === "PWD") { + return __dirname; + } + else { + return null; + } +}; + +fluid.defaults("gpii.tests.flowManager.capture.tests", { + gradeNames: ["fluid.test.testEnvironment", "fluid.test.testCaseHolder"], + modules: [ + { + name: "Simple system capture", + tests: [{ + name: "Check for existing FakeMag Settings", + expect: 2, + sequence: [{ + task: "{config}.server.flowManager.capture.getSystemSettingsCapture", + args: [], + resolve: "gpii.tests.flowManager.capture.checkForFakeMags", + resolveArgs: ["{arguments}.0"] + }] + }] + }, + { + name: "Simple installed solutions fetch", + tests: [{ + name: "Testing get installed solutions", + expect: 3, + sequence: [{ + task: "{config}.server.flowManager.capture.getInstalledSolutionsForCurrentDevice", + args: [], + resolve: "gpii.tests.flowManager.capture.checkForInstalledMags", + resolveArgs: ["{arguments}.0"] + }] + }] + } + ], + components: { + "config": { + type: "gpii.flowManager.tests.capture.fakeData.config" + } + } +}); + +fluid.test.runTests([ + "gpii.tests.flowManager.capture.tests" +]); + +/* + * Tests for formatting raw captures. + */ +gpii.tests.flowManager.capture.formatRawCapturedSettingsTestCases = { + emptyInput: { + message: "Empy Input should produce an empty object", + input: [], + expected: {} + }, + allCorrectSettings: { + message: "There should be 2 solutions each with 2 settings from the all correct payload", + input: [ + { + "fakemag1": [ + { + "settings": { + "magnification": 3 + } + } + ] + }, + { + "fakemag1": [ + { + "settings": { + "invert": false + } + } + ] + }, + { + "fakemag2": [ + { + "settings": { + "magnification": 2, + "invert": true + } + } + ] + } + ], + expected: { + "fakemag1": { + "magnification": 3, + "invert": false + }, + "fakemag2": { + "magnification": 2, + "invert": true + } + } + }, + someErrorsSettings: { + message: "There should still be 2 solutions each with 2 settings from the all payload that contained an error in addition to correct entries.", + input: [ + { + "fakemag1": [ + { + "settings": { + "bouncedelay": 2 + } + } + ] + }, + { + "fakemag1": [ + { + "settings": { + "invert": true + } + } + ] + }, + { + isError: true, + msg: "This didn't work properly during capture" + }, + { + "fakemag2": [ + { + "settings": { + "magnification": 5, + "invert": false + } + } + ] + } + ], + expected: { + "fakemag1": { + "bouncedelay": 2, + "invert": true + }, + "fakemag2": { + "magnification": 5, + "invert": false + } + } + }, + allErrorsSettings: { + message: "Settings with just errors should yield an empty object", + input: [ + { + isError: true, + msg: "This didn't work properly during capture" + }, + { + isError: true, + msg: "This is another error apparently" + } + ], + expected: {} + } +}; + +jqUnit.test("Test Raw Settings Formatting", function () { + fluid.each(gpii.tests.flowManager.capture.formatRawCapturedSettingsTestCases, function (nextTest) { + var result = gpii.flowManager.capture.formatRawCapturedSettings(nextTest.input); + jqUnit.assertDeepEq(nextTest.message, nextTest.expected, result); + }); +}); diff --git a/gpii/node_modules/flowManager/test/DefaultSettingsLoaderTests.js b/gpii/node_modules/flowManager/test/DefaultSettingsLoaderTests.js new file mode 100644 index 000000000..17fb790a2 --- /dev/null +++ b/gpii/node_modules/flowManager/test/DefaultSettingsLoaderTests.js @@ -0,0 +1,231 @@ +/*! +Default Settings Loader Tests + +Copyright 2018 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + kettle = require("kettle"), + jqUnit = fluid.registerNamespace("jqUnit"), + fs = require("fs"), + rimraf = require("rimraf"); + +kettle.loadTestingSupport(); + +require("journal"); +fluid.require("%flowManager/src/DefaultSettingsLoader.js"); + +fluid.registerNamespace("gpii.tests.defaultSettingsLoader"); + +gpii.tests.defaultSettingsLoader.testData = { + defaultSettingsUrl: "https://raw.githubusercontent.com/GPII/universal/master/testData/defaultSettings/defaultSettings.win32.json5", + gpiiSettingsDir: "%flowManager/test/data/temp", + defaultSettingsInCodeBase: "%flowManager/test/data/defaultSettings.json5", + defaultSettings: { + "solution1": "mock default settings" + }, + userOwnSettings: { + "userOwnSolution": "my settings" + } +}; + +fluid.defaults("gpii.tests.defaultSettingsLoader", { + gradeNames: ["gpii.defaultSettingsLoader"], + members: { + // Use a temp dir to not disturb the data in the real GPII settings dir + gpiiSettingsDir: gpii.tests.defaultSettingsLoader.testData.gpiiSettingsDir + }, + defaultSettingsInCodeBase: gpii.tests.defaultSettingsLoader.testData.defaultSettingsInCodeBase, + defaultSettings: gpii.tests.defaultSettingsLoader.testData.defaultSettings, + listeners: { + "onDestroy.cleanup": { + listener: "gpii.tests.defaultSettingsLoader.cleanup", + args: ["{that}.gpiiSettingsDir"] + } + } +}); + +// Create the initial settings file in the code base +gpii.tests.defaultSettingsLoader.prepareDefaultSettingsInCodeBase = function (gpiiSettingsDir, defaultSettingsInCodeBase, defaultSettings) { + // Create the settings dir for the test + var fullDir = fluid.module.resolvePath(gpiiSettingsDir); + if (!fs.existsSync(fullDir)) { + fs.mkdirSync(fullDir); + } + + // Prepare the test by writing an initial settings file to the settings dir + var sourceSettingsFile = fluid.module.resolvePath(defaultSettingsInCodeBase); + gpii.tests.defaultSettingsLoader.createFile(sourceSettingsFile, defaultSettings); +}; + +gpii.tests.defaultSettingsLoader.cleanup = function (gpiiSettingsDir) { + // Remove the system settings directory + var fullDir = fluid.module.resolvePath(gpiiSettingsDir); + rimraf(fullDir, function () { + fluid.log("Cleanup: removed ", fullDir); + }); +}; + +gpii.tests.defaultSettingsLoader.createFile = function (fileLocation, jsonContent) { + fs.writeFileSync(fileLocation, JSON.stringify(jsonContent)); +}; + +gpii.tests.defaultSettingsLoader.verifyResultWithCopyAction = function (defaultSettingsLoader, result) { + jqUnit.assertTrue("The settings file has been copied to the settings dir", fs.existsSync(defaultSettingsLoader.defaultSettingsFile)); + jqUnit.assertDeepEq("get() returns the expected settings", defaultSettingsLoader.options.defaultSettings, result); +}; + +// Test that the user's own settings file already exists in the settings dir +gpii.tests.defaultSettingsLoader.userOwnSettings = function (defaultSettingsLoader) { + // Create the user's own settings in the settings file at the system settings directory + fs.writeFileSync(defaultSettingsLoader.defaultSettingsFile, JSON.stringify(gpii.tests.defaultSettingsLoader.testData.userOwnSettings)); + return defaultSettingsLoader.get(); +}; + +gpii.tests.defaultSettingsLoader.verifyUserOwnSettings = function (defaultSettingsLoader, result) { + jqUnit.assertDeepNeq("The loaded settings is as expected", JSON.stringify(gpii.tests.defaultSettingsLoader.testData.userOwnSettings), result); + jqUnit.assertDeepNeq("The expected settings is different from default settings in the code base", defaultSettingsLoader.options.defaultSettings, result); +}; + +// ==================== Test the fetch from the remote url ==================== +fluid.defaults("gpii.tests.defaultSettingsLoader.testCaseHolder.fetchRemoteUrl", { + gradeNames: ["fluid.test.testCaseHolder"], + components: { + defaultSettingsLoader: { + type: "gpii.tests.defaultSettingsLoader", + options: { + defaultSettingsUrl: gpii.tests.defaultSettingsLoader.testData.defaultSettingsUrl + } + } + }, + modules: [{ + name: "gpii.defaultSettingsLoader Tests - fetch default settings from the remote url", + tests: [{ + expect: 1, + name: "Fetch default settings from the remote url", + task: "{defaultSettingsLoader}.get", + resolve: "jqUnit.assertNotUndefined", + resolveArgs: ["Default settings are fetched from the remote URL", "{arguments}.0"] + }] + }] +}); + +fluid.defaults("gpii.tests.defaultSettingsLoader.fetchRemoteUrl", { + gradeNames: ["fluid.test.testEnvironment"], + components: { + tester: { + type: "gpii.tests.defaultSettingsLoader.testCaseHolder.fetchRemoteUrl" + } + } +}); + +kettle.test.bootstrap("gpii.tests.defaultSettingsLoader.fetchRemoteUrl"); + +// ==================== Test the first fallback of reading the local default settings file ==================== +fluid.defaults("gpii.tests.defaultSettingsLoader.testCaseHolder", { + gradeNames: ["fluid.test.testCaseHolder"], + components: { + defaultSettingsLoader: { + type: "gpii.tests.defaultSettingsLoader", + options: { + defaultSettingsUrl: undefined, // Cannot fetch from the remote url + listeners: { + "onCreate.prepareDefaultSettingsInCodeBase": { + listener: "gpii.tests.defaultSettingsLoader.prepareDefaultSettingsInCodeBase", + args: [ + gpii.tests.defaultSettingsLoader.testData.gpiiSettingsDir, + gpii.tests.defaultSettingsLoader.testData.defaultSettingsInCodeBase, + gpii.tests.defaultSettingsLoader.testData.defaultSettings + ], + priority: "before:prepareSettingsFile" + }, + "onDestroy.moreCleanup": { + listener: "gpii.tests.defaultSettingsLoader.moreCleanup", + args: ["{that}.options.defaultSettingsInCodeBase"] + } + } + } + } + }, + modules: [{ + name: "gpii.defaultSettingsLoader Tests", + tests: [{ + expect: 2, + name: "Test the fallback of reading the local default settings file - when the settings file is not initially copied to the settings dir", + task: "{defaultSettingsLoader}.get", + resolve: "gpii.tests.defaultSettingsLoader.verifyResultWithCopyAction", + resolveArgs: ["{defaultSettingsLoader}", "{arguments}.0"] + }, { + expect: 2, + name: "Test the fallback of reading the local default settings file - when users own settings file is already in the settings directory", + task: "gpii.tests.defaultSettingsLoader.userOwnSettings", + args: ["{defaultSettingsLoader}"], + resolve: "gpii.tests.defaultSettingsLoader.verifyUserOwnSettings", + resolveArgs: ["{defaultSettingsLoader}", "{arguments}.0"] + }] + }] +}); + +gpii.tests.defaultSettingsLoader.moreCleanup = function (defaultSettingsInCodeBase) { + // Remove the default settings file in the code base + var sourceSettingsFile = fluid.module.resolvePath(defaultSettingsInCodeBase); + fs.unlinkSync(sourceSettingsFile); +}; + +fluid.defaults("gpii.tests.defaultSettingsLoader.existInCodeBase", { + gradeNames: ["fluid.test.testEnvironment"], + components: { + tester: { + type: "gpii.tests.defaultSettingsLoader.testCaseHolder" + } + } +}); + +kettle.test.bootstrap("gpii.tests.defaultSettingsLoader.existInCodeBase"); + +// ==================== Test when the default settings file doesn't exist in the code base ==================== +fluid.defaults("gpii.tests.defaultSettingsLoader.testCaseHolder.nonExistentInCodeBase", { + gradeNames: ["fluid.test.testCaseHolder"], + components: { + defaultSettingsLoader: { + type: "gpii.tests.defaultSettingsLoader", + options: { + defaultSettingsUrl: undefined, // Cannot fetch from the remote url + listeners: { + // Do not create the settings file either in the code base or the settings directory + "onCreate.prepareSettingsFile": "fluid.identity" + } + } + } + }, + modules: [{ + name: "gpii.defaultSettingsLoader Tests", + tests: [{ + expect: 1, + name: "Tests when none of the remote url, the settings file in the code base and the settings directory exist", + task: "{defaultSettingsLoader}.get", + resolve: "jqUnit.assertUndefined", + resolveArgs: ["Receive undefined when the settings file in the code base does not exist", "{arguments}.0"] + }] + }] +}); + +fluid.defaults("gpii.tests.defaultSettingsLoader.nonExistentInCodeBase", { + gradeNames: ["fluid.test.testEnvironment"], + components: { + tester: { + type: "gpii.tests.defaultSettingsLoader.testCaseHolder.nonExistentInCodeBase" + } + } +}); + +kettle.test.bootstrap("gpii.tests.defaultSettingsLoader.nonExistentInCodeBase"); diff --git a/gpii/node_modules/flowManager/test/PSPChannelTests.js b/gpii/node_modules/flowManager/test/PSPChannelTests.js new file mode 100644 index 000000000..b6c043682 --- /dev/null +++ b/gpii/node_modules/flowManager/test/PSPChannelTests.js @@ -0,0 +1,565 @@ +/** + * GPII PSP Channel Tests + * + * Copyright 2017 Raising the Floor - International + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + jqUnit = fluid.require("node-jqunit", require, "jqUnit"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.pspChannel"); + +gpii.tests.pspChannel.payloads = { + "connectWithNoUsers": { + "path": [], + "type": "ADD", + "value": { + "gpiiKey": "noUser", + "activePrefsSetName": "gpii-default", + "settingControls": {}, + "preferences": {} + } + }, + "logoutUser": { + "path": [], + "value": null, + "type": "DELETE" + }, + "snapset_1a_loggedIn": { + "path": [], + "type": "ADD", + "value": { + "gpiiKey": "snapset_1a", + "activePrefsSetName": "gpii-default", + // TODO: Consolidate this kind of "canned" SR data so that we don't have to change schemas more than once. + "settingControls": { + "http://registry\\.gpii\\.net/common/DPIScale": { + "solutionName": "DPI Scale", + "value": 1, + "schema": { + "title": "DPI Scale", + "description": "DPI scale factor on default monitor", + "type": "integer", + "default": 0, + "minimum": -2, + "maximum": 4 + }, + "liveness": "live" + }, + "http://registry\\.gpii\\.net/common/cursorSize": { + "solutionName": "Cursor Size", + "value": 1, + "schema": { + "title": "Cursor Size", + "description": "Cursor size", + "type": "number", + "default": 0.5, + "minimum": 0, + "maximum": 1, + "multipleOf": 0.1 + }, + "liveness": "liveRestart" + } + }, + "preferences": { + "name": "Larger 125%", + "contexts": { + "gpii-default": { + "name": "Default preferences" + } + } + } + } + }, + "snapset_2c_loggedIn": { + "path": [], + "type": "ADD", + "value": { + "gpiiKey": "snapset_2c", + "activePrefsSetName": "gpii-default", + // TODO: Consolidate this kind of "canned" SR data so that we don't have to change schemas more than once. + "settingControls": { + "http://registry\\.gpii\\.net/common/cursorSize": { + "solutionName": "Cursor Size", + "value": 1, + "schema": { + "title": "Cursor Size", + "description": "Cursor size", + "type": "number", + "default": 0.5, + "minimum": 0, + "maximum": 1, + "multipleOf": 0.1 + }, + "liveness": "liveRestart" + }, + "http://registry\\.gpii\\.net/common/DPIScale": { + "solutionName": "DPI Scale", + "value": 3, + "schema": { + "title": "DPI Scale", + "description": "DPI scale factor on default monitor", + "type": "integer", + "default": 0, + "minimum": -2, + "maximum": 4 + }, + "liveness": "live" + }, + "http://registry\\.gpii\\.net/common/highContrastTheme": { + "solutionName": "High Contrast theme", + "value": "white-black", + "schema": { + "title": "High Contrast theme", + "description": "High Contrast Theme", + "type": "string", + "default": "black-white", + "enum": [ + "regular-contrast", + "black-white", + "white-black", + "yellow-black", + "black-yellow", + "lime-black", + "black-brown", + "grey-black", + "grey-white" + ], + "enumLabels": [ + "Regular Contrast", + "Black on White", + "White on Black", + "Yellow on Black", + "Black on Yellow", + "Lime on Black", + "Black on Brown", + "Grey on Dark", + "Grey on White" + ] + }, + "liveness": "live" + } + }, + "preferences": { + "name": "Dark & Larger 175%", + "contexts": { + "gpii-default": { + "name": "Default preferences" + } + } + } + } + }, + "context1_loggedIn": { + "path": [], + "type": "ADD", + "value": { + "gpiiKey": "context1", + "activePrefsSetName": "gpii-default", + // TODO: Consolidate this kind of "canned" SR data so that we don't have to change schemas more than once. + "settingControls": { + "http://registry\\.gpii\\.net/common/magnification": { + "solutionName": "Magnification", + "value": 1.5, + "schema": { + "title": "Magnification", + "description": "Level of magnification", + "type": "number", + "default": 1, + "minimum": 1, + "multipleOf": 0.1 + }, + "liveness": "liveRestart" + }, + "http://registry\\.gpii\\.net/common/volume": { + "solutionName": "Volume", + "value": 0.5, + "schema": { + "title": "Volume", + "description": "General volume of the operating system", + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "liveness": "live" + } + }, + "preferences": { + "name": "Multiple Contexts", + "contexts": { + "gpii-default": { + "name": "Default preferences" + }, + "bright": { + "name": "bright" + }, + "noise": { + "name": "noise" + }, + "brightandnoise": { + "name": "bright and noise" + } + } + } + } + }, + "context1_bright": { + "path": [], + "type": "ADD", + "value": { + "gpiiKey": "context1", + "activePrefsSetName": "bright", + // TODO: Consolidate this kind of "canned" SR data so that we don't have to change schemas more than once. + "settingControls": { + "http://registry\\.gpii\\.net/common/magnification": { + "solutionName": "Magnification", + "value": 2, + "schema": { + "title": "Magnification", + "description": "Level of magnification", + "type": "number", + "default": 1, + "minimum": 1, + "multipleOf": 0.1 + }, + "liveness": "liveRestart" + } + }, + "preferences": { + "name": "Multiple Contexts", + "contexts": { + "gpii-default": { + "name": "Default preferences" + }, + "bright": { + "name": "bright" + }, + "noise": { + "name": "noise" + }, + "brightandnoise": { + "name": "bright and noise" + } + } + } + } + } +}; + +gpii.tests.pspChannel.connectionSucceeded = function (data) { + jqUnit.assertValue("Connection between client and server can be established", data); +}; + +gpii.tests.pspChannel.checkPayload = function (data, expected) { + jqUnit.assertDeepEq("Check PSP channel response is as expected", { + "type": "modelChanged", + "payload": gpii.tests.pspChannel.payloads[expected] + }, data); +}; + +fluid.defaults("gpii.tests.pspChannel.client", { + gradeNames: "kettle.test.request.ws", + path: "/pspChannel", + port: "{configuration}.options.mainServerPort", + events: { + connectionSucceeded: null + }, + listeners: { + connectionSucceeded: { + funcName: "gpii.tests.pspChannel.connectionSucceeded", + args: ["{arguments}.0"] + } + } +}); + +fluid.defaults("gpii.tests.pspChannel.gpiiKeyRequest", { + gradeNames: "kettle.test.request.http", + termMap: { + gpiiKey: "{that}.options.gpiiKeyName" + } +}); + +fluid.defaults("gpii.tests.pspChannel.loginRequest", { + gradeNames: "gpii.tests.pspChannel.gpiiKeyRequest", + path: "/user/%gpiiKey/login" +}); + +fluid.defaults("gpii.tests.pspChannel.logoutRequest", { + gradeNames: "gpii.tests.pspChannel.gpiiKeyRequest", + path: "/user/%gpiiKey/logout" +}); + +gpii.tests.pspChannel.testDef = { + name: "PSP Channel - Basic user login/logout and client connect/disconnect tests", + expect: 10, + config: { + configName: "gpii.config.development.local.mock.windows", + configPath: "%gpii-universal/gpii/configs/mocks" + }, + components: { + clientOne: { + type: "gpii.tests.pspChannel.client" + }, + clientTwo: { + type: "gpii.tests.pspChannel.client" + }, + loginUser1a: { + type: "gpii.tests.pspChannel.loginRequest", + options: { + gpiiKeyName: "snapset_1a" + } + }, + loginUser2c: { + type: "gpii.tests.pspChannel.loginRequest", + options: { + gpiiKeyName: "snapset_2c" + } + }, + logoutUser1a: { + type: "gpii.tests.pspChannel.logoutRequest", + options: { + gpiiKeyName: "snapset_1a" + } + } + }, + sequence: [ + { + func: "{clientOne}.connect" + }, + { + event: "{clientOne}.events.onConnect", + listener: "gpii.tests.pspChannel.connectionSucceeded" + }, + { + event: "{clientOne}.events.onReceiveMessage", + listener: "gpii.tests.pspChannel.checkPayload", + args: ["{arguments}.0", "connectWithNoUsers"] + }, + { + func: "{loginUser1a}.send" + }, + { + event: "{clientOne}.events.onReceiveMessage", + listener: "gpii.tests.pspChannel.checkPayload", + args: ["{arguments}.0", "logoutUser"] + }, + { + event: "{clientOne}.events.onReceiveMessage", + listener: "gpii.tests.pspChannel.checkPayload", + args: ["{arguments}.0", "snapset_1a_loggedIn"] + }, + { + func: "{clientOne}.disconnect" + }, + { + func: "{clientTwo}.connect" + }, + { + event: "{clientTwo}.events.onConnect", + listener: "gpii.tests.pspChannel.connectionSucceeded" + }, + { + event: "{clientTwo}.events.onReceiveMessage", + listener: "gpii.tests.pspChannel.checkPayload", + args: ["{arguments}.0", "snapset_1a_loggedIn"] + }, + { + func: "{logoutUser1a}.send" + }, + { + event: "{clientTwo}.events.onReceiveMessage", + listener: "gpii.tests.pspChannel.checkPayload", + args: ["{arguments}.0", "logoutUser"] + }, + { + event: "{clientTwo}.events.onReceiveMessage", + listener: "gpii.tests.pspChannel.checkPayload", + args: ["{arguments}.0", "connectWithNoUsers"] + }, + { + func: "{loginUser2c}.send" + }, + { + event: "{clientTwo}.events.onReceiveMessage", + listener: "gpii.tests.pspChannel.checkPayload", + args: ["{arguments}.0", "logoutUser"] + }, + { + event: "{clientTwo}.events.onReceiveMessage", + listener: "gpii.tests.pspChannel.checkPayload", + args: ["{arguments}.0", "snapset_2c_loggedIn"] + } + ] +}; + +gpii.tests.pspChannel.sendPrefsSetChange = function (client, newPrefsSet) { + client.send({ + type: "modelChanged", + value: { + activePrefsSetName: newPrefsSet + } + }); +}; + +gpii.tests.pspChannel.prefsSetTestDef = { + name: "PSP Channel - Test triggering preferences set changes and receiving updates", + expect: 5, + gradeNames: [ + "gpii.test.integration.testCaseHolder.windows", + "gpii.test.integration.actionHandlersAware.windows" + ], + config: { + configName: "gpii.tests.acceptance.windows.builtIn.config", + configPath: "%gpii-universal/tests/platform/windows/configs" + }, + components: { + pspClient: { + type: "gpii.tests.pspChannel.client" + }, + loginPrefsSetUser: { + type: "gpii.tests.pspChannel.loginRequest", + options: { + gpiiKeyName: "context1" + } + } + }, + sequence: [{ + func: "{pspClient}.connect" + }, { + event: "{pspClient}.events.onConnect", + listener: "gpii.tests.pspChannel.connectionSucceeded" + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspChannel.checkPayload", + args: ["{arguments}.0", "connectWithNoUsers"] + }, { + func: "{loginPrefsSetUser}.send" + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspChannel.checkPayload", + args: ["{arguments}.0", "logoutUser"] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspChannel.checkPayload", + args: ["{arguments}.0", "context1_loggedIn"] + }, { + funcName: "gpii.tests.pspChannel.sendPrefsSetChange", + args: ["{pspClient}", "bright"] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspChannel.checkPayload", + args: ["{arguments}.0", "context1_bright"] + }] +}; + +gpii.test.runCouchTestDefs([ + gpii.tests.pspChannel.testDef, + gpii.tests.pspChannel.prefsSetTestDef +]); + +jqUnit.test("gpii.pspChannel.getPreferenceInfo() tests", function () { + var schemas = { + "http://registry.gpii.net/common/announceCapitals": { + "$schema": "gss-v7-full#", + "title": "Announce capitals", + "description": "Whether to announce capitals", + "type": "boolean", + "default": false + }, + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "$schema": "gss-v7-full#", + "title": "Windows Built-in Screen Magnifier", + "type": "object", + "properties": { + "Invert": { + "title": "Invert Colours", + "description": "Enable colour inversion for Magnifier", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "additionalProperties": false + } + }; + var testCases = { + "commonTermPref": { + name: "Process a common term", + prefsKey: "http://registry.gpii.net/common/announceCapitals", + prefsValue: 4, + expected: { + schema: { + "title": "Announce capitals", + "description": "Whether to announce capitals", + "type": "boolean", + "default": false + }, + solutionName: "Announce capitals", + presentedCommonTerm: "http://registry.gpii.net/common/announceCapitals", + prefsValue: 4, + prefsKeySegs: ["http://registry.gpii.net/common/announceCapitals"] + } + }, + "appTermWithoutSubCommonTerm": { + name: "Process an application specific term without having a common term being a sub-path", + prefsKey: "http://registry.gpii.net/applications/com.microsoft.windows.magnifier", + prefsValue: { + Invert: 0 + }, + expected: { + schema: { + "title": "Invert Colours", + "description": "Enable colour inversion for Magnifier", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + }, + solutionName: "Windows Built-in Screen Magnifier", + presentedCommonTerm: undefined, + prefsValue: 0, + prefsKeySegs: ["http://registry.gpii.net/applications/com.microsoft.windows.magnifier", "Invert"] + } + }, + "appTermWithSubCommonTerm": { + name: "Process an application specific term with a common term being a sub-path", + prefsKey: "http://registry.gpii.net/applications/com.microsoft.windows.magnifier", + prefsValue: { + "http://registry.gpii.net/common/announceCapitals": true + }, + expected: { + schema: { + "title": "Announce capitals", + "description": "Whether to announce capitals", + "type": "boolean", + "default": false + }, + solutionName: "Windows Built-in Screen Magnifier", + presentedCommonTerm: "http://registry.gpii.net/common/announceCapitals", + prefsValue: true, + prefsKeySegs: ["http://registry.gpii.net/applications/com.microsoft.windows.magnifier", "http://registry.gpii.net/common/announceCapitals"] + } + } + }; + + fluid.each(testCases, function (oneTest) { + var result = gpii.pspChannel.getPreferenceInfo(schemas, oneTest.prefsKey, oneTest.prefsValue); + jqUnit.assertDeepEq("gpii.pspChannel.getPreferenceInfo() test - " + oneTest.name, oneTest.expected, result); + }); +}); diff --git a/gpii/node_modules/flowManager/test/PrefsServerDataSourceTests.js b/gpii/node_modules/flowManager/test/PrefsServerDataSourceTests.js new file mode 100644 index 000000000..86befacab --- /dev/null +++ b/gpii/node_modules/flowManager/test/PrefsServerDataSourceTests.js @@ -0,0 +1,171 @@ +/*! +GPII Preferences Server Data Source Tests + +Copyright 2018 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + kettle = require("kettle"), + nock = require("nock"); + +require("flowManager"); + +fluid.require("%gpii-universal/gpii/node_modules/testing/src/NockUtils.js"); + +kettle.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.prefsServerDataSource"); + +gpii.tests.prefsServerDataSource.hostname = "http://gpii.net"; +gpii.tests.prefsServerDataSource.preferencesUrl = "/preferences/%gpiiKey?merge=%merge"; +gpii.tests.prefsServerDataSource.readyUrl = "/ready"; +gpii.tests.prefsServerDataSource.gpiiKey = "gpii-key-test"; +gpii.tests.prefsServerDataSource.preferences = { + "http://registry.gpii.net/common/fontSize": 24 +}; + +gpii.tests.prefsServerDataSource.nockConfig = { + preferencesGet: { + url: fluid.stringTemplate(gpii.tests.prefsServerDataSource.preferencesUrl, { + gpiiKey: gpii.tests.prefsServerDataSource.gpiiKey + }), + status: 200, + response: { + preferences: "user preferences received successfully" + } + }, + preferencesPut: { + url: fluid.stringTemplate(gpii.tests.prefsServerDataSource.preferencesUrl, { + gpiiKey: gpii.tests.prefsServerDataSource.gpiiKey, + merge: "true" + }), + preferences: gpii.tests.prefsServerDataSource.preferences, + status: 200, + response: { + preferences: "saved successfully", + merge: true + } + }, + preferencesPost: { + url: fluid.stringTemplate(gpii.tests.prefsServerDataSource.preferencesUrl), + preferences: gpii.tests.prefsServerDataSource.preferences, + status: 200, + response: { + preferences: "saved successfully" + } + }, + ready: { + url: gpii.tests.prefsServerDataSource.readyUrl, + status: 200, + response: { + isReady: true + } + } +}; + +// Set up mock ajax responses +gpii.tests.prefsServerDataSource.setUpNock = function (config) { + var cloudMock = nock(gpii.tests.prefsServerDataSource.hostname); + + // log nock matches + cloudMock.log(console.log); + + // mock GET requests to "/preferences" + cloudMock.get(config.preferencesGet.url) + .reply(config.preferencesGet.status, config.preferencesGet.response); + + // mock PUT requests to "/preferences" + cloudMock.put(config.preferencesPut.url, gpii.tests.prefsServerDataSource.preferences) + .reply(config.preferencesPut.status, config.preferencesPut.response); + + // mock POST requests to "/preferences" + cloudMock.post(config.preferencesPost.url, gpii.tests.prefsServerDataSource.preferences) + .reply(config.preferencesPost.status, config.preferencesPost.response); + + // mock GET requests to "/ready" + cloudMock.get(config.ready.url) + .reply(config.ready.status, config.ready.response); +}; + +// The customized prefsServerDataSource component for tests +fluid.defaults("gpii.tests.prefsServerDataSource", { + gradeNames: ["gpii.flowManager.prefsServerDataSource"], + prefsServerURL: gpii.tests.prefsServerDataSource.hostname +}); + +fluid.defaults("gpii.tests.prefsServerDataSourceTests", { + gradeNames: ["fluid.test.testEnvironment", "fluid.test.testCaseHolder", "gpii.test.testWithNock"], + invokers: { + setUpNock: { + funcName: "gpii.tests.prefsServerDataSource.setUpNock", + args: gpii.tests.prefsServerDataSource.nockConfig + } + }, + components: { + prefsServerDataSource: { + type: "gpii.tests.prefsServerDataSource" + } + }, + events: { + onResponse: null, + onError: null + } +}); + +fluid.defaults("gpii.tests.prefsServerDataSourceTests.tests", { + gradeNames: ["gpii.tests.prefsServerDataSourceTests"], + modules: [ + { + name: "Get preferences", + tests: { + name: "Test get()", + expect: 1, + sequence: [{ + task: "{prefsServerDataSource}.get", + args: [gpii.tests.prefsServerDataSource.gpiiKey], + resolve: "jqUnit.assertValue", + resolveArgs: ["The user preferences are received", "{arguments}.0.preferences"] + }] + } + }, + { + name: "Update preferences", + tests: { + name: "Test update()", + expect: 1, + sequence: [{ + task: "{prefsServerDataSource}.update", + args: [gpii.tests.prefsServerDataSource.gpiiKey, true, gpii.tests.prefsServerDataSource.preferences], + resolve: "jqUnit.assertTrue", + resolveArgs: ["The preferences are updated successfully", "{arguments}.0.merge"] + }] + } + }, + { + name: "Check the liveness of the preferences server", + tests: { + name: "Test isLive()", + expect: 1, + sequence: [{ + task: "{prefsServerDataSource}.isLive", + args: [], + resolve: "jqUnit.assertTrue", + resolveArgs: ["The expected liveness flag is received", "{arguments}.0"] + }] + } + } + ] +}); + +fluid.test.runTests([ + "gpii.tests.prefsServerDataSourceTests.tests" +]); diff --git a/gpii/node_modules/flowManager/test/SettingsDataSourceTests.js b/gpii/node_modules/flowManager/test/SettingsDataSourceTests.js new file mode 100644 index 000000000..58fede28c --- /dev/null +++ b/gpii/node_modules/flowManager/test/SettingsDataSourceTests.js @@ -0,0 +1,437 @@ +/*! +GPII Untrusted Settings Data Source Tests + +Copyright 2017 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + kettle = require("kettle"), + jqUnit = fluid.registerNamespace("jqUnit"), + nock = require("nock"); + +require("flowManager"); + +fluid.require("%gpii-universal/gpii/node_modules/testing/src/NockUtils.js"); + +kettle.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.settingsDataSource"); + +gpii.tests.settingsDataSource.hostname = "http://gpii.net"; +gpii.tests.settingsDataSource.accessTokenUrl = "/access_token"; +gpii.tests.settingsDataSource.settingsGetUrl = "/%gpiiKey/settings/%device"; +gpii.tests.settingsDataSource.settingsPutUrl = "/%gpiiKey/settings"; +gpii.tests.settingsDataSource.gpiiKey = "gpii-key-test"; +gpii.tests.settingsDataSource.device = { + "OS": "windows 10" +}; +gpii.tests.settingsDataSource.preferencesToUpdate = { + "http://registry.gpii.net/common/fontSize": 24 +}; + +gpii.tests.settingsDataSource.nockConfig = { + accessToken: { + url: gpii.tests.settingsDataSource.accessTokenUrl, + data: { + "grant_type": "password", + "password": "dummy", + "client_id": "pilot-computer", + "client_secret": "pilot-computer-secret", + "username": gpii.tests.settingsDataSource.gpiiKey + }, + status: 200, + response: { + access_token: "access-token-test-in-settingsDataSource", + expiresIn: 3600, + token_type: "Bearer" + } + }, + settingsGet: { + url: fluid.stringTemplate(gpii.tests.settingsDataSource.settingsGetUrl, { + gpiiKey: gpii.tests.settingsDataSource.gpiiKey, + device: encodeURIComponent(JSON.stringify(gpii.tests.settingsDataSource.device)) + }), + authHeader: { + header: "Authorization", + value: /Bearer .*/ + }, + status: 200, + response: { + settings: "user settings in success" + } + }, + settingsPut: { + url: fluid.stringTemplate(gpii.tests.settingsDataSource.settingsPutUrl, { + gpiiKey: gpii.tests.settingsDataSource.gpiiKey + }), + authHeader: { + header: "Authorization", + value: /Bearer .*/ + }, + status: 200, + response: { + gpiiKey: gpii.tests.settingsDataSource.gpiiKey, + message: "Successfully updated." + } + } +}; + +// Set up mock ajax responses +gpii.tests.settingsDataSource.setUpNock = function (config) { + var cloudMock = nock(gpii.tests.settingsDataSource.hostname); + + // log nock matches + cloudMock.log(console.log); + + // mock POST requests to "/access_token" + cloudMock.post(config.accessToken.url, config.accessToken.data) + .reply(config.accessToken.status, config.accessToken.response); + + // mock GET requests to "/settings" + cloudMock.get(config.settingsGet.url) + .matchHeader(config.settingsGet.authHeader.header, config.settingsGet.authHeader.value) + .reply(config.settingsGet.status, config.settingsGet.response); + + // mock PUT requests to "/settings" + cloudMock.put(config.settingsPut.url, gpii.tests.settingsDataSource.preferencesToUpdate) + .matchHeader(config.settingsPut.authHeader.header, config.settingsPut.authHeader.value) + .reply(config.settingsPut.status, config.settingsPut.response); +}; + +// The customized settingsDataSource component for tests +fluid.defaults("gpii.tests.settingsDataSource", { + gradeNames: ["gpii.flowManager.settingsDataSource"], + cloudURL: gpii.tests.settingsDataSource.hostname, + clientCredentialFilePath: "%gpii-universal/gpii/node_modules/flowManager/test/data/clientCredential-correct.json" +}); + +// The base testEnvironment grade to be inherited by all tests +fluid.defaults("gpii.tests.settingsDataSourceTests", { + gradeNames: ["fluid.test.testEnvironment", "fluid.test.testCaseHolder", "gpii.test.testWithNock"], + settingsDataSourceGrade: "gpii.tests.settingsDataSource", // supplied by individual tests + testCaseHolderGrade: null, // supplied by individual tests + invokers: { + setUpNock: { + funcName: "gpii.tests.settingsDataSource.setUpNock", + args: gpii.tests.settingsDataSource.nockConfig + } + }, + distributeOptions: { + settingsDataSourceGrade: { + source: "{that}.options.settingsDataSourceGrade", + target: "{that > settingsDataSource}.type" + } + }, + components: { + settingsDataSource: { + type: "gpii.tests.settingsDataSource" + } + }, + events: { + onResponse: null, + onError: null + } +}); + +// Common test sequence elements +fluid.defaults("fluid.tests.settingsDataSource.sequenceElement.get", { + gradeNames: "fluid.test.sequenceElement", + sequence: [{ + task: "{settingsDataSource}.get", + args: [gpii.tests.settingsDataSource.gpiiKey, gpii.tests.settingsDataSource.device], + resolve: "jqUnit.assertValue", + resolveArgs: ["The user settings are received", "{arguments}.0.settings"] + }] +}); + +fluid.defaults("fluid.tests.settingsDataSource.sequenceElement.set", { + gradeNames: "fluid.test.sequenceElement", + sequence: [{ + task: "{settingsDataSource}.set", + args: [gpii.tests.settingsDataSource.gpiiKey, gpii.tests.settingsDataSource.preferencesToUpdate], + resolve: "jqUnit.assertValue", + resolveArgs: ["The preferences are updated successfully", gpii.tests.settingsDataSource.gpiiKey, "{arguments}.0.gpiiKey"] + }] +}); + +// 1. No saved access token: retrieve and save an access token that then is used to retrieve user settings +// 1.1 Define test sequence elements only used in this test +fluid.defaults("fluid.tests.settingsDataSource.sequenceElement.noInitialAccessToken", { + gradeNames: "fluid.test.sequenceElement", + sequence: [{ + func: "jqUnit.assertDeepEq", + args: ["No saved access token", {}, "{settingsDataSource}.accessTokens"] + }] +}); + +fluid.defaults("fluid.tests.settingsDataSource.sequenceElement.verifyReceivedInitialAccessToken", { + gradeNames: "fluid.test.sequenceElement", + sequence: [{ + func: "jqUnit.assertValue", + args: ["The access token is received and saved", "{settingsDataSource}.accessTokens.gpii-key-test.accessToken"] + }, { + func: "jqUnit.assertValue", + args: ["The expiresIn is received and saved", "{settingsDataSource}.accessTokens.gpii-key-test.timestampExpires"] + }] +}); + +// 1.2 define test sequences +fluid.defaults("gpii.tests.settingsDataSource.sequence.get.noSavedAccessToken", { + gradeNames: "fluid.test.sequence", + sequenceElements: { + noInitialAccessToken: { + gradeNames: "fluid.tests.settingsDataSource.sequenceElement.noInitialAccessToken", + priority: "first" + }, + interactWithCloud: { + gradeNames: "fluid.tests.settingsDataSource.sequenceElement.get", + priority: "after:noInitialAccessToken" + }, + verifyReceivedInitialAccessToken: { + gradeNames: "fluid.tests.settingsDataSource.sequenceElement.verifyReceivedInitialAccessToken", + priority: "after:interactWithCloud" + } + } +}); + +fluid.defaults("gpii.tests.settingsDataSource.sequence.set.noSavedAccessToken", { + gradeNames: "gpii.tests.settingsDataSource.sequence.get.noSavedAccessToken", + sequenceElements: { + interactWithCloud: { + gradeNames: "fluid.tests.settingsDataSource.sequenceElement.set" + } + } +}); + +// 1.3 define tests +fluid.defaults("gpii.tests.settingsDataSourceTests.get.noSavedAccessToken", { + gradeNames: ["gpii.tests.settingsDataSourceTests"], + modules: [{ + name: "A workflow with undefined initial access token: an access token is requested, saved and used to retrieve user settings", + tests: { + name: "Test get()", + expect: 4, + sequenceGrade: "gpii.tests.settingsDataSource.sequence.get.noSavedAccessToken" + } + }] +}); + +fluid.defaults("gpii.tests.settingsDataSourceTests.set.noSavedAccessToken", { + gradeNames: ["gpii.tests.settingsDataSourceTests", "fluid.test.testCaseHolder"], + modules: [{ + name: "A workflow with undefined initial access token: an access token is requested, saved and used to retrieve user settings", + tests: { + name: "Test set()", + expect: 4, + sequenceGrade: "gpii.tests.settingsDataSource.sequence.set.noSavedAccessToken" + } + }] +}); + +// 2. Has unexpired access token: use saved access token to retrieve user settings +// 2.1 Custom "settingsDataSource" test component with an unexpired initial access token +fluid.defaults("gpii.tests.settingsDataSource.validInitialAccessToken", { + gradeNames: ["gpii.tests.settingsDataSource"], + members: { + accessTokens: { + "gpii-key-test": { + accessToken: "a-valid-initial-access-token", + timestampExpires: new Date(new Date().getTime() + 3600 * 1000).toISOString() // expires in 1 hour + } + } + } +}); + +// 2.2 Define test sequence elements only used in this test +fluid.defaults("fluid.tests.settingsDataSource.sequenceElement.verifyValidInitialAccessToken", { + gradeNames: "fluid.test.sequenceElement", + sequence: [{ + func: "gpii.tests.settingsDataSource.assertInitialAccessToken", + args: ["{settingsDataSourceTests}", "{settingsDataSource}", false] + }] +}); + +fluid.defaults("fluid.tests.settingsDataSource.sequenceElement.verifySameAccessToken", { + gradeNames: "fluid.test.sequenceElement", + sequence: [{ + func: "jqUnit.assertEquals", + args: ["The saved access token is same as the initial saved value", "{settingsDataSourceTests}.initialSavedAccessToken", "{settingsDataSource}.accessTokens.gpii-key-test.accessToken"] + }, { + func: "jqUnit.assertEquals", + args: ["The saved timestampExpires is same as the initial saved value", "{settingsDataSourceTests}.initialSavedTimestampExpires", "{settingsDataSource}.accessTokens.gpii-key-test.timestampExpires"] + }] +}); + +// 2.3 define test sequences +fluid.defaults("gpii.tests.settingsDataSource.sequence.get.validInitialAccessToken", { + gradeNames: "fluid.test.sequence", + sequenceElements: { + verifyValidInitialAccessToken: { + gradeNames: "fluid.tests.settingsDataSource.sequenceElement.verifyValidInitialAccessToken", + priority: "first" + }, + interactWithCloud: { + gradeNames: "fluid.tests.settingsDataSource.sequenceElement.get", + priority: "after:verifyValidInitialAccessToken" + }, + verifySameAccessToken: { + gradeNames: "fluid.tests.settingsDataSource.sequenceElement.verifySameAccessToken", + priority: "after:interactWithCloud" + } + } +}); + +fluid.defaults("gpii.tests.settingsDataSource.sequence.set.validInitialAccessToken", { + gradeNames: "gpii.tests.settingsDataSource.sequence.get.validInitialAccessToken", + sequenceElements: { + interactWithCloud: { + gradeNames: "fluid.tests.settingsDataSource.sequenceElement.set" + } + } +}); + +// 2.4 define tests +fluid.defaults("gpii.tests.settingsDataSourceTests.get.validInitialAccessToken", { + gradeNames: ["gpii.tests.settingsDataSourceTests"], + settingsDataSourceGrade: "gpii.tests.settingsDataSource.validInitialAccessToken", + modules: [{ + name: "A workflow with a valid initial access token: the saved access token is used to retrieve user settings", + tests: { + name: "Test get()", + expect: 5, + sequenceGrade: "gpii.tests.settingsDataSource.sequence.get.validInitialAccessToken" + } + }] +}); + +fluid.defaults("gpii.tests.settingsDataSourceTests.set.validInitialAccessToken", { + gradeNames: ["gpii.tests.settingsDataSourceTests"], + settingsDataSourceGrade: "gpii.tests.settingsDataSource.validInitialAccessToken", + modules: [{ + name: "A workflow with a valid initial access token: the saved access token is used to retrieve user settings", + tests: { + name: "Test set()", + expect: 5, + sequenceGrade: "gpii.tests.settingsDataSource.sequence.set.validInitialAccessToken" + } + }] +}); + +// 3. Has expired initial access token: retrieve a new access token before using /settings +// 3.1 Custom "settingsDataSource" test component with an expired initial access token +fluid.defaults("gpii.tests.settingsDataSource.expiredInitialAccessToken", { + gradeNames: ["gpii.tests.settingsDataSource"], + members: { + accessTokens: { + "gpii-key-test": { + accessToken: "a-valid-initial-access-token", + timestampExpires: new Date(new Date().getTime() - 10 * 1000).toISOString() // already expired 10 secs ago + } + } + } +}); + +// 3.2 Define test sequence elements only used in this test +fluid.defaults("fluid.tests.settingsDataSource.sequenceElement.verifyExpiredInitialAccessToken", { + gradeNames: "fluid.test.sequenceElement", + sequence: [{ + func: "gpii.tests.settingsDataSource.assertInitialAccessToken", + args: ["{settingsDataSourceTests}", "{settingsDataSource}", true] + }] +}); + +fluid.defaults("fluid.tests.settingsDataSource.sequenceElement.verifyReceivedValidAccessToken", { + gradeNames: "fluid.test.sequenceElement", + sequence: [{ + func: "jqUnit.assertNotEquals", + args: ["The saved access token is different from the initial saved value", "{settingsDataSourceTests}.initialSavedAccessToken", "{settingsDataSource}.accessTokens.gpii-key-test.accessToken"] + }, { + func: "jqUnit.assertNotEquals", + args: ["The saved timestampExpires is different from the initial saved value", "{settingsDataSourceTests}.initialSavedTimestampExpires", "{settingsDataSource}.accessTokens.gpii-key-test.timestampExpires"] + }] +}); + +// 3.3 define test sequences +fluid.defaults("gpii.tests.settingsDataSource.sequence.get.expiredInitialAccessToken", { + gradeNames: "fluid.test.sequence", + sequenceElements: { + verifyExpiredInitialAccessToken: { + gradeNames: "fluid.tests.settingsDataSource.sequenceElement.verifyExpiredInitialAccessToken", + priority: "first" + }, + interactWithCloud: { + gradeNames: "fluid.tests.settingsDataSource.sequenceElement.get", + priority: "after:verifyExpiredInitialAccessToken" + }, + verifyReceivedValidAccessToken: { + gradeNames: "fluid.tests.settingsDataSource.sequenceElement.verifyReceivedValidAccessToken", + priority: "after:interactWithCloud" + } + } +}); + +fluid.defaults("gpii.tests.settingsDataSource.sequence.set.expiredInitialAccessToken", { + gradeNames: "gpii.tests.settingsDataSource.sequence.get.expiredInitialAccessToken", + sequenceElements: { + interactWithCloud: { + gradeNames: "fluid.tests.settingsDataSource.sequenceElement.set" + } + } +}); + +// 3.4 define tests +fluid.defaults("gpii.tests.settingsDataSourceTests.get.expiredInitialAccessToken", { + gradeNames: ["gpii.tests.settingsDataSourceTests"], + settingsDataSourceGrade: "gpii.tests.settingsDataSource.expiredInitialAccessToken", + modules: [{ + name: "A workflow with a valid initial access token: the saved access token is used to retrieve user settings", + tests: { + name: "Test get()", + expect: 5, + sequenceGrade: "gpii.tests.settingsDataSource.sequence.get.expiredInitialAccessToken" + } + }] +}); + +fluid.defaults("gpii.tests.settingsDataSourceTests.set.expiredInitialAccessToken", { + gradeNames: ["gpii.tests.settingsDataSourceTests"], + settingsDataSourceGrade: "gpii.tests.settingsDataSource.expiredInitialAccessToken", + modules: [{ + name: "A workflow with a valid initial access token: the saved access token is used to update preferences", + tests: { + name: "Test set()", + expect: 5, + sequenceGrade: "gpii.tests.settingsDataSource.sequence.set.expiredInitialAccessToken" + } + }] +}); + +// Shared utility functions +gpii.tests.settingsDataSource.assertInitialAccessToken = function (that, settingsDataSource, isExpired) { + var initialAccessToken = fluid.get(settingsDataSource, ["accessTokens", gpii.tests.settingsDataSource.gpiiKey, "accessToken"]); + var initialTimestampExpires = fluid.get(settingsDataSource, ["accessTokens", gpii.tests.settingsDataSource.gpiiKey, "timestampExpires"]); + + jqUnit.assertValue("A saved access token is in place", initialAccessToken); + jqUnit[isExpired ? "assertTrue" : "assertFalse"]("A saved access token is not expired", gpii.oauth2.getExpiresIn(new Date(), initialTimestampExpires) === 0); + that.initialSavedAccessToken = initialAccessToken; + that.initialSavedTimestampExpires = initialTimestampExpires; +}; + +// Run all tests +fluid.test.runTests([ + "gpii.tests.settingsDataSourceTests.get.noSavedAccessToken", + "gpii.tests.settingsDataSourceTests.set.noSavedAccessToken", + "gpii.tests.settingsDataSourceTests.get.validInitialAccessToken", + "gpii.tests.settingsDataSourceTests.set.validInitialAccessToken", + "gpii.tests.settingsDataSourceTests.get.expiredInitialAccessToken", + "gpii.tests.settingsDataSourceTests.set.expiredInitialAccessToken" +]); diff --git a/gpii/node_modules/flowManager/test/SystemUtilsTests.js b/gpii/node_modules/flowManager/test/SystemUtilsTests.js new file mode 100644 index 000000000..8d444f822 --- /dev/null +++ b/gpii/node_modules/flowManager/test/SystemUtilsTests.js @@ -0,0 +1,123 @@ +/*! +System Utils Tests + +Copyright 2019 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.require("node-jqunit", require, "jqUnit"); + +fluid.require("%flowManager/src/SystemUtils.js"); + +fluid.registerNamespace("gpii.tests.systemUtils"); + +gpii.tests.systemUtils.arrayDifferenceTests = [ + { + "name": "Remove a single element", + inputArray: [ + "settings.configure1", + "settings.configure2", + "settings.configure3", + { + "type": "gpii.windows.updateLanguage", + "currentLanguage": "${{registry}.HKEY_CURRENT_USER\\Control Panel\\Desktop\\PreferredUILanguages}" + } + ], + elementsToRemove: "settings.configure2", + expected: [ + "settings.configure1", + "settings.configure3", + { + "type": "gpii.windows.updateLanguage", + "currentLanguage": "${{registry}.HKEY_CURRENT_USER\\Control Panel\\Desktop\\PreferredUILanguages}" + } + ] + }, { + "name": "Remove multiple elements", + inputArray: [ + "settings.configure1", + "settings.configure2", + "settings.configure3", + { + "type": "gpii.windows.updateLanguage", + "currentLanguage": "${{registry}.HKEY_CURRENT_USER\\Control Panel\\Desktop\\PreferredUILanguages}" + } + ], + elementsToRemove: ["settings.configure2", "settings.configure3"], + expected: [ + "settings.configure1", + { + "type": "gpii.windows.updateLanguage", + "currentLanguage": "${{registry}.HKEY_CURRENT_USER\\Control Panel\\Desktop\\PreferredUILanguages}" + } + ] + }, { + "name": "Remove unfounded elements", + inputArray: [ + "settings.configure1", + "settings.configure2", + "settings.configure3", + { + "type": "gpii.windows.updateLanguage", + "currentLanguage": "${{registry}.HKEY_CURRENT_USER\\Control Panel\\Desktop\\PreferredUILanguages}" + } + ], + elementsToRemove: ["unfounded1", "unfounded2"], + expected: [ + "settings.configure1", + "settings.configure2", + "settings.configure3", + { + "type": "gpii.windows.updateLanguage", + "currentLanguage": "${{registry}.HKEY_CURRENT_USER\\Control Panel\\Desktop\\PreferredUILanguages}" + } + ] + }, { + "name": "The input array stays unchanged when one argument is not provided", + inputArray: [ + "settings.configure1", + "settings.configure2", + "settings.configure3", + { + "type": "gpii.windows.updateLanguage", + "currentLanguage": "${{registry}.HKEY_CURRENT_USER\\Control Panel\\Desktop\\PreferredUILanguages}" + } + ], + elementsToRemove: undefined, + expected: [ + "settings.configure1", + "settings.configure2", + "settings.configure3", + { + "type": "gpii.windows.updateLanguage", + "currentLanguage": "${{registry}.HKEY_CURRENT_USER\\Control Panel\\Desktop\\PreferredUILanguages}" + } + ] + }, { + "name": "Remove element from array in which other items are substring of the element to be removed", + inputArray: [ + "settings.configure", + "settings.configureSecond" + ], + elementsToRemove: "settings.configureSecond", + expected: [ + "settings.configure" + ] + } +]; + +jqUnit.test("gpii.arrayDifference() tests", function () { + fluid.each(gpii.tests.systemUtils.arrayDifferenceTests, function (oneTest) { + gpii.arrayDifference(oneTest.inputArray, oneTest.elementsToRemove); + jqUnit.assertDeepEq("gpii.arrayDifference() test - " + oneTest.name, oneTest.expected, oneTest.inputArray); + }); +}); diff --git a/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.browserChannel.config.json5 b/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.browserChannel.config.json5 new file mode 100644 index 000000000..fee8f3f4c --- /dev/null +++ b/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.browserChannel.config.json5 @@ -0,0 +1,25 @@ +{ + "type": "gpii.flowManager.tests.browserChannel.config", + "options": { + "gradeNames": ["fluid.component"], + "distributeOptions": { + "browserChannel.tests.solutions": { + "record": "%flowManager/test/data/browserChannel_solutionsRegistry.json", + "target": "{that flowManager solutionsRegistryDataSource}.options.path", + "priority": "after:flowManager.solutions" + }, + "browserChannel.tests.deviceReporter": { + "record": "%flowManager/test/data/browserChannel_deviceReporter.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + }, + "browserChannel.tests.platformReporting": { + "record": { + "funcName": "gpii.tests.flowManager.browserChannel.reportPlatform" + }, + "target": "{that deviceReporter platformReporter}.options.invokers.reportPlatform" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.capture.fakeData.config.json5 b/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.capture.fakeData.config.json5 new file mode 100644 index 000000000..d3a12630f --- /dev/null +++ b/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.capture.fakeData.config.json5 @@ -0,0 +1,36 @@ +// +// This configuration is used for testing the snappingshotting Capture API. Capturing setting will only +// ever happen on a users local machine, so there is only an untrusted test configuration. +// +{ + "type": "gpii.flowManager.tests.capture.fakeData.config", + "options": { + "distributeOptions": { + "capture.solutionsRegistry": { + "record": "%gpii-universal/gpii/node_modules/flowManager/test/data/capture_solutionsRegistry.json", + "target": "{that flowManager solutionsRegistryDataSource}.options.path", + "priority": "after:flowManager.solutions" + }, + "capture.deviceReporter": { + "record": "%gpii-universal/gpii/node_modules/flowManager/test/data/capture_deviceReporter.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + }, + "capture.deviceReporterOS": { + "record": { + "funcName": "gpii.tests.flowManager.capture.platformReporter" + }, + "target": "{that deviceReporter platformReporter}.options.invokers.reportPlatform", + "priority": "after:development.installedSolutionsPath" + }, + "capture.resolverConfig": { + "record": { + "type": "gpii.tests.flowManager.capture.standardResolverConfig" + }, + "target": "{that gpii.lifecycleManager.variableResolver}.options.components.resolverConfig", + "priority": "before:capture.solutionsRegistry" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.json5" +} diff --git a/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.config.base.json5 b/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.config.base.json5 new file mode 100644 index 000000000..2d7391a3b --- /dev/null +++ b/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.config.base.json5 @@ -0,0 +1,6 @@ +{ + "type": "gpii.flowManager.tests.config.base", + "mergeConfigs": [ + "%gpii-universal/tests/configs/gpii.config.development.manualTesting.json5" + ] +} diff --git a/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.update.config.json5 b/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.update.config.json5 new file mode 100644 index 000000000..e6cbbef71 --- /dev/null +++ b/gpii/node_modules/flowManager/test/configs/gpii.flowManager.tests.update.config.json5 @@ -0,0 +1,26 @@ +{ + "type": "gpii.flowManager.tests.update.config", + "options": { + "gradeNames": ["fluid.component"], + "distributeOptions": { + "update.tests.solutions": { + "record": "%flowManager/test/data/update_solutionsRegistry.json", + "target": "{that flowManager solutionsRegistryDataSource}.options.path", + "priority": "after:flowManager.solutions" + }, + "update.tests.deviceReporter": { + "record": "%flowManager/test/data/update_deviceReporter.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + }, + "update.tests.deviceReporterOS": { + "record": { + "funcName": "gpii.tests.flowManager.update.platformReporter" + }, + "target": "{that deviceReporter platformReporter}.options.invokers.reportPlatform", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/gpii/node_modules/flowManager/test/data/browserChannel_deviceReporter.json b/gpii/node_modules/flowManager/test/data/browserChannel_deviceReporter.json new file mode 100644 index 000000000..70e3ee2e4 --- /dev/null +++ b/gpii/node_modules/flowManager/test/data/browserChannel_deviceReporter.json @@ -0,0 +1,9 @@ +[ + { + "id": "org.nvda-project" + }, + + { + "id": "org.gnome.orca" + } +] diff --git a/gpii/node_modules/flowManager/test/data/browserChannel_solutionsRegistry.json b/gpii/node_modules/flowManager/test/data/browserChannel_solutionsRegistry.json new file mode 100644 index 000000000..4da58996c --- /dev/null +++ b/gpii/node_modules/flowManager/test/data/browserChannel_solutionsRegistry.json @@ -0,0 +1,52 @@ +{ + "win32": { + "org.nvda-project": { + "name": "Dummy solution registry entry", + "contexts": { + "OS": [ + { + "id": "win32" + } + ] + }, + "settingsHandlers": { + "conf": { + "type": "gpii.settingsHandlers.webSockets", + "options": { + "path": "org.nvda-project" + } + } + }, + "configure": [ + "settings.conf" + ], + "restore": [ + "settings.conf" + ] + }, + "org.gnome.orca": { + "name": "Dummy solution registry entry", + "contexts": { + "OS": [ + { + "id": "linux" + } + ] + }, + "settingsHandlers": { + "conf": { + "type": "gpii.settingsHandlers.webSockets", + "options": { + "path": "org.gnome.orca" + } + } + }, + "configure": [ + "settings.conf" + ], + "restore": [ + "settings.conf" + ] + } + } +} diff --git a/gpii/node_modules/flowManager/test/data/capture_deviceReporter.json b/gpii/node_modules/flowManager/test/data/capture_deviceReporter.json new file mode 100644 index 000000000..eb53914cb --- /dev/null +++ b/gpii/node_modules/flowManager/test/data/capture_deviceReporter.json @@ -0,0 +1,8 @@ +[ + { + "id": "fakemag1" + }, + { + "id": "fakemag2" + } +] diff --git a/gpii/node_modules/flowManager/test/data/capture_fakemag2_settings.json b/gpii/node_modules/flowManager/test/data/capture_fakemag2_settings.json new file mode 100644 index 000000000..a3c3735d9 --- /dev/null +++ b/gpii/node_modules/flowManager/test/data/capture_fakemag2_settings.json @@ -0,0 +1,5 @@ +{ + "invert": 1, + "magnification": 4.5, + "location": "topRight" +} diff --git a/gpii/node_modules/flowManager/test/data/capture_fakemag_settings.json b/gpii/node_modules/flowManager/test/data/capture_fakemag_settings.json new file mode 100644 index 000000000..5b2a4985b --- /dev/null +++ b/gpii/node_modules/flowManager/test/data/capture_fakemag_settings.json @@ -0,0 +1,4 @@ +{ + "invert": true, + "magnification": 2.0 +} diff --git a/gpii/node_modules/flowManager/test/data/capture_solutionsRegistry.json b/gpii/node_modules/flowManager/test/data/capture_solutionsRegistry.json new file mode 100644 index 000000000..4be47fed0 --- /dev/null +++ b/gpii/node_modules/flowManager/test/data/capture_solutionsRegistry.json @@ -0,0 +1,138 @@ +{ + "darwin": { + "fakemag1": { + "name": "Fake Magnifier 1", + "contexts": { + "OS": [ + { + "id": "darwin" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.JSONSettingsHandler", + "liveness": "live", + "options": { + "filename": "${{test}.PWD}/data/capture_fakemag_settings.json" + }, + "capabilitiesTransformations": { + "magnification": "http://registry\\.gpii\\.net/common/magnification" + }, + "supportedSettings": { + "magnification": { + "schema": { + "title": "Magnification Level", + "description": "How many times to magnify content when magnification is enabled.", + "type": "number", + "default": 1.10 + } + } + } + }, + "configuration1": { + "type": "gpii.settingsHandlers.JSONSettingsHandler", + "liveness": "live", + "options": { + "filename": "${{test}.PWD}/data/capture_fakemag_settings.json" + }, + "capabilitiesTransformations": { + "invert": "http://registry\\.gpii\\.net/common/invertColours" + }, + "supportedSettings": { + "invert": { + "schema": { + "title": "Invert Colours", + "description": "Enable colour inversion for Magnifier", + "type": "boolean" + } + } + } + } + }, + "configure": [ + "settings.configuration", + "settings.configuration1" + ], + "restore": [ + "settings.configuration", + "settings.configuration1" + ], + "start": [], + "stop": [], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "fakemag2": { + "name": "Fake Magnifier 2 - fully featured", + "contexts": { + "OS": [ + { + "id": "darwin" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.JSONSettingsHandler", + "liveness": "live", + "options": { + "filename": "${{test}.PWD}/data/capture_fakemag2_settings.json" + }, + "capabilitiesTransformations": { + "magnification": "http://registry\\.gpii\\.net/common/magnification", + "invert": "http://registry\\.gpii\\.net/common/invertColours", + "location": "http://registry\\.gpii\\.net/common/magnifierLocation" + }, + "supportedSettings": { + "magnification": { + "schema": { + "title": "Magnification Level", + "description": "How many times to magnify content when magnification is enabled.", + "type": "number", + "default": 1.10 + } + }, + "invert": { + "schema": { + "title": "Invert Colours", + "description": "Enable colour inversion for Magnifier", + "type": "integer", + "default": 0 + } + }, + "location": { + "schema": { + "title": "Magnifier Location", + "description": "Magnifier location on screen", + "type": "string" + } + } + } + } + }, + "configure": [ + "settings.configuration" + ], + "restore": [ + "settings.configuration" + ], + "start": [ ], + "stop": [ ], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + } + } +} diff --git a/gpii/node_modules/flowManager/test/data/clientCredential-correct.json b/gpii/node_modules/flowManager/test/data/clientCredential-correct.json new file mode 100644 index 000000000..c8a964a69 --- /dev/null +++ b/gpii/node_modules/flowManager/test/data/clientCredential-correct.json @@ -0,0 +1,4 @@ +{ + "client_id": "pilot-computer", + "client_secret": "pilot-computer-secret" +} diff --git a/gpii/node_modules/flowManager/test/data/matchmaker.payload.example.json b/gpii/node_modules/flowManager/test/data/matchmaker.payload.example.json deleted file mode 100644 index cc04d8d2f..000000000 --- a/gpii/node_modules/flowManager/test/data/matchmaker.payload.example.json +++ /dev/null @@ -1,57 +0,0 @@ -[ - { - "name": "GNOME Shell Magnifier", - "id": "org.gnome.desktop.a11y.magnifier", - "version": "3.2.1", - "contexts": { - "OS": { - "id": "linux", - "version": ">=2.6.26" - } - }, - "settingsHandlers": [ - { - "type": "gpii.gsettings.set", - "capabilities": [ - "display.screenEnhancement.magnification", - "display.screenEnhancement.tracking" - ], - "capabilitiesTransformations": { - "screenEnhancement": { - "magnification": "mag-factor", - "tracking": { - "expander": { - "type": "gpii.transform.valueMapper", - "path": "mouse-tracking", - "options": { - "valueMap": { - "mouse": "centered" - } - } - } - } - } - } - } - ], - "launchHandlers": [ - { - "type": "gpii.launch.exec", - "options": { - "start": { - "command": "gsettings set org.gnome.desktop.a11y.applications screen-magnifier-enabled", - "args": [ - "true" - ] - }, - "stop": { - "command": "gsettings set org.gnome.desktop.a11y.applications screen-magnifier-enabled", - "args": [ - "false" - ] - } - } - } - ] - } -] \ No newline at end of file diff --git a/gpii/node_modules/flowManager/test/data/transformer.payload.example.json b/gpii/node_modules/flowManager/test/data/transformer.payload.example.json deleted file mode 100644 index 9cb9c8f50..000000000 --- a/gpii/node_modules/flowManager/test/data/transformer.payload.example.json +++ /dev/null @@ -1,31 +0,0 @@ -[ - { - "id": "org.gnome.desktop.a11y.magnifier", - "settingsHandlers": [ - { - "type": "gpii.gsettings.set, - "settings": { - "mag-factor": 200, - "mouse-tracking": "centered", - "show-cross-hairs": true - }, - "options": {} - } - ], - "launchHandlers": [ - { - "type": "gpii.launch.gsettings", - "start": { - "schema": "org.gnome.desktop.a11y.applications", - "key": "screen-magnifier-enabled", - "value": true - }, - "stop": { - "schema": "org.gnome.desktop.a11y.applications", - "key": "screen-magnifier-enabled", - "value": false - } - } - ] - } -] \ No newline at end of file diff --git a/gpii/node_modules/flowManager/test/data/update_deviceReporter.json b/gpii/node_modules/flowManager/test/data/update_deviceReporter.json new file mode 100644 index 000000000..a3e8027d8 --- /dev/null +++ b/gpii/node_modules/flowManager/test/data/update_deviceReporter.json @@ -0,0 +1,5 @@ +[ + { + "id": "test.dummy" + } +] diff --git a/gpii/node_modules/flowManager/test/data/update_solutionsRegistry.json b/gpii/node_modules/flowManager/test/data/update_solutionsRegistry.json new file mode 100644 index 000000000..634afcc92 --- /dev/null +++ b/gpii/node_modules/flowManager/test/data/update_solutionsRegistry.json @@ -0,0 +1,25 @@ +{ + "win32": { + "test.dummy": { + "name": "Dummy solution registry entry", + "contexts": { + "OS": [ + { + "id": "darwin" + } + ] + }, + "settingsHandlers": { + "myconf": { + "type": "gpii.settingsHandlers.noSettings", + "capabilitiesTransformations": { + "dummySetting1": "http://registry\\.gpii\\.net/common/fontSize", + "dummySetting2": "http://registry\\.gpii\\.net/common/setting2" + } + } + }, + "configure": [], + "restore": [] + } + } +} diff --git a/gpii/node_modules/flowManager/test/shared/BrowserChannelTestDefs.js b/gpii/node_modules/flowManager/test/shared/BrowserChannelTestDefs.js new file mode 100644 index 000000000..9856241bd --- /dev/null +++ b/gpii/node_modules/flowManager/test/shared/BrowserChannelTestDefs.js @@ -0,0 +1,552 @@ +/** + * GPII Flow Manager Browser Channel Tests + * + * Copyright 2013-2020 OCAD University + * Copyright 2015 Emergya + * Copyright 2015 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/kettle/LICENSE.txt + */ +"use strict"; + +var fluid = require("infusion"), + jqUnit = fluid.require("node-jqunit", require, "jqUnit"), + gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.tests.flowManager.browserChannel"); + +// TODO: These payloads are based on the "real" solutions 'org.nvda-project' +// and 'org.gnome.orca'. The reason for the scare-quotes is because the +// solutions are real and declared within the solutions registries used in +// production. However, the solution registry used in these tests is a mock +// (see ./data/browserChannel_solutionsRegistry.json). +// +// The solutions should be replaced with mock solutions, and associated mock +// preferences, a mock solutions registry, and a mock device context. (Note: +// these tests currently also use a mock device reporter payload, and could +// continue to do so, but updated with new mock data. See +// ./data/browserChannel_deviceReporter.json. +// +// As a warning: if mock solutions and mock preferences are used, and the +// preferences file is located in %gpii-universal/tests/data/preferences, then +// the preferences will not validate -- see: +// %gpii-universal/gpii/node_modules/solutionsRegistry/test/SettingsPayloadTests.js +// +// GpiiKeys and prefsSafes are generated from those test preferences +// (%gpii-universal/tests/data/preferences) and then used by the couchDB test +// harness: It is a convenient place to put any test preferences. The +// issue is that using mock solutions/preferences will create validation issues +// elsewhere and that will have to be addressed somehow. +gpii.tests.flowManager.browserChannel.payloads = { + "org.nvda-project": { + "speech.synth": "espeak", + "speech.outputDevice": "Microsoft Sound Mapper", + "speech.symbolLevel": 300, + "speech.espeak.rate": 40, + "speech.espeak.voice": "en-us", + "speech.espeak.pitch": 60, + "speech.espeak.volume": 80, + "reviewCursor.followFocus": "False", + "reviewCursor.followCaret": "True", + "reviewCursor.followMouse": "True", + "keyboard.speakTypedWords": "True", + "speech.espeak.rateBoost": "True", + "keyboard.speakTypedCharacters": "False", + "presentation.reportHelpBalloons": "False", + "speech.espeak.sayCapForCapitals": "True", + "virtualBuffers.autoSayAllOnPageLoad": "False" + }, + "org.gnome.orca": { + "sayAllStyle": 1, + "enableEchoByWord": 1, + "enableEchoByCharacter": 0, + "voices.default.rate": 102.27272727272727, + "enableTutorialMessages": 0, + "voices.default.family": { + "locale": "en", + "name": "en-westindies" + }, + "voices.default.average-pitch": 1, + "voices.default.gain": 7, + "verbalizePunctuationStyle": 0 + } +}; + +// Modify two of the settings for 'org.nvda-project' to test set function via +// BrowserChannel. +gpii.tests.flowManager.browserChannel.nvdaSetSettings = fluid.extend( + true, {}, + gpii.tests.flowManager.browserChannel.payloads["org.nvda-project"], + { + "speech.espeak.rate": 50, + "keyboard.speakTypedCharacters": "False" + } +); + +gpii.tests.flowManager.browserChannel.reportPlatform = function () { + return { + id: "win32", + version: "x86-64" + }; +}; + +gpii.tests.flowManager.browserChannel.checkConnectionRequest = function (data, request) { + request.events.onReceiveMessage.addListener(function (message, request) { + fluid.log("BrowserChannel checkConnectionRequest got onReceiveMessage ", message); + request.events[message.type].fire(message.payload); + }); +}; + +gpii.tests.flowManager.browserChannel.checkClients = function (spec) { + spec = spec || {}; + fluid.each(gpii.tests.flowManager.browserChannel.payloads, function (value, key) { + var count = spec[key] || 0; + jqUnit.assertEquals("Count of clients of type " + key, count, Object.keys(gpii.settingsHandlers.webSockets.instance.clients[key] || {}).length); + }); +}; + +gpii.tests.flowManager.browserChannel.checkErrorResponse = function (gpiiKey, data, request) { + var expMsg = "Got logout request from user " + gpiiKey + ", but the user noUser is logged in. So ignoring the request."; + data = JSON.parse(data); + jqUnit.assertTrue("Received error as expected", data.isError); + jqUnit.assertEquals("Received message as expected", expMsg, data.message); + jqUnit.assertEquals("Received error code 409", 409, request.nativeResponse.statusCode); +}; + +gpii.tests.flowManager.browserChannel.checkPersistentSettings = function (clientId) { + var expectedSettings = gpii.tests.flowManager.browserChannel.payloads[clientId]; + jqUnit.assertDeepEq("The settings for " + clientId + " in persistence are the expected following logon", + expectedSettings, gpii.settingsHandlers.webSockets.instance.getSettingsForId(clientId)); +}; + +gpii.tests.flowManager.browserChannel.checkSettingsAfterSet = function (responses, expectedSettings) { + fluid.each(responses, function (settingsArray, response) { + jqUnit.assertDeepEq( + "The persistent settings for " + response + " are, after setting them,", + expectedSettings, settingsArray[0] + ); + }); +}; + +gpii.tests.flowManager.browserChannel.loginAndSettingsChanged = function (multiArg, spec) { + fluid.each(spec.clientIds, function (clientId) { + var expectedSettings = gpii.tests.flowManager.browserChannel.payloads[clientId]; + gpii.tests.flowManager.browserChannel.checkPersistentSettings(clientId); + jqUnit.assertDeepEq("The settings sent via browserChannel for " + clientId + " are the expected following logon", + expectedSettings, multiArg[clientId][0]); + }); + jqUnit.assertEquals("Login response is correct", "User with GPII key " + spec.gpiiKey + " was successfully logged in.", multiArg.login[0]); +}; + +gpii.tests.flowManager.browserChannel.connectionSucceeded = function (data, solutionId) { + // TODO: Slight risk of race condition here - this message might not arrive until settings have changed + jqUnit.assertDeepEq("Right after connecting, the initial settings transmitted for " + solutionId + " are as in persistence", + gpii.settingsHandlers.webSockets.instance.getSettingsForId(solutionId), + data + ); +}; + +gpii.tests.flowManager.browserChannel.checkRejectedConnection = function (data, solutionId) { + jqUnit.assertDeepEq("An untrusted solutionId can't make use of the browserChannel", { + isError: true, + message: "Rejecting a connection request from '" + solutionId + "'. The solution id was not found in the solutions registry" + }, data); +}; + +fluid.defaults("gpii.tests.flowManager.browserChannel.clientHolder", { + gradeNames: "kettle.test.request.ws", + path: "/browserChannel", + port: "{configuration}.options.mainServerPort", + solutionId: "", + settings: {}, + events: { + onSettingsChanged: null, + changeSettingsReceived: null, + connectionSucceeded: null + }, + listeners: { + connectionSucceeded: { + funcName: "gpii.tests.flowManager.browserChannel.connectionSucceeded", + args: ["{arguments}.0", "{that}.options.solutionId"] + } + }, + invokers: { + sendId: { + func: "{that}.send", + args: { + type: "connect", + payload: { + solutionId: "{that}.options.solutionId" + } + } + }, + sendChangeSettings: { + func: "{that}.send", + args: { + type: "changeSettings", + payload: { + settings: "{that}.options.settings" + } + } + } + } +}); + +fluid.defaults("gpii.tests.flowManager.browserChannel.chromeClient", { + gradeNames: "gpii.tests.flowManager.browserChannel.clientHolder", + solutionId: "org.nvda-project", + settings: gpii.tests.flowManager.browserChannel.nvdaSetSettings +}); + +fluid.defaults("gpii.tests.flowManager.browserChannel.firefoxClient", { + gradeNames: "gpii.tests.flowManager.browserChannel.clientHolder", + solutionId: "org.gnome.orca" +}); + +fluid.defaults("gpii.tests.flowManager.browserChannel.unauthorizedClient", { + gradeNames: "gpii.tests.flowManager.browserChannel.clientHolder", + solutionId: "com.unauthorized.app" +}); + +fluid.defaults("gpii.tests.flowManager.browserChannel.gpiiKeyRequest", { + gradeNames: "kettle.test.request.http", + termMap: { + gpiiKey: "{that}.options.gpiiKey" + } +}); + +fluid.defaults("gpii.tests.flowManager.browserChannel.loginRequest", { + gradeNames: "gpii.tests.flowManager.browserChannel.gpiiKeyRequest", + path: "/user/%gpiiKey/login" +}); + +gpii.tests.flowManager.browserChannel.testLogoutResponse = function (data, gpiiKey) { + jqUnit.assertEquals("Response is correct", "User with GPII key " + gpiiKey + " was successfully logged out.", data); +}; + +fluid.defaults("gpii.tests.flowManager.browserChannel.logoutRequest", { + gradeNames: "gpii.tests.flowManager.browserChannel.gpiiKeyRequest", + path: "/user/%gpiiKey/logout", + invokers: { + checkResponse: { + funcName: "gpii.tests.flowManager.browserChannel.testLogoutResponse", + args: ["{arguments}.0", "{that}.options.gpiiKey"] + } + } +}); + +gpii.tests.flowManager.browserChannel.testDefs = [{ + name: "Flow Manager Simple BrowserChannel tests", + expect: 15, + config: { + configName: "gpii.flowManager.tests.browserChannel.config", + configPath: "%flowManager/test/configs" + }, + events: { + loginAndSettingsChangedNvda: { + events: { + login: "{loginNvda}.events.onComplete", + "org.nvda-project": "{clientOne}.events.onSettingsChanged" + }, + args: ["{arguments}", { + gpiiKey: "{loginNvda}.options.gpiiKey", + clientIds: ["org.nvda-project"] + }] + } + }, + components: { + clientOne: { + type: "gpii.tests.flowManager.browserChannel.chromeClient" + }, + loginNvda: { + type: "gpii.tests.flowManager.browserChannel.loginRequest", + options: { + gpiiKey: "screenreader_nvda" + } + }, + logoutNvda1: { + type: "gpii.tests.flowManager.browserChannel.logoutRequest", + options: { + gpiiKey: "screenreader_nvda" + } + }, + logoutNvda2: { + type: "gpii.tests.flowManager.browserChannel.logoutRequest", + options: { + gpiiKey: "screenreader_nvda" + } + } + }, + sequence: [{ + func: "gpii.tests.flowManager.browserChannel.checkClients" + }, { + func: "{clientOne}.connect" // chromeClient (org.nvda-project) + }, { + event: "{clientOne}.events.onConnect", + listener: "fluid.identity" + }, { + func: "{clientOne}.sendId" + }, { + event: "{clientOne}.events.onReceiveMessage", + listener: "gpii.tests.flowManager.browserChannel.checkConnectionRequest" + }, { + func: "gpii.tests.flowManager.browserChannel.checkClients", + args: { + "org.nvda-project": 1 + } + }, { + func: "{loginNvda}.send" + }, { + event: "{testCaseHolder}.events.loginAndSettingsChangedNvda", + listener: "gpii.tests.flowManager.browserChannel.loginAndSettingsChanged" + }, { + func: "{logoutNvda1}.send" + }, { + event: "{logoutNvda1}.events.onComplete", + listener: "{logoutNvda1}.checkResponse" + }, { + func: "gpii.tests.flowManager.browserChannel.checkClients", + args: { + "org.nvda-project": 1 + } + }, { + func: "{clientOne}.disconnect" + }, { + func: "{logoutNvda2}.send" + }, { + event: "{logoutNvda2}.events.onComplete", + listener: "gpii.tests.flowManager.browserChannel.checkErrorResponse", + args: ["screenreader_nvda", "{arguments}.0", "{arguments}.1"] + }, { + func: "gpii.tests.flowManager.browserChannel.checkClients" + }] +}, { + name: "Flow Manager BrowserChannel tests", + expect: 32, + config: { + configName: "gpii.flowManager.tests.browserChannel.config", + configPath: "%flowManager/test/configs" + }, + events: { + loginAndSettingsChangedChromeAndFirefox: { + events: { + login: "{loginChromeAndFirefox}.events.onComplete", + "org.nvda-project": "{clientTwo}.events.onSettingsChanged", + "org.gnome.orca": "{clientThree}.events.onSettingsChanged" + }, + args: ["{arguments}", { + gpiiKey: "{loginChromeAndFirefox}.options.gpiiKey", + clientIds: ["org.nvda-project", "org.gnome.orca"] + }] + }, + clientOneChangeSettings: { + events: { + "clientOneChangeReceipt": "{clientOne}.events.changeSettingsReceived", + "clientTwoSettingsChanged": "{clientTwo}.events.onSettingsChanged" + }, + args: ["{arguments}", "{clientOne}.options.settings"] + } + }, + components: { + clientOne: { + type: "gpii.tests.flowManager.browserChannel.chromeClient" + }, + clientTwo: { + type: "gpii.tests.flowManager.browserChannel.chromeClient" + }, + clientThree: { + type: "gpii.tests.flowManager.browserChannel.firefoxClient" + }, + clientFour: { + type: "gpii.tests.flowManager.browserChannel.unauthorizedClient" + }, + clientFive: { + type: "gpii.tests.flowManager.browserChannel.chromeClient" + }, + loginNvda: { + type: "gpii.tests.flowManager.browserChannel.loginRequest", + options: { + gpiiKey: "screenreader_nvda" + } + }, + logoutNvda1: { + type: "gpii.tests.flowManager.browserChannel.logoutRequest", + options: { + gpiiKey: "Nvda1" + } + }, + logoutNvda2: { + type: "gpii.tests.flowManager.browserChannel.logoutRequest", + options: { + gpiiKey: "screenreader_nvda" + } + }, + logoutOrca: { + type: "gpii.tests.flowManager.browserChannel.logoutRequest", + options: { + gpiiKey: "screenreader_orca" + } + }, + loginChromeAndFirefox: { + type: "gpii.tests.flowManager.browserChannel.loginRequest", + options: { + gpiiKey: "nvda_and_orca" + } + }, + logoutChromeAndFirefox: { + type: "gpii.tests.flowManager.browserChannel.logoutRequest", + options: { + gpiiKey: "nvda_and_orca" + } + } + }, + sequence: [{ + func: "gpii.tests.flowManager.browserChannel.checkClients" + }, { + func: "{clientOne}.connect" + }, { + event: "{clientOne}.events.onConnect", + listener: "fluid.identity" + }, { + func: "{clientOne}.sendId" + }, { + event: "{clientOne}.events.onReceiveMessage", + listener: "gpii.tests.flowManager.browserChannel.checkConnectionRequest" + }, { + func: "gpii.tests.flowManager.browserChannel.checkClients", + args: { + "org.nvda-project": 1 + } + }, { + func: "{clientTwo}.connect" + }, { + event: "{clientTwo}.events.onConnect", + listener: "fluid.identity" + }, { + func: "{clientTwo}.sendId" + }, { + event: "{clientTwo}.events.onReceiveMessage", + listener: "gpii.tests.flowManager.browserChannel.checkConnectionRequest" + }, { + func: "gpii.tests.flowManager.browserChannel.checkClients", + args: { + "org.nvda-project": 2 + } + }, { + func: "{clientThree}.connect" + }, { + event: "{clientThree}.events.onConnect", + listener: "fluid.identity" + }, { + func: "{clientThree}.sendId" + }, { + event: "{clientThree}.events.onReceiveMessage", + listener: "gpii.tests.flowManager.browserChannel.checkConnectionRequest" + }, { + func: "{clientFour}.connect" + }, { + event: "{clientFour}.events.onConnect", + listener: "fluid.identity" + }, { + func: "{clientFour}.sendId" + }, { + event: "{clientFour}.events.onReceiveMessage", + listener: "gpii.tests.flowManager.browserChannel.checkRejectedConnection", + args: ["{arguments}.0", "{clientFour}.options.solutionId"] + }, { + func: "gpii.tests.flowManager.browserChannel.checkClients", + args: { + "org.nvda-project": 2, + "org.gnome.orca": 1 + } + }, + // All three clients now connected - clientOne sends a "changeSettings" + // request and gets back a receipt, whereas clientTwo receives the changes. + { + func: "{clientOne}.sendChangeSettings" + }, + { + event: "{testCaseHolder}.events.clientOneChangeSettings", + listener: "gpii.tests.flowManager.browserChannel.checkSettingsAfterSet" + }, + // Disconnect client 1 and try a spurious logout + { + func: "{clientOne}.disconnect" + }, { + func: "{logoutNvda1}.send" + }, { + event: "{logoutNvda1}.events.onComplete", + listener: "gpii.tests.flowManager.browserChannel.checkErrorResponse", + args: ["Nvda1", "{arguments}.0", "{arguments}.1"] + }, { + func: "gpii.tests.flowManager.browserChannel.checkClients", + args: { + "org.nvda-project": 1, + "org.gnome.orca": 1 + } + }, + // Log in a user who has settings for two browsers + { + func: "{loginChromeAndFirefox}.send" + }, { + event: "{testCaseHolder}.events.loginAndSettingsChangedChromeAndFirefox", + listener: "gpii.tests.flowManager.browserChannel.loginAndSettingsChanged" + }, { + func: "{logoutChromeAndFirefox}.send" + }, { + event: "{logoutChromeAndFirefox}.events.onComplete", + listener: "{logoutChromeAndFirefox}.checkResponse" + }, { + func: "{clientTwo}.disconnect" + }, { + func: "{logoutOrca}.send" + }, { + event: "{logoutOrca}.events.onComplete", + listener: "gpii.tests.flowManager.browserChannel.checkErrorResponse", + args: ["screenreader_orca", "{arguments}.0", "{arguments}.1"] + }, { + func: "gpii.tests.flowManager.browserChannel.checkClients", + args: { + "org.gnome.orca": 1 + } + }, { + func: "{clientThree}.disconnect" + }, // At this point, zero clients are connected + { + func: "{loginNvda}.send" + }, { + event: "{loginNvda}.events.onComplete", + listener: "gpii.tests.flowManager.browserChannel.loginAndSettingsChanged", + args: [{ // manually construct multiArg since there is only one event + login: ["{arguments}.0"] + }, { + gpiiKey: "{loginNvda}.options.gpiiKey" + }] + }, { + func: "{clientFive}.connect" + }, { + event: "{clientFive}.events.onConnect", + listener: "fluid.identity" + }, { + func: "{clientFive}.sendId" + }, { + event: "{clientFive}.events.onReceiveMessage", + listener: "gpii.tests.flowManager.browserChannel.checkConnectionRequest" + }, { + func: "gpii.tests.flowManager.browserChannel.checkPersistentSettings", + args: "org.nvda-project" + }, { + func: "{clientFive}.disconnect" + }, { + func: "{logoutNvda2}.send" + }, { + event: "{logoutNvda2}.events.onComplete", + listener: "{logoutNvda2}.checkResponse" + }, { + func: "gpii.tests.flowManager.browserChannel.checkClients" + }] +}]; diff --git a/gpii/node_modules/gpii-db-operation/README.md b/gpii/node_modules/gpii-db-operation/README.md new file mode 100644 index 000000000..1f6386e2f --- /dev/null +++ b/gpii/node_modules/gpii-db-operation/README.md @@ -0,0 +1,4 @@ +# gpii-db-operation + +The gpii-db-operation module is responsible for handling CouchDB create/read/update/delete operations for all GPII +components. diff --git a/gpii/node_modules/gpii-db-operation/index.js b/gpii/node_modules/gpii-db-operation/index.js new file mode 100644 index 000000000..869e83e51 --- /dev/null +++ b/gpii/node_modules/gpii-db-operation/index.js @@ -0,0 +1,11 @@ +"use strict"; + +var fluid = require("infusion"); + +fluid.module.register("gpii-db-operation", __dirname, require); + +require("./src/DataStore.js"); +require("./src/DbConst.js"); +require("./src/DbDataStoreUtils.js"); +require("./src/DbDataStore.js"); +require("./src/DbUtils.js"); diff --git a/gpii/node_modules/gpii-db-operation/package.json b/gpii/node_modules/gpii-db-operation/package.json new file mode 100644 index 000000000..649875a29 --- /dev/null +++ b/gpii/node_modules/gpii-db-operation/package.json @@ -0,0 +1,13 @@ +{ + "name": "gpii-db-operation", + "description": "Responsible for handling the CouchDB create/read/update/delete operations for all GPII components", + "version": "0.1.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } +} diff --git a/gpii/node_modules/gpii-db-operation/src/DataStore.js b/gpii/node_modules/gpii-db-operation/src/DataStore.js new file mode 100644 index 000000000..7a84bdc7d --- /dev/null +++ b/gpii/node_modules/gpii-db-operation/src/DataStore.js @@ -0,0 +1,21 @@ +/*! +GPII Base Data Store + +Copyright 2016 OCAD university + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = fluid || require("infusion"); + +fluid.registerNamespace("gpii.dbOperation"); + +fluid.defaults("gpii.dbOperation.dataStore", { + gradeNames: "fluid.component" +}); diff --git a/gpii/node_modules/gpii-db-operation/src/DbConst.js b/gpii/node_modules/gpii-db-operation/src/DbConst.js new file mode 100644 index 000000000..b00ce58a5 --- /dev/null +++ b/gpii/node_modules/gpii-db-operation/src/DbConst.js @@ -0,0 +1,53 @@ +/*! +Copyright 2017 OCAD university + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ +"use strict"; + +var fluid = fluid || require("infusion"); + +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.dbOperation"); + +// The current version of schemas that document structures correspond with. +gpii.dbOperation.schemaVersion = "0.2"; + +// All doc types used for saving different documents into CouchDB +// See [GPII Data Model](https://wiki.gpii.net/w/Keys,_KeyTokens,_and_Preferences#Data_Model) +// regarding accepted fields for each document type. +gpii.dbOperation.docTypes = fluid.freezeRecursive({ + gpiiKey: "gpiiKey", + prefsSafe: "prefsSafe", + clientCredential: "clientCredential", + gpiiAppInstallationClient: "gpiiAppInstallationClient", + gpiiAppInstallationAuthorization: "gpiiAppInstallationAuthorization" +}); + +// All error details that the dbOperation module reports. +gpii.dbOperation.errors = fluid.freezeRecursive({ + missingInput: { + message: "The input field \"%fieldName\" was undefined", + statusCode: 400, + isError: true + }, + missingDoc: { + message: "A record of type \"%docType\" was not found", + statusCode: 400, + isError: true + }, + mismatchedDocType: { + message: "The document type must be \"%docType\" instead of the selected document type \"%selectedDocType\"", + statusCode: 400, + isError: true + }, + unauthorized: { + message: "Unauthorized", + statusCode: 401, + isError: true + } +}); diff --git a/gpii/node_modules/gpii-db-operation/src/DbDataStore.js b/gpii/node_modules/gpii-db-operation/src/DbDataStore.js new file mode 100644 index 000000000..5888e4700 --- /dev/null +++ b/gpii/node_modules/gpii-db-operation/src/DbDataStore.js @@ -0,0 +1,265 @@ +/*! +GPII CouchDB Data Store + +Copyright 2016-2019 OCAD university + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +/* + * gpii.dbOperation.dbDataStore provides APIs for the authorization server to communicate with the backend + * data storage using CouchDB. + * + * This DB data store is a re-writing of the initial synchronized in memory data store. It now uses + * async promise API to satisfy the async database operations. However, the in memory data store continues + * to serve as a good reference with an much simpler logic. It can be found at: + * https://github.com/GPII/universal/blob/820e4919907e56f6412b2e3bab18675d5388b00b/gpii/node_modules/gpii-oauth2/gpii-oauth2-datastore/src/InMemoryDataStore.js + */ + +"use strict"; + +var fluid = fluid || require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +if (typeof require !== "undefined") { + fluid.require("kettle"); +} + +fluid.defaults("gpii.dbOperation.dbDataSource", { + gradeNames: ["kettle.dataSource.URL", "kettle.dataSource.CouchDB"], + baseUrl: null, // Supplied by integrators + port: null, // Supplied by integrators + dbName: null, // Supplied by integrators + requestUrl: null, // Supplied by integrators + termMap: { + }, + directModel: { + }, + notFoundIsEmpty: true, + rules: { + writePayload: { + "": "" + }, + readPayload: { + "": "" + } + }, + // requestUrl needs to be resolved upfront because it contains more string templates that need to be replaced at the + // next round when kettle.dataSource.URL kicks in to compose the actual URL. + // An example of requestUrl is "/%id", in which case the expected url should be "%baseUrl:%port/%dbName/%id" instead + // of having "%requestUrl" embedded. The expander below is to prepare the url that's sensible to kettle.dataSource. + url: { + expander: { + funcName: "fluid.stringTemplate", + args: ["%baseUrl:%port/%dbName%requestUrl", { + requestUrl: "{that}.options.requestUrl", + baseUrl: "{that}.options.baseUrl", + port: "{that}.options.port", + dbName: "{that}.options.dbName" + }] + } + } +}); + +fluid.defaults("gpii.dbOperation.dbDataSource.writable", { + gradeNames: ["gpii.dbOperation.dbDataSource", "kettle.dataSource.CouchDB.writable"], + writable: true, + writeMethod: "PUT" +}); + +fluid.defaults("gpii.dbOperation.dbDataStore", { + gradeNames: ["gpii.dbOperation.dataStore"], + // Supplied by GPII configuration to config all gpii.dbOperation.dbDataSource instances. + // It contains these elements: + // 1. gradeNames: The mixin grade + // 2. baseUrl: The base URL to where the database is located. For example, a default locally installed CouchDB uses http://127.0.1.1 + // 3. port: The port where the database is located. For example, a default locally installed CouchDB uses port 5984 + // 4. dbName: The database name + dataSourceConfig: { + }, + distributeOptions: { + "dbDataStore.dataSourceConfig": { + source: "{that}.options.dataSourceConfig", + target: "{that > gpii.dbOperation.dbDataSource}.options" + } + }, + components: { + findByIdDataSource: { + type: "gpii.dbOperation.dbDataSource", + options: { + requestUrl: "/%id", + termMap: { + id: "%id" + } + } + }, + findPrefsSafeByGpiiKeyDataSource: { + type: "gpii.dbOperation.dbDataSource", + options: { + requestUrl: "/_design/views/_view/findPrefsSafeByGpiiKey?key=%22%gpiiKey%22&include_docs=true", + termMap: { + gpiiKey: "%gpiiKey" + }, + rules: { + readPayload: { + "": "rows.0" + } + } + } + }, + findClientByOauth2ClientIdDataSource: { + type: "gpii.dbOperation.dbDataSource", + options: { + requestUrl: "/_design/views/_view/findClientByOauth2ClientId?key=%22%oauth2ClientId%22&include_docs=true", + termMap: { + oauth2ClientId: "%oauth2ClientId" + }, + rules: { + readPayload: { + "": "rows.0" + } + } + } + }, + findAllViewsDataSource: { + type: "gpii.dbOperation.dbDataSource", + options: { + requestUrl: "/_design/views" + } + }, + findInfoByAccessTokenDataSource: { + type: "gpii.dbOperation.dbDataSource", + options: { + requestUrl: "/_design/views/_view/findInfoByAccessToken?key=%22%accessToken%22&include_docs=true", + termMap: { + accessToken: "%accessToken" + }, + rules: { + readPayload: { + "": "rows.0" + } + } + } + }, + saveDataSource: { + type: "gpii.dbOperation.dbDataSource.writable", + options: { + requestUrl: "/%id", + termMap: { + id: "%id" + } + } + } + }, + invokers: { + findById: { + funcName: "gpii.dbOperation.dbDataStore.findRecord", + args: [ + "{that}.findByIdDataSource", + { + id: "{arguments}.0" + }, + "id" + ] + // id + }, + findGpiiKey: { + func: "{that}.findById" + // gpiiKey + }, + findClientById: { + func: "{that}.findById" + // clientId + }, + findClientCredentialById: { + func: "{that}.findById" + // clientCredentialId + }, + findPrefsSafeByGpiiKey: { + funcName: "gpii.dbOperation.dbDataStore.findRecord", + args: [ + "{that}.findPrefsSafeByGpiiKeyDataSource", + { + gpiiKey: "{arguments}.0" + }, + "gpiiKey", + gpii.dbOperation.dbDataStore.findPrefsSafeByGpiiKeyPostProcess + ] + // gpiiKey + }, + findClientByOauth2ClientId: { + funcName: "gpii.dbOperation.dbDataStore.findRecord", + args: [ + "{that}.findClientByOauth2ClientIdDataSource", + { + oauth2ClientId: "{arguments}.0" + }, + "oauth2ClientId", + gpii.dbOperation.dbDataStore.findClientByOauth2ClientIdPostProcess + ] + // oauth2ClientId + }, + findInfoByAccessToken: { + funcName: "gpii.dbOperation.dbDataStore.findRecord", + args: [ + "{that}.findInfoByAccessTokenDataSource", + { + accessToken: "{arguments}.0" + }, + "accessToken", + gpii.dbOperation.dbDataStore.findInfoByAccessTokenPostProcess + ] + // accessToken + }, + findAllViews: { + funcName: "gpii.dbOperation.dbDataStore.findRecord", + args: ["{that}.findAllViewsDataSource"] + }, + addGpiiKey: { + funcName: "gpii.dbOperation.dbDataStore.addGpiiKey", + args: [ + "{that}.saveDataSource", + "{arguments}.0" + ] + // gpiiKeyData + }, + updateGpiiKey: { + funcName: "gpii.dbOperation.dbDataStore.updateGpiiKey", + args: [ + "{that}.saveDataSource", + "{arguments}.0", + "{arguments}.1" + ] + // gpiiKey, gpiiKeyData + }, + addPrefsSafe: { + funcName: "gpii.dbOperation.dbDataStore.addPrefsSafe", + args: [ + "{that}.saveDataSource", + "{arguments}.0" + ] + // prefsSafeData + }, + updatePrefsSafe: { + funcName: "gpii.dbOperation.dbDataStore.updatePrefsSafe", + args: [ + "{that}.saveDataSource", + "{arguments}.0", + "{arguments}.1" + ] + // prefsSafeId, prefsSafeData + }, + addAuthorization: { + funcName: "gpii.dbOperation.dbDataStore.addAuthorization", + args: [ + "{that}.saveDataSource", + "{arguments}.0" + ] + // authorizationData + } + } +}); diff --git a/gpii/node_modules/gpii-db-operation/src/DbDataStoreUtils.js b/gpii/node_modules/gpii-db-operation/src/DbDataStoreUtils.js new file mode 100644 index 000000000..1fb7fd042 --- /dev/null +++ b/gpii/node_modules/gpii-db-operation/src/DbDataStoreUtils.js @@ -0,0 +1,562 @@ +/*! +GPII CouchDB Data Store Utilities + +Copyright 2016-2019 OCAD university + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = fluid || require("infusion"), + gpii = fluid.registerNamespace("gpii"), + $ = fluid.registerNamespace("jQuery"), + uuid = uuid || require("node-uuid"); + +fluid.registerNamespace("gpii.dbOperation.dbDataStore"); + +/** + * Use the kettle dataSource `get` method to retrieve one record. This function provides extra + * verification on input required fields. It returns an empty object if the record is not found. + * This requires further processing besides using the kettle dataSource `notFoundIsEmpty` option because + * when retrieving CouchDB using views , an empty `rows` array rather than 404 (not found) http + * response code will be received when the record is not found. + * @param {Component} dataSource - An instance of gpii.dbOperation.dbDataSource. + * @param {Object} directModel - The direct model expressing the "coordinates" of the model to be fetched. + * @param {String|Array} valueNotEmpty - One or more required field(s). + * @param {Function} dataProcessFunc - The function to further process the retrieved record when the returned + * record is not empty. + * @return {Promise} A promise for the retrieved record. + */ +gpii.dbOperation.dbDataStore.findRecord = function (dataSource, directModel, valueNotEmpty, dataProcessFunc) { + // Remove or rename CouchDB specific fields such as _id, _rev, type + dataProcessFunc = dataProcessFunc || gpii.dbOperation.dbDataStore.cleanUpDoc; + var promiseTogo = fluid.promise(); + + // Verify required field values. Make sure they are not undefined. + var emptyFields = gpii.dbOperation.dbDataStore.filterEmptyFields(directModel, valueNotEmpty); + + if (emptyFields.length > 0) { + var error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingInput, {fieldName: emptyFields.join(" & ")}); + fluid.log("gpii-db-operation, findRecord(), error: ", error); + promiseTogo.reject(error); // reject #1 of 3. + } else { + var finalDirectModel = fluid.extend(true, {}, dataSource.options.directModel, directModel); + var promise = dataSource.get(finalDirectModel); + promise.then(function (data) { + // TODO: The line below that converts an empty object to undefined is to work around an issue with using the + // kettle notFoundIsEmpty option with fetching couchDB documents by views. The way that notFoundIsEmpty is + // implemented in kettle is that, it returns undefined when encountering a 404 response. However, when querying + // couchdb by views, the returned value would not be a 404 http status code even when the doc is not found. + // The response would still be an object but with an empty "rows" array. An example response is: + // { total_rows: 1, offset: 0, rows: [] } + // This response is then further transformed using kettle readPayload option: + // readPayload: { "": "rows.0.value" } + // Due to an issue with the infusion model transformation described at https://issues.fluidproject.org/browse/FLUID-5969, + // after the transformation, an empty object is eventually received here and then converted into undefined. + // Note that this issue only occurs when querying CouchDB by a view(map) function. when querying CouchDB directly + // by a document id, 404 status is returned and this conversion is not needed. + var result = $.isEmptyObject(data) ? undefined : dataProcessFunc(data); + if (result !== undefined && result.isError) { + promiseTogo.reject(result); // reject #2 of 3. + } else { + promiseTogo.resolve(result); // result can be undefined. + } + }, function (error) { + fluid.log("gpii-db-operation, findRecord(), error: ", error); + promiseTogo.reject(error); // reject #3 of 3. + }); + } + return promiseTogo; +}; + +/** + * Filter the given array valueNotEmpty to return elements that satisfy: + * 1. the element isn't used as a path name in the object; + * 2. the element matches a path name in the object but the corresponding value is undefined. + * Note the given object can NOT be a nested object. + * @param {Object} obj - The object used for path name check. + * @param {String|Array} valueNotEmpty - One or a set of path name(s) to look up in the give obj. + * @return {Array} An subset array of valueNotEmpty. + * For example, gpii.dbOperation.dbDataStore.filterEmptyFields({"a": 1, "c": undefined}, ["a", "b", "c"]) returns ["b", "c"]. + */ +gpii.dbOperation.dbDataStore.filterEmptyFields = function (obj, valueNotEmpty) { + var emptyFields = []; + + valueNotEmpty = fluid.makeArray(valueNotEmpty); + fluid.each(valueNotEmpty, function (fieldName) { + if (obj[fieldName] === undefined) { + emptyFields.push(fieldName); + } + }); + return emptyFields; +}; + +/** + * Remove the CouchDB internal fields: _id and _rev. Also save "_id" field value into "id" field. + * The use of "id" instead of "_id" field name is to maintain the API backward compatibility as data store + * API is expected to output the record identifier in "id" field instead of a couchdb specific name + * of "_id". + * @param {Object} data - An object to transform. + * @return {Object} An object with CouchDB-specific internal fields transformed. + */ +gpii.dbOperation.dbDataStore.cleanUpDoc = function (data) { + if (data) { + data.id = data._id; + delete data._id; + delete data._rev; + } + return data; +}; + +/** Use the kettle dataSource `set` method to create a new record. Before sending the input data to + * CouchDB, it is modified by adding an unique _id field and a proper document type. + * @param {Component} dataSource - An instance of gpii.dbOperation.dbDataSource that handles the record creation. + * @param {String} docType - The document type. See gpii.dbOperation.docTypes defined in + * %gpii-universal/gpii/node_modules/gpii-db-operation/src/DbConst.js. + * @param {String} idName - The name for the unique id field. Usually "id". + * @param {Object} data - The data to be saved in the new record. + * @param {String} [idValue] - [optional] The id value. If not provided, an UUID will be generated. + * @return {Promise} A promise for the save response. +*/ +gpii.dbOperation.dbDataStore.addRecord = function (dataSource, docType, idName, data, idValue) { + var promise = fluid.promise(); + + if (data !== undefined) { + var directModel = {}; + directModel[idName] = idValue || uuid.v4(); + fluid.extend(data, {type: docType}); + var finalDirectModel = fluid.extend(true, {}, dataSource.options.directModel, directModel); + promise = dataSource.set(finalDirectModel, data); + } else { + fluid.log("gpii-db-operation, addRecord(), error occurs: ", error); + var error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingDoc, {docType: docType}); + promise.reject(error); + } + return promise; +}; + +/** Use the kettle dataSource `set` method to udpate a record by its id. + * @param {Component} dataSource - An instance of gpii.dbOperation.dbDataSource that handles the record update. + * @param {String} docType - The expected document type in the updated record. See gpii.dbOperation.docTypes defined in + * %gpii-universal/gpii/node_modules/gpii-db-operation/src/DbConst.js. + * @param {String} docId - The document id. + * @param {Object} data - The data to be updated. + * @return {Promise} A promise for the update response. +*/ +gpii.dbOperation.dbDataStore.updateRecord = function (dataSource, docType, docId, data) { + var promise = fluid.promise(); + var error; + + if (!data) { + error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingDoc, {docType: docType}); + promise.reject(error); + } else if (data.type !== docType) { + error = gpii.dbOperation.composeError(gpii.dbOperation.errors.mismatchedDocType, {docType: docType, selectedDocType: data.type}); + promise.reject(error); + } else { + var directModel = { + id: docId + }; + var finalDirectModel = fluid.extend(true, {}, dataSource.options.directModel, directModel); + promise = dataSource.set(finalDirectModel, data); + } + return promise; +}; + +// General GPII keys Functions +// --------------------------- + +/** + * Add a GPII key + * @param {Component} saveDataSource - The saveDataSource component provided by gpii.dbOperation.dbDataStore. + * @param {Object} gpiiKeyData - The GPII key data. An example of the data: + * { + * gpiiKey: {String}, // Optional + * prefsSafeId: {String}, + * prefsSetId: {String} + * } + * If this parameter is not provided, a new GPII key is still generated but does not associate with a prefs safe. + * @return {Promise} A promise object that carries either a response returned from CouchDB for adding the + * GPII key record. When `gpiiKeyData` is not provided, returns the object {prefsSafeId: null, prefsSetId: null}. + */ +gpii.dbOperation.dbDataStore.addGpiiKey = function (saveDataSource, gpiiKeyData) { + var promiseTogo = fluid.promise(); + var data; + + if (!gpiiKeyData) { + gpiiKeyData = { + prefsSafeId: null, + prefsSetId: null + }; + } + + data = { + schemaVersion: gpii.dbOperation.schemaVersion, + prefsSafeId: gpiiKeyData.prefsSafeId || null, + prefsSetId: gpiiKeyData.prefsSetId || null, + revoked: false, + revokedReason: null, + timestampCreated: gpii.dbOperation.getCurrentTimestamp(), + timestampUpdated: null + }; + + promiseTogo = gpii.dbOperation.dbDataStore.addRecord(saveDataSource, gpii.dbOperation.docTypes.gpiiKey, "id", data, gpiiKeyData.gpiiKey); + + return promiseTogo; +}; + +/** + * Update a GPII key record + * @param {Component} saveDataSource - The saveDataSource component provided by gpii.dbOperation.dbDataStore. + * @param {String} gpiiKey - The GPII key. + * @param {Object} gpiiKeyData - The GPII key data. An example of the data: + * { + * type: {String}, + * schemaVersion: {String}, + * prefsSafeId: {String}, + * prefsSetId: {String}, + * revoked: {String}, + * revokedReason: {String}, + * timestampCreated: {Date}, + * timestampRevoked: {Date} + * } + * + * @return {Promise} A promise object that carries either a response returned from CouchDB for updating the + * GPII key record, or an error if `gpiiKey` or `gpiiKeyData` is not provided or `gpiiKeyData.type` is not + * the doc type for gpiiKey. + */ +gpii.dbOperation.dbDataStore.updateGpiiKey = function (saveDataSource, gpiiKey, gpiiKeyData) { + var promiseTogo = fluid.promise(); + var data, error; + + if (!gpiiKey) { + error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingInput, {fieldName: "gpiiKey"}); + promiseTogo.reject(error); + } else if (!gpiiKeyData) { + error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingDoc, {docType: gpii.dbOperation.docTypes.gpiiKey}); + promiseTogo.reject(error); + } else { + data = { + type: gpiiKeyData.type, + schemaVersion: gpiiKeyData.schemaVersion, + prefsSafeId: gpiiKeyData.prefsSafeId, + prefsSetId: gpiiKeyData.prefsSetId, + revoked: gpiiKeyData.revoked, + revokedReason: gpiiKeyData.revokedReason, + timestampCreated: gpiiKeyData.timestampCreated, + timestampUpdated: gpii.dbOperation.getCurrentTimestamp(), + timestampRevoked: gpiiKeyData.timestampRevoked + }; + + promiseTogo = gpii.dbOperation.dbDataStore.updateRecord(saveDataSource, gpii.dbOperation.docTypes.gpiiKey, gpiiKey, data); + } + return promiseTogo; +}; + +// General Preferences Safes Functions +// ----------------------------------- + +/** + * Transform the data in CouchDB form to a more understandable structure + * @param {Object} data - Contains GPII key and preferences safe information associated with a GPII key. + * An input example: + * { + * key: {String}, // GPII key + * id: {String}, // GPII key + * value: { + * _id: {String}, // prefs Safe id + * gpiiKey: { + * type: {String}, + * schemaVersion: {String}, + * prefsSafeId: {String}, + * prefsSetId: {String}, + * revoked: {Boolean}, + * revokedReason: {String}, + * timestampCreated: {Date}, + * timestampUpdated: {Date}, + * _id: {String}, + * _rev: {String} + * } + * }, + * doc: { + * type: {String}, + * schemaVersion: {String}, + * name: {String}, + * password: {String}, + * email: {String}, + * preferences: {Object}, + * timestampCreated: {Date}, + * timestampUpdated: {Date}, + * _id: {String}, + * _rev: {String} + * } + * } + * @return {Object} An object in the structure: + * { + * gpiiKey: {String}, + * gpiiKeyDetails: {Object}, + * prefsSafe: {Object} + * } + */ +gpii.dbOperation.dbDataStore.findPrefsSafeByGpiiKeyPostProcess = function (data) { + var result; + + if (data && data.doc && data.value) { + result = { + gpiiKey: data.key, + gpiiKeyDetails: gpii.dbOperation.dbDataStore.cleanUpDoc(data.value.gpiiKey), + prefsSafe: data.doc.type === gpii.dbOperation.docTypes.prefsSafe ? gpii.dbOperation.dbDataStore.cleanUpDoc(data.doc) : null + }; + } + + return result; +}; + +/** + * Add a prefs safe + * @param {Component} saveDataSource - The saveDataSource component provided by gpii.dbOperation.dbDataStore. + * @param {Object} prefsSafeData - The prefs safe data. An example of the data: + * { + * prefsSafeType: {String}, + * name: {String}, + * password: {String}, + * email: {String}, + * preferences: {Object} + * } + * + * @return {Promise} A promise object that carries either a response returned from CouchDB for adding the + * prefs safe record, or an error if `prefsSafeData` parameter is not provided. + */ +gpii.dbOperation.dbDataStore.addPrefsSafe = function (saveDataSource, prefsSafeData) { + var promiseTogo = fluid.promise(); + var data; + + if (!prefsSafeData) { + var error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingDoc, {docType: gpii.dbOperation.docTypes.prefsSafe}); + promiseTogo.reject(error); + } else { + data = { + schemaVersion: gpii.dbOperation.schemaVersion, + prefsSafeType: prefsSafeData.prefsSafeType, + name: prefsSafeData.name, + password: prefsSafeData.password, + email: prefsSafeData.email, + preferences: prefsSafeData.preferences, + timestampCreated: gpii.dbOperation.getCurrentTimestamp(), + timestampUpdated: null + }; + + promiseTogo = gpii.dbOperation.dbDataStore.addRecord(saveDataSource, gpii.dbOperation.docTypes.prefsSafe, "id", data); + } + return promiseTogo; +}; + +/** + * Update a prefs safe + * @param {Component} saveDataSource - The saveDataSource component provided by gpii.dbOperation.dbDataStore. + * @param {String} prefsSafeId - The prefs safe id. + * @param {Object} prefsSafeData - The prefs safe data. An example of the data: + * { + * type: {String}, + * schemaVersion: {String}, + * prefsSafeType: {String}, + * name: {String}, + * password: {String}, + * email: {String}, + * preferences: {Object}, + * timestampCreated: {Date} + * } + * @return {Promise} A promise object that carries either a response returned from CouchDB for updating the + * prefs safe record, or an error if `prefsSafeId` or `prefsSafeData` is not provided or `prefsSafeData.type` is not + * the doc type for prefsSafe. + */ +gpii.dbOperation.dbDataStore.updatePrefsSafe = function (saveDataSource, prefsSafeId, prefsSafeData) { + var promiseTogo = fluid.promise(); + var data, error; + + if (!prefsSafeId) { + error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingInput, {fieldName: "prefsSafeId"}); + promiseTogo.reject(error); + } else if (!prefsSafeData) { + error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingDoc, {docType: gpii.dbOperation.docTypes.prefsSafe}); + promiseTogo.reject(error); + } else { + data = { + type: prefsSafeData.type, + schemaVersion: prefsSafeData.schemaVersion, + prefsSafeType: prefsSafeData.prefsSafeType, + name: prefsSafeData.name || null, + password: prefsSafeData.password || null, + email: prefsSafeData.email || null, + preferences: prefsSafeData.preferences, + timestampCreated: prefsSafeData.timestampCreated, + timestampUpdated: gpii.dbOperation.getCurrentTimestamp() + }; + + promiseTogo = gpii.dbOperation.dbDataStore.updateRecord(saveDataSource, gpii.dbOperation.docTypes.prefsSafe, prefsSafeId, data); + } + return promiseTogo; +}; + +// General Client Functions +// ------------------------ + +/** +* Transform the data in CouchDB form to a more understandable structure + * @param {Object} data - Contains both client and client credential information associated with an oauth2 client id. + * An input example: + * { + * key: {String}, // access token + * id: {String}, // authorization id + * value: { + * _id: {String}, // client id + * clientCredential: { + * type: {String}, + * schemaVersion: {String}, + * clientId: {String}, + * oauth2ClientId: {String}, + * oauth2ClientSecret: {String}, + * revoked: {Boolean}, + * revokedReason: {String}, + * timestampCreated: {Date}, + * timestampRevoked: {Date}, + * _id: {String}, + * _rev: {String} + * } + * }, + * doc: { + * type: {String}, // client type + * schemaVersion: {String}, + * name: {String}, + * computerType: {String}, + * timestampCreated: {Date}, + * timestampUpdated: {Date}, + * _id: {String}, + * _rev: {String} + * } + * } + * @return {Object} An object in the structure: + * { + * oauth2ClientId: {String}, + * client: {Object}, + * clientCredential: {Object} + * } + */ +gpii.dbOperation.dbDataStore.findClientByOauth2ClientIdPostProcess = function (data) { + var result; + + if (data && data.doc && data.value) { + result = { + oauth2ClientId: data.key, + client: gpii.dbOperation.dbDataStore.cleanUpDoc(data.doc), + clientCredential: gpii.dbOperation.dbDataStore.cleanUpDoc(data.value.clientCredential) + }; + } + + return result; +}; + +// General Authorization Functions +// ------------------------------------ + +/** +* Transform the data in CouchDB form to a more understandable structure + * @param {Object} data - Contains both client and authorization information associated with an access token. + * An input example: + * { + * key: {String}, // access token + * id: {String}, // authorization id + * value: { + * _id: {String}, // client id + * authorization: { + * type: {String}, + * schemaVersion: {String}, + * clientId: {String}, + * gpiiKey: {String}, + * accessToken: {String}, + * revoked: {Boolean}, + * revokedReason: {String}, + * timestampCreated: {Date}, + * timestampRevoked: {Date}, + * timestampExpires: {Date}, + * _id: {String}, + * _rev: {String} + * } + * }, + * doc: { + * type: {String}, // client type + * name: {String}, + * computerType: {String}, + * timestampCreated: {Date}, + * timestampUpdated: {Date}, + * _id: {String}, + * _rev: {String} + * } + * } + * @return {Object} An object in the structure: + * { + * accessToken: {String}, + * authorization: {Object} + * } + */ +gpii.dbOperation.dbDataStore.findInfoByAccessTokenPostProcess = function (data) { + var result; + + if (data && data.doc && data.value) { + result = { + accessToken: data.key, + clientCredential: gpii.dbOperation.dbDataStore.cleanUpDoc(data.doc), + authorization: gpii.dbOperation.dbDataStore.cleanUpDoc(data.value.authorization) + }; + } + + return result; +}; + +/** + * Add an authorization + * @param {Component} saveDataSource - The saveDataSource component provided by gpii.dbOperation.dbDataStore. + * @param {Object} authorizationData - The authorization data. An example of gpiiAppInstallationAuthorization data: + * + * gpiiAppInstallationAuthorization: + * { + * clientId: {String}, + * gpiiKey: {String}, + * clientCredentialId: {String}, + * accessToken: {String}, + * timestampExpires: {String} + * } + * + * @return {Promise} A promise object that carries either a response returned from CouchDB for adding the + * authorization record, or an error if `authorizationData` parameter is not provided. + */ +gpii.dbOperation.dbDataStore.addAuthorization = function (saveDataSource, authorizationData) { + var promiseTogo = fluid.promise(); + var data; + + if (!authorizationData) { + var error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingDoc, {docType: gpii.dbOperation.docTypes.gpiiAppInstallationAuthorization}); + promiseTogo.reject(error); + } else { + data = { + schemaVersion: gpii.dbOperation.schemaVersion, + clientId: authorizationData.clientId, + gpiiKey: authorizationData.gpiiKey, + clientCredentialId: authorizationData.clientCredentialId, + accessToken: authorizationData.accessToken, + revoked: false, + revokedReason: null, + timestampCreated: gpii.dbOperation.getCurrentTimestamp(), + timestampRevoked: null, + timestampExpires: authorizationData.timestampExpires + }; + + promiseTogo = gpii.dbOperation.dbDataStore.addRecord(saveDataSource, gpii.dbOperation.docTypes.gpiiAppInstallationAuthorization, "id", data); + } + return promiseTogo; +}; diff --git a/gpii/node_modules/gpii-db-operation/src/DbUtils.js b/gpii/node_modules/gpii-db-operation/src/DbUtils.js new file mode 100644 index 000000000..4113d1bc7 --- /dev/null +++ b/gpii/node_modules/gpii-db-operation/src/DbUtils.js @@ -0,0 +1,38 @@ +/*! +GPII Database Utilities + +Copyright 2017 OCAD university + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = fluid || require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.dbOperation"); + +/** Use `fluid.stringTemplate` method to replace terms within error.message with actual values. + * @param {Object} error - An object that contains an element keyed by "message". + * @param {Object} termMap - An object that contains the mapping between terms used within error.message and their actual values. + * @return {Object} The input object "error" with terms in error.message being replaced by actual values. +*/ +gpii.dbOperation.composeError = function (error, termMap) { + var err = fluid.copy(error); + err.message = fluid.stringTemplate(err.message, termMap); + return err; +}; + +/** + * Returns the current time in a human readable string that also naturally sort in chronological order. + * See http://www.ecma-international.org/ecma-262/5.1/#sec-15.9.5.43 + * @return {String} The current time in ISO string format. + */ +gpii.dbOperation.getCurrentTimestamp = function () { + return new Date().toISOString(); +}; diff --git a/gpii/node_modules/gpii-db-operation/test/DbDataStoreTests.js b/gpii/node_modules/gpii-db-operation/test/DbDataStoreTests.js new file mode 100644 index 000000000..466f6a9f7 --- /dev/null +++ b/gpii/node_modules/gpii-db-operation/test/DbDataStoreTests.js @@ -0,0 +1,526 @@ +/** + GPII DB Data Store Tests + + Copyright 2016-2019 OCAD University + + Licensed under the New BSD license. You may not use this file except in + compliance with this License. + + You may obtain a copy of the License at + https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +require("gpii-couchdb-test-harness"); +gpii.test.couchdb.loadTestingSupport(); + +require("gpii-db-operation"); +require("./DbDataStoreTestsUtils.js"); + +// The test data is from %gpii-db-operation/test/data/*.json +fluid.defaults("gpii.tests.dbDataStore.findGpiiKey", { + gradeNames: ["gpii.tests.dbDataStore.environment"], + rawModules: [{ + name: "Test findGpiiKey()", + tests: [ + { + name: "Find a GPII key record by a GPII key", + sequence: [{ + task: "{dbDataStore}.findGpiiKey", + args: ["chrome_high_contrast"], + resolve: "jqUnit.assertDeepEq", + resolveArgs: ["The expected GPII key data is received", gpii.tests.dbDataStore.testData.gpiiKeyChromehcDefault, "{arguments}.0"] + }] + }, + { + name: "Finding a GPII key record by a non-existing GPII key returns undefined", + sequence: [ + { + task: "{dbDataStore}.findGpiiKey", + args: ["non-existing"], + resolve: "jqUnit.assertUndefined", + resolveArgs: ["Finding an user by a non-existing GPII key returns undefined", "{arguments}.0"] + }] + }, + { + name: "Not providing a GPII key returns 400 status code and error message", + sequence: [{ + task: "{dbDataStore}.findGpiiKey", + args: [], + reject: "jqUnit.assertDeepEq", + rejectArgs: ["The expected error is received", { + message: "The input field \"id\" was undefined", + statusCode: 400, + isError: true + }, "{arguments}.0"] + }] + } + ] + }] +}); + +fluid.defaults("gpii.tests.dbDataStore.findPrefsSafeByGpiiKey", { + gradeNames: ["gpii.tests.dbDataStore.environment"], + rawModules: [{ + name: "Test findPrefsSafeByGpiiKey()", + tests: [ + { + name: "Find preferences by a proper GPII key that associates with a prefs safe", + sequence: [{ + task: "{dbDataStore}.findPrefsSafeByGpiiKey", + args: ["chrome_high_contrast"], + resolve: "jqUnit.assertDeepEq", + resolveArgs: ["The expected client data is received", gpii.tests.dbDataStore.testData.findPrefsSafeByGpiiKey_withPrefsSafe, "{arguments}.0"] + }] + }, + { + name: "Find preferences by a proper GPII key that does not associate with any prefs safe", + sequence: [{ + task: "{dbDataStore}.findPrefsSafeByGpiiKey", + args: ["chrome_high_contrast_dark"], + resolve: "jqUnit.assertDeepEq", + resolveArgs: ["The expected client data is received", gpii.tests.dbDataStore.testData.findPrefsSafeByGpiiKey_noPrefsSafe, "{arguments}.0"] + }] + }, + { + name: "Finding preferences by a non-existing GPII key returns undefined", + sequence: [{ + task: "{dbDataStore}.findPrefsSafeByGpiiKey", + args: ["non-existing"], + resolve: "jqUnit.assertUndefined", + resolvArgs: ["Finding preferences by a non-existing GPII key returns undefined", "{arguments}.0"] + }] + }, + { + name: "Not providing an GPII key returns 400 status code and error message", + sequence: [{ + task: "{dbDataStore}.findPrefsSafeByGpiiKey", + args: [], + reject: "jqUnit.assertDeepEq", + rejectArgs: ["The expected error is received", { + message: "The input field \"gpiiKey\" was undefined", + statusCode: 400, + isError: true + }, "{arguments}.0"] + }] + }] + }] +}); + +fluid.defaults("gpii.tests.dbDataStore.findClientById", { + gradeNames: ["gpii.tests.dbDataStore.environment"], + rawModules: [{ + name: "Test findClientById()", + tests: [ + { + name: "Find a client record by a proper client id", + sequence: [{ + task: "{dbDataStore}.findClientById", + args: ["gpiiAppInstallationClient-1"], + resolve: "jqUnit.assertDeepEq", + resolveArgs: ["The expected client data is received", gpii.tests.dbDataStore.testData.client1, "{arguments}.0"] + }] + }, + { + name: "Finding a client record by a non-existing client id returns undefined", + sequence: [{ + task: "{dbDataStore}.findClientById", + args: ["non-existing"], + resolve: "jqUnit.assertUndefined", + resolveArgs: ["Finding a client by a non-existing client id returns undefined", "{arguments}.0"] + }] + }, + { + name: "Not providing client id returns 400 status code and error message", + sequence: [{ + task: "{dbDataStore}.findClientById", + args: [], + reject: "jqUnit.assertDeepEq", + rejectArgs: ["The expected error is received", { + message: "The input field \"id\" was undefined", + statusCode: 400, + isError: true + }, "{arguments}.0"] + }] + } + ] + }] +}); + +fluid.defaults("gpii.tests.dbDataStore.findClientCredentialById", { + gradeNames: ["gpii.tests.dbDataStore.environment"], + rawModules: [{ + name: "Test findClientCredentialById()", + tests: [ + { + name: "Find a client credential record by a proper client credential id", + sequence: [{ + task: "{dbDataStore}.findClientCredentialById", + args: ["clientCredential-1"], + resolve: "jqUnit.assertDeepEq", + resolveArgs: ["The expected client data is received", gpii.tests.dbDataStore.testData.clientCredential1, "{arguments}.0"] + }] + }, + { + name: "Finding a client record by a non-existing client credential id returns undefined", + sequence: [{ + task: "{dbDataStore}.findClientCredentialById", + args: ["non-existing"], + resolve: "jqUnit.assertUndefined", + resolveArgs: ["Finding a client by a non-existing client credential id returns undefined", "{arguments}.0"] + }] + }, + { + name: "Not providing client credential id returns 400 status code and error message", + sequence: [{ + task: "{dbDataStore}.findClientCredentialById", + args: [], + reject: "jqUnit.assertDeepEq", + rejectArgs: ["The expected error is received", { + message: "The input field \"id\" was undefined", + statusCode: 400, + isError: true + }, "{arguments}.0"] + }] + } + ] + }] +}); + +fluid.defaults("gpii.tests.dbDataStore.findClientByOauth2ClientId", { + gradeNames: ["gpii.tests.dbDataStore.environment"], + rawModules: [{ + name: "Test findClientByOauth2ClientId()", + tests: [ + { + name: "Find a client record by a proper oauth2 client id", + sequence: [{ + task: "{dbDataStore}.findClientByOauth2ClientId", + args: ["net.gpii.ajc.bakersfield"], + resolve: "jqUnit.assertDeepEq", + resolveArgs: ["The expected client data is received", gpii.tests.dbDataStore.testData.findClientByOauth2ClientId, "{arguments}.0"] + }] + }, + { + name: "Finding a client record by a non-existing oauth2 client id returns undefined", + sequence: [{ + task: "{dbDataStore}.findClientByOauth2ClientId", + args: ["non-existing"], + resolve: "jqUnit.assertUndefined", + resolveArgs: ["Finding a client by a non-existing oauth2 client id returns undefined", "{arguments}.0"] + }] + }, + { + name: "Not providing an oauth2 client id returns 400 status code and error message", + sequence: [{ + task: "{dbDataStore}.findClientByOauth2ClientId", + args: [], + reject: "jqUnit.assertDeepEq", + rejectArgs: ["The expected error is received", { + message: "The input field \"oauth2ClientId\" was undefined", + statusCode: 400, + isError: true + }, "{arguments}.0"] + }] + } + ] + }] +}); + +fluid.defaults("gpii.tests.dbDataStore.findInfoByAccessToken", { + gradeNames: ["gpii.tests.dbDataStore.environment"], + rawModules: [{ + name: "Test findInfoByAccessToken()", + tests: [ + { + name: "Find an authorization information by a GPII app installation access token", + sequence: [{ + task: "{dbDataStore}.findInfoByAccessToken", + args: ["gpii-app-installation-accessToken-1"], + resolve: "jqUnit.assertDeepEq", + resolveArgs: ["The expected data is received", gpii.tests.dbDataStore.testData.findGpiiAppInstallationAuthorizationByAccessToken, "{arguments}.0"] + }] + }, + { + name: "Returns undefined when the authorization is revoked", + sequence: [{ + task: "{dbDataStore}.findInfoByAccessToken", + args: ["gpii-app-installation-accessToken-3"], + resolve: "jqUnit.assertDeepEq", + resolveArgs: ["The expected undefined is received", undefined, "{arguments}.0"] + }] + }, + { + name: "Not providing an input argument returns 400 status code and error message", + sequence: [{ + task: "{dbDataStore}.findInfoByAccessToken", + args: [], + reject: "jqUnit.assertDeepEq", + rejectArgs: ["The expected error is received", { + message: "The input field \"accessToken\" was undefined", + statusCode: 400, + isError: true + }, "{arguments}.0"] + }] + }, + { + name: "Find by a non-existing access token returns undefined", + sequence: [{ + task: "{dbDataStore}.findInfoByAccessToken", + args: ["non-existing"], + resolve: "jqUnit.assertDeepEq", + resolveArgs: ["The expected error is received", undefined, "{arguments}.0"] + }] + } + ] + }] +}); + +fluid.defaults("gpii.tests.dbDataStore.findAllViews", { + gradeNames: ["gpii.tests.dbDataStore.environment"], + rawModules: [{ + name: "Test findAllViews()", + tests: [{ + name: "Find all views", + sequence: [{ + task: "{dbDataStore}.findAllViews", + args: [], + resolve: "jqUnit.assertValue", + resolveArgs: ["The views received", "{arguments}.0.views"] + }] + }] + }] +}); + +fluid.defaults("gpii.tests.dbDataStore.addGpiiKey", { + gradeNames: ["gpii.tests.dbDataStore.environment"], + rawModules: [{ + name: "Test addGpiiKey()", + tests: [ + { + name: "Add a GPII key - auto generates the GPII key if the key value is not provided at the input", + sequence: [{ + task: "gpii.tests.dbDataStore.executeFnAndVerifyResults", + args: ["{dbDataStore}", "addGpiiKey", [gpii.tests.dbDataStore.testData.gpiiKeyToCreate_noGpiiKey], "{testCaseHolder}"], // dbDataStore, fnName, fnArgs, caseHolder + resolve: "gpii.tests.dbDataStore.verifyFetchedGpiiKey", + resolveArgs: ["{arguments}.0", gpii.tests.dbDataStore.testData.gpiiKeyToCreate_noGpiiKey, "{that}"] + }] + }, + { + name: "Add a GPII key - use the provided GPII key if it is provided at the input", + sequence: [{ + task: "gpii.tests.dbDataStore.executeFnAndVerifyResults", + args: ["{dbDataStore}", "addGpiiKey", [gpii.tests.dbDataStore.testData.gpiiKeyToCreate_withGpiiKey], "{testCaseHolder}"], // dbDataStore, fnName, fnArgs, caseHolder + resolve: "gpii.tests.dbDataStore.verifyFetchedGpiiKey", + resolveArgs: ["{arguments}.0", gpii.tests.dbDataStore.testData.gpiiKeyToCreate_withGpiiKey, "{that}"] + }] + }, + { + name: "Not providing gpiiKeyData still creates a new GPII key but it does not associate with a prefs safe.", + sequence: [{ + task: "gpii.tests.dbDataStore.executeFnAndVerifyResults", + args: ["{dbDataStore}", "addGpiiKey", [undefined], "{testCaseHolder}"], // dbDataStore, fnName, fnArgs, caseHolder + resolve: "gpii.tests.dbDataStore.verifyFetchedGpiiKey", + resolveArgs: ["{arguments}.0", { + prefsSafeId: null, + prefsSetId: null + }, "{that}"] + }] + } + ] + }] +}); + +fluid.defaults("gpii.tests.dbDataStore.updateGpiiKey", { + gradeNames: ["gpii.tests.dbDataStore.environment"], + rawModules: [{ + name: "Test updateGpiiKey()", + tests: [ + { + name: "Update a GPII key", + sequence: [{ + task: "gpii.tests.dbDataStore.executeFnAndVerifyResults", + args: ["{dbDataStore}", "updateGpiiKey", ["chrome_high_contrast", gpii.tests.dbDataStore.testData.gpiiKeyToUpdate], "{testCaseHolder}"], // dbDataStore, fnName, fnArgs, caseHolder + resolve: "gpii.tests.dbDataStore.verifyUpdatedRecord", + resolveArgs: ["{arguments}.0", gpii.tests.dbDataStore.testData.gpiiKeyToUpdate, "{that}"] + }] + }, + { + name: "Not providing gpiiKey returns error", + sequence: [{ + task: "{dbDataStore}.updateGpiiKey", + args: [undefined, gpii.tests.dbDataStore.testData.gpiiKeyToUpdate], + reject: "jqUnit.assertDeepEq", + rejectArgs: ["The expected error is received", { + message: "The input field \"gpiiKey\" was undefined", + statusCode: 400, + isError: true + }, "{arguments}.0"] + }] + }, + { + name: "Updating an empty object returns error", + sequence: [{ + task: "{dbDataStore}.updateGpiiKey", + args: ["chrome_high_contrast", undefined], + reject: "jqUnit.assertDeepEq", + rejectArgs: ["The expected error is received", { + message: "A record of type \"gpiiKey\" was not found", + statusCode: 400, + isError: true + }, "{arguments}.0"] + }] + }, + { + name: "Updating a document type other than gpiiKey returns an error", + sequence: [{ + task: "{dbDataStore}.updateGpiiKey", + args: ["prefsSafe-1", {type: "non-gpiiKey"}], + reject: "jqUnit.assertDeepEq", + rejectArgs: ["The expected error is received", { + message: "The document type must be \"gpiiKey\" instead of the selected document type \"non-gpiiKey\"", + statusCode: 400, + isError: true + }, "{arguments}.0"] + }] + } + ] + }] +}); + +fluid.defaults("gpii.tests.dbDataStore.addPrefsSafe", { + gradeNames: ["gpii.tests.dbDataStore.environment"], + rawModules: [{ + name: "Test addPrefsSafe()", + tests: [ + { + name: "Add a prefs safe", + sequence: [{ + task: "gpii.tests.dbDataStore.executeFnAndVerifyResults", + args: ["{dbDataStore}", "addPrefsSafe", [gpii.tests.dbDataStore.testData.prefsSafeToCreate], "{testCaseHolder}"], // dbDataStore, fnName, fnArgs, caseHolder + resolve: "gpii.tests.dbDataStore.verifyCreatedPrefsSafe", + resolveArgs: ["{arguments}.0", gpii.tests.dbDataStore.testData.prefsSafeToCreate, "{that}"] + }] + }, + { + name: "Adding an empty object returns error", + sequence: [{ + task: "{dbDataStore}.addPrefsSafe", + args: [undefined], + reject: "jqUnit.assertDeepEq", + rejectArgs: ["The expected error is received", { + message: "A record of type \"prefsSafe\" was not found", + statusCode: 400, + isError: true + }, "{arguments}.0"] + }] + } + ] + }] +}); + +fluid.defaults("gpii.tests.dbDataStore.updatePrefsSafe", { + gradeNames: ["gpii.tests.dbDataStore.environment"], + rawModules: [{ + name: "Test updatePrefsSafe()", + tests: [ + { + name: "Update a prefs safe", + sequence: [{ + task: "gpii.tests.dbDataStore.executeFnAndVerifyResults", + args: ["{dbDataStore}", "updatePrefsSafe", ["prefsSafe-1", gpii.tests.dbDataStore.testData.prefsSafeToUpdate], "{testCaseHolder}"], // dbDataStore, fnName, fnArgs, caseHolder + resolve: "gpii.tests.dbDataStore.verifyUpdatedRecord", + resolveArgs: ["{arguments}.0", gpii.tests.dbDataStore.testData.prefsSafeToUpdate, "{that}"] + }] + }, + { + name: "Not providing prefsSafeId returns error", + sequence: [{ + task: "{dbDataStore}.updatePrefsSafe", + args: [undefined, gpii.tests.dbDataStore.testData.prefsSafeToUpdate], + reject: "jqUnit.assertDeepEq", + rejectArgs: ["The expected error is received", { + message: "The input field \"prefsSafeId\" was undefined", + statusCode: 400, + isError: true + }, "{arguments}.0"] + }] + }, + { + name: "Updating an empty object returns error", + sequence: [{ + task: "{dbDataStore}.updatePrefsSafe", + args: ["prefsSafe-1", undefined], + reject: "jqUnit.assertDeepEq", + rejectArgs: ["The expected error is received", { + message: "A record of type \"prefsSafe\" was not found", + statusCode: 400, + isError: true + }, "{arguments}.0"] + }] + }, + { + name: "Updating a document type other that prefsSafe returns an error", + sequence: [{ + task: "{dbDataStore}.updatePrefsSafe", + args: ["prefsSafe-1", {type: "non-prefsSafe"}], + reject: "jqUnit.assertDeepEq", + rejectArgs: ["The expected error is received", { + message: "The document type must be \"prefsSafe\" instead of the selected document type \"non-prefsSafe\"", + statusCode: 400, + isError: true + }, "{arguments}.0"] + }] + }] + }] +}); + +fluid.defaults("gpii.tests.dbDataStore.addAuthorization", { + gradeNames: ["gpii.tests.dbDataStore.environment"], + rawModules: [{ + name: "Test addAuthorization()", + tests: [ + { + name: "Add a GPII app installation authorization", + sequence: [{ + task: "gpii.tests.dbDataStore.executeFnAndVerifyResults", + args: ["{dbDataStore}", "addAuthorization", [gpii.tests.dbDataStore.testData.gpiiAppInstallationAuthorizationToCreate], "{testCaseHolder}"], // dbDataStore, fnName, fnArgs, caseHolder + resolve: "gpii.tests.dbDataStore.verifyFetchedGpiiAppInstallationAuthorization", + resolveArgs: ["{arguments}.0", gpii.tests.dbDataStore.testData.gpiiAppInstallationAuthorizationToCreate, "{that}"] + }] + }, + { + name: "Adding an empty object returns error", + sequence: [{ + task: "{dbDataStore}.addAuthorization", + args: [undefined], + reject: "jqUnit.assertDeepEq", + rejectArgs: ["The expected error is received", { + message: "A record of type \"gpiiAppInstallationAuthorization\" was not found", + statusCode: 400, + isError: true + }, "{arguments}.0"] + }] + } + ] + }] +}); + +fluid.test.runTests([ + "gpii.tests.dbDataStore.findGpiiKey", + "gpii.tests.dbDataStore.findClientById", + "gpii.tests.dbDataStore.findClientCredentialById", + "gpii.tests.dbDataStore.findPrefsSafeByGpiiKey", + "gpii.tests.dbDataStore.findClientByOauth2ClientId", + "gpii.tests.dbDataStore.findInfoByAccessToken", + "gpii.tests.dbDataStore.findAllViews", + "gpii.tests.dbDataStore.addGpiiKey", + "gpii.tests.dbDataStore.updateGpiiKey", + "gpii.tests.dbDataStore.addPrefsSafe", + "gpii.tests.dbDataStore.updatePrefsSafe", + "gpii.tests.dbDataStore.addAuthorization" +]); diff --git a/gpii/node_modules/gpii-db-operation/test/DbDataStoreTestsUtils.js b/gpii/node_modules/gpii-db-operation/test/DbDataStoreTestsUtils.js new file mode 100644 index 000000000..318770bf3 --- /dev/null +++ b/gpii/node_modules/gpii-db-operation/test/DbDataStoreTestsUtils.js @@ -0,0 +1,326 @@ +/** +GPII DB Data Store Tests + +Copyright 2016-2019 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.require("node-jqunit", require, "jqUnit"); + +require("gpii-couchdb-test-harness"); +gpii.test.couchdb.loadTestingSupport(); + +fluid.defaults("gpii.tests.dbDataStore.environment", { + gradeNames: ["gpii.test.couchdb.environment.base"], + databases: { + gpii: { + data: [ + "%gpii-db-operation/test/data/clientCredentials.json", + "%gpii-db-operation/test/data/gpiiAppInstallationAuthorizations.json", + "%gpii-db-operation/test/data/gpiiAppInstallationClients.json", + "%gpii-db-operation/test/data/gpiiKeys.json", + "%gpii-db-operation/test/data/prefsSafes.json", + "%gpii-universal/testData/dbData/views.json" + ] + } + }, + components: { + testCaseHolder: { + type: "gpii.tests.dbDataStore.baseTestCaseHolder" + } + }, + distributeOptions: { + source: "{that}.options.rawModules", + target: "{that > testCaseHolder}.options.rawModules" + }, + mergePolicy: { + rawModules: "noexpand" + } +}); + +fluid.defaults("gpii.tests.dbDataStore.baseTestCaseHolder", { + gradeNames: ["gpii.test.couchdb.caseHolder"], + events: { + onResponse: null, + onError: null + }, + components: { + dbDataStore: { + type: "gpii.dbOperation.dbDataStore", + options: { + dataSourceConfig: { + baseUrl: "http://localhost", + port: "{gpii.tests.dbDataStore.environment}.options.couch.port", + dbName: "gpii" + } + } + } + } +}); + +gpii.tests.dbDataStore.executeFnAndVerifyResults = function (dbDataStore, fnName, fnArgs, caseHolder) { + var outerPromise = fluid.promise(); + + dbDataStore[fnName].apply(null, fnArgs).then( + function (results) { + caseHolder.lastSavedId = results.id; + dbDataStore.findById(results.id).then(outerPromise.resolve, outerPromise.reject); + }, + outerPromise.reject + ); + return outerPromise; +}; + +gpii.tests.dbDataStore.verifyFetched = function (response, expected, that) { + jqUnit.assertEquals("The fetched document id matches the saved record", that.lastSavedId, response.id); + jqUnit.assertLeftHand("The data is saved successfully", expected, response); +}; + +gpii.tests.dbDataStore.verifyFetchedGpiiKey = function (response, expected, that) { + // verify the GPII key + var gpiiKey = expected.gpiiKey; + if (gpiiKey) { + jqUnit.assertEquals("The \"id\" value has been set to the provided key", gpiiKey, response.id); + } else { + jqUnit.assertNotUndefined("The \"id\" value has been set to an auto generated key", response.id); + } + + // verify other fields + var nonIdFields = fluid.censorKeys(expected, ["gpiiKey"]); + gpii.tests.dbDataStore.verifyFetched(response, nonIdFields, that); + jqUnit.assertEquals("The \"type\" value has been set to default", gpii.dbOperation.docTypes.gpiiKey, response.type); + jqUnit.assertEquals("The \"schemaVersion\" value has been set to default", gpii.dbOperation.schemaVersion, response.schemaVersion); + jqUnit.assertFalse("The \"revoked\" value has been set to false", response.revoked); + jqUnit.assertNull("The \"revokedReason\" value has been set to null", response.revokedReason); + jqUnit.assertNotUndefined("The \"timestampCreated\" value has been created", response.timestampCreated); + jqUnit.assertNull("The \"timestampUpdated\" value has been set to null", response.timestampUpdated); + jqUnit.assertNull("The \"timestampRevoked\" value has been set to null", response.timestampRevoked); +}; + +gpii.tests.dbDataStore.verifyCreatedPrefsSafe = function (response, expected, that) { + gpii.tests.dbDataStore.verifyFetched(response, expected, that); + jqUnit.assertEquals("The \"type\" value has been set to default", gpii.dbOperation.docTypes.prefsSafe, response.type); + jqUnit.assertEquals("The \"schemaVersion\" value has been set to default", gpii.dbOperation.schemaVersion, response.schemaVersion); + jqUnit.assertEquals("The \"prefsSafeType\" value has been set to default", expected.prefsSafeType, response.prefsSafeType); + jqUnit.assertNotUndefined("The \"timestampCreated\" value has been created", response.timestampCreated); + jqUnit.assertNull("The \"timestampUpdated\" value has been set to null", response.timestampUpdated); +}; + +gpii.tests.dbDataStore.verifyUpdatedRecord = function (response, expected, that) { + gpii.tests.dbDataStore.verifyFetched(response, expected, that); + jqUnit.assertNotUndefined("The \"timestampUpdated\" value has been created", response.timestampUpdated); +}; + +gpii.tests.dbDataStore.verifyFetchedGpiiAppInstallationAuthorization = function (response, expected, that) { + gpii.tests.dbDataStore.verifyFetched(response, expected, that); + jqUnit.assertEquals("The \"type\" value has been set to default", gpii.dbOperation.docTypes.gpiiAppInstallationAuthorization, response.type); + jqUnit.assertEquals("The \"schemaVersion\" value has been set to default", gpii.dbOperation.schemaVersion, response.schemaVersion); + jqUnit.assertFalse("The \"revoked\" value has been set to false", response.revoked); + jqUnit.assertNull("The \"revokedReason\" value has been set to null", response.revokedReason); + jqUnit.assertNotUndefined("The \"timestampCreated\" value has been created", response.timestampCreated); + jqUnit.assertNull("The \"timestampRevoked\" value has been set to null", response.timestampRevoked); +}; + +gpii.tests.dbDataStore.testData = { + gpiiKeyChromehcDefault: { + "id": "chrome_high_contrast", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": "prefsSafe-1", + "prefsSetId": "gpii-default", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + }, + client1: { + "id": "gpiiAppInstallationClient-1", + "type": "gpiiAppInstallationClient", + "schemaVersion": "0.2", + "name": "AJC-Bakersfield", + "computerType": "public", + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + }, + clientCredential1: { + "id": "clientCredential-1", + "type": "clientCredential", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-1", + "oauth2ClientId": "net.gpii.ajc.bakersfield", + "oauth2ClientSecret": "client_secret_ajc_bakersfield", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampRevoked": null + }, + findPrefsSafeByGpiiKey_withPrefsSafe: { + "gpiiKey": "chrome_high_contrast", + "gpiiKeyDetails": { + "id": "chrome_high_contrast", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": "prefsSafe-1", + "prefsSetId": "gpii-default", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + }, + "prefsSafe": { + "id": "prefsSafe-1", + "type": "prefsSafe", + "schemaVersion": "0.2", + "prefsSafeType": "user", + "name": null, + "password": null, + "email": null, + "preferences": { + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/matchMakerType": "ruleBased", + "http://registry.gpii.net/common/fontSize": 24, + "http://registry.gpii.net/common/foregroundColor": "white", + "http://registry.gpii.net/common/backgroundColor": "black", + "http://registry.gpii.net/common/fontFaceFontName": [ + "Comic Sans" + ], + "http://registry.gpii.net/common/fontFaceGenericFontFace": "sans serif", + "http://registry.gpii.net/common/magnification": 2 + } + } + } + } + }, + "timestampCreated": "2017-12-01T18:43:32.889Z", + "timestampUpdated": null + } + }, + findPrefsSafeByGpiiKey_noPrefsSafe: { + "gpiiKey": "chrome_high_contrast_dark", + "gpiiKeyDetails": { + "id": "chrome_high_contrast_dark", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": null, + "prefsSetId": null, + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + }, + "prefsSafe": null + }, + findClientByOauth2ClientId: { + "oauth2ClientId": "net.gpii.ajc.bakersfield", + "client": { + "type": "gpiiAppInstallationClient", + "schemaVersion": "0.2", + "name": "AJC-Bakersfield", + "computerType": "public", + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null, + "id": "gpiiAppInstallationClient-1" + }, + "clientCredential": { + "type": "clientCredential", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-1", + "oauth2ClientId": "net.gpii.ajc.bakersfield", + "oauth2ClientSecret": "client_secret_ajc_bakersfield", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampRevoked": null, + "id": "clientCredential-1" + } + }, + gpiiKeyToCreate_noGpiiKey: { + "prefsSafeId": "a-new-prefs-safe-id", + "prefsSetId": "gpii-default" + }, + gpiiKeyToCreate_withGpiiKey: { + "gpiiKey": "a-new-gpii-key", + "prefsSafeId": "a-new-prefs-safe-id", + "prefsSetId": "gpii-default" + }, + gpiiKeyToUpdate: { + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": "prefsSafe-1", + "prefsSetId": "updated_name", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-05-29T17:54:00.000Z", + "timestampRevoked": "2017-06-29T17:54:00.000Z" + }, + prefsSafeToCreate: { + "prefsSafeType": "user", + "name": null, + "password": null, + "email": null, + "preferences": { + "test": "test" + } + }, + prefsSafeToUpdate: { + "type": "prefsSafe", + "schemaVersion": "0.2", + "prefsSafeType": "snapset", + "name": "updated_name", + "password": "updated_password", + "email": "updated_email", + "preferences": { + "test": "test" + }, + "timestampCreated": "2017-05-29T17:54:00.000Z" + }, + gpiiAppInstallationAuthorizationToCreate: { + "clientId": "client-1", + "gpiiKey": "chrome_high_contrast", + "clientCredentialId": "clientCredential-1", + "accessToken": "gpii-app-installation-accessToken-1", + "timestampExpires": "3020-05-29T17:54:00.000Z" + }, + findGpiiAppInstallationAuthorizationByAccessToken: { + "accessToken": "gpii-app-installation-accessToken-1", + "clientCredential": { + "type": "clientCredential", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-1", + "oauth2ClientId": "net.gpii.ajc.bakersfield", + "oauth2ClientSecret": "client_secret_ajc_bakersfield", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampRevoked": null, + "id": "clientCredential-1" + }, + "authorization": { + "type": "gpiiAppInstallationAuthorization", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-1", + "gpiiKey": "chrome_high_contrast", + "clientCredentialId": "clientCredential-1", + "accessToken": "gpii-app-installation-accessToken-1", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-05-29T17:54:00.000Z", + "timestampRevoked": null, + "timestampExpires": "3020-05-30T17:54:00.000Z", + "id": "gpiiAppInstallationAuthorization-1" + } + } +}; diff --git a/gpii/node_modules/gpii-db-operation/test/data/clientCredentials.json b/gpii/node_modules/gpii-db-operation/test/data/clientCredentials.json new file mode 100644 index 000000000..312503480 --- /dev/null +++ b/gpii/node_modules/gpii-db-operation/test/data/clientCredentials.json @@ -0,0 +1,26 @@ +[ + { + "_id": "clientCredential-1", + "type": "clientCredential", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-1", + "oauth2ClientId": "net.gpii.ajc.bakersfield", + "oauth2ClientSecret": "client_secret_ajc_bakersfield", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampRevoked": null + }, + { + "_id": "clientCredential-2", + "type": "clientCredential", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-2", + "oauth2ClientId": "net.gpii.ajc.richmond", + "oauth2ClientSecret": "client_secret_ajc_richmond", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampRevoked": null + } +] diff --git a/gpii/node_modules/gpii-db-operation/test/data/gpiiAppInstallationAuthorizations.json b/gpii/node_modules/gpii-db-operation/test/data/gpiiAppInstallationAuthorizations.json new file mode 100644 index 000000000..2a2453459 --- /dev/null +++ b/gpii/node_modules/gpii-db-operation/test/data/gpiiAppInstallationAuthorizations.json @@ -0,0 +1,44 @@ +[ + { + "_id": "gpiiAppInstallationAuthorization-1", + "type": "gpiiAppInstallationAuthorization", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-1", + "gpiiKey": "chrome_high_contrast", + "clientCredentialId": "clientCredential-1", + "accessToken": "gpii-app-installation-accessToken-1", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-05-29T17:54:00.000Z", + "timestampRevoked": null, + "timestampExpires": "3020-05-30T17:54:00.000Z" + }, + { + "_id": "gpiiAppInstallationAuthorization-2", + "type": "gpiiAppInstallationAuthorization", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-2", + "gpiiKey": "chrome_high_contrast", + "clientCredentialId": "clientCredential-2", + "accessToken": "gpii-app-installation-accessToken-2", + "revoked": false, + "revokedReason": null, + "timestampCreated": "3020-05-29T17:54:00.000Z", + "timestampRevoked": null, + "timestampExpires": "3020-05-30T17:54:00.000Z" + }, + { + "_id": "gpiiAppInstallationAuthorization-3", + "type": "gpiiAppInstallationAuthorization", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-2", + "gpiiKey": "chrome_high_contrast", + "clientCredentialId": "clientCredential-2", + "accessToken": "gpii-app-installation-accessToken-3", + "revoked": true, + "revokedReason": null, + "timestampCreated": "2017-05-29T12:54:00.000Z", + "timestampRevoked": null, + "timestampExpires": "2017-05-30T13:54:00.000Z" + } +] diff --git a/gpii/node_modules/gpii-db-operation/test/data/gpiiAppInstallationClients.json b/gpii/node_modules/gpii-db-operation/test/data/gpiiAppInstallationClients.json new file mode 100644 index 000000000..adf2efc56 --- /dev/null +++ b/gpii/node_modules/gpii-db-operation/test/data/gpiiAppInstallationClients.json @@ -0,0 +1,20 @@ +[ + { + "_id": "gpiiAppInstallationClient-1", + "type": "gpiiAppInstallationClient", + "schemaVersion": "0.2", + "name": "AJC-Bakersfield", + "computerType": "public", + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + }, + { + "_id": "gpiiAppInstallationClient-2", + "type": "gpiiAppInstallationClient", + "schemaVersion": "0.2", + "name": "AJC-Richmond", + "computerType": "public", + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + } +] diff --git a/gpii/node_modules/gpii-db-operation/test/data/gpiiKeys.json b/gpii/node_modules/gpii-db-operation/test/data/gpiiKeys.json new file mode 100644 index 000000000..3b318f739 --- /dev/null +++ b/gpii/node_modules/gpii-db-operation/test/data/gpiiKeys.json @@ -0,0 +1,24 @@ +[ + { + "_id": "chrome_high_contrast", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": "prefsSafe-1", + "prefsSetId": "gpii-default", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + }, + { + "_id": "chrome_high_contrast_dark", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": null, + "prefsSetId": null, + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + } +] diff --git a/gpii/node_modules/gpii-db-operation/test/data/prefsSafes.json b/gpii/node_modules/gpii-db-operation/test/data/prefsSafes.json new file mode 100644 index 000000000..9fd2dfc47 --- /dev/null +++ b/gpii/node_modules/gpii-db-operation/test/data/prefsSafes.json @@ -0,0 +1,33 @@ +[ + { + "_id": "prefsSafe-1", + "type": "prefsSafe", + "schemaVersion": "0.2", + "prefsSafeType": "user", + "name": null, + "password": null, + "email": null, + "preferences": { + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/matchMakerType": "ruleBased", + "http://registry.gpii.net/common/fontSize": 24, + "http://registry.gpii.net/common/foregroundColor": "white", + "http://registry.gpii.net/common/backgroundColor": "black", + "http://registry.gpii.net/common/fontFaceFontName": [ + "Comic Sans" + ], + "http://registry.gpii.net/common/fontFaceGenericFontFace": "sans serif", + "http://registry.gpii.net/common/magnification": 2 + } + } + } + } + }, + "timestampCreated": "2017-12-01T18:43:32.889Z", + "timestampUpdated": null + } +] diff --git a/gpii/node_modules/gpii-ini-file/README.md b/gpii/node_modules/gpii-ini-file/README.md new file mode 100644 index 000000000..b052f5a59 --- /dev/null +++ b/gpii/node_modules/gpii-ini-file/README.md @@ -0,0 +1,175 @@ +# The INI file reader/writer. + +Reads and writes INI files. + +The main goal of this library is to only make small specific changes to an existing INI file, while preserving the +formatting of the rest of the file. + +In order to implement this, the same thing could not be done with the original ini library, +[GPII/ini](https://github.com/GPII/ini), where the required changes were too significant for its original charm to +remain. So, while looking roughly the same from the outside, this is a re-write rather than a fork. + +## INI file format + +INI file can look something like this: + +```ini +# comments +; other comments + +# = or : delimiters. +key=value1 +key2:value2 +key3 = value3 + +# Sections (and sub-sections) +[section] +key="section-value" + +[[subsection]] +key=subsection value + +[section2] +# Multiple lines +multi-line="""line1 +line2 +line3""" +multi-line2=line1 + line2 + line3 +``` + +Which will parse to the following: + +```json5 +{ + key: "value1", + key2: "value2", + key3: "value3", + section: { + key: "section-value", + subsection: {key: "subsection value"} + }, + section2: { + "multi-line": "line1\nline2\nline3", + "multi-line2": "line1\nline2\nline3" + } +} +``` + +See [test/read-test.ini](test/read-test.ini) for an extreme example. + +When writing to an existing file, the content is modified rather than re-writing. Effort is made to respect the +unchanged values and current format of the file. An updated version of the above example will look like this: + +```ini +# comments +; other comments + +# = or : delimiters. +key=new value +key2:another new value +key3 = and again + +# Sections +[section] +key="quotes respected" +# sub-sections +[[subsection]] +key=subsection value + +new1=new value +[section2] +# Multiple lines +multi-line="""modified line1 +modified line2 +modified line3""" +multi-line2=modified line1 + modified line2 + modified line3 + +[newSection] +newValue=value +``` + +Compare [test/write-test.ini](test/write-test.ini) with [test/write-test.expect.ini](test/write-test.expect.ini) for an +example. + +## Usage + +### Reading + +```snippet +/** + * Reads INI file content, returning the parsed data as an object. + * + * @param {String} content The ini file content. + * @param {Object} options Options: + * @param {Boolean} options.strings true to always return strings, otherwise try to return numbers and booleans for + * unquoted values, where appropriate. + * @param {Boolean} options.firstDuplicate true to use the first occurrence of a value with a duplicate name, otherwise + * the last is used. [default: false] + * + * @return {Object} The parsed ini file data. + */ +gpii.iniFile.read = function (content, options) { + // ... +}; +``` + +### Writing + +```snippet +/** + * Writes an object to existing INI file content. + * + * @param {String} input The ini file content to update (can be empty). + * @param {Object} data The new settings data. + * @param {iniFile.WriteOptions} options INI file output options. + * @return {String} The new ini file content. + */ +gpii.iniFile.write = function (input, data, options) { + // ... +}; +``` + +```javascript +/** + * Options for writing ini files with gpii.iniFile.write(). + * + * @typedef {Object} iniFile.WriteOptions + * + * @property {String} keyValueDelimiter - Text which separates keys and values, for new values. [default: "="]. + * @property {Boolean} keepUndefined - true to keep items and sections that are not in data. Use to define values + * without reading the file beforehand. + * @property {String} multilineStyle - How new multi-line values are written: + * "'''" or '"""': Surround the value with 3x single or double quotes (default). + * "indent": Indent the additional lines. + * "escape": Use an escaped n (\n). + * anything else: Wrap the value with the given value. + * @property {String} quote - For new values, quote them "always", "never", "strings" (for only strings), or + * "spaces" (if the value starts or ends with a space) [default: "spaces"]. + * @property {String} quoteChar - The quote character for new values when quoting. [default: " (double quote)]. + * @property {String} eol - The file's end of line character(s). [default: auto-detect] + */ +``` + +## How it works + +Parsing is performed by a single regular expression ([ini.regex](ini.regex)), which calls a function when it matches +either a section header (`[example]`), or a `key=value` pair. + +When reading data, it fills an object with the matched values. + +When writing, it will replace the matches with text from an existing object. Only the text in the value is replaced, and +only if it is different to the stored value. + +## Limitations + +Sub-sections can't have the same name as a value in the same section, eg: + +```ini +[section1] +same-name=value +[[same-name]] +``` diff --git a/gpii/node_modules/gpii-ini-file/index.js b/gpii/node_modules/gpii-ini-file/index.js new file mode 100644 index 000000000..79f42a2a7 --- /dev/null +++ b/gpii/node_modules/gpii-ini-file/index.js @@ -0,0 +1,572 @@ +/* + * INI File reader/writer + * + * Copyright 2018 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +var fs = require("fs"), + os = require("os"), + XRegExp = require("xregexp"); + +gpii.iniFile = fluid.registerNamespace("gpii.iniFile"); + +var pattern = fs.readFileSync(__dirname + "/ini.regex"); +gpii.iniFile.regex = new XRegExp(pattern, "gmsx"); + +/** + * Options for writing ini files with gpii.iniFile.write(). + * + * @typedef {Object} iniFile.WriteOptions + * + * @property {String} keyValueDelimiter - Text which separates keys and values, for new values. [default: "="]. + * @property {Boolean} keepUndefined - true to keep items and sections that are not in data. Use to define values + * without reading the file beforehand. + * @property {String} multilineStyle - How new multi-line values are written: + * "'''" or '"""': Surround the value with 3x single or double quotes (default). + * "indent": Indent the additional lines. + * "escape": Use an escaped n (\n). + * anything else: Wrap the value with the given value. + * @property {String} quote - For new values, quote them "always", "never", "strings" (for only strings), or + * "spaces" (if the value starts or ends with a space) [default: "spaces"]. + * @property {String} quoteChar - The quote character for new values when quoting. [default: " (double quote)]. + * @property {String} eol - The file's end of line character(s). [default: auto-detect] + */ + +/** + * Callback for iniFile.parse(), invoked at the start of a new section. + * + * @callback iniFile.sectionBegin + * @param {Object} state The state object that was passed to the parse function. + * @param {Array} sectionPath The path to the new section object. + * @return {String} The text to add before the new section, or fluid.NO_VALUE to remove the section. Ignored when + * reading + */ + +/** + * Callback for iniFile.parse(), invoked at the end of a section, after all of its sub-sections have been parsed. + * + * @callback iniFile.sectionEnd + * @param {Object} state The state object that was passed to the parse function. + * @param {Array} sectionPath The path to the section object. + * @param {Boolean} eof true if the end of section is the end of the file (rather than just before another section). + * @return {String} The text to add at the end of the section. Ignored when reading. + */ + +/** + * Callback for iniFile.parse(), called when a value has been parsed. + * + * @callback iniFile.gotValue + * @param {String} sectionPath The path of the section. + * @param {String} key The value name. + * @param {String} value The value. + * @param {Boolean} quoted true if the value was quoted. + * @return {String} The new value, fluid.NO_VALUE to remove the value, or undefined if it's unchanged. Ignored when + * reading. + */ + +/** + * Parse an INI file, invoking call backs at interesting points: start/end of a section, and on a key=value. + * + * If options.write=true, the return values of the callbacks will be used to modify the content, the entirety of which + * is then returned by this function. + * + * It uses the regular expression in ./ini.regex to perform the actual parsing. + * + * @param {String} content The INI file content. + * @param {Object} handlers Object containing the callbacks. + * @param {iniFile.sectionBegin} handlers.sectionBegin - called at the start of a new section, returning a string to + * write before the section line, if options.write=true. + * @param {iniFile.sectionEnd} handlers.sectionEnd - called at the end of a section, after all of its sub-sections + * have been seen. Returning a string to write at the end of the section, if options.write=true. + * @param {iniFile.gotValue} handlers.gotValue - called when a value has been parsed. If options.write=true, return the + * text to replace the content of the value, undefined for no update, or fluid.NO_VALUE to remove the key=value pair. + * @param {Object} options Parser options. + * @param {Boolean} options.write True if writing. + * @param {Object} options.state An object to pass to the handler callbacks. + * @return {Object|String} The data from the INI file, or the new file content if options.write=true. + */ +gpii.iniFile.parse = function (content, handlers, options) { + options = Object.assign({}, options); + + var sectionPath = []; + + if (handlers.sectionBegin) { + handlers.sectionBegin(options.state, sectionPath); + } + + var regexFunction = options.write ? XRegExp.replace : XRegExp.forEach; + + var output = regexFunction(content, gpii.iniFile.regex, function (match) { + return gpii.iniFile.parse.processMatch(match, sectionPath, handlers, options); + }); + + var append = gpii.iniFile.parse.endSection(sectionPath, 0, handlers.sectionEnd, true, options); + if (options.write && append) { + // If the content doesn't end with a new line, then add one before the new addition. + var hasEol = output.endsWith(options.state.options.eol); + if (!hasEol) { + output += options.state.options.eol; + if (append.endsWith(options.state.options.eol)) { + append = append.trimRight(); + } + } + output += append; + } + + return options.write && output; +}; + +/** + * Called during parsing when a [section] has ended, either due to the start of another section, or at the end of file. + * Adjust the current path to match the new depth, calling the sectionEnd callback for each level. + * + * @param {Array} sectionPath Path of the section that's ending. + * @param {Number} depth The new section path depth. + * @param {iniFile.sectionEnd} sectionEnd The end of section callback + * @param {Boolean} eof true if the end of section is the end of the file (rather than just before another section). + * @param {Object} options Parser options. + * @param {Boolean} options.write True if writing. + * @param {Object} options.state An object to pass to the handler callbacks. + * @return {String} If writing, the text to append to the section. + */ +gpii.iniFile.parse.endSection = function (sectionPath, depth, sectionEnd, eof, options) { + var result = ""; + // Go back up the stack (if required) + while (depth <= sectionPath.length) { + // At this point, the sub-sections for the current section are done. + if (sectionEnd) { + var endContent = sectionEnd(options.state, sectionPath, eof); + if (options.write && endContent) { + result += endContent; + } + } + if (sectionPath.length === 0) { + break; + } + sectionPath.pop(); + } + return result; +}; + +/** + * Processes a match from the INI parser. + * + * @param {Object} match The match from the regular expression. + * @param {String} match.section The section name, if matching a section header, otherwise undefined. + * @param {String} match.sectionCount The depth of the section (the [ characters). + * @param {String} match.key Name of the value. + * @param {String} match.prefix Everything up to the value (key and surrounding whitespace). + * @param {String} match.indent The indentation whitespace. + * @param {String} match.value The value, if unquoted. + * @param {String} match.value_ml_indent Multi-line value, if indented. + * @param {String} match.value_ml_quote Multi-line value, if quoted. + * @param {String} match.value_quote The value, if quoted. + * @param {String} match.qqq The quotes used for value_ml_quote + * @param {String} match.q The quotes used for value_quote + * @param {String} match.suffix Everything after the value, up to and including the newline. + * @param {Array} sectionPath The path of the current section. + * @param {Object} handlers Parser callbacks. + * @param {Object} options INI file options. + * @return {String} The replacement string, if writing. + */ +gpii.iniFile.parse.processMatch = function (match, sectionPath, handlers, options) { + var result = options.write && match.toString(); + if (match.section) { + // The number of ['s in the section line. + var depth = match.sectionCount.length; + + if (depth > sectionPath.length + 1) { + fluid.log("INI file parse error: Current section is not a direct descendant of the previous section"); + } + + var endContent = gpii.iniFile.parse.endSection(sectionPath, depth, handlers.sectionEnd, false, options); + if (options.write) { + result = endContent + result; + } + + sectionPath.push(match.section); + + if (handlers.sectionBegin) { + var prepend = handlers.sectionBegin(options.state, sectionPath); + if (options.write && prepend) { + result = prepend + result; + } + } + + } else { + // It's some type of key=value pair. + var value = fluid.find( + [match.value, match.value_ml_indent, match.value_ml_quote, match.value_quote], fluid.identity); + + if (match.value_quote) { + value = value.replace("\\\"", "\""); + } else if (match.value_ml_indent || match.value_ml_quote) { + value = value.replace(/\n[^\S\n]+/g, "\n"); + } + + if (handlers.gotValue) { + var quoted = match.value_ml_quote !== undefined || match.value_quote !== undefined; + var newValue = handlers.gotValue(options.state, sectionPath, match.key, value, quoted); + if (options.write) { + if (newValue === fluid.NO_VALUE) { + result = ""; + } else if (newValue !== undefined) { + var quote = (match.qqq || match.q || ""); + if (match.value_ml_indent) { + newValue = newValue.replace(/\n/g, "\n" + match.indent + " ".repeat(4)); + } + result = match.prefix + quote + newValue + quote + match.suffix; + } + } + } + } + + return result; +}; + +/** + * Stringify a value - converts it to a string as returned by JSON.stringify. + * @param {Mixed} value The value. + * @return {String} The stringified version of value. + */ +gpii.iniFile.stringify = function (value) { + if (value === null || value === undefined) { + return ""; + } else { + return typeof(value) === "string" ? value : JSON.stringify(value); + } +}; + +/** + * Generates the text of a new key=value pair, or a new section and its values and sub-sections if value is an + * object. + * + * @param {String} key The name of it. + * @param {String|Number|Boolean} value The value. + * @param {Array} path The path of the containing section. + * @param {iniFile.WriteOptions} options INI file output options. + * @return {Array} Array of lines for the new value or sub-section. + */ +gpii.iniFile.writeValue = function (key, value, path, options) { + var output = []; + if (fluid.isPlainObject(value)) { + // The section header. + output.push(""); + output.push("[".repeat(path.length + 1) + key + "]".repeat(path.length + 1)); + // Section items. + fluid.each(Object.keys(value), function (subkey) { + output.push.apply(output, gpii.iniFile.writeValue(subkey, value[subkey], path.concat(key), options)); + }); + } else { + var newValue = gpii.iniFile.stringify(value); + + if (newValue.indexOf("\n") >= 0) { + // A multi-line value. + switch (options.multilineStyle) { + case "indent": + // eol is added at the end to ensure the next line isn't included in the value if it happens to be + // at the same indentation as the value. + newValue = newValue.replace(/\n/g, options.eol + " ") + options.eol; + break; + case "escape": + newValue = newValue.replace(/\n/g, "\\n"); + break; + case "\"\"\"": + case "'''": + default: + newValue = options.multilineStyle + newValue + options.multilineStyle; + break; + } + } else { + var quote = (options.quote === "always" || (options.quote === "string" && typeof(s) === "string")); + quote = quote || (options.quote === "spaces" && /^\s|\s$/.test(newValue)); + if (quote) { + newValue = options.quoteChar + newValue + options.quoteChar; + } + } + output.push(key + options.keyValueDelimiter + newValue); + } + + return output; +}; + +/** + * Get the values that have not been written to for the current section (that is, the new values). + * + * @param {iniFile.WriteState} state The parser state. + * @param {Array} sectionPath The path to the section object. + * @param {Boolean} subSections true to return only sub-sections (objects), otherwise return values. + * @return {Array} The key-value pairs that have yet to be written. + */ +gpii.iniFile.getUnwrittenValues = function (state, sectionPath, subSections) { + var items = []; + var section = fluid.get(state.data, sectionPath); + if (section) { + var allKeys = Object.keys(section); + var path = sectionPath.length ? sectionPath.join(state.pathSep) + state.pathSep : ""; + fluid.each(allKeys, function (key) { + if (!state.valuesWritten[path + key]) { + var value = section[key]; + if (fluid.isPlainObject(value) === !!subSections) { + items.push({ + key: key, + value: value + }); + } + } + }); + } + return items; +}; + +/** + * Parser state used for INI file writing. + * @typedef {Object} iniFile.WriteState + * @property {Object} data The data being written. + * @property {Object} currentSection The object of the section currently being dealt with. + * @property {Array} currentPath Path of the current section. + * @property {String} currentPathString Path of the current section, as a string. + * @property {String} pathSep Use to separate path segments, while allowing '.' (dot) in the section/key names. + * @property {Object} valuesWritten A hash-map of the value paths that have been written, or where already there. + * @property {iniFile.WriteOptions} options The INI file writing options + */ + +/** + * Writes an object to existing INI file content. + * + * @param {String} input The ini file content to update (can be empty). + * @param {Object} data The new settings data. + * @param {iniFile.WriteOptions} options INI file output options. + * @return {String} The new ini file content. + */ +gpii.iniFile.write = function (input, data, options) { + + var state = { + data: data, + // The object of the section currently being dealt with. + currentSection: null, + // Path of the current section. + currentPath: [], + currentPathString: "", + // Use to separate path segments, while allowing '.' (dot) in the section/key names. + pathSep: "][", + // A hash-map of the value paths that have been written, or where already there. + valuesWritten: {}, + options: Object.assign({ + keyValueDelimiter: "=", + keepUndefined: false, + eol: undefined, + quoteChar: "\"", + quote: "spaces", + multilineStyle: "\"\"\"" + }, options) + }; + + if (state.options.eol === undefined) { + var m = /\r\n|[\r\n]/.exec(input); + state.options.eol = m && m[0] || os.EOL; + } + + return gpii.iniFile.parse(input, gpii.iniFile.write, {state: state, write: true}); +}; + +/** + * Called at the start of a new section. + * Output unwritten values to the current section (before the new one). + * + * @param {iniFile.WriteState} state The parser state. + * @param {Array} sectionPath The path to the new section object. + * @return {String} The text to add before the new section, or fluid.NO_VALUE to remove the section. + */ +gpii.iniFile.write.sectionBegin = function (state, sectionPath) { + var output = []; + if (sectionPath.length > 0) { + // Add the values to the previous section that haven't already been added. + fluid.each(gpii.iniFile.getUnwrittenValues(state, state.currentPath, false), function (tuple) { + output.push.apply(output, gpii.iniFile.writeValue(tuple.key, tuple.value, sectionPath, state.options)); + }); + } + state.currentPath = sectionPath.slice(); + state.currentSection = fluid.get(state.data, state.currentPath); + + state.currentPathString = state.currentPath.join(state.pathSep); + state.valuesWritten[state.currentPathString] = true; + + return output.length === 0 ? "" : (output.join(state.options.eol) + state.options.eol); +}; + +/** + * Called at the end of a section, after all of its sub-sections have been parsed. + * Output the unwritten sub-sections of the section. + * + * @param {iniFile.WriteState} state The parser state. + * @param {Array} sectionPath The path to the section object. + * @param {Boolean} eof true if the end of section is the end of the file (rather than just before another section). + * @return {String} The text to add at the end of the section. + */ +gpii.iniFile.write.sectionEnd = function (state, sectionPath, eof) { + var output = []; + + if (eof || sectionPath.length === 0 && state.currentPathString === "") { + // Special case where at the end of a file with no sections, where sectionBegin wouldn't have been + // called. + var content = gpii.iniFile.write.sectionBegin(state, ["dummy"]); + if (content && content.length > 0) { + output.push(content.trim()); + } + } + + // Add the sub-sections to the section that haven't already been added. + fluid.each(gpii.iniFile.getUnwrittenValues(state, sectionPath, true), function (tuple) { + output.push.apply(output, gpii.iniFile.writeValue(tuple.key, tuple.value, sectionPath, state.options)); + }); + + state.valuesWritten[state.currentPathString] = true; + + return output.length === 0 ? "" : (output.join(state.options.eol) + state.options.eol); +}; + +/** + * Called when a value has been parsed. Return the updated value, if required. + * + * @param {iniFile.WriteState} state The parser state. + * @param {String} sectionPath The path of the section. + * @param {String} key The value name. + * @param {String} value The value. + * @return {String} The new value, fluid.NO_VALUE to remove the value, or undefined if it's unchanged. + */ +gpii.iniFile.write.gotValue = function (state, sectionPath, key, value) { + var result; + + if (state.currentSection && state.currentSection.hasOwnProperty(key)) { + var newValue = gpii.iniFile.stringify(state.currentSection[key]); + + var p = (state.currentPathString ? state.currentPathString + state.pathSep : "") + key; + state.valuesWritten[p] = true; + if (value !== newValue) { + result = newValue; + } + } else if (!state.options.keepUndefined) { + result = fluid.NO_VALUE; + } + + return result; +}; + +/** + * Reads INI file content, returning the parsed data as an object. + * + * @param {String} content The ini file content. + * @param {Object} options Options: + * @param {Boolean} options.strings true to always return strings, otherwise try to return numbers and booleans for + * unquoted values, where appropriate. + * @param {Boolean} options.firstDuplicate true to use the first occurrence of a value with a duplicate name, otherwise + * the last is used. [default: false] + * + * @return {Object} The parsed ini file data. + */ +gpii.iniFile.read = function (content, options) { + var state = { + result: {}, + options: Object.assign({ + firstDuplicate: false + }, options) + }; + + gpii.iniFile.parse(content, gpii.iniFile.read, {state:state}); + + return state.result; +}; + +/** + * Called at the start of a new section. Create an empty object for it. + * @param {Object} state The parser state. + * @param {Array} sectionPath The path to the new section object. + */ +gpii.iniFile.read.sectionBegin = function (state, sectionPath) { + var existing = sectionPath.length && fluid.get(state.result, sectionPath); + if (existing) { + if (fluid.isPlainObject(existing)) { + fluid.log("iniFile - Warning: Merging sections with duplicate name: ", + sectionPath.join(".")); + } else { + fluid.log("iniFile - Warning: Overwriting key=value with section of the same name: ", + sectionPath.join(".")); + fluid.set(state.result, sectionPath, {}); + } + } +}; + +/** + * Called when a value has been parsed. + * + * @param {Object} state The parser state. + * @param {String} sectionPath The path of the section. + * @param {String} key The value name. + * @param {String} value The value. + * @param {Boolean} quoted true if the value was quoted. + */ +gpii.iniFile.read.gotValue = function (state, sectionPath, key, value, quoted) { + // Ignore the value if it's already set, if only setting first duplicates. + var set = !state.options.firstDuplicate || (fluid.get(state.result, sectionPath.concat(key), value) === undefined); + if (!state.options.strings && !quoted) { + // Try to convert it to a non-string value. + if (/^([0-9]+|true|false|null)$/.test(value)) { + value = JSON.parse(value); + } + } + if (set) { + fluid.set(state.result, sectionPath.concat(key), value); + } +}; + +/** + * Wrapper for read(), that reads from the given file. + * @param {String} path The ini file. + * @param {Object} options Options for read(). + * @return {Object} The result of read(). + */ +gpii.iniFile.readFile = function (path, options) { + var content; + try { + content = fs.readFileSync(path, (options && options.encoding) || "utf-8"); + } catch (e) { + content = ""; + } + return gpii.iniFile.read(content, options); +}; + +/** + * Wrapper for write(), that reads the initial data from the given file. + * + * @param {String} path The ini file. + * @param {Object} data The new settings data. + * @param {Object} options Options for write(). + * @return {Object} The result of write(). + */ +gpii.iniFile.writeFromFile = function (path, data, options) { + var content; + try { + content = fs.readFileSync(path, fluid.get(options, "encoding") || "utf-8"); + } catch (e) { + content = ""; + } + return gpii.iniFile.write(content, data, options); +}; diff --git a/gpii/node_modules/gpii-ini-file/ini.regex b/gpii/node_modules/gpii-ini-file/ini.regex new file mode 100644 index 000000000..610d49fb2 --- /dev/null +++ b/gpii/node_modules/gpii-ini-file/ini.regex @@ -0,0 +1,49 @@ +# The ini file regular expression. +# Groups: +# - section: The section name +# - sectionCount: The [ characters. +# or: +# - key: The name of the value +# - value, value_ml_indent, value_ml_quote, or value_quote: The value. +# - prefix: Everything up to the value +# - indent: The indentation +# - qqq: The quotes used around value_lines +# - q: The quote used around value_quote +# - suffix: Everything after the value + quotes (normally just \n) + +# Skip leading space and comments +^ +(?!\s*[;#]) # Comments +( + # Section name, including surrounding brackets + \s*((?\[+)(?
[^\]]+)\]+) +| + # key=value + ( + # everything before the value + (? + (?[^\S\n]*) + # The key name + (?[^\n=]*?) + ((?!$)\s)*[=:]((?!$)\s)* + ) + # values + ( + # multi-line, wrapped with 3 double or single quotes + ((?"{3}|'{3})(?((?!\k).)*)\k) + # quoted + |((?["'])(?[^\n]*)\k) + # unquoted multi-line + |(? + [^\n]* # first line + (\n\k[^\S\n]+(?![#;\s])[^\n]*(?=\n))+ # next indented lines + ) + # plain + |(?[^\r\n]*$) + ) + # whitespace after the value + (? + [^\S\n]*(\r?\n)? + ) + ) +) diff --git a/gpii/node_modules/gpii-ini-file/package.json b/gpii/node_modules/gpii-ini-file/package.json new file mode 100644 index 000000000..4bfe853dd --- /dev/null +++ b/gpii/node_modules/gpii-ini-file/package.json @@ -0,0 +1,13 @@ +{ + "name": "gpii-iniFile", + "description": "The INI file reader/writer", + "version": "0.3.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } +} diff --git a/gpii/node_modules/gpii-ini-file/test/iniFileTests.js b/gpii/node_modules/gpii-ini-file/test/iniFileTests.js new file mode 100644 index 000000000..2079f17a1 --- /dev/null +++ b/gpii/node_modules/gpii-ini-file/test/iniFileTests.js @@ -0,0 +1,433 @@ +/* + * iniFile Tests + * + * Copyright 2018 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"); + +var fs = require("fs"), + JSON5 = require("json5"); + + +var jqUnit = fluid.require("node-jqunit"); +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.tests.iniFile"); + +require("../index.js"); + +var teardowns = []; + +jqUnit.module("gpii.tests.iniFile", { + teardown: function () { + while (teardowns.length) { + teardowns.pop()(); + } + } +}); + +gpii.tests.iniFile.parserTestData = fluid.freezeRecursive({ + input: [ + "key1=value1", + "key2=value2", + "not-a-key", // ignored + "[section1]", + "s1key1=value-s1key1", + "s1key2=value-s1key2", + "# comment", // ignored + "[section2]", + "s2key1=value-s2key1", + "s2key2=\"value-s2key2\"", + "[[section2A]]", + "s2Akey1=\"\"\"value-s2Akey1", + "line2-s2Akey1\"\"\"", + "s2Akey2=value-s2Akey2", + "[[section2B]]", + "s2Bkey1=value-s2Bkey1", + " line2-s2Bkey1", + "s2Bkey2=value-s2Bkey2" + ], + expect: [ + // implied + ["sectionBegin", "state", []], + // key1=value1 + ["gotValue", "state", [], "key1", "value1", false], + // key2=value2 + ["gotValue", "state", [], "key2", "value2", false], + // [section1] + ["sectionBegin", "state", ["section1"]], + // s1key1=value-s1key1 + ["gotValue", "state", ["section1"], "s1key1", "value-s1key1", false], + // s1key2=value-s1key2 + ["gotValue", "state", ["section1"], "s1key2", "value-s1key2", false], + // # comment + // [section2] + ["sectionEnd", "state", ["section1"], false], + ["sectionBegin", "state", ["section2"]], + // s2key1=value-s2key1 + ["gotValue", "state", ["section2"], "s2key1", "value-s2key1", false], + // s2key2="value-s2key2" + ["gotValue", "state", ["section2"], "s2key2", "value-s2key2", true], + // [[section2A]] + ["sectionBegin", "state", ["section2", "section2A"]], + // s2Akey1="""value-s2Akey1 + // line2-s2Akey1""" + ["gotValue", "state", ["section2", "section2A"], "s2Akey1", "value-s2Akey1\nline2-s2Akey1", true], + // s2Akey2=value-s2Akey2 + ["gotValue", "state", ["section2", "section2A"], "s2Akey2", "value-s2Akey2", false], + // [[section2B]] + ["sectionEnd", "state", ["section2", "section2A"], false], + ["sectionBegin", "state", ["section2", "section2B"]], + // s2Bkey1=value-s2Bkey1 + // line2-s2Bkey1 + ["gotValue", "state", ["section2", "section2B"], "s2Bkey1", "value-s2Bkey1\nline2-s2Bkey1", false], + // s2Bkey2=value-s2Bkey + ["gotValue", "state", ["section2", "section2B"], "s2Bkey2", "value-s2Bkey2", false], + // implied from eof + ["sectionEnd", "state", ["section2", "section2B"], true], + ["sectionEnd", "state", ["section2"], true], + ["sectionEnd", "state", [], true] + ] +}); + +/** + * Shows the deep differences between two objects, in the console. + * + * @param {Object} expected What's expected. + * @param {Object} actual What really happened. + * @return {Boolean} true if both objects are the same. + */ +gpii.tests.iniFile.showDiff = function (expected, actual) { + + var diff = {changeMap: {}, changes: 0, unchanged: 0}; + var same = fluid.model.diff(actual, expected, diff); + + if (!same) { + + var inspect = function (changeMap, path) { + return fluid.transform(changeMap, function (value, key) { + if (fluid.isPlainObject(value)) { + return inspect(value, path + (path && ".") + key); + } else { + // fluid.get can't support 0 length key names. + var exp = fluid.get(expected, path); + var act = fluid.get(actual, path); + return { + expected: exp && exp[key] || undefined, + actual: act && act[key] || undefined + }; + } + }); + }; + + var output = inspect(diff.changeMap, ""); + fluid.log(diff.changeMap); + fluid.log("Differences between actual and expected:", output); + } + + return same; +}; + +/** + * Extracts expected data from an INI file. The expected data is a json object made from the lines starting with "#@". + * + * @param {String} iniFile The ini file to load. + * @return {Object} The data object, and INI file content (without the #@ lines). + */ +gpii.tests.iniFile.loadTestData = function (iniFile) { + var orig = fs.readFileSync(iniFile, "utf8"); + + // Extract the expected data. The expect json string is a combination of lines beginning with #@, and the + // ini content is the other lines. + var expectedLines = ["{"]; + var iniContent = orig.replace(/^#@([^\n]*?([,{:])?)\n|^.*$/gm, function (all, expect, end) { + var result; + if (expect) { + // A "#@" line. Add it to the expected lines, and remove from the result. + if (!end) { + expect += ","; + } + expectedLines.push(expect); + result = ""; + } else { + // A content line; Keep it in the result. + expectedLines.push("// " + all); + result = all; + } + + return result; + }); + + expectedLines.push("}"); + var expectedJson = expectedLines.join("\n"); + + // Parse the extracted json. + var data; + try { + data = JSON5.parse(expectedJson); + } catch (e) { + var output; + if (e.lineNumber) { + // Provide a clue where the error is. + output = expectedLines.slice(Math.max(e.lineNumber - 10, 0), e.lineNumber).join("\n"); + } else { + output = expectedLines; + } + + // Need to change the log length to provide anything meaningful. + var oldChars = fluid.logObjectRenderChars; + fluid.logObjectRenderChars = 0xfffff; + + fluid.log("expectedJson:\n", output); + + fluid.logObjectRenderChars = oldChars; + jqUnit.fail(e); + iniContent = data = null; + } + + return fluid.freezeRecursive({ + content: iniContent, + data: data + }); +}; + +// Tests the parser calls the correct callbacks with the correct values. +jqUnit.test("inifile parser", function () { + + var testData = gpii.tests.iniFile.parserTestData; + + jqUnit.expect(testData.expect.length * 2); + + var expectedIndex = 0; + var gotCallback = function (name) { + return function () { + var expect = testData.expect[expectedIndex++].slice(); + var expectedName = expect.shift(); + + jqUnit.assertEquals("The expected handler should be called.", expectedName, name); + + var args = fluid.makeArray(arguments); + jqUnit.assertDeepEq("The handler should be called with expected parameters.", expect, args); + + }; + }; + + var iniContent = testData.input.join("\n"); + gpii.iniFile.parse(iniContent, { + sectionBegin: gotCallback("sectionBegin"), + sectionEnd: gotCallback("sectionEnd"), + gotValue: gotCallback("gotValue") + }, {state: "state"}); +}); + +// Tests reading an ini file, with gpii.iniFile.read(). +jqUnit.test("inifile reading", function () { + + var testData = gpii.tests.iniFile.loadTestData(__dirname + "/read-test.ini"); + + jqUnit.assertTrue("Test data should load", testData && testData.data && testData.content); + + // Parse the ini file. + var iniData = gpii.iniFile.read(testData.content); + jqUnit.assertTrue("gpii.iniFile.read should return an object", fluid.isPlainObject(iniData)); + + // And again, checks there isn't any state left over. + var iniDataAgain = gpii.iniFile.read(testData.content); + jqUnit.assertDeepEq("gpii.iniFile.read should be repeatable", iniData, iniDataAgain); + + // Compare it to the expected data. + var same = gpii.tests.iniFile.showDiff(testData.data, iniData); + + // Both asserts should be the same + jqUnit.assertDeepEq("ini data should match the expected (assertDeepEq)", iniData, testData.data); + jqUnit.assertTrue("ini data should match the expected (showDiff)", same); +}); + +// Test readFile() returns the same result as read(). +jqUnit.test("readFile", function () { + + // Parse the file. + var filename = __dirname + "/read-test.ini"; + var readFileData = gpii.iniFile.readFile(filename); + + // Parse the content. + var content = fs.readFileSync(filename, "utf8"); + var readData = gpii.iniFile.read(content); + + jqUnit.assertDeepEq("readFile and read should return the same", readFileData, readData); + + // Make sure it didn't touch anything. + var contentAfter = fs.readFileSync(filename, "utf8"); + jqUnit.assertEquals("readFile should not modify the file.", content, contentAfter); +}); + +// Test writing ini data, with gpii.iniFile.write(). +jqUnit.test("inifile writing", function () { + + var testIniPath = __dirname + "/write-test.ini"; + var expectedIniPath = __dirname + "/write-test.expect.ini"; + + var content = fs.readFileSync(testIniPath, "utf8"); + + var iniData = gpii.iniFile.read(content); + var writtenContent = gpii.iniFile.write(content, iniData); + + // No changes made to the data, so nothing should have changed in the content. + jqUnit.assertEquals("INI content should match original after no change", content, writtenContent); + + var newIniData = fluid.copy(iniData); + + // Change all the values, removing those whose name begin with remove_. + var changeValues = function (obj) { + fluid.each(obj, function (value, key) { + if (key.startsWith("remove_")) { + delete obj[key]; + } else if (fluid.isPlainObject(value)) { + changeValues(value); + } else { + var newValue = "abc " + key + " xyz"; + + if (key.startsWith("ml_")) { + var num = parseInt(key[3]) || 3; + var lines = []; + for (var l = 0; l < num; l++) { + lines.push(newValue + " line" + l); + } + newValue = lines.join("\n"); + } + + obj[key] = newValue; + } + }); + }; + + changeValues(newIniData); + + // Make some specific changes + newIniData.empty_me = ""; + newIniData.null_me = null; + newIniData.number_me1 = 42; + newIniData.number_me2 = 12345; + newIniData.true_me1 = true; + newIniData.true_me2 = true; + + // Add some new ones + newIniData.new_empty = ""; + newIniData.new_null = null; + newIniData.new_number = 42; + newIniData.new_true = true; + + newIniData.newKey1 = "new value"; + newIniData.section1.newKey1 = "new value 1, in section1"; + newIniData.section1.newKey2 = "new value 2"; + newIniData.newSection1 = { + newKey1: "new value 1, in new section 1", + newKey3: "new value 3" + }; + newIniData.newSection2 = { + newKey1: "new value 1, in new section 2", + newKey4: "new value 4", + newSubSection: { + newKey1: "new value 1, in new sub section", + newKey5: "new value 5" + } + }; + + // Write the ini data. + writtenContent = gpii.iniFile.write(content, newIniData); + + // Read the expected ini file. + var expectedContent = fs.readFileSync(expectedIniPath, "utf8"); + + // Compare the textual result. + jqUnit.assertEquals("The written content should contain the same text as the expected ini file.", + expectedContent, writtenContent); + + // See if it matches the parsed expected ini (it really should, considering the content is identical). + var writtenData = gpii.iniFile.read(writtenContent); + var expectedData = gpii.iniFile.read(expectedContent); + + var same = gpii.tests.iniFile.showDiff(expectedData, writtenData); + jqUnit.assertTrue("Written data should match the expected data.", same); + jqUnit.assertDeepEq("Written data should match the expected data (assertDeepEq).", + expectedData, writtenData); + + // Write the original data back again. + var writtenOriginalContent = gpii.iniFile.write(writtenContent, iniData); + + // Even though it contains the same data, the text will be different because things have been removed and re-added, + // so just compare the parsed data. + var writtenOriginalData = gpii.iniFile.read(writtenOriginalContent); + same = gpii.tests.iniFile.showDiff(iniData, writtenOriginalData); + jqUnit.assertTrue("Re-written original data should match the original data.", same); + jqUnit.assertDeepEq("Re-written original data should match the original data (assertDeepEq).", + iniData, writtenOriginalData); +}); + +// Tests writing data to empty content. +jqUnit.test("inifile writing (empty file)", function () { + + var filename = __dirname + "/write-test.ini"; + var iniData = gpii.iniFile.readFile(filename); + + var writtenContent = gpii.iniFile.write("", iniData); + + // Check if the written content parses to the same data as the input. + var writtenData = gpii.iniFile.read(writtenContent); + + var same = gpii.tests.iniFile.showDiff(iniData, writtenData); + jqUnit.assertTrue("Written data should match the parsed expected file.", same); + jqUnit.assertDeepEq("Written data should match the parsed expected file (assertDeepEq).", + iniData, writtenData); + +}); + +// Tests writing data to new file. +jqUnit.test("inifile reading/writing non-existent file", function () { + + var filename = __dirname + "/does-not-exist" + Math.random(); + + var readContent = gpii.iniFile.readFile(filename, { key: "value" }); + jqUnit.assertDeepEq("Non-existent file should be read as though it's empty", {}, readContent); + + var writtenContent = gpii.iniFile.writeFromFile(filename, { key: "value" }); + jqUnit.assertEquals("INI content should be as expected", "key=value", writtenContent.trim()); + + // Check writing an empty object creates an empty file. + var writtenContent2 = gpii.iniFile.writeFromFile(filename, { }); + jqUnit.assertEquals("Empty INI content should be as expected", "", writtenContent2.trim()); +}); + +jqUnit.test("iniFile add values to last section", function () { + + var content = "[section]\nkey1=a\nkey2=b"; + var dataExpect = { + section: { + key1: "a", + key2: "b" + } + }; + + var data = gpii.iniFile.read(content); + jqUnit.assertDeepEq("Content without trailing newline should parse correctly", dataExpect, data); + + data.section.key3 = "abc"; + data.section.key2 = "123"; + var writtenContent = gpii.iniFile.write(content, data); + var expectedContent = "[section]\nkey1=a\nkey2=123\nkey3=abc"; + jqUnit.assertDeepEq("Last item should be changed", expectedContent, writtenContent); +}); diff --git a/gpii/node_modules/gpii-ini-file/test/read-test.ini b/gpii/node_modules/gpii-ini-file/test/read-test.ini new file mode 100644 index 000000000..db675d80e --- /dev/null +++ b/gpii/node_modules/gpii-ini-file/test/read-test.ini @@ -0,0 +1,608 @@ +# A test .ini file. +# +# Lines starting with "#@" are joined to produce a JSON object describing the expected data object that the parser +# will produce. The #@ lines are stripped before parsing. +# + +# This is a comment. +; And this + # Also indented + ; like this +# comment ; with another +; other # way +# # # +### +# +; ; ; +;;; +; +; # ; # ; ; ; # # # ; +; # ; # ; ; ; # # # ; +# ; # ; # ; ; ; # # # ; + +# normal key=value +key=value1 +# two together +key2=value2 +key3=value3 +# surrounding lines + +key4=value4 +key5=value5 + +key6=value6 + +#@ key: "value1" +#@ key2: "value2" +#@ key3: "value3" +#@ key4: "value4" +#@ key5: "value5" +#@ key6: "value6" + += +keyonly= +=valueonly + +# The "=" line will set the zero length key to "", but that will be over written by the "=valueonly" line. +#@ keyonly: "" +#@ "": "valueonly" + +# Ignored +not_a_key_or_value + + +100=numbered-key-value +#@ "100": "numbered-key-value" + +number = 123 +boolon = true +booloff = false +null=null +not_number = "123" +not_boolon = "true" +not_booloff = "false" +not_null="null" +#@ number: 123 +#@ boolon: true +#@ booloff: false +#@ null: null +#@ not_number: "123" +#@ not_boolon: "true" +#@ not_booloff: "false" +#@ not_null: "null" + + + +# Spaces (surrounding space is ignored) +spaced = value +spacedL =value +spacedR= value +spaced key name = value +spaced key nameL =value +spaced key nameR= value + + indented and spaced = value + indented and spaced again = value +unindented and spaced = value + +#@ spaced: "value" +#@ spacedL: "value" +#@ spacedR: "value" +#@ "spaced key name": "value" +#@ "spaced key nameL": "value" +#@ "spaced key nameR": "value" +#@ "indented and spaced": "value" +#@ "indented and spaced again": "value" +#@ "unindented and spaced": "value" + + +# Spaces in keys and values +notspaced = value with spaces +spaced key name2 = value with spaces +spaced key name left =value with spaces +spaced key name right= value with spaces +spaced key name none=value with spaces +double space = value +double space and value = valueA valueB + +#@ "notspaced": "value with spaces" +#@ "spaced key name2": "value with spaces" +#@ "spaced key name left": "value with spaces" +#@ "spaced key name right": "value with spaces" +#@ "spaced key name none": "value with spaces" +#@ "double space": "value" +#@ "double space and value": "valueA valueB" + + +equals=valueA=valueB +doubleequals==value + +#@ equals: "valueA=valueB" +#@ doubleequals: "=value" + +# Comments should be on their own line +value_commentH=valueA # valueB +value_commentS=valueA ; valueB +value_commentHS=valueA # valueB ; valueC +value_commentSH=valueA ; valueB # valueC +value_commentHH=valueA # valueB # valueC +value_commentSS=valueA ; valueB ; valueC + +#@ value_commentH: "valueA # valueB" +#@ value_commentS: "valueA ; valueB" +#@ value_commentHS: "valueA # valueB ; valueC" +#@ value_commentSH: "valueA ; valueB # valueC" +#@ value_commentHH: "valueA # valueB # valueC" +#@ value_commentSS: "valueA ; valueB ; valueC" + + +# Colons as delimiters +colon1:value +colon2 : value +colon3 : value +double_colon::value + +#@ colon1: "value" +#@ colon2: "value" +#@ colon3: "value" +#@ double_colon: ":value" + +# Mixing : and = +colon_value_equals:valueA=valueB +equals_value_colon=valueA:valueB + +colon_equals_value:=value +equals_colon_value=:value +colon_equals:= +equals_colon=: + +#@ colon_value_equals: "valueA=valueB" +#@ equals_value_colon: "valueA:valueB" +#@ colon_equals_value: "=value" +#@ equals_colon_value: ":value" +#@ colon_equals: "=" +#@ equals_colon: ":" + +cee:valueA=valueB=valueC +cec:valueA=valueB:valueC +cce:valueA:valueB=valueC +ccc:valueA:valueB:valueC +eee=valueA=valueB=valueC +eec=valueA=valueB:valueC +ece=valueA:valueB=valueC +ecc=valueA:valueB:valueC + +#@ cee: "valueA=valueB=valueC" +#@ cec: "valueA=valueB:valueC" +#@ cce: "valueA:valueB=valueC" +#@ ccc: "valueA:valueB:valueC" +#@ eee: "valueA=valueB=valueC" +#@ eec: "valueA=valueB:valueC" +#@ ece: "valueA:valueB=valueC" +#@ ecc: "valueA:valueB:valueC" + + +# Quoted values +quoted_ns="value" +quoted_spc = "value" +quoted_spcL ="value" +quoted_spcR= "value" + +quoted_spc_value = "valueA valueB valueC" +quoted_spc_valueL = " valueA valueB valueC" +quoted_spc_valueR = "valueA valueB valueC " +quoted_spc_valueB = " valueA valueB valueC " + +quotes_only="" +#@ quoted_ns: "value" +#@ quoted_spc: "value" +#@ quoted_spcL: "value" +#@ quoted_spcR: "value" +#@ quoted_spc_value: "valueA valueB valueC" +#@ quoted_spc_valueL: " valueA valueB valueC" +#@ quoted_spc_valueR: "valueA valueB valueC " +#@ quoted_spc_valueB: " valueA valueB valueC " +#@ quotes_only: "" + +# Values are quoted if it starts and ends with a quote, the content is not parsed. +quoted_inner = "valueA"valueB" +quoted_inner_escape = "valueA\"valueB" + +quote_twice=""value"" +quote_twiceL=""value" +quote_twiceR="value"" + +quoted_brokeL = "value +quoted_brokeR = value" +quoted_broke_innerL = "valueA"valueB +quoted_broke_innerR = value"valueB" + +notquoted_inner = valueA"valueB +notquoted_inner2 = valueA"valueB"valueC + +#@ quoted_inner: 'valueA"valueB' +#@ quoted_inner_escape: 'valueA"valueB' +#@ quote_twice: '"value"' +#@ quote_twiceL: '"value' +#@ quote_twiceR: 'value"' +#@ quoted_brokeL: '"value' +#@ quoted_brokeR: 'value"' +#@ quoted_broke_innerL: 'valueA' +#@ quoted_broke_innerR: 'value"valueB"' +#@ notquoted_inner: 'valueA"valueB' +#@ notquoted_inner2: 'valueA"valueB"valueC' + + +# Single quotes +single_quote='value' +single_quote_spc = 'value' +single_quote_only='' + +#@ single_quote: "value" +#@ single_quote_spc: "value" +#@ single_quote_only: "" + +single_double='valueA"valueB' +single_doubleL='"value' +single_doubleR='value"' +single_doubleB='"value"' +single_double_only='"' +single_double_odd='" +single_double_value='value" + +#@ single_double: 'valueA"valueB' +#@ single_doubleL: '"value' +#@ single_doubleR: 'value"' +#@ single_doubleB: '"value"' +#@ single_double_only: '"' +#@ single_double_odd: '\'"' +#@ single_double_value: '\'value"' + + +double_single="valueA'valueB" +double_singleL="'value" +double_singleR="value'" +double_singleB="'value'" +double_single_only="'" +double_single_odd="' +double_single_value="value' + +#@ double_single: "valueA'valueB" +#@ double_singleL: "'value" +#@ double_singleR: "value'" +#@ double_singleB: "'value'" +#@ double_single_only: "'" +#@ double_single_odd: "\"'" +#@ double_single_value: "\"value'" + + +# Quotes in keys are not special +"quoted_key"=value +"quoted=key"=value + +#@ '"quoted_key"': "value" +#@ '"quoted': 'key"=value' + +# Multi-line values, with tripple quote +1line="""value""" + +2line="""valueA +valueB""" + +3line="""valueA +valueB +valueC""" + +# indentation is stripped. +3indent="""valueA + valueB + valueC""" + + +#@ "1line": "value" +#@ "2line": "valueA\nvalueB" +#@ "3line": "valueA\nvalueB\nvalueC" +#@ "3indent": "valueA\nvalueB\nvalueC" + +ml_quote_empty="""""" +#@ ml_quote_empty: "" + +# new-lines before and after should remain. +ml_newlineL=""" +value""" +ml_newlineR="""value +""" +ml_newlineB=""" +value +""" +ml_newline=""" +""" + +#@ ml_newlineL: "\nvalue" +#@ ml_newlineR: "value\n" +#@ ml_newlineB: "\nvalue\n" +#@ ml_newline: "\n" + + +# content should not be parsed. +ml_with_quote=""" +"value" +""" + +ml_with_values=""" +inside1=value +inside2:value +inside3="value" +[inside4] +inside5='value' +# +; +""" + +#@ ml_with_quote: '\n"value"\n' +#@ ml_with_values: "\ninside1=value\ninside2:value\ninside3=\"value\"\n[inside4]\ninside5='value'\n#\n;\n" + +# Tripple single-quote +ml_single_quote='''valueA +valueB''' +ml_single_quote2=''' +valueA +valueB''' +ml_single_quote3='''valueA +valueB +''' +ml_single_quote4='''valueA +valueB +''' + +#@ ml_single_quote: "valueA\nvalueB" +#@ ml_single_quote2: "\nvalueA\nvalueB" +#@ ml_single_quote3: "valueA\nvalueB\n" +#@ ml_single_quote4: "valueA\nvalueB\n" + +# Different quote inside another shouldn't matter +ml_quote_mix1="""valueA'''valueB""" + +ml_quote_mix2=""" +valueA'''valueB""" + +ml_quote_mix3="""valueA +'''valueB""" + +ml_quote_mix4="""valueA'''valueB +""" + +ml_quote_mix5=""" +valueA'''valueB +""" + +ml_quote_mix6=""" +valueA +'''valueB +""" + +ml_quote_mix7=""" +valueA +''' +valueB +""" + +#@ ml_quote_mix1: "valueA'''valueB" +#@ ml_quote_mix2: "\nvalueA'''valueB" +#@ ml_quote_mix3: "valueA\n'''valueB" +#@ ml_quote_mix4: "valueA'''valueB\n" +#@ ml_quote_mix5: "\nvalueA'''valueB\n" +#@ ml_quote_mix6: "\nvalueA\n'''valueB\n" +#@ ml_quote_mix7: "\nvalueA\n'''\nvalueB\n" + + +# New-lines via indentation +ml_indent=line1 + line2 + line3 + +ml_indent_comment=line1 + line2 + line3 + # comment not part of the value. + +ml_indent_kv=line1 + line2 + line3 + part:of=value +ml_indent_kv_after=value + + ml_indent_deep=line1 + line2 + line3 + ml_indent_deep_after=value + +#@ ml_indent: "line1\nline2\nline3" +#@ ml_indent_comment: "line1\nline2\nline3" +#@ ml_indent_kv: "line1\nline2\nline3\npart:of=value" +#@ ml_indent_kv_after: "value" +#@ ml_indent_deep: "line1\nline2\nline3" +#@ ml_indent_deep_after: "value" + +# Sections and sub-sections. + +# Different values with the same key can exist in different sections +sectionX=outer +#@ sectionX: "outer" + +[section1] +section1_key1=value +section1_key2=value +sectionX=section1 + +#@ section1: +#@ { section1_key1: "value", +#@ section1_key2: "value", +#@ sectionX: "section1" } + +[section2] +section2_key1=value +sectionX=section2 + +[[subsection2A]] +subsection2A=value2A +sectionX=subsection2A + +[[subsection2B]] +subsection2B=value2B +sectionX=subsection2B + +#@ section2: +#@ { section2_key1: "value", +#@ sectionX: "section2", +#@ subsection2A: { subsection2A: "value2A", sectionX: "subsection2A" }, +#@ subsection2B: { subsection2B: "value2B", sectionX: "subsection2B" } } + + +[section3] +section3_key=valueA +sectionX=section3 + +[[subsection3]] +subsection3_key=valueB +sectionX=subsection3 + +[[[subsubsection3]]] +subsubsection3_key=valueC +sectionX=subsubsection3 + +[[[[subsubsubsection3]]]] +subsubsubsection3_key=valueD +sectionX=subsubsubsection3 + +[[[subsubsection3B]]] +subsubsubsection3B_key=valueE +sectionX=subsubsection3B + +#@ section3: +#@ { section3_key: "valueA", +#@ sectionX: "section3", +#@ subsection3: +#@ { subsection3_key: "valueB", +#@ sectionX: "subsection3", +#@ subsubsection3: +#@ { subsubsection3_key: "valueC", sectionX: "subsubsection3", +#@ subsubsubsection3: { subsubsubsection3_key: "valueD", sectionX: "subsubsubsection3" } }, +#@ subsubsection3B: +#@ { subsubsubsection3B_key: "valueE", sectionX: "subsubsection3B" } +#@ } +#@ } + +[section4] +section4=value +sectionX=section4 + + [[indented]] + key=indented + +[[notindented]] +key=notindented + +#@ section4: +#@ { section4: "value", +#@ sectionX: "section4", +#@ indented: { key: "indented" }, +#@ notindented: { key: "notindented" } } + +[empty_section] + +[empty_section_no_space_below] +[empty_section_no_space_between] +[empty_section_no_space_above] + +[empty_with_sub] +[[empty_sub]] + +[section_no_space] +key=value +[section_no_space2] +key=value + +[section_space] + +key=value + +#@ section_no_space: { key: "value" } +#@ section_no_space2: { key: "value" } +#@ section_space: { key: "value" } + +[section.with.dot] +key=1st +keyA=value + +[section] +[[with.dot]] +key=2nd +keyB=value + +#@ "section.with.dot": { key: "1st", keyA: "value" } +#@ section: { "with.dot": { key: "2nd", keyB: "value" } } + +# Sections with the same name are merged. +[duplicate] +dupkey=first +keyA=valueA + +[duplicate] +dupkey=second +keyB=valueB + +#@ duplicate: { dupkey: "second", keyA: "valueA", keyB: "valueB" } + +[duplicate2] +key=value +[[duplicate_sub]] +dupkey=first +keyA=valueA +[[duplicate_sub]] +dupkey=second +keyB=valueB + +#@ duplicate2: +#@ { key: "value", +#@ duplicate_sub: { dupkey: "second", keyA: "valueA", keyB: "valueB" } } + +# Sub-section can have the same name as the container. +[sub_section_same] +key1=parent +keyA=valueA + +[[sub_section_same]] +key1=child +keyB=valueB + +#@ sub_section_same: +#@ { key1: "parent" +#@ keyA: "valueA" +#@ sub_section_same: { key1: "child", keyB: "valueB" } } + +# A value will be over-written by a sub-section of the same name +[duplicate_section+key] +dup=value +key=1st +keyA=valueA +[[dup]] +keyB=valueB +key=2nd + +#@ "duplicate_section+key": +#@ { key: "1st" +#@ keyA: "valueA", +#@ dup: { keyB: "valueB", key: "2nd" } } + + +[one space] +a=1 +[two two space] +b=1 +#@ "one space": { "a": 1 } +#@ "two two space": { "b": 1 } + +[final_section] +complete=yes +#@ final_section: { complete: "yes" } diff --git a/gpii/node_modules/gpii-ini-file/test/write-test.expect.ini b/gpii/node_modules/gpii-ini-file/test/write-test.expect.ini new file mode 100644 index 000000000..f49c80a47 --- /dev/null +++ b/gpii/node_modules/gpii-ini-file/test/write-test.expect.ini @@ -0,0 +1,254 @@ +# +# write-test.ini: An ini file to test writing +# write-test.expect.ini: The expected output of a modifying write-test.ini +# +# Keys and sections begining with "remove_" will be removed. +# Keys begining with "ml_" will have multi-line values added. + +# normal key=value +key=abc key xyz +key2=abc key2 xyz + +100=abc 100 xyz + +# set an empty value +empty=abc empty xyz +# some changes +empty_me= +null_me= +number_me1=42 +number_me2="12345" +true_me1=true +true_me2="true" + +# removal + +keep1=abc keep1 xyz +keep2=abc keep2 xyz + + +# Spaces (surrounding space should be kept) +spaced = abc spaced xyz +spacedL =abc spacedL xyz +spacedR= abc spacedR xyz + +# Spaces in values and around delimiter +notspaced = abc notspaced xyz +spaced2 = abc spaced2 xyz +spaced left =abc spaced left xyz +spaced right= abc spaced right xyz +spaced none=abc spaced none xyz +double space = abc double space xyz +double space and value = abc double space and value xyz + +# Colons as delimiters +colon1:abc colon1 xyz +colon2 : abc colon2 xyz +colon3 : abc colon3 xyz + +colon+equal:abc colon+equal xyz +equal+colon=abc equal+colon xyz + + +# Quoted values +quoted_ns="abc quoted_ns xyz" +quoted_spc = "abc quoted_spc xyz" +quoted_spcL ="abc quoted_spcL xyz" +quoted_spcR= "abc quoted_spcR xyz" + +quoted_spc_value = "abc quoted_spc_value xyz" +quoted_spc_valueL = "abc quoted_spc_valueL xyz" +quoted_spc_valueR = "abc quoted_spc_valueR xyz" +quoted_spc_valueB = "abc quoted_spc_valueB xyz" + +quote"in"key = abc quote"in"key xyz +quote"in"key2 = "abc quote"in"key2 xyz" + +quotes_only="abc quotes_only xyz" + +# Single quotes +single_quote='abc single_quote xyz' +single_quote_spc = 'abc single_quote_spc xyz' +single_quote_only='abc single_quote_only xyz' + +# Multi-line values, with triple quote +ml_1line="""abc ml_1line xyz line0""" + +ml_2line="""abc ml_2line xyz line0 +abc ml_2line xyz line1""" + +ml_3line="""abc ml_3line xyz line0 +abc ml_3line xyz line1 +abc ml_3line xyz line2""" + + +triple_quote_empty1="""abc triple_quote_empty1 xyz""" + +# Triple single-quote +ml_single_quote='''abc ml_single_quote xyz line0 +abc ml_single_quote xyz line1 +abc ml_single_quote xyz line2''' + +# New-lines via indentation +ml_indent=abc ml_indent xyz line0 + abc ml_indent xyz line1 + abc ml_indent xyz line2 + +ml_indent_comment=abc ml_indent_comment xyz line0 + abc ml_indent_comment xyz line1 + abc ml_indent_comment xyz line2 + # comment not part of the value. + +ml_indent_kv=abc ml_indent_kv xyz line0 + abc ml_indent_kv xyz line1 + abc ml_indent_kv xyz line2 +ml_indent_kv_after=abc ml_indent_kv_after xyz line0 + abc ml_indent_kv_after xyz line1 + abc ml_indent_kv_after xyz line2 +not_ml_indent_kv_after=abc not_ml_indent_kv_after xyz + + ml_indent_deep=abc ml_indent_deep xyz line0 + abc ml_indent_deep xyz line1 + abc ml_indent_deep xyz line2 + ml_indent_deep_after=abc ml_indent_deep_after xyz line0 + abc ml_indent_deep_after xyz line1 + abc ml_indent_deep_after xyz line2 + not_ml_indent_deep_after=abc not_ml_indent_deep_after xyz + +# Different values with the same key can exist in different sections +sectionX=abc sectionX xyz +new_empty= +new_null= +new_number=42 +new_true=true +newKey1=new value + +[section1] +section1_key1=abc section1_key1 xyz +section1_key2=abc section1_key2 xyz +sectionX=abc sectionX xyz +newKey1=new value 1, in section1 +newKey2=new value 2 + +[section2] +section2_key1=abc section2_key1 xyz +sectionX=abc sectionX xyz + +[[subsection2A]] +subsection2A=abc subsection2A xyz +sectionX=abc sectionX xyz + +[[subsection2B]] +subsection2B=abc subsection2B xyz +sectionX=abc sectionX xyz + +[section3] +section3_key=abc section3_key xyz +sectionX=abc sectionX xyz + +[[subsection3]] +subsection3_key=abc subsection3_key xyz +sectionX=abc sectionX xyz + +[[[subsubsection3]]] +subsubsection3_key=abc subsubsection3_key xyz +sectionX=abc sectionX xyz + +[[[[subsubsubsection3]]]] +subsubsubsection3_key=abc subsubsubsection3_key xyz +sectionX=abc sectionX xyz + +[[[subsubsection3B]]] +subsubsubsection3B_key=abc subsubsubsection3B_key xyz +sectionX=abc sectionX xyz + +[section4] +section4=abc section4 xyz +sectionX=abc sectionX xyz + + [[indented]] + key=abc key xyz + +[[notindented]] +key=abc key xyz + + +[empty_section] + +[empty_section_no_space_below] +[empty_section_no_space_between] +[empty_section_no_space_above] + +[empty_with_sub] +[[empty_sub]] + +[section_no_space] +key=abc key xyz +[section_no_space2] +key=abc key xyz + +[section_space] + +key=abc key xyz + + +[section.with.dot] +key=abc key xyz +keyA=abc keyA xyz + +[section] +[[with.dot]] +key=abc key xyz +keyB=abc keyB xyz + +[section onespace] +key=abc key xyz +keyA=abc keyA xyz + +[section two space] +key=abc key xyz +keyB=abc keyB xyz + +# Sub-section can have the same name as the container. +[sub_section_same] +key1=abc key1 xyz +keyA=abc keyA xyz + +[[sub_section_same]] +key1=abc key1 xyz +keyB=abc keyB xyz + +# Remove sections - the section header doesn't get removed, but the contents do. +[section_remain1] +key_should_remain1=abc key_should_remain1 xyz +[remove_section1] +[section_remain2] +key_should_remain1=abc key_should_remain1 xyz + +[remove_section2] +[[subsection_should_empty2]] +[[[subsubsection_should_empy22]]] + + +[section_keep] +key_should_remain=abc key_should_remain xyz +[[remove_section3]] +[[section_remains]] +key_should_remain=abc key_should_remain xyz + + +[final_section] +complete=abc complete xyz + + +[newSection1] +newKey1=new value 1, in new section 1 +newKey3=new value 3 + +[newSection2] +newKey1=new value 1, in new section 2 +newKey4=new value 4 + +[[newSubSection]] +newKey1=new value 1, in new sub section +newKey5=new value 5 diff --git a/gpii/node_modules/gpii-ini-file/test/write-test.ini b/gpii/node_modules/gpii-ini-file/test/write-test.ini new file mode 100644 index 000000000..d510724e7 --- /dev/null +++ b/gpii/node_modules/gpii-ini-file/test/write-test.ini @@ -0,0 +1,243 @@ +# +# write-test.ini: An ini file to test writing +# write-test.expect.ini: The expected output of a modifying write-test.ini +# +# Keys and sections begining with "remove_" will be removed. +# Keys begining with "ml_" will have multi-line values added. + +# normal key=value +key=value1 +key2=value2 + +100=numbered-key-value + +# set an empty value +empty= +# some changes +empty_me=not empty +null_me= +number_me1=42 +number_me2="text" +true_me1= +true_me2="text" + +# removal +remove_key1=remove1 +remove_key2=remove2 + +keep1=value +remove_key3=remove3 +keep2=value + + +# Spaces (surrounding space should be kept) +spaced = value +spacedL =value +spacedR= value + +# Spaces in values and around delimiter +notspaced = value with spaces +spaced2 = value with spaces +spaced left =value with spaces +spaced right= value with spaces +spaced none=value with spaces +double space = value +double space and value = valueA valueB + +# Colons as delimiters +colon1:value +colon2 : value +colon3 : value + +colon+equal:=value +equal+colon=:value + + +# Quoted values +quoted_ns="value" +quoted_spc = "value" +quoted_spcL ="value" +quoted_spcR= "value" + +quoted_spc_value = "valueA valueB valueC" +quoted_spc_valueL = " valueA valueB valueC" +quoted_spc_valueR = "valueA valueB valueC " +quoted_spc_valueB = " valueA valueB valueC " + +quote"in"key = value +quote"in"key2 = "value" + +quotes_only="" + +# Single quotes +single_quote='value' +single_quote_spc = 'value' +single_quote_only='' + +# Multi-line values, with triple quote +ml_1line="""value""" + +ml_2line="""valueA +valueB""" + +ml_3line="""valueA +valueB +valueC""" + + +triple_quote_empty1="""""" + +# Triple single-quote +ml_single_quote='''valueA +valueB''' + +# New-lines via indentation +ml_indent=line1 + line2 + line3 + +ml_indent_comment=line1 + line2 + line3 + # comment not part of the value. + +ml_indent_kv=line1 + line2 + line3 + part:of=value +ml_indent_kv_after=line1 + line2 + line3 +not_ml_indent_kv_after=line1 + + ml_indent_deep=line1 + line2 + line3 + ml_indent_deep_after=line1 + line2 + line3 + not_ml_indent_deep_after=value + +# Different values with the same key can exist in different sections +sectionX=outer + +[section1] +section1_key1=value +section1_key2=value +sectionX=section1 + +[section2] +section2_key1=value +sectionX=section2 + +[[subsection2A]] +subsection2A=value2A +sectionX=subsection2A + +[[subsection2B]] +subsection2B=value2B +sectionX=subsection2B + +[section3] +section3_key=valueA +sectionX=section3 + +[[subsection3]] +subsection3_key=valueB +sectionX=subsection3 + +[[[subsubsection3]]] +subsubsection3_key=valueC +sectionX=subsubsection3 + +[[[[subsubsubsection3]]]] +subsubsubsection3_key=valueD +sectionX=subsubsubsection3 + +[[[subsubsection3B]]] +subsubsubsection3B_key=valueE +sectionX=subsubsection3B + +[section4] +section4=value +sectionX=section4 + + [[indented]] + key=indented + +[[notindented]] +key=notindented + + +[empty_section] + +[empty_section_no_space_below] +[empty_section_no_space_between] +[empty_section_no_space_above] + +[empty_with_sub] +[[empty_sub]] + +[section_no_space] +key=value +[section_no_space2] +key=value + +[section_space] + +key=value + + +[section.with.dot] +key=1st +keyA=value + +[section] +[[with.dot]] +key=2nd +keyB=value + +[section onespace] +key=123 +keyA=aa + +[section two space] +key=321 +keyB=bb + +# Sub-section can have the same name as the container. +[sub_section_same] +key1=parent +keyA=valueA + +[[sub_section_same]] +key1=child +keyB=valueB + +# Remove sections - the section header doesn't get removed, but the contents do. +[section_remain1] +key_should_remain1=value +[remove_section1] +key_should_go=value +[section_remain2] +key_should_remain1=value + +[remove_section2] +key_should_go2=value +[[subsection_should_empty2]] +key_should_go2=value +[[[subsubsection_should_empy22]]] +key_should_go22=value + + +[section_keep] +key_should_remain=value +[[remove_section3]] +key_should_go3=value +[[section_remains]] +key_should_remain=value + + +[final_section] +complete=true + diff --git a/gpii/node_modules/gpii-oauth2/README.md b/gpii/node_modules/gpii-oauth2/README.md new file mode 100644 index 000000000..328ae5636 --- /dev/null +++ b/gpii/node_modules/gpii-oauth2/README.md @@ -0,0 +1,10 @@ +# GPII OAuth2 + +For details on the GPII supported grant type and its workflow from the point of view of a GPII app application, see the +[GPII OAuth 2 Guide](http://wiki.gpii.net/w/GPII_OAuth_2_Guide). + +## Tests + +- `gpii-oauth2-authz-server/test/webTests/all-tests.html` +- `gpii-oauth2-authz-server/test/html/AuthGrantFinderTests.html` +- `gpii-oauth2-authz-server/test/html/AuthorizationServiceTests.html` diff --git a/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/index.js b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/index.js new file mode 100644 index 000000000..2b85f24bb --- /dev/null +++ b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/index.js @@ -0,0 +1,3 @@ +"use strict"; + +require("./src/AuthServer.js"); diff --git a/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/package.json b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/package.json new file mode 100644 index 000000000..71326faa6 --- /dev/null +++ b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/package.json @@ -0,0 +1,14 @@ +{ + "name": "gpii-oauth2-authorization-server", + "version": "0.1.0", + "dependencies": { + "body-parser": "^1.9.2", + "connect-ensure-login": "^0.1.1", + "express-handlebars": "^1.1.0", + "express-session": "^1.9.1", + "oauth2orize": "^1.0.1", + "passport": "^0.2.1", + "passport-local": "^1.0.0", + "passport-oauth2-client-password": "^0.1.2" + } +} diff --git a/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/AuthGrantFinder.js b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/AuthGrantFinder.js new file mode 100644 index 000000000..7e335415d --- /dev/null +++ b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/AuthGrantFinder.js @@ -0,0 +1,66 @@ +/*! +Copyright 2016-2019 OCAD university + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +/* eslint-env browser */ +/* eslint strict: ["error", "function"] */ + +var fluid = fluid || require("infusion"); + +(function () { + + "use strict"; + + var gpii = fluid.registerNamespace("gpii"); + + fluid.defaults("gpii.oauth2.authGrantFinder", { + gradeNames: ["fluid.component"], + components: { + authorizationService: { + type: "gpii.oauth2.authorizationService" + } + }, + invokers: { + getGrantForAccessToken: { + funcName: "gpii.oauth2.authGrantFinder.getGrantForAccessToken", + args: ["{that}.authorizationService", "{arguments}.0"] + // accessToken + } + } + }); + + // Return a promise object that contains the granted privilege for the access token. + // This function looks up access tokens granted for GPII app installations to find the match. + gpii.oauth2.authGrantFinder.getGrantForAccessToken = function (authorizationService, accessToken) { + var promiseTogo = fluid.promise(); + var authorizationPromise = authorizationService.getInfoByAccessToken(accessToken); + var grant; + + authorizationPromise.then(function (authRecord) { + if (authRecord) { + if (authRecord.authorization.type === gpii.dbOperation.docTypes.gpiiAppInstallationAuthorization && + gpii.oauth2.getExpiresIn(authorizationService.getCurrentDate(), authRecord.authorization.timestampExpires) > 0) { + grant = { + accessToken: accessToken, + gpiiKey: authRecord.authorization.gpiiKey, + allowSettingsGet: true, + allowSettingsPut: true, + allowedPrefsToWrite: authRecord.clientCredential.allowedPrefsToWrite || null + }; + } + } + promiseTogo.resolve(grant); + }); + + return promiseTogo; + }; +})(); diff --git a/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/AuthServer.js b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/AuthServer.js new file mode 100644 index 000000000..09e95c2c3 --- /dev/null +++ b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/AuthServer.js @@ -0,0 +1,301 @@ +/*! +Copyright 2014-2019 OCAD university + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var bodyParser = require("body-parser"); +var oauth2orize = require("oauth2orize"); +var passportModule = require("passport"); +var ClientPasswordStrategy = require("passport-oauth2-client-password").Strategy; + +var fluid = require("infusion"); + +require("../../gpii-oauth2-utilities"); +require("./AuthorizationService"); +require("./AuthGrantFinder"); +require("./ClientService"); +require("./CodeGenerator"); + +var gpii = fluid.registerNamespace("gpii"); + +// gpii.oauth2.oauth2orizeServer +// ----------------------------- + +gpii.oauth2.createOauth2orizeServer = function () { + return oauth2orize.createServer(); +}; + +fluid.defaults("gpii.oauth2.oauth2orizeServer", { + gradeNames: ["fluid.component"], + members: { + oauth2orizeServer: { + expander: { + func: "gpii.oauth2.createOauth2orizeServer" + } + } + }, + listeners: { + "onCreate.listenOauth2orize": { + listener: "gpii.oauth2.oauth2orizeServer.listenOauth2orize", + args: ["{that}.oauth2orizeServer", "{clientService}", "{authorizationService}", "{dataStore}"] + } + } +}); + +gpii.oauth2.oauth2orizeServer.listenOauth2orize = function (oauth2orizeServer, clientService, authorizationService, dataStore) { + oauth2orizeServer.serializeClient(function (client, done) { + return done(null, client.id); + }); + + oauth2orizeServer.deserializeClient(function (id, done) { + var clientPromise = clientService.getClientById(id); + gpii.oauth2.oauth2orizeServer.promiseToDone(clientPromise, done); + }); + + /** + * Processes OAuth2 resource owner GPII key grant requests. + * @param {Object} clientInfo - The client and its privilege info. + * @typedef {Object} clientInfo + * @property {Object} client - All fields from "client" document for this client. + * @property {Object} clientCredential - Includes listed client credential fields. + * @property {String} clientCredential.id - The internal client credential ID. + * @property {Array} clientCredential.allowedIPBlocks - Allowed IP ranges that requests are sent from. + * @property {Boolean} clientCredential.isCreateGpiiKeyAllowed - Whether the client has privilege to create new GPII keys. + * @property {Boolean} clientCredential.isCreatePrefsSafeAllowed - Whether the client has privilege to create new prefs safes. + * + * @param {String} username - The username value in the grant request. + * @param {String} password - The password value in the grant request. + * @param {String} scope - The scope value in the grant request. + * @param {Object} body - The request body in the grant request. + * @param {Object} authInfo - The additional information passed in via the previous middleware for passing + * the grant request for extra client privilege checks. + * @param {Function} done - The oauth2orizeServer endpoint function to grant or reject when a client requests authorization. + * @return {Object} The result of gpii.oauth2.oauth2orizeServer.promiseToDone() that contains the response to the grant request. + */ + oauth2orizeServer.exchange(oauth2orize.exchange.password(function (clientInfo, username, password, scope, body, authInfo, done) { + var ip = authInfo.req.headers["x-forwarded-for"] || authInfo.req.socket.remoteAddress; + + // GPII-3717: Client privilege checks: + // 1. If the value of "clientInfo.clientCredential.allowedIPBlocks" is null or undefined, skip the ip + // verification and go ahead to assign an access token. If this value is provided, the IP of the incoming + // request must be within the allowed IP blocks before assigning an access token; + // 2. If the client does not have privilege to create new GPII keys and prefs safes but requesting an access + // token for a nonexistent GPII key, the request will be rejected. + if (clientInfo.clientCredential.allowedIPBlocks && !gpii.oauth2.isIPINRange(ip, clientInfo.clientCredential.allowedIPBlocks)) { + // 1. Reject if the ip address is not within the allowed range. + fluid.log("authServer: unauthorized /access_token request because the IP of the incoming request (" + ip + ") is not within the allowed IP blocks: " + clientInfo.clientCredential.allowedIPBlocks); + gpii.oauth2.oauth2orizeServer.promiseToDone(fluid.promise().reject(gpii.dbOperation.errors.unauthorized), done); + } else if (!clientInfo.clientCredential.isCreateGpiiKeyAllowed || !clientInfo.clientCredential.isCreatePrefsSafeAllowed) { + // 2. For clients who don't have privilege to read/write nonexistent GPII keys: + // 2.1 If the request is for an existing GPII key, grant an access token; + // 2.2 If the request is for an nonexistent GPII key, reject; + var gpiiKeyPromise = dataStore.findGpiiKey(username); + gpiiKeyPromise.then(function (data) { + if (data) { + // The GPII key exists. Proceed to grant an access token + gpii.oauth2.oauth2orizeServer.grantAccessToken(authorizationService, username, clientInfo.client.id, clientInfo.clientCredential.id, done); + } else { + // Reject the request because the request GPII key does not exists and the client does not have + // privilege to . + fluid.log("authServer: unauthorized /access_token request the client (with the clientCredentialId ", clientInfo.clientCredential.clientCredentialId, ") does not have privilege to access user settings for the nonexistent GPII key (", username, ")."); + gpii.oauth2.oauth2orizeServer.promiseToDone(fluid.promise().reject(gpii.dbOperation.errors.unauthorized), done); + } + }); + } else { + // For clients who have privilege to read/write nonexistent GPII keys, grant an access token. + gpii.oauth2.oauth2orizeServer.grantAccessToken(authorizationService, username, clientInfo.client.id, clientInfo.clientCredential.id, done); + } + })); +}; + +gpii.oauth2.oauth2orizeServer.grantAccessToken = function (authorizationService, gpiiKey, clientId, clientCredentialId, done) { + var passwordPromise = authorizationService.grantGpiiAppInstallationAuthorization(gpiiKey, clientId, clientCredentialId); + + var authorizationMapper = function (authorization) { + return authorization.accessToken; + }; + + var paramsMapper = function (params) { + // extra parameters to be included in the `oauth2orizeServer` response except `accessToken` + return fluid.censorKeys(params, "accessToken"); + }; + + var authorizationPromise = fluid.promise.map(passwordPromise, authorizationMapper); + var paramsPromise = fluid.promise.map(passwordPromise, paramsMapper); + + gpii.oauth2.oauth2orizeServer.promiseToDone(authorizationPromise, done, paramsPromise); +}; + +// gpii.oauth2.passport +// -------------------- + +gpii.oauth2.createPassport = function () { + return new passportModule.Passport(); +}; + +fluid.defaults("gpii.oauth2.passport", { + gradeNames: ["fluid.component"], + members: { + passport: { + expander: { + func: "gpii.oauth2.createPassport" + } + } + }, + listeners: { + onCreate: { + listener: "gpii.oauth2.passport.listenPassport", + args: ["{that}.passport", "{clientService}"] + } + } +}); + +// TODO what name here? +gpii.oauth2.passport.listenPassport = function (passport, clientService) { + // ClientPasswordStrategy reads the client_id and client_secret from the + // request body. Can also use a BasicStrategy for HTTP Basic authentication. + passport.use(new ClientPasswordStrategy( + function (oauth2ClientId, oauth2ClientSecret, done) { + var clientPromise = clientService.authenticateClient(oauth2ClientId, oauth2ClientSecret); + gpii.oauth2.oauth2orizeServer.promiseToDone(clientPromise, done); + } + )); +}; + +// gpii.oauth2.authServer +// ---------------------- + +// An empty grade to guide resolution of IoC expressions onto a suitable gpii.dbOperation.dataStore +fluid.defaults("gpii.oauth2.dataStoreHolder", { + gradeNames: ["fluid.component"] +}); + +fluid.defaults("gpii.oauth2.authServer", { + gradeNames: ["fluid.component", "gpii.oauth2.dataStoreHolder"], + members: { + expressApp: { + expander: { + func: "gpii.oauth2.createExpressApp" + } + } + }, + components: { + oauth2orizeServer: { + type: "gpii.oauth2.oauth2orizeServer" + }, + passport: { + type: "gpii.oauth2.passport" + }, + dataStore: { + type: "gpii.dbOperation.dataStore" // variants here + }, + authorizationService: { + type: "gpii.oauth2.authorizationService", + options: { + components: { + dataStore: "{gpii.oauth2.dataStoreHolder}.dataStore" + } + } + }, + clientService: { + type: "gpii.oauth2.clientService", + options: { + components: { + dataStore: "{gpii.oauth2.dataStoreHolder}.dataStore" + } + } + } + }, + events: { + onContributeRouteHandlers: null + }, + listeners: { + onContributeRouteHandlers: { + listener: "gpii.oauth2.authServer.contributeRouteHandlers", + args: ["{that}", "{that}.oauth2orizeServer.oauth2orizeServer", + "{that}.passport.passport"] + }, + "onCreate.registerBodyParser": "gpii.oauth2.authServer.registerBodyParser" + } +}); + +gpii.oauth2.authServer.registerBodyParser = function (that) { + that.expressApp.use(gpii.oauth2.jsonBodyParser()); + that.expressApp.use(gpii.oauth2.urlencodedBodyParser()); +}; + +fluid.defaults("gpii.oauth2.authServer.standalone", { + gradeNames: ["gpii.oauth2.authServer"], + listeners: { + "onCreate.onContributeRouteHandlers": { + priority: "after:registerBodyParser", + func: "{that}.events.onContributeRouteHandlers.fire" + } + } +}); + +gpii.oauth2.jsonBodyParser = function () { + return bodyParser.json({ + // Increase the limit of the request body to 50MB instead of the default 100KB. + // This solves the error "request entity too large" when the responded lifecycle + // instruction payload is larger than 100KB. + // See: https://github.com/expressjs/body-parser#limit-1 + // and https://stackoverflow.com/questions/19917401/error-request-entity-too-large + limit: 52428800 + }); +}; + +gpii.oauth2.urlencodedBodyParser = function () { + return bodyParser.urlencoded({ + // See the comment above for bodyParser.json() about the necessity to increase the limit. + limit: 52428800, + extended: true + }); +}; + +gpii.oauth2.authServer.contributeRouteHandlers = function (that, oauth2orizeServer, passport) { + that.expressApp.post("/access_token", + passport.authenticate("oauth2-client-password", { session: false }), + // A workaround to get node "request" variable passed into oauth2orize supported exchanges: + // https://github.com/jaredhanson/oauth2orize/issues/182#issuecomment-253571341 + function (req, res, next) { + req.authInfo.req = req; + next(); + }, + oauth2orizeServer.token(), + oauth2orizeServer.errorHandler() + ); +}; + +/** + * An utility function to parse a promise object to determine whether to grant or reject an authorization. The grant occurs in the promise resolve callback while + * the reject occurs in the promise reject callback. + * @param {Promise} promise - The promise object to determine the grant or reject an authorization. + * @param {Function} done - The oauth2orizeServer endpoint function to grant or reject when a client requests authorization. + * @param {Object} paramsPromise - Contains additional parameters to be included in the success response. + * See [oauth2orize in github](https://github.com/jaredhanson/oauth2orize) for more information + */ +gpii.oauth2.oauth2orizeServer.promiseToDone = function (promise, done, paramsPromise) { + promise.then(function (data) { + if (paramsPromise) { + paramsPromise.then(function (params) { + done(null, data, null, params); + }); + } else { + done(null, data); + } + }, function (error) { + var responseError = new oauth2orize.OAuth2Error(error.message, null, null, error.statusCode); + done(responseError, false); + }); +}; diff --git a/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/AuthorizationService.js b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/AuthorizationService.js new file mode 100644 index 000000000..b6d140b40 --- /dev/null +++ b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/AuthorizationService.js @@ -0,0 +1,146 @@ +/*! +Copyright 2014-2019 OCAD university + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +/* eslint-env browser */ +/* eslint strict: ["error", "function"] */ + +var fluid = fluid || require("infusion"); + +(function () { + + "use strict"; + + var gpii = fluid.registerNamespace("gpii"); + + fluid.registerNamespace("gpii.oauth2.authorizationService"); + + // Small function to return the current date. Used in part to ensure that we can compare canned dates in tests. + gpii.oauth2.authorizationService.getCurrentDate = function () { + return new Date(); + }; + + fluid.defaults("gpii.oauth2.authorizationService", { + gradeNames: ["fluid.component"], + components: { + dataStore: { + type: "gpii.dbOperation.dataStore" + }, + codeGenerator: { + type: "gpii.oauth2.codeGenerator" + } + }, + invokers: { + grantGpiiAppInstallationAuthorization: { + funcName: "gpii.oauth2.authorizationService.grantGpiiAppInstallationAuthorization", + args: ["{dataStore}", "{codeGenerator}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] + // gpiiKey, clientId, clientCredentialId + }, + getInfoByAccessToken: { + func: "{dataStore}.findInfoByAccessToken" + // accessToken + }, + getCurrentDate: { + funcName: "gpii.oauth2.authorizationService.getCurrentDate" + } + } + }); + + // APIs for GPII App Installation clients + + /** + * Grant an authorization for the give GPII app installation. The gpii key will be verified before the access token is returned. + * @param {Component} dataStore - An instance of gpii.dbOperation.dbDataStore. + * @param {Component} codeGenerator - An instance of gpii.oauth2.codeGenerator. + * @param {String} gpiiKey - A GPII key. + * @param {String} clientId - A client id. + * @param {String} clientCredentialId - A client credential id. + * @return {Promise} A promise object whose resolved value is the access token. An error will be returned if the GPII key is not found. + */ + gpii.oauth2.authorizationService.grantGpiiAppInstallationAuthorization = function (dataStore, codeGenerator, gpiiKey, clientId, clientCredentialId) { + var promiseTogo = fluid.promise(); + + if (!gpiiKey || !clientId || !clientCredentialId) { + var error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingInput, {fieldName: "GPII key, client ID or client credential ID"}); + promiseTogo.reject(error); + } else { + var clientPromise = dataStore.findClientById(clientId); + var clientCredentialPromise = dataStore.findClientCredentialById(clientCredentialId); + + // TODO: Update the usage of fluid.promise.sequence() once https://issues.fluidproject.org/browse/FLUID-5938 is resolved. + var sources = [clientPromise, clientCredentialPromise]; + var promisesSequence = fluid.promise.sequence(sources); + + promisesSequence.then(function (responses) { + var clientRec = responses[0]; + var clientCredentialRec = responses[1]; + + var error; + + if (!clientRec || clientRec.type !== gpii.dbOperation.docTypes.gpiiAppInstallationClient) { + fluid.log("authorizationService, granting GPII app installation authorization: invalid client or the type of the client with the client id (" + clientId + ") is not \"" + gpii.dbOperation.docTypes.gpiiAppInstallationClient + "\""); + error = gpii.dbOperation.composeError(gpii.dbOperation.errors.unauthorized); + promiseTogo.reject(error); + } else if (!clientCredentialRec || clientCredentialRec.type !== gpii.dbOperation.docTypes.clientCredential) { + fluid.log("authorizationService, granting GPII app installation authorization: invalid client credential or the type of the client credential with id (" + clientCredentialId + ") is not \"" + gpii.dbOperation.docTypes.clientCredential + "\""); + error = gpii.dbOperation.composeError(gpii.dbOperation.errors.unauthorized); + promiseTogo.reject(error); + } else if (clientCredentialRec.clientId !== clientId) { + fluid.log("authorizationService, granting GPII app installation authorization: the client id (" + clientCredentialRec.clientId + ") that the client credential belongs to does not match the client id (" + clientId + ") that requests the authorization"); + error = gpii.dbOperation.composeError(gpii.dbOperation.errors.unauthorized); + promiseTogo.reject(error); + } else { + // Re-issue a new access token every time. In the case that multiple requests were made for the same "client credential + GPII key" + // combination, the access token would be different for each request in the audit log. In the case that one request was detected to + // be from an attacker, invoking the associating access token would not affect other access tokens or the real user. + gpii.oauth2.authorizationService.createGpiiAppInstallationAuthorization(promiseTogo, dataStore, codeGenerator, gpiiKey, clientId, clientCredentialId, gpii.oauth2.defaultTokenLifeTimeInSeconds); + } + }); + } + + return promiseTogo; + }; + + /** + * @param {Promise} promiseTogo - Modified by the function with objects to be resolved or to fail. + * @param {Component} dataStore - An instance of gpii.dbOperation.dbDataStore. + * @param {Component} codeGenerator - An instance of gpii.oauth2.codeGenerator. + * @param {String} gpiiKey - A GPII key. + * @param {String} clientId - An unique client id. + * @param {String} clientCredentialId - An unique client credential id. + * @param {String} expiresIn - The lifetime in seconds of the access token. + * + * If promiseTogo is fired, the first argument will be the returned values. + */ + gpii.oauth2.authorizationService.createGpiiAppInstallationAuthorization = function (promiseTogo, dataStore, codeGenerator, gpiiKey, clientId, clientCredentialId, expiresIn) { + var accessToken = codeGenerator.generateAccessToken(); + + var addGpiiAppInstallationAuthorizationPromise = dataStore.addAuthorization({ + clientId: clientId, + gpiiKey: gpiiKey, + clientCredentialId: clientCredentialId, + accessToken: accessToken, + timestampExpires: gpii.oauth2.getTimestampExpires(new Date(), expiresIn) + }); + + var mapper = function () { + // The created access token is resolved for promiseTogo eventually + return { + accessToken: accessToken, + expiresIn: expiresIn + }; + }; + var authorizationPromise = fluid.promise.map(addGpiiAppInstallationAuthorizationPromise, mapper); + fluid.promise.follow(authorizationPromise, promiseTogo); + }; + +})(); diff --git a/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/ClientService.js b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/ClientService.js new file mode 100644 index 000000000..b1b908831 --- /dev/null +++ b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/ClientService.js @@ -0,0 +1,75 @@ +/*! +Copyright 2014 OCAD university +Copyright 2017 OCAD university +Copyright 2019 OCAD university + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"); + +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.oauth2"); + +fluid.defaults("gpii.oauth2.clientService", { + gradeNames: ["fluid.component"], + components: { + dataStore: { + type: "gpii.dbOperation.dataStore" + } + }, + invokers: { + authenticateClient: { + funcName: "gpii.oauth2.clientService.authenticateClient", + args: ["{dataStore}", "{arguments}.0", "{arguments}.1"] + // oauth2ClientId, expectedOauth2ClientSecret + }, + getClientById: { + func: "{dataStore}.findClientById" + } + } +}); + +// To verify a client information matches the expected value +// +// @dataStore (Object) - an instance of a data store component such as gpii.dbOperation.dbDataStore +// @oauth2ClientId (String) - an OAuth2 client id +// @expectedOauth2ClientSecret (String) - The expected OAuth2 client secret +// +// @return (Promise) - A promise object that contains either a client record or an error +gpii.oauth2.clientService.authenticateClient = function (dataStore, oauth2ClientId, expectedOauth2ClientSecret) { + var promiseTogo = fluid.promise(); + var clientPromise = dataStore.findClientByOauth2ClientId(oauth2ClientId); + + clientPromise.then(function (record) { + var client = record ? record.client : undefined; + var clientCredential = record ? record.clientCredential : undefined; + + if (client && clientCredential && fluid.get(clientCredential, ["oauth2ClientSecret"]) === expectedOauth2ClientSecret) { + promiseTogo.resolve({ + client: client, + clientCredential: { + id: clientCredential.id, + allowedIPBlocks: clientCredential.allowedIPBlocks, + isCreateGpiiKeyAllowed: !!clientCredential.isCreateGpiiKeyAllowed, + isCreatePrefsSafeAllowed: !!clientCredential.isCreatePrefsSafeAllowed + } + }); + } else { + fluid.log("clientService: unauthorized client with oauth2ClientId - " + oauth2ClientId); + promiseTogo.reject(gpii.dbOperation.errors.unauthorized); + } + }, function (err) { + promiseTogo.reject(err); + }); + return promiseTogo; +}; diff --git a/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/CodeGenerator.js b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/CodeGenerator.js new file mode 100644 index 000000000..df35f2b22 --- /dev/null +++ b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/src/CodeGenerator.js @@ -0,0 +1,43 @@ +/*! +Copyright 2014 OCAD university + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"); +var crypto = require("crypto"); + +var gpii = fluid.registerNamespace("gpii"); + +// The gpii.oauth2.codeGenerator component is responsible for generating OAuth 2 access tokens. +// The codes and access tokens generated must be unguessable. +// +// The implementation uses the Node.js crypto module and was originally part of the +// gpii.oauth2.authorizationService component. +// The code generation functionality was moved to its own component to +// remove dependencies on Node.js from the authorizationService, so +// that the authorzationService could be unit tested with +// browser-based testing. + +fluid.defaults("gpii.oauth2.codeGenerator", { + gradeNames: ["fluid.component"], + invokers: { + generateAccessToken: "gpii.oauth2.codeGenerator.generateAccessToken" + } +}); + +gpii.oauth2.codeGenerator.generateAccessToken = function () { + // TODO: Ensure that handles cannot be guessed + // TODO: crypto.randomBytes can fail if there is not enough entropy + // see http://nodejs.org/api/crypto.html + return crypto.randomBytes(16).toString("hex"); +}; diff --git a/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/authGrantFinderTests.js b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/authGrantFinderTests.js new file mode 100644 index 000000000..03f61de81 --- /dev/null +++ b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/authGrantFinderTests.js @@ -0,0 +1,180 @@ +/*! + + Copyright 2016-2017 OCAD university + Copyright 2019 Raising the Floor International + Copyright 2019 OCAD University + + Licensed under the New BSD license. You may not use this file except in + compliance with this License. + + The research leading to these results has received funding from the European Union's + Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + + You may obtain a copy of the License at + https://github.com/GPII/universal/blob/master/LICENSE.txt + +*/ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +require("node-jqunit"); + +fluid.require("%gpii-universal"); +gpii.loadTestingSupport(); + +require("./authTestFixtures"); + +fluid.defaults("gpii.tests.oauth2.authorizationService", { + gradeNames: ["gpii.oauth2.authorizationService"], + invokers: { + getCurrentDate: { + funcName: "fluid.identity", + args: [new Date("2019-01-17T07:01:00.001Z")] + } + }, + components: { + codeGenerator: { + type: "fluid.emptySubcomponent" + } + } +}); + +fluid.defaults("gpii.tests.oauth2.authGrantFinder", { + gradeNames: ["gpii.oauth2.authGrantFinder"], + components: { + authorizationService: { + type: "gpii.tests.oauth2.authorizationService" + } + } +}); + +fluid.defaults("gpii.tests.oauth2.authGrantFinder.caseHolder", { + gradeNames: ["gpii.tests.oauth2.caseHolder"], + dataSourceConfig: { + baseUrl: "http://localhost", + port: 25984, + dbName: "gpii" + }, + distributeOptions: [ + { + record: "gpii.dbOperation.dbDataStore", + target: "{that dataStore}.type" + }, + { + source: "{that}.options.dataSourceConfig", + target: "{that dataStore}.options.dataSourceConfig" + } + ], + components: { + authGrantFinder: { + type: "gpii.tests.oauth2.authGrantFinder", + options: { + gradeNames: ["gpii.tests.dbOperation.dbDataStore.base"] + } + } + } +}); + +fluid.defaults("gpii.tests.oauth2.authGrantFinder.testEnvironment", { + gradeNames: ["gpii.tests.oauth2.baseEnvironment"], + databases: { + gpii: { + data: [ + "%gpii-universal/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/data/authGrantFinderTests-data.json", + "%gpii-universal/testData/dbData/views.json" + ] + } + }, + components: { + caseHolder: { + type: "gpii.tests.oauth2.authGrantFinder.caseHolder" + } + } +}); + +// All expected results +gpii.tests.oauth2.authGrantFinder.expected = { + regular: { + accessToken: "Bakersfield_AJC_access_token", + gpiiKey: "carol_gpii_key", + allowSettingsGet: true, + allowSettingsPut: true, + allowedPrefsToWrite: [] + }, + backwardsCompatibility: { + accessToken: "schemaV0.1_access_token", + gpiiKey: "carol_gpii_key", + allowSettingsGet: true, + allowSettingsPut: true, + allowedPrefsToWrite: null + } +}; + +fluid.defaults("gpii.tests.oauth2.authGrantFinder.cases", { + gradeNames: ["gpii.tests.oauth2.authGrantFinder.testEnvironment"], + components: { + caseHolder: { + options: { + modules: [ + { + name: "Test getGrantForAccessToken() with an access token for resource owner GPII key grant type", + tests: [{ + name: "getGrantForAccessToken() returns the authorization info in the format for the resource owner GPII key grant type", + sequenceGrade: "gpii.tests.oauth2.sequenceGrade", + sequence: [{ + task: "{authGrantFinder}.getGrantForAccessToken", + args: ["Bakersfield_AJC_access_token"], + resolve: "jqUnit.assertDeepEq", + resolveArgs: ["The expected authorization info is returned", gpii.tests.oauth2.authGrantFinder.expected.regular, "{arguments}.0"] + }] + }] + }, + { + name: "Test getGrantForAccessToken() accommodates the backwards compatibility for the data model in the schema version 0.1", + tests: [{ + name: "getGrantForAccessToken() returns the authorization info in the format for the resource owner GPII key grant type", + sequenceGrade: "gpii.tests.oauth2.sequenceGrade", + sequence: [{ + task: "{authGrantFinder}.getGrantForAccessToken", + args: ["schemaV0.1_access_token"], + resolve: "jqUnit.assertDeepEq", + resolveArgs: ["The expected authorization info is returned", gpii.tests.oauth2.authGrantFinder.expected.backwardsCompatibility, "{arguments}.0"] + }] + }] + }, + { + name: "Test getGrantForAccessToken() returns undefined for an expired access token", + tests: [{ + name: "getGrantForAccessToken() returns undefined for an expired access token", + sequenceGrade: "gpii.tests.oauth2.sequenceGrade", + sequence: [{ + task: "{authGrantFinder}.getGrantForAccessToken", + args: ["Bakersfield_AJC_access_token_expired"], + resolve: "jqUnit.assertUndefined", + resolveArgs: ["The expected authorization info is returned", "{arguments}.0"] + }] + }] + }, + { + name: "Test getGrantForAccessToken()", + tests: [{ + name: "getGrantForAccessToken() returns undefined for an unknown access token", + sequenceGrade: "gpii.tests.oauth2.sequenceGrade", + sequence: [{ + task: "{authGrantFinder}.getGrantForAccessToken", + args: ["unknown"], + resolve: "jqUnit.assertUndefined", + resolveArgs: ["undefined should be received for an unknown access token", "{arguments}.0"] + }] + }] + } + ] + } + } + } +}); + +fluid.test.runTests([ + "gpii.tests.oauth2.authGrantFinder.cases" +]); diff --git a/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/authTestFixtures.js b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/authTestFixtures.js new file mode 100644 index 000000000..d66948730 --- /dev/null +++ b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/authTestFixtures.js @@ -0,0 +1,53 @@ +/* + + Test fixtures common to the auth tests. + + */ +"use strict"; +var fluid = require("infusion"); + +fluid.registerNamespace("gpii.tests.oauth2"); + + +fluid.defaults("gpii.tests.oauth2.caseHolder", { + gradeNames: ["fluid.test.testCaseHolder"], + events: { + createService: null + } +}); + +fluid.defaults("gpii.tests.oauth2.createService", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [{ + func: "{caseHolder}.events.createService.fire" + }] +}); + +fluid.defaults("gpii.tests.oauth2.sequenceGrade", { + gradeNames: ["fluid.test.sequence"], + sequenceElements: { + startCouch: { + gradeNames: "gpii.test.startCouchSequence", + priority: "before:sequence" + }, + createPreferencesService: { + gradeNames: "gpii.tests.oauth2.createService", + priority: "after:startCouch" + }, + stopCouch: { + gradeNames: "gpii.test.stopCouchSequence", + priority: "after:sequence" + } + } +}); + + +// We use the same base grade as the main harness, but avoid merging with that to avoid picking up the wrong test data. +fluid.defaults("gpii.tests.oauth2.baseEnvironment", { + gradeNames: ["gpii.test.couchdb.environment.base"], + components: { + caseHolder: { + type: "gpii.tests.oauth2.caseHolder" + } + } +}); diff --git a/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/authorizationServiceTests.js b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/authorizationServiceTests.js new file mode 100644 index 000000000..0ea714337 --- /dev/null +++ b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/authorizationServiceTests.js @@ -0,0 +1,264 @@ +/*! + + Copyright 2015-2017 OCAD university + Copyright 2019 Raising the Floor International + + Licensed under the New BSD license. You may not use this file except in + compliance with this License. + + The research leading to these results has received funding from the European Union's + Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + + You may obtain a copy of the License at + https://github.com/GPII/universal/blob/master/LICENSE.txt + +*/ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +require("node-jqunit"); + + +require("./authTestFixtures"); + +fluid.require("%gpii-universal"); +gpii.loadTestingSupport(); + +// The mock codeGenerator for testing +fluid.defaults("gpii.tests.oauth2.mockCodeGenerator", { + gradeNames: ["fluid.component"], + invokers: { + generateAccessToken: "gpii.tests.oauth2.mockCodeGenerator.generateAccessToken" + } +}); + +gpii.tests.oauth2.mockCodeGenerator.generateAccessToken = function () { + return "test-access-token"; +}; + +fluid.defaults("gpii.tests.oauth2.authorizationService.caseHolder", { + gradeNames: ["gpii.tests.oauth2.caseHolder"], + dataSourceConfig: { + baseUrl: "http://localhost", + port: 25984, + dbName: "gpii" + }, + distributeOptions: [{ + source: "{that}.options.dataSourceConfig", + target: "{that dataStore}.options.dataSourceConfig" + }], + components: { + authorizationService: { + type: "gpii.oauth2.authorizationService", + //createOnEvent: "onFixturesConstructed", + options: { + gradeNames: ["gpii.tests.dbOperation.dbDataStore.base"], + components: { + dataStore: { + type: "gpii.dbOperation.dbDataStore" + }, + codeGenerator: { + type: "gpii.tests.oauth2.mockCodeGenerator" + } + } + } + } + } +}); + + +// The base test environment without any data +fluid.defaults("gpii.tests.oauth2.authorizationService.testEnvironment", { + gradeNames: ["gpii.tests.oauth2.baseEnvironment"], + databases: { + gpii: { + data: [ + "%gpii-universal/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/data/authorizationServiceTests-data.json", + "%gpii-universal/testData/dbData/views.json" + ] + } + }, + components: { + caseHolder: { + type: "gpii.tests.oauth2.authorizationService.caseHolder" + } + } +}); + +// All expected results +gpii.tests.oauth2.authorizationService.expected = { + success: { + accessToken: "test-access-token", + expiresIn: 3600 + }, + unauthorized: { + message: "Unauthorized", + statusCode: 401, + isError: true + }, + missingInput: { + message: "The input field \"GPII key, client ID or client credential ID\" was undefined", + statusCode: 400, + isError: true + } +}; + +fluid.defaults("gpii.tests.oauth2.authorizationService.withData.grantGpiiAppInstallationAuthorization", { + gradeNames: ["gpii.tests.oauth2.authorizationService.testEnvironment"], + components: { + caseHolder: { + options: { + modules: [{ + name: "Test grantGpiiAppInstallationAuthorization()", + tests: [ + { + name: "grantGpiiAppInstallationAuthorization() returns an access token", + sequenceGrade: "gpii.tests.oauth2.sequenceGrade", + sequence: [{ + task: "{authorizationService}.grantGpiiAppInstallationAuthorization", + args: ["alice_gpii_key", "gpiiAppInstallationClient-1", "clientCredential-1"], + resolve: "jqUnit.assertDeepEq", + resolveArgs: [ + "The access token should be received in an expected format", + gpii.tests.oauth2.authorizationService.expected.success, + "{arguments}.0" + ] + }] + }, + { + name: "grantGpiiAppInstallationAuthorization() returns an access token when the gpii key record is not found in the database", + sequenceGrade: "gpii.tests.oauth2.sequenceGrade", + sequence: [{ + task: "{authorizationService}.grantGpiiAppInstallationAuthorization", + args: ["non-existent-gpii-key", "gpiiAppInstallationClient-1", "clientCredential-1"], + resolve: "jqUnit.assertDeepEq", + resolveArgs: [ + "The access token should be received in an expected format", + gpii.tests.oauth2.authorizationService.expected.success, + "{arguments}.0" + ] + }] + }, + { + name: "grantGpiiAppInstallationAuthorization() returns error when a gpii key is not provided in the argument list", + sequenceGrade: "gpii.tests.oauth2.sequenceGrade", + sequence: [{ + task: "{authorizationService}.grantGpiiAppInstallationAuthorization", + args: [undefined, "gpiiAppInstallationClient-1", "clientCredential-1"], + reject: "jqUnit.assertDeepEq", + rejectArgs: [ + "The error is returned when a gpii key is missing", + gpii.tests.oauth2.authorizationService.expected.missingInput, + "{arguments}.0" + ] + }] + }, + { + name: "grantGpiiAppInstallationAuthorization() returns error when a client id is not provided in the argument list", + sequenceGrade: "gpii.tests.oauth2.sequenceGrade", + sequence: [{ + task: "{authorizationService}.grantGpiiAppInstallationAuthorization", + args: ["alice_gpii_key", undefined, "clientCredential-1"], + reject: "jqUnit.assertDeepEq", + rejectArgs: [ + "The error is returned when a client id is missing", + gpii.tests.oauth2.authorizationService.expected.missingInput, + "{arguments}.0" + ] + }] + }, + { + name: "grantGpiiAppInstallationAuthorization() returns error when a client credential id is not provided in the argument list", + sequenceGrade: "gpii.tests.oauth2.sequenceGrade", + sequence: [{ + task: "{authorizationService}.grantGpiiAppInstallationAuthorization", + args: ["alice_gpii_key", "gpiiAppInstallationClient-1", undefined], + reject: "jqUnit.assertDeepEq", + rejectArgs: [ + "The error is returned when a client credential id is missing", + gpii.tests.oauth2.authorizationService.expected.missingInput, + "{arguments}.0" + ] + }] + }, + { + name: "grantGpiiAppInstallationAuthorization() returns error when the client record is not found in the database", + sequenceGrade: "gpii.tests.oauth2.sequenceGrade", + sequence: [{ + task: "{authorizationService}.grantGpiiAppInstallationAuthorization", + args: ["alice_gpii_key", "non-existent-client-id", "clientCredential-1"], + reject: "jqUnit.assertDeepEq", + rejectArgs: [ + "The error is returned when the client record is not found in the database", + gpii.tests.oauth2.authorizationService.expected.unauthorized, + "{arguments}.0" + ] + }] + }, + { + name: "grantGpiiAppInstallationAuthorization() returns error when the client credential record is not found in the database", + sequenceGrade: "gpii.tests.oauth2.sequenceGrade", + sequence: [{ + task: "{authorizationService}.grantGpiiAppInstallationAuthorization", + args: ["alice_gpii_key", "gpiiAppInstallationClient-1", "non-existent-clientCredential-id"], + reject: "jqUnit.assertDeepEq", + rejectArgs: [ + "The error is returned when the client credential record is not found in the database", + gpii.tests.oauth2.authorizationService.expected.unauthorized, + "{arguments}.0" + ] + }] + }, + { + name: "grantGpiiAppInstallationAuthorization() returns error when the client type is not \"gpiiAppInstallationClient\"", + sequenceGrade: "gpii.tests.oauth2.sequenceGrade", + sequence: [{ + task: "{authorizationService}.grantGpiiAppInstallationAuthorization", + args: ["alice_gpii_key", "gpiiAppInstallationClient-2", "clientCredential-1"], + reject: "jqUnit.assertDeepEq", + rejectArgs: [ + "The error is returned when the client type is not \"gpiiAppInstallationClient\"", + gpii.tests.oauth2.authorizationService.expected.unauthorized, + "{arguments}.0" + ] + }] + }, + { + name: "grantGpiiAppInstallationAuthorization() returns error when the client credential type is not \"clientCredential\"", + sequenceGrade: "gpii.tests.oauth2.sequenceGrade", + sequence: [{ + task: "{authorizationService}.grantGpiiAppInstallationAuthorization", + args: ["alice_gpii_key", "gpiiAppInstallationClient-1", "clientCredential-3"], + reject: "jqUnit.assertDeepEq", + rejectArgs: [ + "The error is returned when the client credential type is not \"clientCredential\"", + gpii.tests.oauth2.authorizationService.expected.unauthorized, + "{arguments}.0" + ] + }] + }, + { + name: "grantGpiiAppInstallationAuthorization() returns error when the client credential does not belong to the client that requests for the authorization", + sequenceGrade: "gpii.tests.oauth2.sequenceGrade", + sequence: [{ + task: "{authorizationService}.grantGpiiAppInstallationAuthorization", + args: ["alice_gpii_key", "gpiiAppInstallationClient-1", "clientCredential-2"], + reject: "jqUnit.assertDeepEq", + rejectArgs: [ + "The error is returned when the client credential does not belong to the client that requests for the authorization", + gpii.tests.oauth2.authorizationService.expected.unauthorized, + "{arguments}.0" + ] + }] + } + ] + }] + } + } + } + +}); +fluid.test.runTests([ + "gpii.tests.oauth2.authorizationService.withData.grantGpiiAppInstallationAuthorization" +]); diff --git a/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/data/authGrantFinderTests-data.json b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/data/authGrantFinderTests-data.json new file mode 100644 index 000000000..174cd9ffc --- /dev/null +++ b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/data/authGrantFinderTests-data.json @@ -0,0 +1,86 @@ +[ + { + "_id": "gpiiKey-1", + "type": "gpiiKey", + "gpiiKey": "carol_gpii_key" + }, + { + "_id": "client-1", + "type": "gpiiAppInstallationClient", + "name": "Bakersfield AJC - PC1", + "computerType": "public", + "timestampCreated": "2019-05-28T18:11:22.101Z", + "timestampRevoked": null + }, + { + "_id": "client-schemaV0.1", + "type": "gpiiAppInstallationClient", + "name": "Client for testing the backwards compatibility for data model schema 0.1", + "computerType": "public", + "timestampCreated": "2019-05-28T18:11:22.101Z", + "timestampRevoked": null + }, + { + "_id": "clientCredential-1", + "type": "clientCredential", + "clientId": "client-1", + "oauth2ClientId": "nova-computer", + "oauth2ClientSecret": "nova-computer-secret", + "allowedIPBlocks": [ + "::1/128" + ], + "allowedPrefsToWrite": [], + "isCreateGpiiKeyAllowed": true, + "isCreatePrefsSafeAllowed": true, + "revoked": false, + "revokedReason": null, + "timestampCreated": "2019-05-28T18:11:22.101Z", + "timestampRevoked": null + }, + { + "_id": "clientCredential-schemaV0.1", + "type": "clientCredential", + "clientId": "client-schemaV0.1", + "oauth2ClientId": "schemaV0.1", + "oauth2ClientSecret": "schemaV0.1-secret", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2019-05-28T18:11:22.101Z", + "timestampRevoked": null + }, + { + "_id": "gpiiAppInstallationAuthorization-1", + "type": "gpiiAppInstallationAuthorization", + "clientId": "client-1", + "gpiiKey": "carol_gpii_key", + "clientCredentialId": "clientCredential-1", + "accessToken": "Bakersfield_AJC_access_token", + "revoked": false, + "timestampCreated": "2019-01-17T07:00:00.001Z", + "timestampRevoked": null, + "timestampExpires": "2019-01-17T07:01:40.001Z" + }, + { + "_id": "gpiiAppInstallationAuthorization-schemaV0.1", + "type": "gpiiAppInstallationAuthorization", + "clientId": "client-schemaV0.1", + "gpiiKey": "carol_gpii_key", + "clientCredentialId": "clientCredential-schemaV0.1", + "accessToken": "schemaV0.1_access_token", + "revoked": false, + "timestampCreated": "2019-01-17T07:00:00.001Z", + "timestampRevoked": null, + "timestampExpires": "2019-01-17T07:01:40.001Z" + }, + { + "_id": "gpiiAppInstallationAuthorization-expired", + "type": "gpiiAppInstallationAuthorization", + "clientId": "client-1", + "gpiiKey": "carol_gpii_key", + "accessToken": "Bakersfield_AJC_access_token_expired", + "revoked": false, + "timestampCreated": "2019-01-17T07:00:00.001Z", + "timestampRevoked": null, + "timestampExpires": "2019-01-17T07:00:40.001Z" + } +] diff --git a/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/data/authorizationServiceTests-data.json b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/data/authorizationServiceTests-data.json new file mode 100644 index 000000000..476d31065 --- /dev/null +++ b/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/data/authorizationServiceTests-data.json @@ -0,0 +1,67 @@ +[ + { + "_id": "alice_gpii_key", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": "prefsSafe-1", + "prefsSetId": "gpii-default", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + }, + { + "_id": "gpiiAppInstallationClient-1", + "type": "gpiiAppInstallationClient", + "schemaVersion": "0.2", + "name": "AJC1", + "computerType": "public", + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + }, + { + "_id": "clientCredential-1", + "type": "clientCredential", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-1", + "oauth2ClientId": "client_id_AJC1", + "oauth2ClientSecret": "client_secret_AJC1", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampRevoked": null + }, + { + "_id": "gpiiAppInstallationClient-2", + "type": "unknownClient", + "schemaVersion": "0.2", + "name": "test", + "computerType": "public", + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + }, + { + "_id": "clientCredential-2", + "type": "clientCredential", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-2", + "oauth2ClientId": "client_id_test", + "oauth2ClientSecret": "client_secret_test", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampRevoked": null + }, + { + "_id": "clientCredential-3", + "type": "unknownClientCredential", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-2", + "oauth2ClientId": "client_id_test-3", + "oauth2ClientSecret": "client_secret_test-3", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-22T18:11:22.101Z", + "timestampRevoked": null + } +] diff --git a/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/index.js b/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/index.js new file mode 100644 index 000000000..f462194ba --- /dev/null +++ b/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/index.js @@ -0,0 +1,5 @@ +"use strict"; + +require("./src/OAuth2Const.js"); +require("./src/OAuth2Express.js"); +require("./src/OAuth2Utilities.js"); diff --git a/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Const.js b/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Const.js new file mode 100644 index 000000000..276cfb2f3 --- /dev/null +++ b/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Const.js @@ -0,0 +1,21 @@ +/*! +Copyright 2017 OCAD university + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ +"use strict"; + +var fluid = fluid || require("infusion"); + +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.oauth2"); + +// The default value of the number of seconds that access tokens become invalid. +gpii.oauth2.defaultTokenLifeTimeInSeconds = 3600; diff --git a/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Express.js b/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Express.js new file mode 100644 index 000000000..7faa838cb --- /dev/null +++ b/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Express.js @@ -0,0 +1,28 @@ +/*! +Copyright 2014 OCAD university + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ +"use strict"; + +var express = require("express"); + +var fluid = require("infusion"); + +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.oauth2"); + +gpii.oauth2.createExpressApp = function () { + return express(); +}; + +gpii.oauth2.expressStatic = function (root) { + return express["static"](root); +}; diff --git a/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Utilities.js b/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Utilities.js new file mode 100644 index 000000000..51a1f0fb4 --- /dev/null +++ b/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/src/OAuth2Utilities.js @@ -0,0 +1,125 @@ +/*! +Copyright 2014-2017 OCAD university + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ +"use strict"; + +var fluid = fluid || require("infusion"); + +var gpii = fluid.registerNamespace("gpii"); +var ipaddr = require("ipaddr.js"); + +fluid.registerNamespace("gpii.oauth2"); + +gpii.oauth2.parseAccessTokenFromRequest = function (req) { + if (req.headers && req.headers.authorization) { + var parts = req.headers.authorization.split(/\s+/); + if (parts.length === 2 && parts[0].toLowerCase() === "bearer") { + return parts[1]; + } + } + return undefined; +}; + +gpii.oauth2.getAuthorization = function (accessToken, authGrantFinder) { + var promiseTogo = fluid.promise(); + + if (!accessToken) { + promiseTogo.reject(gpii.dbOperation.errors.unauthorized); + } else { + promiseTogo = authGrantFinder.getGrantForAccessToken(accessToken); + } + return promiseTogo; +}; + +gpii.oauth2.walkMiddleware = function (middleware, i, req, res, next) { + // TODO best way to check if middleware is a single function? + if (typeof middleware === "function") { + return middleware(req, res, next); + } + if (i >= middleware.length) { + return next(); + } else { + return middleware[i](req, res, function () { + return gpii.oauth2.walkMiddleware(middleware, i + 1, req, res, next); + }); + } +}; + +gpii.oauth2.mapPromiseToResponse = function (promise, response) { + promise.then(function () { + response.sendStatus(200); + }, function (err) { + response.sendStatus(err.statusCode); + }); +}; + +/** + * Calculate the timestamp of currentTime + expiresIn. + * @param {Date} timestampStarts - A start timestamp in the format returned by Date(). + * @param {Number} expiresIn - The number of seconds that the expiration will occur. + * @return {String} A date in simpilified ISO string format. + */ +gpii.oauth2.getTimestampExpires = function (timestampStarts, expiresIn) { + if (!timestampStarts) { + return undefined; + } + return new Date(timestampStarts.getTime() + expiresIn * 1000).toISOString(); +}; + +/** + * Compare the current time with the expiresIn time and return the number of seconds that the expiration will occur. + * @param {Date} timestampStarts - A start timestamp in the format returned by Date(). + * @param {String} timestampExpires - A string in the format returned by Date().toISOString(). + * @return {Number} The number of seconds that the expiration will occur. Return 0 if the given timestampExpires < the current timestamp. + */ +gpii.oauth2.getExpiresIn = function (timestampStarts, timestampExpires) { + if (!timestampStarts || !timestampExpires) { + return undefined; + } + + var startsTimeInMsec = timestampStarts.getTime(); + var expiresTimeInMsec = new Date(timestampExpires).getTime(); + return expiresTimeInMsec > startsTimeInMsec ? Math.round((expiresTimeInMsec - startsTimeInMsec) / 1000) : 0; +}; + +/** + * Verify if the given IP address is in the allowed IP blocks. + * @param {String} ipAddress - An IP to verify. + * @param {Array} allowedIPBlocks - An array of allowed IP blocks. + * @return {Boolean} Return true if the IP is within the range. Otherwise, return false. + */ +gpii.oauth2.isIPINRange = function (ipAddress, allowedIPBlocks) { + allowedIPBlocks = fluid.makeArray(allowedIPBlocks); + var addr = ipaddr.process(ipAddress); + + for (var i in allowedIPBlocks) { + var oneIPBlock = allowedIPBlocks[i]; + if (oneIPBlock.indexOf("/") > 0) { + // The IP block specifies a range of IP addresses such as "192.168.1.0/24"(IPV4) or "::1/128"(IPV6) + try { + if (addr.match(ipaddr.parseCIDR(oneIPBlock))) { + return true; + } + } catch (e) { + // Catch the error when kinds of ipAddress and oneIPBlock don't match: one is in IPV4 format while + // the other is in IPV6 format. Ignore this error and continue with verifying other IP blocks. + } + } else { + // The IP block specifies a single IP address such as "192.168.1.0"(IPV4) or "::1"(IPV6) + var range = ipaddr.parse(oneIPBlock); + if (addr.toString() === range.toString()) { + return true; + } + } + }; + return false; +}; diff --git a/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/test/OAuth2UtilitiesTests.js b/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/test/OAuth2UtilitiesTests.js new file mode 100644 index 000000000..4c70de49e --- /dev/null +++ b/gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/test/OAuth2UtilitiesTests.js @@ -0,0 +1,225 @@ +/*! +Copyright 2014-2019 OCAD university + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); +var jqUnit = fluid.require("node-jqunit"); + +fluid.registerNamespace("gpii.tests.oauth2"); +fluid.registerNamespace("gpii.tests.oauth2.testdata"); + +require("../src/OAuth2Utilities.js"); + +jqUnit.module("GPII OAuth2 Utilities"); + +gpii.tests.oauth2.makeRequestWithAuthorizationHeader = function (authHeaderValue) { + return { + headers: { + authorization: authHeaderValue + } + }; +}; + +gpii.tests.oauth2.testdata.badlyFormedAuthHeaderRequests = [ + gpii.tests.oauth2.makeRequestWithAuthorizationHeader(""), + gpii.tests.oauth2.makeRequestWithAuthorizationHeader("Bearer"), + gpii.tests.oauth2.makeRequestWithAuthorizationHeader("BearerAAA"), + gpii.tests.oauth2.makeRequestWithAuthorizationHeader("Bearer AAA BBB") +]; + +gpii.tests.oauth2.testdata.goodAuthHeaderRequests = [ + { + req: gpii.tests.oauth2.makeRequestWithAuthorizationHeader("Bearer AAA"), + expected: "AAA" + }, + { + req: gpii.tests.oauth2.makeRequestWithAuthorizationHeader("bearer BBB"), + expected: "BBB" + }, + { + req: gpii.tests.oauth2.makeRequestWithAuthorizationHeader("Bearer CCC"), + expected: "CCC" + } +]; + +gpii.tests.oauth2.makeTestMiddleware = function (called, key) { + return function (req, res, next) { + jqUnit.assertEquals("request", "request", req); + jqUnit.assertEquals("response", "response", res); + called[key] = true; + next(); + }; +}; + + +jqUnit.test("parseAccessTokenFromRequest() returns undefined if no Authorization header", function () { + jqUnit.assertUndefined("undefined for empty req", + gpii.oauth2.parseAccessTokenFromRequest({})); + jqUnit.assertUndefined("undefined for empty headers", + gpii.oauth2.parseAccessTokenFromRequest({ headers: {} })); +}); + +jqUnit.test("parseAccessTokenFromRequest() returns undefined for badly formed Authorization header", function () { + jqUnit.expect(4); + gpii.tests.oauth2.testdata.badlyFormedAuthHeaderRequests.forEach(function (req) { + jqUnit.assertUndefined("expect undefined", gpii.oauth2.parseAccessTokenFromRequest(req)); + }); +}); + +jqUnit.test("parseAccessTokenFromRequest() returns access token from Authorization header", function () { + jqUnit.expect(3); + gpii.tests.oauth2.testdata.goodAuthHeaderRequests.forEach(function (pair) { + jqUnit.assertEquals(pair.expected, pair.expected, gpii.oauth2.parseAccessTokenFromRequest(pair.req)); + }); +}); + +jqUnit.asyncTest("Walk an empty array of middleware", function () { + jqUnit.expect(0); + gpii.oauth2.walkMiddleware([], 0, "request", "response", function () { jqUnit.start(); }); +}); + +jqUnit.asyncTest("Walk an array of 1 middleware", function () { + jqUnit.expect(3); + var called = {}; + var middleware1 = gpii.tests.oauth2.makeTestMiddleware(called, "middleware1"); + var check = function () { + jqUnit.assertTrue("middleware1 called", called.middleware1); + jqUnit.start(); + }; + gpii.oauth2.walkMiddleware([middleware1], 0, "request", "response", check); +}); + +jqUnit.asyncTest("Walk an array of 2 middleware", function () { + jqUnit.expect(6); + var called = {}; + var middleware1 = gpii.tests.oauth2.makeTestMiddleware(called, "middleware1"); + var middleware2 = gpii.tests.oauth2.makeTestMiddleware(called, "middleware2"); + var check = function () { + jqUnit.assertTrue("middleware1 called", called.middleware1); + jqUnit.assertTrue("middleware2 called", called.middleware2); + jqUnit.start(); + }; + gpii.oauth2.walkMiddleware([middleware1, middleware2], 0, "request", "response", check); +}); + +jqUnit.asyncTest("Walk a single middleware function", function () { + jqUnit.expect(3); + var called = {}; + var middleware1 = gpii.tests.oauth2.makeTestMiddleware(called, "middleware1"); + var check = function () { + jqUnit.assertTrue("middleware1 called", called.middleware1); + jqUnit.start(); + }; + gpii.oauth2.walkMiddleware(middleware1, 0, "request", "response", check); +}); + +jqUnit.test("Test gpii.oauth2.getTimestampExpires()", function () { + jqUnit.expect(3); + + var testCases = [{ + timestampStarts: new Date("2017-09-22"), + expiresIn: 60, + expected: "2017-09-22T00:01:00.000Z" + }, { + timestampStarts: null, + expiresIn: 60, + expected: undefined + }, { + timestampStarts: undefined, + expiresIn: 60, + expected: undefined + }]; + + fluid.each(testCases, function (testCase) { + var result = gpii.oauth2.getTimestampExpires(testCase.timestampStarts, testCase.expiresIn); + var msg = "The calculated timestampExpires for the start timestamp \"" + testCase.timestampExpires + "\" expiring in " + testCase.expiresIn + " seconds is: " + testCase.expected; + + jqUnit.assertEquals(msg, testCase.expected, result); + }); +}); + +jqUnit.test("Test gpii.oauth2.getExpiresIn()", function () { + jqUnit.expect(6); + + var testCases = [{ + timestampStarts: new Date("2017-09-22"), + timestampExpires: "2017-05-29T17:54:00.000Z", + expected: 0 + }, { + timestampStarts: null, + timestampExpires: new Date("2017-09-22"), + expected: undefined + }, { + timestampStarts: new Date("2017-09-22"), + timestampExpires: null, + expected: undefined + }, { + timestampStarts: undefined, + timestampExpires: new Date("2017-09-22"), + expected: undefined + }, { + timestampStarts: new Date("2017-09-22"), + timestampExpires: undefined, + expected: undefined + }, { + timestampStarts: new Date("2017-09-22"), + timestampExpires: "2017-09-22T00:01:00.000Z", + expected: 60 + }]; + + fluid.each(testCases, function (testCase) { + var result = gpii.oauth2.getExpiresIn(testCase.timestampStarts, testCase.timestampExpires); + var msg = "The timestamp \"" + testCase.timestampExpires + "\" will expire in " + testCase.expected + " seconds"; + + jqUnit.assertEquals(msg, testCase.expected, result); + }); +}); + +jqUnit.test("Test gpii.oauth2.isIPINRange()", function () { + jqUnit.expect(6); + + var testCases = [{ + ipAddress: "192.168.1.1", + allowedIPBlocks: "192.168.1.1", + expected: true + }, { + ipAddress: "192.168.1.1", + allowedIPBlocks: "192.168.1.0/24", + expected: true + }, { + ipAddress: "192.168.1.1", + allowedIPBlocks: "102.1.5.2/24", + expected: false + }, { + ipAddress: "::ffff:127.0.0.1", + allowedIPBlocks: ["127.0.0.1/8", "172.0.0.0/8", "::1"], + expected: true + }, { + // Checks array of CIDR's and string + ipAddress: "192.168.1.1", + allowedIPBlocks: ["102.1.5.2/24", "192.168.1.0/24", "106.1.180.84"], + expected: true + }, { + // Compare IPv6 with IPv4 + ipAddress: "195.58.1.62", + allowedIPBlocks: ["::1/128", "125.92.12.53"], + expected: false + }]; + + fluid.each(testCases, function (testCase) { + var result = gpii.oauth2.isIPINRange(testCase.ipAddress, testCase.allowedIPBlocks); + jqUnit[testCase.expected ? "assertTrue" : "assertFalse"]("The IP verification result is expected", testCase.expected, result); + }); +}); diff --git a/gpii/node_modules/gpii-oauth2/index.js b/gpii/node_modules/gpii-oauth2/index.js new file mode 100644 index 000000000..be19fd693 --- /dev/null +++ b/gpii/node_modules/gpii-oauth2/index.js @@ -0,0 +1,9 @@ +"use strict"; + +var fluid = require("infusion"); + +fluid.module.register("gpii-oauth2", __dirname, require); + +require("gpii-db-operation"); +require("./gpii-oauth2-utilities"); +require("./gpii-oauth2-authz-server"); diff --git a/gpii/node_modules/gpii-user-errors/bundles/gpii-userErrors-messageBundle_en.json5 b/gpii/node_modules/gpii-user-errors/bundles/gpii-userErrors-messageBundle_en.json5 new file mode 100644 index 000000000..96bb124f8 --- /dev/null +++ b/gpii/node_modules/gpii-user-errors/bundles/gpii-userErrors-messageBundle_en.json5 @@ -0,0 +1,29 @@ +{ + "gpii_userErrors_EADDRINUSE-title": "Morphic can't start", + "gpii_userErrors_EADDRINUSE-subhead": "There is another application listening on the same port", + "gpii_userErrors_EADDRINUSE-details": "Stop the other running application and try again. If the problem is still present, contact Morphic Technical Support.", + "gpii_userErrors_KeyInFail-title": "Oops.", + "gpii_userErrors_KeyInFail-subhead": "Something went wrong.", + "gpii_userErrors_KeyInFail-details": "Try restarting your computer. If this problem continues, contact Morphic Technical Support.", + "gpii_userErrors_NoConnection-title": "No Internet connection", + "gpii_userErrors_NoConnection-subhead": "Morphic cannot see the cloud to get your settings.", + "gpii_userErrors_NoConnection-details": "Check your internet connection and try again.", + "gpii_userErrors_WriteSettingFail-title": "A setting was not changed", + "gpii_userErrors_WriteSettingFail-subhead": "Morphic couldn't change one of the settings you asked for.", + "gpii_userErrors_WriteSettingFail-details": "You can try again or make the change manually. If the problems continues, try restarting the program or your computer.\ +The setting that Morphic could not change is: %settingName in %appName", + // Note that the "ReadSettingFail" error is currently not fired + "gpii_userErrors_ReadSettingFail-title": "Couldn't read setting", + "gpii_userErrors_ReadSettingFail-subhead": "Morphic couldn't read one of the settings that you asked for.", + "gpii_userErrors_ReadSettingFail-details": "If the problem continues, try restarting the program or your computer.\ +The setting that Morphic could not read is: %settingName of application %appName", + "gpii_userErrors_StartApplicationFail-title": "Morphic can't start %appName", + "gpii_userErrors_StartApplicationFail-subhead": "To turn on the setting you requested, Morphic needs to start %appName.", + "gpii_userErrors_StartApplicationFail-details": "You can start the program yourself. If this problem continues, try restarting your computer.", + "gpii_userErrors_StopApplicationFail-title": "Morphic can't start %appName", + "gpii_userErrors_StopApplicationFail-subhead": "Morphic needs to stop %appName to turn off a setting.", + "gpii_userErrors_StopApplicationFail-details": "You can stop the program yourself. If this problem continues, try restarting your computer.", + "gpii_userErrors_SaveToCloudFail-title": "Your settings were not saved", + "gpii_userErrors_SaveToCloudFail-subhead": "Your settings have not been saved to your online account and are not available to use on other computers.", + "gpii_userErrors_SaveToCloudFail-details": "Check your internet connection and your Morphic account password and try again." +} diff --git a/gpii/node_modules/gpii-user-errors/index.js b/gpii/node_modules/gpii-user-errors/index.js new file mode 100644 index 000000000..06df19960 --- /dev/null +++ b/gpii/node_modules/gpii-user-errors/index.js @@ -0,0 +1,25 @@ +/* + * User Errors + * + * Copyright 2018 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"); + +fluid.module.register("gpii-user-errors", __dirname, require); + +require("./src/UserErrors.js"); diff --git a/gpii/node_modules/gpii-user-errors/package.json b/gpii/node_modules/gpii-user-errors/package.json new file mode 100644 index 000000000..7fc0f87a2 --- /dev/null +++ b/gpii/node_modules/gpii-user-errors/package.json @@ -0,0 +1,13 @@ +{ + "name": "gpii-userErrors", + "description": "User errors", + "version": "0.3.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } +} diff --git a/gpii/node_modules/gpii-user-errors/src/UserErrors.js b/gpii/node_modules/gpii-user-errors/src/UserErrors.js new file mode 100644 index 000000000..047144f45 --- /dev/null +++ b/gpii/node_modules/gpii-user-errors/src/UserErrors.js @@ -0,0 +1,54 @@ +/* + * GPII User Errors + * + * Copyright 2018 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = fluid || require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.userErrors"); + +/** The channel for receiving and transmitting errors from the core GPII architecture which are of interest to end + * users and which could/should be surfaced at the UI level. Any component wishing to signal such an error should + * fire the `userError` event held here, with an Error payload including (at least) fields: + * {Boolean} isError - Holding the value `true` identifying the payload as an error + * {String} messageKey - A key for the message displayed to the user, which is indirected into the message bundle + * held in ../bundles/gpii-userErrors-messageBundle_xx.json5 by the following process: + * i) The prefix gpii-userErrors_ is added + * ii) The three suffices -title, -subhead and -details each lead to descriptions of the error at successive layers + * of detail. + * {Object} [originalError] - [optional] Any upstream error object that led to this error being fired. + * The current common architecture document describing the available errors and their purposes is held at + * [GPII Error Messages Spreadsheet](https://docs.google.com/spreadsheets/d/1ybyf9xoYoArjswWBhhBWECrrJdTPspI6FAuzjtcNmsM/edit) and the UX + * designs showing the three forms of message displayed in the PSP are at + * [Style Guide for Error Messages](https://docs.google.com/document/d/1rTMjpwH8d9HQ6hXoG6-qlsuf9F7oX-s37MJclFHgCYE/edit) + */ +fluid.defaults("gpii.userErrors", { + gradeNames: "fluid.component", + events: { + // Fired with an Error payload when an error representing a user error is to be signalled + userError: null + } +}); + +// The constant that defines the node.js error code indicating connection problems. +gpii.userErrors.connectionErrorCode = ["ECONNREFUSED", "ETIMEDOUT", "ECONNRESET", "ENOTFOUND"]; + +/** + * Check if the given error code indicates a connection problem. Return true if the code does indicate a connection + * problem. Otherwise, return false. + * @param {String} code - An node.js error code. + * @return {Boolean} Return true if the code does indicate a connection problem. Otherwise, return false. + */ +gpii.userErrors.isConnectionError = function (code) { + return gpii.userErrors.connectionErrorCode.indexOf(code) !== -1; +}; diff --git a/gpii/node_modules/gpii-user-errors/test/html/UserErrorsTest.html b/gpii/node_modules/gpii-user-errors/test/html/UserErrorsTest.html new file mode 100644 index 000000000..1a4fe3829 --- /dev/null +++ b/gpii/node_modules/gpii-user-errors/test/html/UserErrorsTest.html @@ -0,0 +1,39 @@ + + + + + GPII User Errors Tests + + + + + + + + + + + + + + + + + + + + + + + + + + +

GPII User Errors Tests

+

+
+

+
    + + + diff --git a/gpii/node_modules/gpii-user-errors/test/js/UserErrorsTests.js b/gpii/node_modules/gpii-user-errors/test/js/UserErrorsTests.js new file mode 100644 index 000000000..f366bd737 --- /dev/null +++ b/gpii/node_modules/gpii-user-errors/test/js/UserErrorsTests.js @@ -0,0 +1,43 @@ +/*! +GPII User Errors Tests + +Copyright 2018 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +/* eslint-env browser */ +/* eslint strict: ["error", "function"] */ + +/* global jqUnit, fluid */ + +(function () { + "use strict"; + + var gpii = fluid.registerNamespace("gpii"); + + jqUnit.test("Test gpii.userErrors.isConnectionError()", function () { + var testCases = [{ + code: "ECONNREFUSED", + expected: true + }, { + code: "ETIMEDOUT", + expected: true + }, { + code: "ECONNRESET", + expected: true + }, { + code: "NONERROR", + expected: false + }]; + + fluid.each(testCases, function (oneTestCase) { + jqUnit.assertEquals("The return on the connection error code is expected", oneTestCase.expected, gpii.userErrors.isConnectionError(oneTestCase.code)); + }); + }); + +})(); diff --git a/gpii/node_modules/journal/README.md b/gpii/node_modules/journal/README.md new file mode 100644 index 000000000..eec617e4e --- /dev/null +++ b/gpii/node_modules/journal/README.md @@ -0,0 +1,12 @@ +# Journal + +The Journal component is responsible for Journalling the state of the system's settings +in order to be able to recover from system crashes or corruption of the settings state. + +The Journal determines a GPII base directory to write the journal in, which is a subdirectory +of the user's settings directory named "gpii". The user's settings directory is determined +by a system dependent algorithm - + +* On Windows, the directory is determined by the environment variable APPDATA, [as recommended by Microsoft](https://blogs.msdn.microsoft.com/patricka/2010/03/18/where-should-i-store-my-data-and-configuration-files-if-i-target-multiple-os-versions/). +* On every other platform, it uses the value of the node standard API + [`os.homedir`](https://nodejs.org/api/os.html#os_os_homedir) diff --git a/gpii/node_modules/journal/index.js b/gpii/node_modules/journal/index.js new file mode 100644 index 000000000..77eb0d10d --- /dev/null +++ b/gpii/node_modules/journal/index.js @@ -0,0 +1,9 @@ +"use strict"; + +var fluid = require("infusion"); + +fluid.module.register("journal", __dirname, require); + +require("./src/SettingsDir.js"); +require("./src/JournalIdParser.js"); +require("./src/Journal.js"); diff --git a/gpii/node_modules/journal/package.json b/gpii/node_modules/journal/package.json new file mode 100644 index 000000000..edde58908 --- /dev/null +++ b/gpii/node_modules/journal/package.json @@ -0,0 +1,13 @@ +{ + "name": "journal", + "description": "Responsible for journalling the state of the system's settings in order to be able to recover from system crashes or corruption of the settings state", + "version": "0.3.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } +} diff --git a/gpii/node_modules/journal/src/Journal.js b/gpii/node_modules/journal/src/Journal.js new file mode 100644 index 000000000..89a2ea381 --- /dev/null +++ b/gpii/node_modules/journal/src/Journal.js @@ -0,0 +1,273 @@ +/** + * GPII Journal.js + * + * Responsible for journalling the state of the system's settings in order to be able to recover from system crashes or corruption of the settings state + * + * Copyright 2016 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + */ + +"use strict"; + +var fluid = require("infusion"), + kettle = fluid.registerNamespace("kettle"), + gpii = fluid.registerNamespace("gpii"), + fs = require("fs"), + writeFileAtomic = require("write-file-atomic"), + glob = require("glob"); + +/** Manages reading and writing of journal files held within the "GPII settings directory", + * managed in a suitable platform-specific area of the user's settings files. + */ + +fluid.defaults("gpii.journal", { + gradeNames: "fluid.component", + maxOldJournals: 20, + components: { + settingsDir: { + type: "gpii.settingsDir" + } + }, + events: { + onUpdateSnapshot: null + }, + listeners: { + "onCreate.readJournals": "{that}.readJournals()", + "onCreate.trimJournals": { + funcName: "gpii.journal.trimJournals", + args: "{that}", + priority: "after:readJournals" + }, + "onCreate.findCrashed": { + funcName: "gpii.journal.findCrashed", + args: "{that}", + priority: "after:trimJournals" + } + }, + invokers: { + readJournals: "gpii.journal.readJournals({that})", + // args: {that}, {session}, {originalSettings}, closed + writeJournal: "gpii.journal.writeJournal" + } +}); + +/** A mixin grade supplied to gpii.journal accounting for its interaction with a lifecycleManager. + * In the main architecture this is supplied with the journal's definition in gpii.flowManager.local, + * which also incidentally assures that this component will definitely be constructed after the + * lifecycleManager. + */ + +fluid.defaults("gpii.journalLifecycleManager", { + gradeNames: "fluid.component", + listeners: { + "{lifecycleManager}.events.onSessionSnapshotUpdate": { + namespace: "writeJournal", + func: "{that}.writeJournal", + args: ["{that}", "{arguments}.1", "{arguments}.1.model.originalSettings", false] + }, + "{lifecycleManager}.events.onSessionStop": { + namespace: "writeJournal", + func: "{that}.writeJournal", + args: ["{that}", "{arguments}.1", "{arguments}.1.model.originalSettings", true] + } + }, + invokers: { + restoreJournal: { + funcName: "gpii.journal.restoreJournal", + args: ["{that}", "{lifecycleManager}", "{arguments}.0"] + // journalId + } + } +}); + + +gpii.journal.restoreJournal = function (journalThat, lifecycleManager, journalId) { + var parsed = gpii.journal.parseId(journalId); + if (parsed.isError) { + fluid.fail(parsed.message); + } else { + fluid.log("Restoring journal with id " + journalId); + var journal = gpii.journal.fetchJournal(journalThat.journalFiles, parsed); + if (!journal) { + return fluid.promise().reject({ + isError: true, + message: "journal Id " + journalId + " could not be looked up to a journal" + }); + } else { + fluid.log("Restoring journal snapshot with contents " + JSON.stringify(journal, null, 2)); + return lifecycleManager.restoreSnapshot(journal.originalSettings); + } + } +}; + +/** A mixin grade supplied to gpii.journal which converts it into a kettle app hosting an HTTP endpoint + * currently just exposing the single function of restoring a particular journal + */ + +fluid.defaults("gpii.journalApp", { + gradeNames: "kettle.app", + invokers: { + dateToRestoreUrl: "gpii.journalApp.dateToRestoreUrl({that}, {kettle.server}, {arguments}.0)" + }, + serverUrl: "http://localhost", // TODO: provide a scheme either in Kettle or in universal for discovering this + listeners: { + "{lifecycleManager}.events.onSessionStart": { + namespace: "logRestoreUrl", + func: "gpii.journalApp.logRestoreUrl", + args: ["{that}", "{arguments}.0"] + } + }, + templates: { + listJournals: "Journals List

    Click on a journal link to restore the system to its state at that time

    %journals", + journalLink: "

    System snapshot taken at %journalTime%crashedString

    " + }, + requestHandlers: { + restore: { + route: "/journal/restore/:journalId", + method: "get", + type: "gpii.journal.restoreJournal.handler" + }, + journals: { + route: "/journal/journals.html", + method: "get", + type: "gpii.journal.journals.handler" + } + } +}); + +gpii.journalApp.dateToRestoreUrl = function (that, server, date) { + var baseUrl = that.options.serverUrl + ":" + server.options.port; + var pathTemplate = that.options.requestHandlers.restore.route.replace(":", "%"); + var journalId = ">" + new Date(date).toISOString(); + var pathReplaced = kettle.dataSource.URL.resolveUrl(pathTemplate, {journalId: journalId}); + var fullUrl = baseUrl + pathReplaced; + return fullUrl; +}; + +gpii.journalApp.logRestoreUrl = function (that, gradeName) { + if (gradeName === "gpii.lifecycleManager.userSession") { + var fullUrl = that.dateToRestoreUrl(Date.now()); + fluid.log(fluid.logLevel.WARN, "New user session starting: In future, you can visit the URL " + + fullUrl + " to restore the system's settings as they were at this point"); + } +}; + +fluid.defaults("gpii.journal.restoreJournal.handler", { + gradeNames: ["kettle.request.http"], + invokers: { + handleRequest: { + funcName: "gpii.journal.restoreJournal.handleRequest", + args: [ "{gpii.journal}", "{that}"] + } + } +}); + +gpii.journal.restoreJournal.handleRequest = function (journal, request) { + var journalId = request.req.params.journalId; + var restorePromise = journal.restoreJournal(journalId); + fluid.promise.follow(restorePromise, request.handlerPromise); +}; + +fluid.defaults("gpii.journal.journals.handler", { + gradeNames: ["kettle.request.http"], + invokers: { + handleRequest: { + funcName: "gpii.journal.journals.handleRequest", + args: [ "{gpii.journal}", "{that}"] + } + } +}); + +gpii.journal.journals.handleRequest = function (journal, request) { + journal.readJournals(); + var links = fluid.transform(journal.journalFiles, function (journalFile) { + var terms = { + journalUrl: journal.dateToRestoreUrl(journalFile.createTime), + journalTime: new Date(journalFile.createTime).toLocaleString(), + crashedString: journalFile.closed ? "" : ": crashed session" + }; + return fluid.stringTemplate(journal.options.templates.journalLink, terms); + }); + var linkBlock = links.join("\n"); + var page = fluid.stringTemplate(journal.options.templates.listJournals, { + journals: linkBlock + }); + request.res.header("Content-Type", "text/html"); + request.events.onSuccess.fire(page); +}; + +gpii.journal.writeJournal = function (that, session, snapshot, closed) { + // This is a temporary fix to work around GPII-3791 issue before the proper fix is in place. + if (!session.createTime) { + fluid.log("Journal: stop proceeding with writeJournal() as session.createTime is undefined"); + return; + } + var dir = that.settingsDir.getGpiiSettingsDir(); + var filename = "journal-" + gpii.journal.formatTimestamp(session.createTime) + ".json"; + var payload = { + gpiiKey: session.model.gpiiKey, + closed: closed, + createTime: session.createTime, + originalSettings: snapshot + }; + var formatted = JSON.stringify(payload, null, 4); + var fullPath = dir + "/" + filename; + fluid.log("Writing journal file to path " + fullPath); + writeFileAtomic.sync(fullPath, formatted); +}; + +// A comparator to sort journal entries in increasing order of age (newest first) +gpii.journal.dateComparator = function (journalA, journalB) { + return journalB.createTime - journalA.createTime; +}; + +gpii.journal.readJournals = function (that) { + var dir = that.settingsDir.getGpiiSettingsDir(); + var journalFileNames = glob.sync(dir + "/journal-*.json"); + fluid.log("Got journalFileNames ", journalFileNames); + var journalFiles = fluid.transform(journalFileNames, function (journalFileName) { + var readPromise = kettle.JSON.readFileSync(journalFileName, "reading journal file " + journalFileName); + var togo; + readPromise.then(function (read) { + togo = read; + }, function (err) { + fluid.log(fluid.logLevel.WARN, "Error reading journal file " + journalFileName + ": " + err.message); + togo = err; + }); + togo.journalFileName = journalFileName; + return togo; + }); + journalFiles.sort(gpii.journal.dateComparator); + that.journalFiles = journalFiles; +}; + +gpii.journal.trimJournals = function (that) { + if (that.journalFiles.length > that.options.maxOldJournals) { + for (var i = that.options.maxOldJournals; i < that.journalFiles.length; ++i) { + var journal = that.journalFiles[i]; + var fileName = journal.journalFileName; + fluid.log("Removing old journal file " + fileName + " since maximum number of old journals is " + that.options.maxOldJournals); + try { + fs.unlinkSync(fileName); + } catch (e) { + fluid.log(fluid.logLevel.WARN, "Error deleting old journal " + fileName + ": " + e); + } + } + that.journalFiles.length = that.options.maxOldJournals; + } +}; + +gpii.journal.findCrashed = function (that) { + var firstCrashed = fluid.find_if(that.journalFiles, function (journalFile) { + return journalFile.closed === false; + }); + if (firstCrashed) { + that.crashedJournal = firstCrashed; + fluid.log(fluid.logLevel.WARN, "Found crashed journal file on startup: " + JSON.stringify(firstCrashed, null, 2)); + } else { + fluid.log("Found no crashed journals on startup"); + } +}; diff --git a/gpii/node_modules/journal/src/JournalIdParser.js b/gpii/node_modules/journal/src/JournalIdParser.js new file mode 100644 index 000000000..e025eaaa2 --- /dev/null +++ b/gpii/node_modules/journal/src/JournalIdParser.js @@ -0,0 +1,108 @@ +/** + * GPII JournalIdParser.js + * + * Responsible for journalling the state of the system's settings in order to be able to recover from system crashes or corruption of the settings state + * + * Copyright 2016 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + */ + +"use strict"; + +var fluid = fluid || require("infusion"); + +var gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.journal"); + + +gpii.journal.idFormatError = fluid.freezeRecursive({ + isError: true, + message: "The only supported formats for journalId are HEAD, HEAD~, HEAD~2, etc. or <1995-10-9T10:11:12, >2009-11-29T01:02:59, etc." +}); + +gpii.journal.parseHeadId = function (journalId) { + var index = journalId.substring("HEAD".length), depth = NaN; + if (index.length > 0) { + if (index.charAt(0) === "~") { + var depthString = index.substring(1); + depth = depthString.length === 0 ? 1 : fluid.parseInteger(depthString); + } else { + return gpii.journal.idFormatError; + } + } else { + depth = 0; + } + if (Number.isNaN(depth)) { + return gpii.journal.idFormatError; + } else { + return { + type: "HEAD", + depth: depth + }; + } +}; + +gpii.journal.parseDateId = function (journalId) { + var date = Date.parse(journalId.substring(1)); + return Number.isNaN(date) ? gpii.journal.idFormatError : { + type: "DATE", + before: journalId.charAt(0) === "<" ? 1 : -1, + date: date + }; +}; + +gpii.journal.idParsers = { + "HEAD": gpii.journal.parseHeadId, + ">": gpii.journal.parseDateId, + "<": gpii.journal.parseDateId +}; + +/** Parse a journalId specification string into a JSON form + * @param {String} journalId - A journalId string beginning with HEAD, < or >. + * @return {Object} A {ParsedJournalId} holding a field `type` (either "HEAD" or "DATE") and other type-specific fields. + */ + +gpii.journal.parseId = function (journalId) { + var togo = gpii.journal.idFormatError; + fluid.each(gpii.journal.idParsers, function (func, start) { + if (journalId.startsWith(start)) { + togo = func(journalId); + } + }); + return togo; +}; + +/** Dereference a journalId given an array of journal files + * @param {Array} journalFiles - An array of JournalFile objects, sorted into increasing order of age. + * @param {Object} parsed - A {ParsedJournalId} as returned from `gpii.journal.parseId` which will be used to index into `journalFiles. + * @return {JournalFile|undefined} One of the JournalFile entries supplied, or undefined if the supplied journalId does not match. + */ +gpii.journal.fetchJournal = function (journalFiles, parsed) { + if (parsed.type === "HEAD") { + return journalFiles[parsed.depth]; + } else { + var before = parsed.before; + var toSearch = before === 1 ? journalFiles : fluid.makeArray(journalFiles).reverse(); + // For "before" - stop on the first one in reverse order (original) which is before + // For "after" - stop on the first one in forward order (reversed) which is after + return fluid.find_if(toSearch, function (journal) { + return journal.createTime * before <= parsed.date * before; + }); + } +}; + +/** Formats a millisecond timestamp into an ISO-8601 date string which is safe to + * appear in a filename + * @param {Number} time - A millisecond timestamp, as dispensed from `Date.now()`. + * @return {String} An ISO-8601 date stamp without colon characters, e.g. 2016-07-05T220712.549Z. + */ +gpii.journal.formatTimestamp = function (time) { + var date = new Date(time); + var stamp = date.toISOString(); + var safeStamp = stamp.replace(/:/g, ""); // This is still a valid ISO-8601 date string, but Date.parse() will no longer parse it + return safeStamp; +}; diff --git a/gpii/node_modules/journal/src/SettingsDir.js b/gpii/node_modules/journal/src/SettingsDir.js new file mode 100644 index 000000000..8614f5890 --- /dev/null +++ b/gpii/node_modules/journal/src/SettingsDir.js @@ -0,0 +1,101 @@ +/** + * GPII SettingsDir.js + * + * Determines the user's settings directory on a variety of platforms, + * and manages establishment of a subdirectory "/gpii" to hold writable files + * for the system's journal and other purposes + * + * Copyright 2016 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + os = require("os"), + fs = require("fs"); + +fluid.defaults("gpii.settingsDir", { + gradeNames: ["fluid.component", "fluid.contextAware"], + contextAwareness: { + platform: { + checks: { + test: { + contextValue: "{gpii.contexts.test}", + gradeNames: "gpii.settingsDir.temp", + priority: "first" + }, + windows: { + contextValue: "{gpii.contexts.windows}", + gradeNames: "gpii.settingsDir.windows" + }, + linux: { + contextValue: "{gpii.contexts.linux}", + gradeNames: "gpii.settingsDir.standard" + } + }, + defaultGradeNames: "gpii.settingsDir.temp" + } + }, + members: { + gpiiSettingsDir: "@expand:{that}.getGpiiSettingsDir()" + }, + invokers: { + getGpiiSettingsDir: "gpii.settingsDir.gpii({that}.getBaseSettingsDir)", + getBaseSettingsDir: "fluid.notImplemented" + }, + listeners: { + "onCreate.createGpiiDir": "gpii.settingsDir.createGpii" + } +}); + +fluid.defaults("gpii.settingsDir.temp", { + invokers: { + getBaseSettingsDir: "gpii.settingsDir.baseDir.temp" + } +}); + +fluid.defaults("gpii.settingsDir.windows", { + invokers: { + getBaseSettingsDir: "gpii.settingsDir.baseDir.windows" + } +}); + +fluid.defaults("gpii.settingsDir.standard", { + invokers: { + getBaseSettingsDir: "gpii.settingsDir.baseDir.standard" + } +}); + +gpii.settingsDir.createGpii = function (that) { + fluid.log(fluid.logLevel.WARN, "Creating GPII settings directory in ", that.gpiiSettingsDir); + try { // See code skeleton in http://stackoverflow.com/questions/13696148/node-js-create-folder-or-use-existing + fs.mkdirSync(that.gpiiSettingsDir); + } catch (e) { + if (e.code !== "EEXIST") { + fluid.fail("Unable to create GPII settings directory, code is " + e.code + ": exception ", e); + } + } +}; + +gpii.settingsDir.gpii = function (getBaseSettingsDir) { + return getBaseSettingsDir() + "/gpii"; +}; + +fluid.registerNamespace("gpii.settingsDir.baseDir"); + +gpii.settingsDir.baseDir.windows = function () { + return process.env.APPDATA; +}; + +gpii.settingsDir.baseDir.standard = function () { + return os.homedir(); +}; + +gpii.settingsDir.baseDir.temp = function () { + return os.tmpdir(); +}; diff --git a/gpii/node_modules/journal/test/html/JournalIdParserTests.html b/gpii/node_modules/journal/test/html/JournalIdParserTests.html new file mode 100644 index 000000000..50d05e87f --- /dev/null +++ b/gpii/node_modules/journal/test/html/JournalIdParserTests.html @@ -0,0 +1,37 @@ + + + + + GPII Journal ID Parser Tests + + + + + + + + + + + + + + + + + + + + + + + + + +

    GPII Journal ID Parser Tests

    +

    +
    +

    +
      + + diff --git a/gpii/node_modules/journal/test/js/JournalIdParserTests.js b/gpii/node_modules/journal/test/js/JournalIdParserTests.js new file mode 100644 index 000000000..51e8de3a6 --- /dev/null +++ b/gpii/node_modules/journal/test/js/JournalIdParserTests.js @@ -0,0 +1,155 @@ +/** + * GPII Journal ID Parser Tests + * + * Copyright 2016 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +/* global jqUnit */ + +"use strict"; + +var fluid = fluid || require("infusion"); + +(function () { + + var gpii = fluid.registerNamespace("gpii"); + + fluid.registerNamespace("gpii.tests.journal.idParser"); + + gpii.tests.journal.idParser.fixtures = [{ + toParse: "HEAD", + expected: { + type: "HEAD", + depth: 0 + } + }, { + toParse: "HEAD~", + expected: { + type: "HEAD", + depth: 1 + } + }, { + toParse: "HEAD~1", + expected: { + type: "HEAD", + depth: 1 + } + }, { + toParse: "HEAD~2", + expected: { + type: "HEAD", + depth: 2 + } + }, { + toParse: "HEAD~x", + expected: gpii.journal.idFormatError + }, { + toParse: "Unfortunate", + expected: gpii.journal.idFormatError + }, { + toParse: "<2000-01-01", + expected: { + type: "DATE", + before: 1, + date: 946684800000 + } + }, { + toParse: ">1997-07-16T12:00:00.155Z", + expected: { + type: "DATE", + before: -1, + date: 869054400155 + } + }, { + toParse: ">1997-07-16X12:00:00.155Z", + expected: gpii.journal.idFormatError + } + ]; + + jqUnit.test("Parsing journal ID specifications", function () { + fluid.each(gpii.tests.journal.idParser.fixtures, function (fixture, index) { + var parsed = gpii.journal.parseId(fixture.toParse); + jqUnit.assertDeepEq("Expected result for fixture " + index + ": " + fixture.toParse, fixture.expected, parsed); + }); + }); + + gpii.tests.journal.idParser.journalFiles = [{ + id: 1, + date: "2016-07-21T11:10:42.584Z" + }, { + id: 2, + date: "2016-07-20T20:58:52.654Z" + }, { + id: 3, + date: "2016-07-20T20:58:38.942Z" + }, { + id: 4, + date: "2016-07-20T20:57:01.402Z" + }, { + id: 5, + date: "2016-07-20T20:56:41.981Z" + }]; + + gpii.tests.journal.idParser.fetchFixtures = [{ + journalId: "HEAD", + expected: 1 + }, { + journalId: "HEAD~", + expected: 2 + }, { + journalId: "HEAD~5", + expected: undefined + }, { + journalId: ">2016-07-20T20:58:52.000Z", + expected: 2 + }, { + journalId: "<2016-07-20T20:58:52.000Z", + expected: 3 + }, { + journalId: ">2000-01-01T00:00:00.000Z", + expected: 5 + }, { + journalId: "<2000-01-01T00:00:00.000Z", + expected: undefined + }, { + journalId: ">2020-01-01T00:00:00.000Z", + expected: undefined + }, { + journalId: "<2020-01-01T00:00:00.000Z", + expected: 1 + }, { + journalId: ">2016-07-21T11:10:42.584Z", + expected: 1 + }, { + journalId: "<2016-07-21T11:10:42.584Z", + expected: 1 + }]; + + jqUnit.test("Journal fetch test", function () { + var journalFiles = fluid.transform(gpii.tests.journal.idParser.journalFiles, function (journalFile) { + return { + id: journalFile.id, + createTime: Date.parse(journalFile.date) + }; + }); + fluid.each(gpii.tests.journal.idParser.fetchFixtures, function (fixture, index) { + var parsed = gpii.journal.parseId(fixture.journalId); + var fetched = gpii.journal.fetchJournal(journalFiles, parsed); + var fetchedId = fluid.get(fetched, "id"); + jqUnit.assertEquals("Expected result for fixture " + index + ": " + fixture.journalId, fixture.expected, fetchedId); + }); + }); + + jqUnit.test("Datestamp to filename formatter", function () { + var date = new Date("2016-07-20T20:58:52.000Z").getTime(); + var sanitized = gpii.journal.formatTimestamp(date); + jqUnit.assertEquals("Sanitized datestamp to remove colon characters", "2016-07-20T205852.000Z", sanitized); + }); + +})(); diff --git a/gpii/node_modules/lifecycleActions/index.js b/gpii/node_modules/lifecycleActions/index.js index a666bac2a..8036e8268 100644 --- a/gpii/node_modules/lifecycleActions/index.js +++ b/gpii/node_modules/lifecycleActions/index.js @@ -1,5 +1,7 @@ +"use strict"; + var fluid = require("infusion"); -var loader = fluid.getLoader(__dirname); +fluid.module.register("lifecycleActions", __dirname, require); -loader.require("./src/LifecycleActions.js"); \ No newline at end of file +require("./src/LifecycleActions.js"); diff --git a/gpii/node_modules/lifecycleActions/package.json b/gpii/node_modules/lifecycleActions/package.json index 23e7ab83c..0c231cac7 100644 --- a/gpii/node_modules/lifecycleActions/package.json +++ b/gpii/node_modules/lifecycleActions/package.json @@ -1,19 +1,13 @@ { - "name": "lifecycleActions", - "description": "Located on user device. Common (portable) definitions for lifecycle actions.", - "version": "0.1", - "author": "GPII", - "bugs": "http://wiki.gpii.net/index.php/Main_Page", - "homepage": "http://gpii.net/", - "dependencies": {}, - "licenses": [ - { - "type": "BSD-3-Clause", - "url": "http://www.opensource.org/licenses/BSD-3-Clause" - } - ], - "keywords": ["gpii", "accessibility", "settings", "fluid", "IoC", "Inversion of Control", "configuration", "evented"], - "repository": "git://github.com/GPII/universal.git", - "main": "./index.js", - "engines": { "node" : ">=0.1.9" } -} \ No newline at end of file + "name": "lifecycleActions", + "description": "Located on user device. Common (portable) definitions for lifecycle actions", + "version": "0.3.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } +} diff --git a/gpii/node_modules/lifecycleActions/src/LifecycleActions.js b/gpii/node_modules/lifecycleActions/src/LifecycleActions.js index dc30af7ad..08dd20c0c 100644 --- a/gpii/node_modules/lifecycleActions/src/LifecycleActions.js +++ b/gpii/node_modules/lifecycleActions/src/LifecycleActions.js @@ -1,43 +1,44 @@ -/*! -Standard Lifecycle Actions Definitions - -Copyright 2012 Antranig Basman - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -(function () { - "use strict"; - - var fluid = require("infusion"), - child_process = require("child_process"), - gpii = fluid.registerNamespace("gpii"); - - fluid.registerNamespace("gpii.launch"); - - fluid.defaults("gpii.launch.exec", { - gradeNames: "fluid.function", - argumentMap: { - command: 0, - options: 1 - } - }); - - gpii.launch.exec = child_process.exec; - - fluid.defaults("gpii.launch.spawn", { - gradeNames: "fluid.function", - argumentMap: { - command: 0, - args: 1, - options: 2 - } - }); - - gpii.launch.spawn = child_process.spawn; - -})(); +/* + * Standard Lifecycle Actions Definitions + * + * Copyright 2012 Antranig Basman + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + child_process = require("child_process"), + gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.launch"); + +fluid.defaults("gpii.launch.exec", { + gradeNames: "fluid.function", + argumentMap: { + command: 0, + options: 1 + } +}); + +gpii.launch.exec = child_process.exec; + +fluid.defaults("gpii.launch.spawn", { + gradeNames: "fluid.function", + argumentMap: { + command: 0, + args: 1, + options: 2 + } +}); + +gpii.launch.spawn = child_process.spawn; diff --git a/gpii/node_modules/lifecycleManager/README.md b/gpii/node_modules/lifecycleManager/README.md index 39ff44b9a..589781732 100644 --- a/gpii/node_modules/lifecycleManager/README.md +++ b/gpii/node_modules/lifecycleManager/README.md @@ -1,37 +1,3 @@ -Lifecycle Manager [Server] -=== +# Lifecycle Manager -A server app built to be deployed with node.js server that serves as the wrapper -around the component responsible for setting and unsetting user preferences. - ---- - -### Dependencies - -[infusion](https://github.com/fluid-project/infusion) framework. - -### Installation instructions: - -Firstly, install node and npm. - -Run the following command in your newly checked out Lifecycle Manager [Server] -repository. This will install all dependencies that are required by the -Lifecycle Manager [Server]. - - npm install - -### Run - -To run Lifecycle Manager Server simply type: - - [NODE_ENV={environment}] node node_modules/kettle/lib/init.js path/to/configs/folder - -- Default environment is development. -- Path to configs folder can be absolute or relative to the current user - directory. - -For example: - - node node_modules/kettle/lib/init.js gpii/node_modules/lifecycleManager/configs/ - node node_modules/kettle/lib/init.js /Users/{userName}/universal/gpii/node_modules/lifecycleManager/configs/ - NODE_ENV=production node node_modules/kettle/lib/init.js gpii/node_modules/lifecycleManager/configs/ \ No newline at end of file +A core component of the GPII's FlowManager, responsible for orchestrating invocation of settings handlers and solutions. diff --git a/gpii/node_modules/lifecycleManager/configs/base.json b/gpii/node_modules/lifecycleManager/configs/base.json deleted file mode 100644 index f9bfac26c..000000000 --- a/gpii/node_modules/lifecycleManager/configs/base.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "modules": [ - "lifecycleManager" - ] -} diff --git a/gpii/node_modules/lifecycleManager/configs/development.json b/gpii/node_modules/lifecycleManager/configs/development.json deleted file mode 100644 index 3fe366139..000000000 --- a/gpii/node_modules/lifecycleManager/configs/development.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "typeName": "lifecycleManagerServer.development", - "options": { - "gradeNames": ["autoInit", "fluid.littleComponent"], - "components": { - "server": { - "type": "kettle.server", - "options": { - "logging": true, - "components": { - "lifecycleManager": { - "type": "gpii.lifecycleManagerServer" - } - } - } - } - } - }, - "includes": [ - "./base.json" - ] -} diff --git a/gpii/node_modules/lifecycleManager/configs/kettleModuleLoader.js b/gpii/node_modules/lifecycleManager/configs/kettleModuleLoader.js deleted file mode 100644 index 7e349acd5..000000000 --- a/gpii/node_modules/lifecycleManager/configs/kettleModuleLoader.js +++ /dev/null @@ -1,16 +0,0 @@ -/*! -Kettle Module Loader. - -Copyright 2012-2013 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/kettle/LICENSE.txt -*/ - -// The purpose of this file is to be copied to a location for which the -// require function is needed. It allows to find node modules relative -// to that location that are otherwise non-resolvable. -module.exports = require; diff --git a/gpii/node_modules/lifecycleManager/configs/production.json b/gpii/node_modules/lifecycleManager/configs/production.json deleted file mode 100644 index 7b7459134..000000000 --- a/gpii/node_modules/lifecycleManager/configs/production.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "typeName": "lifecycleManagerServer.production", - "includes": [ - "./development.json" - ] -} diff --git a/gpii/node_modules/lifecycleManager/index.js b/gpii/node_modules/lifecycleManager/index.js index 333bfaa2e..4fed8193e 100644 --- a/gpii/node_modules/lifecycleManager/index.js +++ b/gpii/node_modules/lifecycleManager/index.js @@ -1,6 +1,19 @@ +"use strict"; + var fluid = require("infusion"); -var loader = fluid.getLoader(__dirname); +fluid.module.register("lifecycleManager", __dirname, require); -loader.require("./src/LifecycleManager.js"); -loader.require("./src/LifecycleManagerServer.js"); +require("gpii-user-errors"); +fluid.require("%flowManager/src/SystemUtils.js"); +// MatchMaking.js is used in both flowManager and lifecycleManager modules for: +// * Cloud Based Flow Manager: to perform matchmaking in the cloud without the present of lifecycleManager; +// * lifecycleManager: to handle user logon requests. +fluid.require("%flowManager/src/MatchMaking.js"); +require("./src/Resolvers.js"); +require("./src/LifecycleManagerSession.js"); +require("./src/PrivateMatchMaker.js"); +require("./src/UserLogonRequest.js"); +require("./src/UserLogonStateChange.js"); +require("./src/LifecycleManagerUtils.js"); +require("./src/LifecycleManager.js"); diff --git a/gpii/node_modules/lifecycleManager/package.json b/gpii/node_modules/lifecycleManager/package.json index 5e97740e9..f67fa5079 100644 --- a/gpii/node_modules/lifecycleManager/package.json +++ b/gpii/node_modules/lifecycleManager/package.json @@ -1,19 +1,13 @@ { - "name": "lifecycleManager", - "description": "Located on user device. Invoked by the FlowManager to manage the lifecycle and configuration of a particular solution.", - "version": "0.1", - "author": "GPII", - "bugs": "http://wiki.gpii.net/index.php/Main_Page", - "homepage": "http://gpii.net/", - "dependencies": {}, - "licenses": [ - { - "type": "BSD-3-Clause", - "url": "http://www.opensource.org/licenses/BSD-3-Clause" - } - ], - "keywords": ["gpii", "accessibility", "settings", "fluid", "IoC", "Inversion of Control", "configuration", "evented"], - "repository": "git://github.com/GPII/universal.git", - "main": "./index.js", - "engines": { "node" : ">=0.1.9" } -} \ No newline at end of file + "name": "lifecycleManager", + "description": "Located on user device. Invoked by the FlowManager to manage the lifecycle and configuration of a particular solution.", + "version": "0.3.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } +} diff --git a/gpii/node_modules/lifecycleManager/src/LifecycleManager.js b/gpii/node_modules/lifecycleManager/src/LifecycleManager.js index f3f9fe0c3..ed434a0fb 100644 --- a/gpii/node_modules/lifecycleManager/src/LifecycleManager.js +++ b/gpii/node_modules/lifecycleManager/src/LifecycleManager.js @@ -1,298 +1,1536 @@ /*! -Lifecycle Manager + * Lifecycle Manager + * + * Copyright 2012 Antranig Basman + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ -Copyright 2012 Antranig Basman +"use strict"; - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -/*global require */ - -var fluid = fluid || require("infusion"); -var $ = fluid.registerNamespace("jQuery"); -var gpii = fluid.registerNamespace("gpii"); +var fluid = fluid || require("infusion"), + gpii = fluid.registerNamespace("gpii"), + $ = fluid.registerNamespace("jQuery"); (function () { fluid.defaults("gpii.lifecycleManager", { - gradeNames: ["fluid.eventedComponent", "autoInit"], + gradeNames: ["fluid.modelComponent", "fluid.contextAware"], + retryOptions: { // Options governing how often to recheck whether settings are set (and in future, to check for running processes) + rewriteEvery: 0, // This feature is now disabled except in integration tests, causing settings handler instability via GPII-2522 + numRetries: 12, // Make 12 attempts over a period of 12 seconds to discover whether settings are set + retryInterval: 1000 + }, + debounceTimeMs: 1500, // The debounce time in milliseconds for /proximityTriggered endpoint to reject the following request with the same GPII key components: { + // Note: The subcomponent "privateMatchMaker" is distributed from flowManager so it doesn't disturb lifecycle manager unit tests. + // TODO: privateMatchMaker will eventually be factored into the UserLogonRequest where key-in matchmaker requests are handled. + // privateMatchMaker: { + // type: "gpii.lifecycleManager.privateMatchMaker" + // }, + userSession: { + type: "gpii.lifecycleManager.userSession" + }, + restoreSession: { + type: "gpii.lifecycleManager.restoreSession", + options: { + model: { + gpiiKey: "restore" + } + } + }, variableResolver: { - type: "gpii.lifecycleManager.resolver" + type: "gpii.lifecycleManager.variableResolver" + }, + nameResolver: { + type: "gpii.lifecycleManager.nameResolver" + }, + userErrors: { + type: "gpii.userErrors" } }, - members: { - activeSessions: {} - }, - invokers: { - stop: { - funcName: "gpii.lifecycleManager.stop", - args: ["{that}.activeSessions", "{arguments}.0", - "{arguments}.1"] - }, - start: { - funcName: "gpii.lifecycleManager.start", - args: ["{that}.activeSessions", "{that}.variableResolver", - "{that}.stop", "{arguments}.0", "{arguments}.1", - "{arguments}.2"] - } - } - }); - - fluid.defaults("gpii.lifecycleManager.resolver", { - gradeNames: ["fluid.eventedComponent", "autoInit"], - components: { - resolverConfig: { - type: "gpii.lifecycleManager.standardResolverConfig" + dynamicComponents: { + userLogonRequests: { + type: "{arguments}.0", + createOnEvent: "onUserLogonRequestReceived", + options: { + gpiiKey: "{arguments}.1", + requestPromise: "{arguments}.2" + } } }, members: { - resolvers: { - expander: { - func: "gpii.lifecycleManager.resolver.computeResolvers", - args: "{that}.resolverConfig.options.resolvers" - } + /* action queue for high-level lifecycle manager tasks (start, stop or update). + * The entries in the action queue are of the format { func: , arg: } where + * is a single-argument function that returns a promise. The promise should be resolved when + * the function is complete (including side-effects). + * A more detailed description: https://github.com/GPII/universal/tree/master/documentation/LifecycleMananger.md + */ + actionQueue: [], + userLogonRequestQueue: [] + }, + model: { + // Holds the GPII key from the last environmental login + lastEnvironmentalLoginGpiiKey: undefined, + currentUserLogon: { // Holds information about the last non-noUser login/logout action + // for /proximityTriggered endpoint to verify its debounce rule. + type: undefined, // "login"/"logout" + gpiiKey: undefined, // string with GPII key + timeStamp: 0 }, - fetcher: { - expander: { - func: "gpii.resolversToFetcher", - args: "{that}.resolvers" + logonChange: { // Holds information about the most recent login/logout action so its progress may be tracked in a modelised way + type: undefined, // "login"/"logout" + inProgress: false, // boolean + gpiiKey: undefined, // string with GPII key + timeStamp: 0 + } + }, + events: { + onSessionStart: null, // fired with [gradeName, gpiiKey] - notify listeners + onSessionSnapshotUpdate: null, // fired with [{lifecycleManager}, {session}, originalSettings] + onSessionStop: null, // fired with [{lifecycleManager}, {session}] + onAutoSaveRequired: null, // fired with [gpiiKey, {session}.model.preferences] + onClearActionQueue: null, + onUserLogonRequestReceived: null, // fired with [gradeName, reqeustId, gpiiKey, requestPromise] + onQueueEmpty: null, + onClearUserLogonRequestQueue: null + }, + listeners: { + "onSessionSnapshotUpdate.log": "gpii.lifecycleManager.logSnapshotUpdate", + "onCreate.createActionFunctions": "gpii.lifecycleManager.createActionQueueFunctions", + "onClearActionQueue.clearQueue": "gpii.lifecycleManager.clearQueue({that}.actionQueue, 'action')", + "onClearUserLogonRequestQueue.clearQueue": "gpii.lifecycleManager.clearQueue({that}.userLogonRequestQueue, 'user logon request')" + }, + actionQueueFunctions: { + expander: { // manually expanded via the createActionQueueFunctions function + type: "fluid.noexpand", + value: { + stop: "{that}.processStop", + start: "{that}.processStart", + read: "{that}.processRead", + update: "{that}.processUpdate" } } }, invokers: { - resolve: { - funcName: "gpii.lifecycleManager.resolver.resolve", - args: ["{arguments}.0", "{that}.fetcher", "{arguments}.1"] + // Handle login, logout and proximityTriggered requests + performLogin: { + funcName: "gpii.lifecycleManager.performLogin", + args: ["{that}", "{arguments}.0", "{arguments}.1"] + // gpiiKey, loginInfo + }, + performLogout: { + funcName: "gpii.lifecycleManager.performLogout", + args: ["{that}", "{arguments}.0"] + // gpiiKey + }, + performProximityTriggered: { + funcName: "gpii.lifecycleManager.performProximityTriggered", + args: ["{that}", "{arguments}.0"] + // gpiiKey + }, + replayEnvironmentalLogin: { + funcName: "gpii.lifecycleManager.replayEnvironmentalLogin", + args: ["{that}", "{that}.model.lastEnvironmentalLoginGpiiKey"] + }, + isBounce: { + funcName: "gpii.lifecycleManager.isBounce", + args: ["{that}.model.logonChange", "{that}.model.currentUserLogon", "{that}.options.debounceTimeMs", "{arguments}.0"] + // gpiiKey + }, + errorResponse: { + funcName: "gpii.lifecycleManager.errorResponse", + args: ["{arguments}.0", "{arguments}.1", "{arguments}.2", "{arguments}.3"] + // requestPromise or requestComponent, message, statusCode, ignoreUserErrors + }, + + // get the current active GPII key + getActiveSessionGpiiKey: { + funcName: "gpii.lifecycleManager.getActiveSessionGpiiKey", + args: ["{that}.userSession"] + }, + addLifecycleInstructionsToPayload: { + funcName: "gpii.lifecycleManager.addLifecycleInstructionsToPayload", + args: ["{arguments}.0"] + // fullPayload + }, + updateActivePrefsSetName: { + funcName: "gpii.lifecycleManager.updateActivePrefsSetName", + args: ["{that}", "{arguments}.0", "{arguments}.1"] + // fullPayload, newPrefsSetName + }, + prefsSetNameChanged: { + funcName: "gpii.lifecycleManager.prefsSetNameChanged", + args: ["{that}", "{arguments}.0"] + // newPrefsSetName + }, + applyPreferences: { + funcName: "gpii.lifecycleManager.applyPreferences", + args: ["{that}", "{arguments}.0", "{arguments}.1"] + // preferences, prefsAppliedLocalEvent + }, + readPreferences: { + funcName: "gpii.lifecycleManager.readPreferences", + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] + // preferences, preferenceReadSuccessEvent, preferenceReadFailEvent + }, + // Return lifecycleManager.userSession by default. If a GPII key is provided, return the session of the given key. + getSession: { + funcName: "gpii.lifecycleManager.getSession", + args: ["{that}.userSession", "{that}.restoreSession", "{arguments}.0"] + // GPII key + }, + // Handle the action queue + addToActionQueue: { + funcName: "gpii.lifecycleManager.addToActionQueue", + args: ["{that}", "{that}.actionQueue", "{arguments}.0"] + }, + processActionQueue: { + funcName: "gpii.lifecycleManager.processActionQueue", + args: ["{that}", "{that}.actionQueue"] + }, + // Handle the user logon request queue + addToUserLogonRequestQueue: { + funcName: "gpii.lifecycleManager.addToUserLogonRequestQueue", + args: ["{that}", "{that}.userLogonRequestQueue", "{arguments}.0"] + // queueItem + }, + processUserLogonRequestQueue: { + funcName: "gpii.lifecycleManager.processUserLogonRequestQueue", + args: ["{that}", "{that}.userLogonRequestQueue"] + }, + endProcessOneUserLogonRequest: { + funcName: "gpii.lifecycleManager.endProcessOneUserLogonRequest", + args: ["{lifecycleManager}"] + // queueItem + }, + // start: manually created function which should be called on user login (i.e. configuring the system) + // stop: manually created function which should be called on user logout (i.e. restoring the system) + // read: manually created function which should be called on reading settings of an already configured system + // update: manually created function which should be called on update (i.e. changes in the setting of an already configured system) + processStart: { // should not be used directly, use the manually created 'start' invoker instead + funcName: "gpii.lifecycleManager.processStart", + args: [ "{that}", "{that}.userSession", "{arguments}.0"] + // Use IoC declaration "{that}.userSession" to force the instantiation of "userSession" subcomponent + }, + processStop: { // should not be used directly, use the manually created 'stop' invoker instead + funcName: "gpii.lifecycleManager.processStop", + args: [ "{that}", "{arguments}.0"] + }, + processRead: { // should not be used directly, use the manually created 'read' invoker instead + funcName: "gpii.lifecycleManager.processRead", + args: ["{that}", "{arguments}.0"] + // solutions + }, + processUpdate: { // should not be used directly, use the manually created 'update' invoker instead + funcName: "gpii.lifecycleManager.processUpdate", + args: [ "{that}", "{arguments}.0"] + // finalPayload + }, + applySolution: { + funcName: "gpii.lifecycleManager.applySolution", + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "{arguments}.3", "{arguments}.4"] + // solutionId, solutionRecord, session, lifecycleBlocksKeys, rootAction + }, + executeActions: { + funcName: "gpii.lifecycleManager.executeActions", + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "{arguments}.3", "{arguments}.4"] + // solutionId, settingsHandlers, actions, session, rootAction + }, + invokeSettingsHandlerGet: { + funcName: "gpii.lifecycleManager.invokeSettingsHandlerGet", + args: ["{that}", "{arguments}.0", "{arguments}.1"] + // solutionId, settingsHandlers + }, + invokeSettingsHandlerSet: { + funcName: "gpii.lifecycleManager.invokeSettingsHandlerSet", + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] + // solutionId, settingsHandlers, handlerType + }, + restoreSnapshot: { + funcName: "gpii.lifecycleManager.restoreSnapshot", + args: ["{that}", "{arguments}.0"] + // originalSettings + }, + getSolutionRunningState: { + funcName: "gpii.lifecycleManager.getSolutionRunningState", + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] + // solutionId, solution, session } } }); - gpii.lifecycleManager.resolver.computeResolvers = function (resolvers) { - return fluid.transform(resolvers, fluid.getGlobalValue); - }; - - gpii.lifecycleManager.resolver.resolve = function (material, fetcher, extraFetcher) { - return fluid.expand(material, { - bareContextRefs: false, - // TODO: FLUID-4932 - the framework currently has no wildcard - // support in mergePolicy. - mergePolicy: { - 0: { - capabilitiesTransformations: { - "*": { - noexpand: true - } - } - } - }, - fetcher: gpii.combineFetchers(fetcher, extraFetcher) + fluid.defaults("gpii.test.lifecycleManager.integration", { + // Test this only in integration testing scenarios + retryOptions: { + rewriteEvery: 3 + } + }); + + fluid.contextAware.makeAdaptation({ + distributionName: "gpii.test.lifecycleManager.integration.adaptation", + targetName: "gpii.lifecycleManager", + adaptationName: "integrationTest", + checkName: "integrationTest", + record: { + contextValue: "{gpii.contexts.test.integration}", + gradeNames: "gpii.test.lifecycleManager.integration" + } + }); + + /** + * Function to check whether a proximity trigger should bounce. + * Debounce rule: any RFID actions is ignored for if a login/logout for is in progress + * OR if the last login/logout process for finished less than 1.5 seconds ago + * If no user is logged in and debounce doesn't apply, log in + * If is logged in and debounce doesn't apply, log out + * If another user is already logged in or in the process of logging in or out, log that user out and log in + * + * @param {Component} currentLogonChange - The value of lifecycleManager.model.logonChange. + * @param {Component} currentUserLogon - The value of lifecycleManager.model.currentUserLogon. + * @param {Integer} debounceTimeMs - The time in milliseconds for which to debounce after last proximityTrigger was fired. + * @param {String} gpiiKey - The GPII key of the user for whom the proximityTrigger was fired. + * @return {Boolean} Returns true if the proximityTrigger should be ignored (i.e. is considered a bounce). This is done according + * to the following rule: any RFID actions is ignored for if a login/logout for is in progress + * OR if the last login/logout process for finished less than {debounceTimeMs} milliseconds ago + */ + gpii.lifecycleManager.isBounce = function (currentLogonChange, currentUserLogon, debounceTimeMs, gpiiKey) { + // if login/logout is in progress for we consider it a bounce + var now = Date.now(); + + if (currentLogonChange.gpiiKey === "noUser" && currentLogonChange.type === "login" && !currentLogonChange.inProgress) { + return currentUserLogon.gpiiKey === gpiiKey && now - currentUserLogon.timeStamp < debounceTimeMs; + } else if (gpiiKey === currentLogonChange.gpiiKey) { + return currentLogonChange.inProgress || now - currentLogonChange.timeStamp < debounceTimeMs; + } else { + return false; + } + }; + + /** + * Function to perform proximityTriggered keyin and keyout. + * + * @param {Component} that - An instance of gpii.lifecycleManager. + * @param {String} gpiiKey - The GPII key of the user for whom the keyin or keyout was performed. + * @return {Promise} The promise whose resolved value contains the logon success message and rejected value contains + * the fail message. + */ + gpii.lifecycleManager.performProximityTriggered = function (that, gpiiKey) { + fluid.log("proximity triggered by GPII key: ", gpiiKey); + var promiseTogo = fluid.promise(); + + // find currently logged in user: + var activeGpiiKey = that.getActiveSessionGpiiKey(); + + if (that.isBounce(gpiiKey)) { + fluid.log("Proximity trigger ignored due to bounce rules"); + that.errorResponse(promiseTogo, "Proximity trigger ignored due to bounce rules. Please wait current logon change is complete", 429); + return promiseTogo; + } + + // if the login user is "reset", trigger the reset process via login with "reset" key + if (gpiiKey === "reset") { + promiseTogo = that.addToUserLogonRequestQueue({ + gpiiKey: "reset", + logonState: "login" + }); + return promiseTogo; + } + + // if user is already logged in, log them out. If it's a different user who is logged in + // log that user out before logging in with the new user + if (activeGpiiKey) { + var previousUser = activeGpiiKey; + if (previousUser !== gpiiKey) { + that.addToUserLogonRequestQueue({ + gpiiKey: previousUser, + logonState: "logout" + }); + promiseTogo = that.addToUserLogonRequestQueue({ + gpiiKey: gpiiKey, + logonState: "login" + }); + } else { + promiseTogo = that.addToUserLogonRequestQueue({ + gpiiKey: previousUser, + logonState: "logout" + }); + } + } + return promiseTogo; + }; + + /** + * Shared function to perform keyin and keyout. + * + * @param {Component} that - An instance of gpii.lifecycleManager. + * @param {String} gpiiKey - The GPII key of the user for whom the keyin or keyout was performed. + * @param {Object} loginInfo - [optional] Extra login information + * @param {Boolean} loginInfo.isEnvironmentalLogin - Whether this login is an environmental login. For example, + * logins via USB or NFC are not environmental login while the windows auto logins are environmental login. When + * this argument is not provided, this login is considered as a non-environmental login. + * @return {Promise} The promise whose resolved value contains the logon success message and rejected value contains + * the fail message. + */ + gpii.lifecycleManager.performLogin = function (that, gpiiKey, loginInfo) { + // If the active key is "noUser", log it out before logging in another key + var activeGpiiKey = that.getActiveSessionGpiiKey(); + var lastLogonRequest = that.userLogonRequestQueue[that.userLogonRequestQueue.length - 1]; + loginInfo = loginInfo || {}; + + // Add the logout of noUser to the user logon request queue when: + // 1. There is a current active key; + // 2. The last logon request in the queue is to key in this current active key. This case sometimes occurs + // when a key in request is still in progress or is waiting in the logon request queue to be processed. + if (activeGpiiKey || lastLogonRequest && lastLogonRequest.gpiiKey === activeGpiiKey && lastLogonRequest.logonState === "login") { + that.addToUserLogonRequestQueue({ + gpiiKey: activeGpiiKey, + logonState: "logout" + }); + } + + if (loginInfo.isEnvironmentalLogin) { + that.applier.change("lastEnvironmentalLoginGpiiKey", gpiiKey); + } + + // Login the requested key + var logonPromise = that.addToUserLogonRequestQueue({ + gpiiKey: gpiiKey, + logonState: "login" }); + return logonPromise; }; - gpii.resolversToFetcher = function (resolvers) { - return function (parsed) { - var resolver = resolvers[parsed.context]; - return !resolver? undefined : ( - typeof(resolver) === "function" ? - resolver(parsed.path) : fluid.get(resolver, parsed.path)); - }; + /** + * Shared function to perform keyout. + * + * @param {Component} that - An instance of gpii.lifecycleManager. + * @param {String} gpiiKey - The GPII key of the user for whom the keyin or keyout was performed. + * @return {Promise} The promise whose resolved value contains the logon success message and rejected value contains + * the fail message. + */ + gpii.lifecycleManager.performLogout = function (that, gpiiKey) { + var logonPromise = that.addToUserLogonRequestQueue({ + gpiiKey: gpiiKey, + logonState: "logout" + }); + return logonPromise; }; - gpii.combineFetchers = function (main, fallback) { - return fallback ? function (parsed) { - var fetched = main(parsed); - return fetched === undefined? fallback(parsed) : fetched; - } : main; + /** + * To re-apply the last environmental login. + * This function is used by gpii-app to easily trigger a re-login of the last environmental login. + * The replay is only performed when: + * 1. The active GPII key is "noUser", in which case for some reason (e.g. the "reset to standard" function has + * been activated) the user has become logged out. + * 2. The active GPII key is the environmental login key. + * + * @param {Component} that - An instance of gpii.lifecycleManager. + * @param {String} lastEnvironmentalLoginGpiiKey - The GPII key of the last environmental login. + * @return {Promise} The promise whose resolved value contains the login success message and rejected value contains + * the fail message. + */ + gpii.lifecycleManager.replayEnvironmentalLogin = function (that, lastEnvironmentalLoginGpiiKey) { + var promiseTogo = fluid.promise(); + var activeGpiiKey = that.getActiveSessionGpiiKey(); + if (activeGpiiKey === "noUser" || activeGpiiKey === lastEnvironmentalLoginGpiiKey) { + promiseTogo = that.performLogin(lastEnvironmentalLoginGpiiKey, true); + } else { + promiseTogo.reject({ + isError: true, + message: "Replay environmental login ignored because the current active GPII key is not \"noUser\" or the last GPII key used for the environmental login" + }); + } + return promiseTogo; }; - // TODO: Use this silly system until demands blocks are fixed via FLUID-4392 - // The idea is that users will derive grades from this one which add in - // further resolvers, and then issue a single demands block which replaces - // this class with their own impl. Still doesn't easily allow for multi-dim. - // extension - fluid.defaults("gpii.lifecycleManager.standardResolverConfig", { - gradeNames: ["fluid.littleComponent", "autoInit"], - resolvers: { - environment: "gpii.lifecycleManager.environmentResolver" + /** + * This function is used in 2 cases: + * 1. Report an error at the early stage of a logon request processing before a dynamic user logon request component + * is created. At this moment, the report is onto a request promise by calling requestPromise.reject(). An typical + * example is when a proximityTriggered request is bounced. See performProximityTriggered(); + * 2. Report an error after a dynamic user logon request component ("gpii.lifecycleManager.loginRequest" or + * "gpii.lifecycleManager.logoutRequest") has been constructed and the error occurs when the request component is processing + * the logon. This error will be reported to the request component via request.events.onError.fire(). The example of this + * type spreads out on the handler grade gpii.lifecycleManager.userLogonHandling. + * + * @param {Promise|Component} togo - When `togo` is a requst promise, its reject() is invoked with the constructed message. + * When `togo` is an instance of "gpii.lifecycleManager.loginRequest" or "gpii.lifecycleManager.logoutRequest", the error is + * reported by firing onError event with the constructed message. + * @param {String} message - The error message to be included in the constructed message. + * @param {Number} statusCode - The status code to be included in the constructed message. + * @param {Boolean} ignoreUserErrors - The flag that indicate whether this error should be reported to users via userErrors. + */ + gpii.lifecycleManager.errorResponse = function (togo, message, statusCode, ignoreUserErrors) { + var error = { + statusCode: statusCode || 500, + message: message || "Unknown error occurred on logon change action", + ignoreUserErrors: ignoreUserErrors || false + }; + + if (fluid.isPromise(togo)) { + togo.reject(error); + } else { + togo.events.onError.fire(error); } - }); + }; + + /** + * Manually constructs the functions in the actionQueueFunctions options. + * This is done manually to avoid resolving the "{that}" part of the function to be called (e.g. "{that}.processStart") + * @param {Component} that - An instance of gpii.lifecycleManager. + */ + gpii.lifecycleManager.createActionQueueFunctions = function (that) { + fluid.each(that.options.actionQueueFunctions, function (queueFunc, invokerName) { + that[invokerName] = function (arg) { + return that.addToActionQueue({ + func: queueFunc, + invokerName: invokerName, + arg: arg + }); + }; + }); + }; + + /** + * Add lifecycle instructions to the payload. + * @param {Object} fullPayload - The payload to add lifecycle instructions. + * @return {Object} - The payload that has lifecycle instructions added. + */ + gpii.lifecycleManager.addLifecycleInstructionsToPayload = function (fullPayload) { + fullPayload.activeConfiguration = { + inferredConfiguration: fluid.extend(true, {}, fullPayload.matchMakerOutput.inferredConfiguration[fullPayload.activePrefsSetName]) + }; + var lifecycleInstructions = gpii.transformer.configurationToSettings(fullPayload.activeConfiguration.inferredConfiguration, fullPayload.solutionsRegistryEntries); + fluid.set(fullPayload, "activeConfiguration.lifecycleInstructions", lifecycleInstructions); + return fullPayload; + }; + + /** + * Transform preferences to final payload and apply it to the local computer. + * @param {Component} that - An instance of gpii.lifecycleManager. + * @param {Object} preferences - The preferences to be transformed and applied. + * @param {String|Array} prefsAppliedLocalEvent - Fired once the preferences are applied to the local computer. + */ + gpii.lifecycleManager.applyPreferences = function (that, preferences, prefsAppliedLocalEvent) { + fluid.log("Lifecycle Manager: Received updated preferences ", preferences); + + // Grab current session + var userSession = that.getSession(); + + // tweak payload to be in preferences set format and find out if the changed preferences contain new ones + var parsedPrefs = {}; + var hasNewPrefs = false; + var previousPrefs = fluid.get(userSession.model.preferences, ["contexts", userSession.model.activePrefsSetName, "preferences"]); + var oldPrefsPaths = gpii.transformer.computeDemandedSettingsPaths(previousPrefs); + + fluid.each(preferences, function (spec, pref) { + // Filter out preferences in the pspChannel request that have no value change but were initially recorded + // in "pspChannel.model.settingControls" for reporting default schema values + if (spec.value !== undefined) { + var prefPath = fluid.pathUtil.parseEL(pref); + fluid.set(parsedPrefs, prefPath, spec.value); + + if (oldPrefsPaths.indexOf(prefPath[0]) === -1) { + hasNewPrefs = true; + } + } + }); + + // GPII-3378: 2 routes for applying incoming preferences based on whether they contain new preferences that are not + // part of userSession.model.preferences: + // 1. When there are new preferences, preform a round of local matchmaking and use its result to apply preferences; + // 2. When there aren't new preferences, use the previous matchmaking result from userSession.model to apply preferences. + gpii.lifecycleManager[hasNewPrefs ? "applyWithNewPrefs" : "applyExistingPrefs"](that, userSession, parsedPrefs, prefsAppliedLocalEvent); + }; + + // This function is called when preferences to be applied contain new preferences that are not in session.model.preferences. + // It performs a round of local matchmaking and uses the matchmaking result to apply all preferences including new ones. + // This is because when GPII runs in the untrusted config, for the security reason, the matchamking results fetched from + // the cloud for the previous preferences only contains the filtered results on solution registry and inferred configurations + // (etc) that are relevant to the previous preferences. This result is not enough for applying new preferences. + gpii.lifecycleManager.applyWithNewPrefs = function (that, session, parsedPrefs, prefsAppliedLocalEvent) { + var newPrefsSet = {}; + fluid.set(newPrefsSet, ["contexts", session.model.activePrefsSetName, "preferences"], parsedPrefs); + var updatedPrefSet = fluid.extend({}, session.model.preferences, newPrefsSet); + var activeGpiiKey = that.getActiveSessionGpiiKey(); + + // Perform a local matchmaking to get matched solutions for existing and new preferences. + var matchMakingPromise = that.privateMatchMaker.doMatch(activeGpiiKey, updatedPrefSet); + + matchMakingPromise.then(function (newPayload) { + var fullPayload = fluid.extend(true, {}, session.model); + fluid.set(fullPayload, ["preferences"], updatedPrefSet); + fluid.set(fullPayload, ["matchMakerOutput", "inferredConfiguration"], newPayload.matchMakerOutput.inferredConfiguration); + fluid.set(fullPayload, ["solutionsRegistryEntries"], newPayload.solutionsRegistryEntries); + fluid.set(fullPayload, ["currentPreferences"], gpii.lifecycleManager.getPreferencesForSet(fullPayload.preferences, fullPayload.activePrefsSetName)); + that.addLifecycleInstructionsToPayload(fullPayload); + + session.applier.change("", fullPayload, "ADD", "userUpdate"); + that.update(fullPayload).then(prefsAppliedLocalEvent.fire); + }); + }; + + // Being the counterpart of applyWithNewPrefs(), this function is called when preferences to be applied only contain + // existing preferences that are in session.model.preferences. It uses the existing matchmaking result stored in + // session.model to apply these preferences. + gpii.lifecycleManager.applyExistingPrefs = function (that, session, parsedPrefs, prefsAppliedLocalEvent) { + // Only the user updated preferences will be saved to the cloud, which is identified by firing the model change request + // with source === "userUpdate". Note that changes requests on model.preferences can also be triggered by initial + // key-in or the change of the preferences set. The preferences change from these actions should not be saved to the cloud. + session.applier.change(["preferences", "contexts", session.model.activePrefsSetName, "preferences"], parsedPrefs, "ADD", "userUpdate"); + + // get new inferred configuration to be applied: + var newInferred = gpii.matchMakerFramework.utils.updateInferredConfiguration(session.model.preferences, session.model.matchMakerOutput.inferredConfiguration, session.model.solutionsRegistryEntries); + + // full payload from session: + var fullPayload = fluid.extend(true, {}, session.model); + fluid.set(fullPayload, ["matchMakerOutput", "inferredConfiguration"], newInferred); + that.addLifecycleInstructionsToPayload(fullPayload); + + session.applier.change("", { + activeConfiguration: fullPayload.activeConfiguration, + currentPreferences: gpii.lifecycleManager.getPreferencesForSet(fullPayload.preferences, fullPayload.activePrefsSetName) + }); + + that.update(fullPayload).then(prefsAppliedLocalEvent.fire); + }; + + /** + * Read corresponding setting values of the requested preferences. The read result is applied as + * a model change to userSession.model.currentPreferences. + * @param {Component} that - An instance of gpii.lifecycleManager. + * @param {Object} preferences - The preferences to read settings for. An example is: + * "http://registry\\.gpii\\.net/common/magnification": { + * value: 3 + * } + * @param {Event} preferenceReadSuccessEvent - The event fired when the read succeeds. + * @param {Event} preferenceReadFailEvent - The event fired when the read fails. + */ + gpii.lifecycleManager.readPreferences = function (that, preferences, preferenceReadSuccessEvent, preferenceReadFailEvent) { + fluid.log("Lifecycle Manager: Received preferences to read ", preferences); + var session = that.getSession(); + + // Mock values for running preferences thru matchMaker + var parsedPreferences = {}; + var activePrefsSetName = "gpii-default"; + + fluid.each(preferences, function (spec, pref) { + var prefPath = fluid.pathUtil.parseEL(pref); + fluid.set(parsedPreferences, prefPath, spec.value); + }); + + var onePrefsSet = {}; + fluid.set(onePrefsSet, ["contexts", activePrefsSetName, "preferences"], parsedPreferences); + + // Perform a local matchmaking to get matched solutions for preferences. + var matchMakingPromise = that.privateMatchMaker.doMatch("readSetting", onePrefsSet); + matchMakingPromise.then(function (payload) { + fluid.set(payload, ["activePrefsSetName"], activePrefsSetName); + fluid.set(payload, ["preferences"], onePrefsSet); + that.addLifecycleInstructionsToPayload(payload); + + // compile a list of solution registry entries for all matched solutoins + var matchMakerSolutionRegistryEntries = fluid.get(payload, ["activeConfiguration", "lifecycleInstructions"]); + var settingsPromise = that.read(matchMakerSolutionRegistryEntries); + + settingsPromise.then(function (allSettingValues) { + // As lifecycleManager.read() returns values for all settings that matched solutions support, + // only filter out setting values that are in the matchmaker result. + var matchMakerSettings = gpii.lifecycleManager.getMatchMakerSettings(allSettingValues, matchMakerSolutionRegistryEntries); + + // Transform settings to preferences + var transformed = gpii.lifecycleManager.transformSettingsToPrefs(matchMakerSettings, matchMakerSolutionRegistryEntries); + + // Filter matched preferences + var matchedPrefs = gpii.lifecycleManager.filterMatchedPrefs(transformed, parsedPreferences); + + // Use cases discovered that will result in "matchedPrefs" being an empty object include: + // 1. Settings handler that have get() defined but not implemented - https://issues.gpii.net/browse/GPII-4217 + // 2. "inverseCapabilitiesTransformations" blocks for one or more settings are not defined. In this case, + // although setting values are read successfully, when these values are transformed into preference values via + // gpii.lifecycleManager.transformSettingsToPrefs(), an empty object will return - https://issues.gpii.net/browse/GPII-4216 + if ($.isEmptyObject(matchedPrefs)) { + preferenceReadFailEvent.fire(); + } else { + session.applier.change(["currentPreferences"], fluid.extend(true, fluid.copy(session.model.currentPreferences), matchedPrefs)); + preferenceReadSuccessEvent.fire(); + } + }, preferenceReadFailEvent.fire); + }); + }; + + /* + * Get preferences block for a certain context. + * @param {Object} prefsSets - An object containing one or more preferences sets. + * @param {String} prefsSetName - A name of a preferences set. + * @return {Object} The actual preferences for the active context. + */ + gpii.lifecycleManager.getPreferencesForSet = function (prefsSets, prefsSetName) { + var prefs = fluid.get(prefsSets, ["contexts", prefsSetName, "preferences"]); + return prefs || {}; + }; + + /** + * Return the user's GPII key that is associated with the active session. + * @param {Component} userSession - An instance of gpii.lifecycleManager.userSession. + * @return {String} The GPII key of the active session. + */ + gpii.lifecycleManager.getActiveSessionGpiiKey = function (userSession) { + return userSession.model.gpiiKey; + }; - gpii.lifecycleManager.environmentResolver = function (name) { - return process.env[name]; + /** + * Return the active session. The logic is: + * 1. If a GPII key is not provided, return the instance of gpii.lifecycleManager.userSession; + * 2. If a GPII key is provided: + * 2.1 The given key is "restore", return the instance of gpii.lifecycleManager.restoreSession; + * 2.2 The given key is NOT "restore", checks if gpii.lifecycleManager.userSession is handling the given GPII key. + * If yes, return gpii.lifecycleManager.userSession. Otherwise, return undefined. + * @param {Component} userSession - An instance of gpii.lifecycleManager.userSession. + * @param {Component} restoreSession - An instance of gpii.lifecycleManager.restoreSession. + * @param {String} [gpiiKey] - [optional] A GPII key. If provided, return the matched session for the given GPII + * key. If no matched session is found, return undefined. + * @return {Component} Could be an instance of gpii.lifecycleManager.userSession or an instance of + * gpii.lifecycleManager.restoreSession or undefined. + */ + gpii.lifecycleManager.getSession = function (userSession, restoreSession, gpiiKey) { + return gpiiKey ? userSession.model.gpiiKey === gpiiKey ? userSession : + gpiiKey === "restore" ? restoreSession : undefined + : userSession; }; - // Transforms the handlerSpec (handler part of the payload) to the model - // required by the settingsHandler + /* Transforms the handlerSpec (handler part of the transformer's response payload) to a form + * accepted by a settingsHandler - we use a 1-element array holding the payload for a single solution + * per handler + */ gpii.lifecycleManager.specToSettingsHandler = function (solutionId, handlerSpec) { - var returnObj = {}; + var returnObj = {}, + settings = {}; + + if (handlerSpec.supportedSettings === undefined) { + // if supportedSettings directive is not present, pass all settings: + settings = handlerSpec.settings; + } else { + // we cant simply use fluid.filterKeys because that wont handle the cases where + // there are 'undefined' values for the keys in handlerSpec.settings + // TODO: Kaspar believes that the reason for filtering happening here rather than in the MatchMaker is + // that the transformation of common terms into application specific settings doesn't occur until the + // transformation stage - so we don't have the full list of app-specific settings to filter until now. + for (var settingName in handlerSpec.supportedSettings) { + if (handlerSpec.settings && settingName in handlerSpec.settings) { + settings[settingName] = handlerSpec.settings[settingName]; + } + } + } returnObj[solutionId] = [{ - settings: handlerSpec.settings, + settings: settings, options: handlerSpec.options }]; - return returnObj; // NB array removed here + + return returnObj; }; - gpii.lifecycleManager.invariantSettings = { - "type": "type", - "options": "options" + // Transform the response from the handler SET to a format that we can persist in models before passing to handler SET on restore + // - "oldValue" becomes {type: "ADD", value: } + // - `undefined` value becomes {type: "DELETE"} + gpii.lifecycleManager.responseToSnapshot = function (solutionId, handlerResponse) { + var unValued = gpii.settingsHandlers.setResponseToSnapshot(handlerResponse); + var armoured = gpii.settingsHandlers.settingsPayloadToChanges(unValued); + // Note - we deal in these 1-element arrays just for simplicity in the LifecycleManager. A more efficient + // implementation might send settings for multiple solutions to the same settingsHandler in a single request. + // Note that in the session's snapshots, this level of array containment has been removed. + return fluid.get(armoured, [solutionId, 0]); }; - gpii.lifecycleManager.responseToSnapshotRules = { - "*.*.settings.*": { - transform: { - type: "value", - inputPath: "oldValue" - } - } + gpii.lifecycleManager.invokeSettingsHandlerGet = function (that, solutionId, handlerSpec) { + // first prepare the payload for the settingsHandler in question - a more efficient + // implementation might bulk together payloads destined for the same handler + var settingsHandlerPayload = gpii.lifecycleManager.specToSettingsHandler(solutionId, handlerSpec); + var resolvedName = that.nameResolver.resolveName(handlerSpec.type, "settingsHandler"); + return gpii.settingsHandlers.dispatchSettingsHandlerGet(resolvedName, settingsHandlerPayload); }; - fluid.model.escapedPath = function () { - var path = ""; - for (var i = 0; i < arguments.length; ++i) { - path = fluid.pathUtil.composePath(path, arguments[i]); + /** + * @param {Component} that - The gpii.lifecycleManager + * @param {String} solutionId - The id of the solution for which the settings handler should be invoked + * @param {Object} handlerSpec - A single settings handler specification. + * @param {String} handlerType - The name of the handler block (i.e. "settings" or "launchers") + * @return {Promise} - A promise that will be resolved with a settings snapshot. + * Payload example: + * http://wiki.gpii.net/index.php/Settings_Handler_Payload_Examples + * Transformer output: + * http://wiki.gpii.net/index.php/Transformer_Payload_Examples + */ + gpii.lifecycleManager.invokeSettingsHandlerSet = function (that, solutionId, handlerSpec, handlerType) { + // first prepare the payload for the settingsHandler in question - a more efficient + // implementation might bulk together payloads destined for the same handler + var settingsHandlerPayload = gpii.lifecycleManager.specToSettingsHandler(solutionId, handlerSpec); + var resolvedName = that.nameResolver.resolveName(handlerSpec.type, "settingsHandler"); + var setSettingsPromise = gpii.settingsHandlers.dispatchSettingsHandlerSet(resolvedName, settingsHandlerPayload, that.options.retryOptions); + var togo = fluid.promise(); + var setSettingsFail = function (error) { + togo.reject(error); + var userMessageKey = handlerType === "launchHandlers" ? + handlerSpec.settings.running ? "StartApplicationFail" : "StopApplicationFail" + : "WriteSettingFail"; + that.userErrors.events.userError.fire({ + isError: true, + messageKey: userMessageKey, + originalError: error + }); + }; + + setSettingsPromise.then(function (handlerResponse) { + // update the settings section of our snapshot to contain the new information + var settingsSnapshot = gpii.lifecycleManager.responseToSnapshot(solutionId, handlerResponse); + // Settings handlers may or may not return options (currently it seems they all do) - gain resistance to this by restoring the + // original "options" supplied to them. + fluid.each(handlerSpec, function (entry, key) { + if (key !== "settings") { + settingsSnapshot[key] = fluid.copy(entry); + } + }); + togo.resolve(settingsSnapshot); + }, setSettingsFail); + + return togo; + }; + + + gpii.lifecycleManager.invokeAction = function (action, nameResolver) { + var resolvedName = nameResolver.resolveName(action.type, "action"); + return fluid.invokeGradedFunction(resolvedName, action); + }; + + /* Compensate for the effect of simpleminded merging when applied to a snapshot where an "DELETE" is merged on top of an "ADD" */ + gpii.lifecycleManager.cleanDeletes = function (value) { + if (value.type === "DELETE") { + delete value.value; } - return path; + return value; }; - // Transform the response from the handler to a format that we can pass back - // to it - gpii.lifecycleManager.responseToSnapshot = function (solutionId, handlerResponse) { - var unValued = fluid.model.transform(handlerResponse, - gpii.lifecycleManager.responseToSnapshotRules, {isomorphic: true}); - // TODO: Should eventually be able to do this final stage through - // transformation too - return fluid.get(unValued, fluid.model.escapedPath(solutionId, "0"), - fluid.model.escapedGetConfig); - }; - - // Payload example: - // http://wiki.gpii.net/index.php/Settings_Handler_Payload_Examples - // Transformer output: - // http://wiki.gpii.net/index.php/Transformer_Payload_Examples - gpii.lifecycleManager.invokeSettingsHandlers = function (solutionId, settingsHandlers) { - // array just indexed by number, each one holds one handler for this id - return fluid.transform(settingsHandlers, function (handlerSpec) { - // first prepare the payload for the settingsHandler in question - - // a more efficient implementation might bulk together payloads - // destined for the same handler - var settingsHandlerPayload = - gpii.lifecycleManager.specToSettingsHandler(solutionId, - handlerSpec); - //send the payload to the settingsHandler - var handlerResponse = fluid.invokeGlobalFunction(handlerSpec.type, - [settingsHandlerPayload]); - var settingsSnapshot = gpii.lifecycleManager.responseToSnapshot( - solutionId, handlerResponse); - var invariant = fluid.model.transform(handlerSpec, - gpii.lifecycleManager.invariantSettings); - return $.extend(true, {}, handlerSpec, settingsSnapshot); - // update the settings section of our snapshot to contain the new - // format + /* Remove all the settings blocks from a solutions registry entry */ + gpii.lifecycleManager.removeSettingsBlocks = function (solutionEntry) { + fluid.each(["settingsHandlers", "launchHandlers"], function (handlersBlock) { + solutionEntry[handlersBlock] = fluid.transform(solutionEntry[handlersBlock], function (handler) { + return fluid.filterKeys(handler, "settings", true); + }); }); + return solutionEntry; }; + /* Applies snapshotted settings from a single settingsHandler block attached to a single solution into the "originalSettings" + * model snapshot area in the LifecycleManager's session. Tightly bound to executeSettingsAction, executes one-to-one with it + * with almost identical argument list. + */ + gpii.lifecycleManager.recordSnapshotInSession = function (that, snapshot, solutionId, solutionRecord, session, handlerType, settingsHandlerBlockName, rootAction) { + if (rootAction === "start" || rootAction === "update") { + var toSnapshot = gpii.lifecycleManager.removeSettingsBlocks(fluid.copy(solutionRecord)); - gpii.lifecycleManager.invokeAction = function (action) { - var defaults = fluid.defaults(action.type); - if (!defaults || !defaults.argumentMap) { - fluid.fail("Error in action definition - " + action.type + - " cannot be looked up to a function with a proper argument map: ", - action); + // do not remove the settingshandlers blocks, since we need them when restoring the system. + // This is particularly relevant for launch handlers, where we will need to run the "get" directives on logout to decide + // whether we an application is running or not, and consequently, whether to run the "update" or "stop" block + toSnapshot[handlerType][settingsHandlerBlockName] = snapshot; + // keep the settings that are already stored from the + // original snapshot, but augment it with any settings from the new snapshot + // that were not present in the original snapshot. + // + // This is relevant when doing an update for obvious reasons + // (and tested) in LifecycleManagerTests.js "Updating with normal reference to settingsHandler block, and 'undefined' value stored in snapshot" + // + // It is also relevant for logins ("start" root action), in case a solution is already running. + // This would trigger a call to its "update" block. If that in turn eg. looks like the following + // [ "stop", "configure", "start" ] we would want the original state recorded during the "stop" + // action to persist - even when the "start" block is later run + var mergedSettings = fluid.extend(true, {}, toSnapshot, session.model.originalSettings[solutionId]); + var cleanedSettings = gpii.lifecycleManager.transformSolutionSettings(mergedSettings, gpii.lifecycleManager.cleanDeletes); + session.applier.change(["originalSettings", solutionId], cleanedSettings); + } else if (rootAction === "restore" || rootAction === "stop") { + // no-op - during a restore action we don't attempt to create a further snapshot + } else { + fluid.fail("Unrecognised rootAction " + rootAction); } - var args = []; - fluid.each(defaults.argumentMap, function (value, key) { - args[value] = action[key]; + }; + + /* In the case we are servicing a "restore" rootAction, wrap "dangerous" promises which perform system actions so + * that they do not reject, to ensure that we continue to try to restore the system come what may - GPII-2160 + */ + gpii.lifecycleManager.wrapRestorePromise = function (promise, rootAction) { + return rootAction === "restore" && fluid.isPromise(promise) ? + gpii.rejectToLog(promise, " while restoring journal snapshot") : promise; + }; + + /** + * @param {Object} that - The lifecycle manager component. + * @param {String} solutionId - The ID of the solution for which to execute the settings. + * @param {Object} solutionRecord - The solution registry entry for the solution. + * @param {Component} session - The current session component. This function will attach the + * solution record to the 'currentSettings' of the session's model (if successful). + * @param {String} handlerType - The name of the handler block (i.e. "settings" or "launchers"). + * @param {String} settingsHandlerBlockName - Should be a reference to a settings block from the + * settingsHandlers section. + * @param {String} rootAction - The root action on the LifecycleManager which is being serviced: "start", "stop", + * "update", "restore" or "isRunning". + * @return {Function} A nullary function (a task), that once executed will set the settings returning a promise + * that will be resolved once the settings are successfully set. + */ + gpii.lifecycleManager.executeSettingsAction = function (that, solutionId, solutionRecord, session, handlerType, settingsHandlerBlockName, rootAction) { + var settingsHandlerBlock = solutionRecord[handlerType][settingsHandlerBlockName]; + if (settingsHandlerBlock === undefined) { + fluid.fail("Reference to non-existing settingsHandler block named " + settingsHandlerBlockName + + " in solution " + solutionId); + } + return function () { + var expanded = session.localResolver(settingsHandlerBlock); + if (rootAction === "isRunning") { // only run get and return directly if where checking for running applications + return that.invokeSettingsHandlerGet(solutionId, expanded); + } else { + var settingsPromise = that.invokeSettingsHandlerSet(solutionId, expanded, handlerType); + + settingsPromise.then(function (snapshot) { + session.applier.change(["currentSettings", solutionId], solutionRecord); + gpii.lifecycleManager.recordSnapshotInSession(that, snapshot, solutionId, solutionRecord, session, + handlerType, settingsHandlerBlockName, rootAction); + }); + return gpii.lifecycleManager.wrapRestorePromise(settingsPromise, rootAction); + } + }; + }; + + /** For the complete entry for a single solution, transform each settingsHandler block by a supplied function - traditionally + * either gpii.settingsHandlers.changesToSettings or gpii.settingsHandlers.settingsToChanges . + * This is traditionally called during the "stop" action to unarmour all the settingsHandler blocks by converting from changes + * back to settings. In a future version of the SettingsHandler API, this will not be necessary. + * This is called during the "stop" action to convert the snapshotted "originalSettings" model material back to material + * suitable for being sent to executeSettingsAction, as well as at the corresponding point during the "journal restore" + * operation + * @param {Object} solutionSettings - A settings block for a single solution, holding a member named `settingsHandlers`. + * @param {Function} transformer - A function which will transform one settingsHandlers block in the supplied `solutionSettings`. + * @return {Promise} A promise that will be resolved with the results of the transformation or rejected on error. + */ + gpii.lifecycleManager.transformSolutionSettings = function (solutionSettings, transformer) { + var togo = fluid.copy(solutionSettings); // safe since armoured + fluid.each(["settingsHandlers", "launchHandlers"], function (handlersBlock) { + togo[handlersBlock] = fluid.transform(solutionSettings[handlersBlock], function (handler) { + return gpii.settingsHandlers.transformOneSolutionSettings(handler, transformer); + }); + // avoid the handlerBlock to be `undefined` if it's not defined in solutionsRegistry entry. This is necessary for testing assertion purposes + if (togo[handlersBlock] === undefined) { + delete togo[handlersBlock]; + } }); - return fluid.invokeGlobalFunction(action.type, args); + + return togo; + }; + + // TODO: THIS IS CALLED ONLY FROM TEST CASES! + /* As for gpii.lifecycleManager.transformSolutionSettings, only transforms the complete collection of stored solutions + * (e.g. a value like session.model.originalSettings) + */ + gpii.lifecycleManager.transformAllSolutionSettings = function (allSettings, transformer) { + return fluid.transform(allSettings, function (solutionSettings) { + return gpii.lifecycleManager.transformSolutionSettings(solutionSettings, transformer); + }); + }; + + /** Upgrades a promise rejection payload (or Error) by suffixing an additional "while" reason into its `message` field. If the + * payload is already an Error, its `message` field will be updated in place, otherwise a shallow clone of the original payload + * will be taken to perform the update. + * @param {Object|Error} originError - A rejection payload. This should (at least) have the member `isError: true` set, as well + * as a String `message` holding a rejection reason. + * @param {String} whileMsg - A message describing the activity which led to this error. + * @return {Object} The rejected payload formed by shallow cloning the supplied argument (if it is not an `Error`) and + * suffixing its `message` member. + */ + // TODO: Duplicate of kettle.upgradeError to avoid dependence on Kettle in this file. This needs to go into a new module once + // Kettle is factored up. + gpii.upgradeError = function (originError, whileMsg) { + var error = originError instanceof Error ? originError : fluid.extend({}, originError); + error.message = originError.message + whileMsg; + return error; + }; + + /** Transform a promise into one that always resolves, by intercepting its reject action and converting it to a logging action + * plus a resolve with an optionally supplied value. + * The error payload's message will be logged to `fluid.log` with the priority `fluid.logLevel.WARN`. + * @param {Promise} promise - The promise to be transformed. + * @param {String} whileMsg - A suffix to be applied to the message, by the action of the utility `gpii.upgradeError`. + * This will typically begin with the text " while". + * @param {Any} [resolveValue] - [optional] An optional value to be supplied to `resolve` of the returned promise, when the + * underlying promise rejects. + * @return {Promise} The wrapped promise which will resolve whether the supplied promise resolves or rejects. + */ + // TODO: In theory this is a fairly generic promise algorithm, but in practice there are some further subtleties - for + // example, a better system would instead replace fluid.promise.sequence with a version that allowed some form of + // "soft rejection" so that there might be a chance to signal failures to the user. + gpii.rejectToLog = function (promise, whileMsg, resolveValue) { + var togo = fluid.promise(); + promise.then(function (value) { + togo.resolve(value); + }, function (error) { + gpii.upgradeError(error, whileMsg); + fluid.log(fluid.logLevel.WARN, error.message); + fluid.log(fluid.logLevel.WARN, error.stack); + togo.resolve(resolveValue); + }); + return togo; }; - // Returns the results from any settings action, builds up action returns in - // argument "actionResults" - gpii.lifecycleManager.executeActions = function (solutionId, settingsHandlers, actions, sessionState) { - var togo; - for (var i = 0; i < actions.length; ++ i) { - var action = actions[i]; + /* + * Infer an action block such as "start", "stop", "configure", etc. + * + * If some lifecycle block is not present, the system will use default actions for each block. These are as follows: + * * "start", "stop" and "isRunning": will by default be references to all launch handler blocks + * * "configure" and "restore": will by default be references to all settings handler blocks + * * "update": depends on the solutions "liveness" value where "live" means set settings only and + * "liveRestart"/"manualRestart"/"OSRestart" means a [stop, configure, start] cycle + */ + gpii.lifecycleManager.inferActionBlockSteps = function (actionName, solutionRecord) { + var steps = []; + if (["start", "stop", "isRunning"].indexOf(actionName) !== -1 && solutionRecord.launchHandlers) { + // grab all lifecycle block names and make these the steps + steps = fluid.transform(Object.keys(solutionRecord.launchHandlers), function (stepName) { + return "launchers." + stepName; + }); + } else if (["configure", "restore"].indexOf(actionName) !== -1 && solutionRecord.settingsHandlers) { + // grab all settingshandler block names and make these the steps + steps = fluid.transform(Object.keys(solutionRecord.settingsHandlers), function (stepName) { + return "settings." + stepName; + }); + } else if (actionName === "update") { + // check liveness: + var liveness = gpii.matchMakerFramework.utils.getLeastLiveness([solutionRecord]); + if (liveness === "live") { + steps = [ "configure" ]; + } else if (["liveRestart", "manualRestart", "OSRestart"].indexOf(liveness) !== -1) { + steps = [ "stop", "configure", "start"]; + } + } + return steps; + }; + + /** Called for each solution during "start", "stop" and "update" phases + * Actions to be performed are held in array "actions" and the settingsHandlers block from "solutions" (either Transformer + * output, or snapshot output from "start" phase) encodes the settings to be set. + * Returns the results from the settings action present in the list, and builds up action returns in session state + * "actionResults" field (so that these may be referenced from context expressions in further actions). + * @param {Component:LifecycleManager} that - The LifecycleManager instance of type `gpii.lifecycleManager`. + * @param {String} solutionId - The id of the solution for which actions are to be performed. + * @param {Object} solutionRecord - Either the solution's registry entry (on start) or the fabricated "restore" entry (on stop). + * @param {Component:LifecycleManagerSession} session - The LifecycleManager Session of type `gpii.lifecycleManager.session. + * @param {String} actionBlock - The key for the particular block in the solutions registry entry which is being acted on - this + * may differ from `rootAction` because, e.g. an "update" action may be serviced by stopping and starting the solution, etc. + * @param {String} rootAction - The root action on the LifecycleManager which is being serviced: "start", "stop", "update" or + * "restore". + * @return {Promise} A promise that will resolve when all the sequential actions have been performed. Any returned values + * will be built up as a side-effect within session.currentSettings, session.originalSettings, session.actionResults etc. + * If the `rootAction` is "restore", this promise will never reject, and instead the system will continue to make best + * efforts to continue executing actions even in the face of failures. + */ + + gpii.lifecycleManager.executeActions = function (that, solutionId, solutionRecord, session, actionBlock, rootAction) { + var steps = solutionRecord[actionBlock] || gpii.lifecycleManager.inferActionBlockSteps(actionBlock, solutionRecord); + if (steps === undefined) { + fluid.log("No " + actionBlock + " actions defined for solution " + solutionId); + return fluid.promise().resolve(); + } + var sequence = fluid.transform(steps, function (action) { if (typeof(action) === "string") { - if (action === "setSettings" || action === "restoreSettings") { - var expanded = sessionState.localResolver(settingsHandlers); - togo = gpii.lifecycleManager.invokeSettingsHandlers( - solutionId, expanded); + // if the action is a reference to a settings block (settings. where is a key to + // the settings handler block) + if (action.startsWith("settings.")) { + var settingsHandlerBlockName = action.substring("settings.".length); + return gpii.lifecycleManager.executeSettingsAction(that, solutionId, solutionRecord, session, "settingsHandlers", settingsHandlerBlockName, rootAction); + } else if (action.startsWith("launchers.")) { + var launchHandlerBlockName = action.substring("launchers.".length); + + if (actionBlock === "isRunning") { + // if we're just checking for the run state, don't actually modify the solutionRecord with settings: + solutionRecord = fluid.copy(solutionRecord); + } + // Set appropriate settings (i.e. { running: true } if we're in a start block, else { running: false }. + var launchSettings = { running: actionBlock === "start" ? true : false }; + fluid.set(solutionRecord, [ "launchHandlers", launchHandlerBlockName, "settings"], launchSettings); + return gpii.lifecycleManager.executeSettingsAction(that, solutionId, solutionRecord, session, "launchHandlers", launchHandlerBlockName, rootAction); + } else if (actionBlock === "update") { + // Keywords: "start", "stop", "configure" are allowed here as well, and + // and will result in evaluating the respective block + // TODO (GPII-1230) Fix this up so we don't always run the full start and stops (including) + // system restoration, etc. + if (action === "start" || action === "configure" || action === "stop") { + return function () { + return that.executeActions(solutionId, solutionRecord, session, action, rootAction); + }; + } else { + fluid.fail("Unrecognised string action in LifecycleManager: " + action + + " inside 'update' section for solution " + solutionId); + } } + } else { // We allow free lifecycle actions, but strongly discourage them, since we don't have control of logging state like with settings/launch handlers + return function () { + var expanded = session.localResolver(action); + var result = gpii.lifecycleManager.invokeAction(expanded, that.nameResolver); + if (action.name) { + session.applier.change(["actionResults", action.name], result); + } + // TODO: It seems we have never supported asynchronous actions + }; } - else { - var expanded = sessionState.localResolver(action); - var result = gpii.lifecycleManager.invokeAction(expanded); - if (action.name) { - sessionState.actionResults[action.name] = result; - } + }); + return fluid.promise.sequence(sequence); + }; + + /** Invoked on "start", "update", "stop" and "restore" phases - in addition to forwarding to + * gpii.lifecycleManager.executeActions, it is responsible for saving the settings that are being set (when the fullSnapshot + * is true) and storing the list of applied solutions to the session state + * + * @param {Component} that - The gpii.lifecycleManager. + * @param {String} solutionId - The ID of the solution. + * @param {Object} solutionRecord - a solution record with settings that are to be applied to the system. + * @param {Object} session - The object holding the state of the system. This is updated in place by the settings application + * process, if `rootAction` is start. + * @param {Array} lifecycleBlockKeys - Array of ordered strings denoting which lifecycle blocks to run (supported values here + * are "configure", "start" and/or "update"). + * @param {String} rootAction - Either "start", "update" or "stop" depending on the lifecycleManager phase which is executing. + * @return {Promise} The same promise yielded by executeActions - the stateful construction of the session state is tacked + * onto this promise as a side-effect. + */ + gpii.lifecycleManager.applySolution = function (that, solutionId, solutionRecord, session, lifecycleBlockKeys, rootAction) { + var promises = fluid.transform(lifecycleBlockKeys, function (key) { + // Courtesy to allow GPII-580 journalling tests to be expressed in-process - better expressed with FLUID-5790 cancellable promises + if (!fluid.isDestroyed(that)) { + return that.executeActions(solutionId, solutionRecord, session, key, rootAction); } - } - return togo; + }); + return fluid.promise.sequence(promises); }; /** + * Based on the current state of an application (ie. if it is running or not) and the desired state, + * this function returns an array of actions for the lifecycle manager to run to get it to its + * recorded state + * + * @param {Boolean} currentRunState - True if application is currently running。 + * @param {Boolean} desiredRunState - True if application should be running. + * @param {Boolean} isRestore - If this is true, a "restore" will be used as the configuration action + * in the array that is returned. Else a "configure" will be used. In general, isRestore should be provided and + * true only of this is called by the logout/restore functionality of the system. + * @return {Array} An array of actions (strings) that needs to be run to restore the application + * to its original state. + */ + gpii.lifecycleManager.calculateLifecycleActions = function (currentRunState, desiredRunState, isRestore) { + var configurationType = isRestore ? "restore" : "configure"; + var actions; + if (currentRunState === true) { // if it's already running + if (desiredRunState === false) { // and it was not running on start + actions = [ "stop", configurationType ]; + } else { // else update it + actions = [ "update" ]; + } + } else { // if it is not running + if (desiredRunState === true) { // and it was running when we started + actions = [ configurationType, "start" ]; + } else { // just restore settings + actions = [ "restore" ]; + } + } + return actions; + }; + + /** Common utility used by gpii.lifecycleManager.stop and gpii.lifecycleManager.restoreSnapshot + * @param {Component} that - The gpii.lifecycleManager. + * @param {Component} session - An instance of gpii.lifecycleManager.session component which must contain + * * A `originalSettings` snapshot in its model. + * * A `localResolver` member for expanding material. + * @param {String} rootAction - Must be either "stop" or "restore". + * @return {Promise} A promise for the action of restoring the system. + */ + gpii.lifecycleManager.restoreSystem = function (that, session, rootAction) { + var tasks = []; + fluid.each(session.model.originalSettings, function (changesSolutionRecord, solutionId) { + tasks.push(function () { + // check the current state of the solution to decide whether we should run the + // "restore", "update", or "stop" + return that.getSolutionRunningState(solutionId, changesSolutionRecord, session); + }); + tasks.push(function () { + if (!fluid.isDestroyed(that)) { // See above comment for GPII-580 + var solutionRecord = gpii.lifecycleManager.transformSolutionSettings(changesSolutionRecord, gpii.settingsHandlers.changesToSettings); + // TODO: This is a bit of a mess since we get it from the snapshot rather than from our explicit check for "isRunning" - we assume these agree + var recordedRunState = gpii.lifecycleManager.getSolutionRunningStateFromSnapshot(solutionRecord); + var currentRunState = session.model.runningOnLogin[solutionId]; + + var actions = gpii.lifecycleManager.calculateLifecycleActions(currentRunState, recordedRunState, true); + // build structure for returned values (for later reset) + return that.applySolution(solutionId, solutionRecord, session, actions, rootAction); + } + }); + }); + var sequence = fluid.promise.sequence(tasks); + + // // TODO: In theory we could stop all solutions in parallel + return sequence; + }; + + /** Restore a snapshot of settings, perhaps captured in the journal. This constructs a "fake" session using the special GPII key "restore" + * @param {Component} that - The gpii.lifecycleManager. + * @param {Object} originalSettings - The system snapshot to be restored. + * @return {Promise} A promise for the action of restoring the system + */ + gpii.lifecycleManager.restoreSnapshot = function (that, originalSettings) { + var session = that.restoreSession; + that.events.onSessionStart.fire("gpii.lifecycleManager.restoreSession", "restore"); + session.applier.change("originalSettings", originalSettings); + var restorePromise = gpii.lifecycleManager.restoreSystem(that, session, "restore"); + + return fluid.promise.map(restorePromise, function () { + return { // TODO: The standard response yield is unhelpful, consisting of the returns of any actions in "stop" + message: "The system's settings were restored from a snapshot", + payload: originalSettings + }; + }); + }; + + gpii.lifecycleManager.getSolutionRunningStateFromSnapshot = function (solutionSnapshot) { + // get isRunning entry from snapshot and block to run: + var isRunningBlock = fluid.makeArray(solutionSnapshot.isRunning || gpii.lifecycleManager.inferActionBlockSteps("isRunning", solutionSnapshot))[0]; + if (isRunningBlock && isRunningBlock.indexOf("launchers.") === 0) { + var settingsHandlerBlockName = isRunningBlock.substring("launchers.".length); + var recordedState = fluid.get(solutionSnapshot, ["launchHandlers", settingsHandlerBlockName, "settings", "running"]); + return recordedState; + } + }; + + gpii.lifecycleManager.getSolutionRunningState = function (that, solutionId, solutionRecord, session) { + if (!fluid.isDestroyed(that)) { + var promise = that.executeActions(solutionId, solutionRecord, session, "isRunning", "isRunning"); + var togo = fluid.promise(); + promise.then(function (val) { + var isRunning = fluid.get(val, [0, solutionId, 0, "settings", "running"]); + // TODO: This model field is named very poorly since we update it on logout as well + session.applier.change(["runningOnLogin", solutionId], isRunning); + // Create and return a new "response" variable instead of using session.model.runningOnLogin is because + // session.model.runningOnLogin won't get updated when isRunning === undefined. + var response = {}; + fluid.set(response, ["runningOnLogin", solutionId], isRunning); + togo.resolve(response); + }); + return togo; + } + }; + + /* + * The lifecycleManager action queue is used to hold the high-level actions that needs to happen, + * such as starting the login process, starting logout process, starting the update process. + * The entries in the action queue are of the format { func: , invokerName: , arg: } where + * is the name of a single-argument function that returns a promise. The promise should be resolved when + * the function is complete (including side-effects). is the name of the invoker that was called on the lifecycleManager + * for triggering this addToActionQueue function. + * The action queue is run sequentially, and an item is considered "done" once the promise returned by its + * function is resolved. + */ + gpii.lifecycleManager.addToActionQueue = function (that, queue, item) { + var newItem = fluid.copy(item); + newItem.promise = fluid.promise(); + queue.push(newItem); + if (queue.length === 1) { + that.processActionQueue(); + } + return newItem.promise; + }; + + gpii.lifecycleManager.clearQueue = function (queue, queueType) { + while (queue.length > 0) { + var item = queue.shift(); + item.promise.reject("clearQueue: clearing lifecycleManager promise from the " + queueType + " queue - remaining items: " + queue.length); + } + }; + + gpii.lifecycleManager.processActionQueue = function (that, queue) { + // Pick the first item and process. This item will be removed from the queue once the processing completes + var item = queue[0]; + var func = fluid.makeInvoker(that, { + func: item.func, + args: [ item.arg ] + }, ""); + var promise = func(); + + promise.then(function (val) { + // resolve the original promise of the item; + item.promise.resolve(val); + // The processing on the first item completes. Remove it from the queue. + queue.shift(); + // process next item in action queue if it exists + if (queue.length > 0) { + that.processActionQueue(queue); + } + }, function (error) { + // The processing on the first item completes. Remove it from the queue. + queue.shift(); + fluid.log(fluid.logLevel.FAIL, "An error occurred in an item of the lifecyclemanager's action queue" + + "(invoker: " + item.invokerName + ", func: " + item.func + "), so clearing action queue. Error was ", error); + that.events.onClearActionQueue.fire(); + item.promise.reject(error); + }); + }; + + /* * Structure of lifecycleManager options: * userid: userid, * actions: either start or stop configuration from solutions registry * settingsHandlers: transformed settings handler blocks */ - gpii.lifecycleManager.stop = function (activeSessions, options, callback) { - var userToken = options.userToken; - var sessionState = activeSessions[userToken]; - if (!sessionState) { - callback(false); - return; + gpii.lifecycleManager.processStop = function (that, options) { + var userSession = that.userSession; + if (userSession.model.gpiiKey !== options.gpiiKey) { + var failPromise = fluid.promise(); + failPromise.reject("No session was found when attempting keyout"); + return failPromise; } + var restorePromise = gpii.lifecycleManager.restoreSystem(that, userSession, "stop"); - fluid.each(sessionState.solutions, function (solution) { - gpii.lifecycleManager.executeActions(solution.id, - solution.settingsHandlers, solution.lifecycleManager.stop, - sessionState); + restorePromise.then(function () { + that.events.onSessionStop.fire(that, userSession); + + // Clean up and reinitialize session.model in preparation for the the next key in + userSession.createTime = undefined; + userSession.localFetcher = undefined; + + var transaction = userSession.applier.initiate("local", "SessionCleanup"); + transaction.fireChangeRequest({ + path: "", + type: "DELETE" + }); + transaction.fireChangeRequest({ + path: "", + type: "ADD", + value: { + gpiiKey: undefined, + actionResults: {}, + originalSettings: {}, + currentSettings: {}, + currentPreferences: {} + } + }); + transaction.commit(); + }); + return restorePromise; + }; + + /** + * Read setting values of one or more solutions. + * @param {Object} that - An instance of gpii.lifecycleManager. + * @param {Object} solutions - Solutions with setting to be read from the system. + * @return {Promise} A promise whose resolved value is the result of setting values. + */ + gpii.lifecycleManager.processRead = function (that, solutions) { + var handlerGetPromises = []; + fluid.each(solutions, function (solution, solutionID) { + var newSolution = gpii.lifecycleManager.transformSolutionSettings(solution, gpii.settingsHandlers.changesToSettings); + fluid.each(newSolution.settingsHandlers, function (handlerSpec) { + // If supportedSettings is not specified, don't attempt to get settings + if (!handlerSpec.supportedSettings) { + return; + } + handlerSpec.settings = handlerSpec.supportedSettings; + + // TODO: The code below is to work around https://issues.gpii.net/browse/GPII-3677. + // It transforms the paths in capabilitiesTransforms to the schema information needed for + // executing get() for the settings handler "gpii.windows.spiSettingsHandler". + if (handlerSpec.type === "gpii.windows.spiSettingsHandler") { + fluid.each(handlerSpec.settings, function (handler, handlerKey) { + if (handlerSpec.capabilitiesTransformations[handlerKey].path.transform) { + handlerSpec.settings[handlerKey].path = + handlerSpec.capabilitiesTransformations[handlerKey].path.transform.input; + }; + if (handlerSpec.capabilitiesTransformations[handlerKey].path.literalValue) { + handlerSpec.settings[handlerKey].path = + handlerSpec.capabilitiesTransformations[handlerKey].path.literalValue; + }; + }); + } + handlerGetPromises.push(that.invokeSettingsHandlerGet(solutionID, handlerSpec)); + }); }); - delete activeSessions[userToken]; - callback(true); + return fluid.promise.sequence(handlerGetPromises); }; - gpii.lifecycleManager.start = function (activeSessions, resolver, stop, options, solutions, callback) { - var userToken = options.userToken; - if (activeSessions[userToken]) { - // TODO: develop async architecture to prevent rat's nest of - // callbacks - stop({userToken: userToken}, fluid.identity); + /** + * Apply lifecycle instructions to the local computer. + * @param {Component} that - A instance of gpii.lifecycleManager. + * @param {Object} finalPayload - The payload to be applied to the local computer. + * @return {Promise} A promise with the result of applied solutions. + */ + gpii.lifecycleManager.processUpdate = function (that, finalPayload) { + var gpiiKey = finalPayload.gpiiKey, + lifecycleInstructions = finalPayload.activeConfiguration.lifecycleInstructions; + var userSession = that.getSession(gpiiKey); + if (!userSession) { // if user has logged out since the update was added to action queue + var msg = "User with GPII key " + gpiiKey + " has no active session, so ignoring update request"; + fluid.log(msg); + return fluid.promise().resolve(msg); } - var sessionState = { - actionResults: {} + var currentSettings = userSession.model.currentSettings; + + var promises = []; + fluid.each(lifecycleInstructions, function (solution, solutionId) { + var settingChangedForThisSolution = true; + var settingChanges = fluid.copy(solution); + var appliedSettingChanges = currentSettings[solutionId]; + if (appliedSettingChanges) { + var equal = fluid.model.diff(settingChanges.settingsHandlers, appliedSettingChanges.settingsHandlers); + if (equal) { + // no settings changed for this solution + settingChangedForThisSolution = false; + } + } + // Re-apply setting changes for this solution only if there are setting(s) being changes, by comparing + // with setting changes applied before . + if (settingChangedForThisSolution) { + promises.push(that.applySolution(solutionId, settingChanges, userSession, [ "update" ], "update")); + } + }); + return fluid.promise.sequence(promises); + }; + + gpii.lifecycleManager.processStart = function (that, userSession, finalPayload) { + var gpiiKey = finalPayload.gpiiKey, + lifecycleInstructions = finalPayload.activeConfiguration.lifecycleInstructions; + + if (userSession.model.gpiiKey === gpiiKey) { + var failPromise = fluid.promise(); + failPromise.reject("User already logged in when processing start item in action queue. Aborting"); + return failPromise; } + that.events.onSessionStart.fire("gpii.lifecycleManager.userSession", gpiiKey); + fluid.set(finalPayload, ["currentPreferences"], gpii.lifecycleManager.getPreferencesForSet(finalPayload.preferences, finalPayload.activePrefsSetName)); + var filteredPayload = fluid.filterKeys(finalPayload, ["gpiiKey", "preferences", "activePrefsSetName", "activeConfiguration", "solutionsRegistryEntries", "matchMakerOutput", "currentPreferences"]); + userSession.applier.change("", filteredPayload); // TODO: One day after the applier is refactored this will explicitly need to be a "MERGE" + userSession.createTime = Date.now(); + userSession.localFetcher = gpii.lifecycleManager.computeLocalFetcher(userSession); - // let the user's token as well as any named action results accumulated - // to date be resolvable for any future action - sessionState.localFetcher = gpii.combineFetchers( - gpii.resolversToFetcher({userToken: userToken}), - gpii.resolversToFetcher(sessionState.actionResults)); + // This "start" action will result in the original settings of the system (i.e. those that + // were on the system before the user logged in) being stored inside userSession.model.originalSettings. + // When "stop" is called this payload will be used to restore the settings back to their + // original state. + var tasks = []; + fluid.each(lifecycleInstructions, function (solution, solutionId) { + tasks.push(function () { + if (!fluid.isDestroyed(that)) { // See above comment for GPII-580 + // check the current state of the solution to decide whether we should run the + // "update", "start" or "stop" directive + return that.getSolutionRunningState(solutionId, solution, userSession); + } + }); + tasks.push(function () { + if (!fluid.isDestroyed(that)) { // See above comment for GPII-580 + // if solution is already running, call "update" directive - else use "start" directive + var isRunning = userSession.model.runningOnLogin[solutionId]; + var actions = gpii.lifecycleManager.calculateLifecycleActions(isRunning, solution.active); - sessionState.localResolver = function (material) { - return resolver.resolve(material, sessionState.localFetcher); - }; + // build structure for returned values (for later reset) + return that.applySolution(solutionId, solution, userSession, actions, "start"); + } + }); + }); + // Note that these promises are only sequenced for their side-effects (the ones on user session state within applySolution and on the system at large + // via the settings handlers) + return fluid.promise.sequence(tasks); + }; - // Data is an array of solutions with settingsHandlers and - // launchHandlers for each solution - sessionState.solutions = fluid.transform(solutions, function (solution) { - //build structure for returned values (for later reset) - var togo = fluid.copy(solution); - togo.settingsHandlers = gpii.lifecycleManager.executeActions( - solution.id, solution.settingsHandlers, - solution.lifecycleManager.start, sessionState); - delete togo.lifecycleManager.start; - return togo; + /** + * Function to add the current name of the preferences set to the full + * payload output from the matchmaking process. The current name is given + * by one of (in priority sequence): + * 1. a passed in name (newPrefsSetName) + * 2. the LifecycleManager's session's model (activePrefsSetName) + * 3. "gpii-default" + * + * @param {Component} that - An instance of LifecycleManager component + * @param {Object} fullPayload - The full structure output from the matchmaking process. NOTE: This object will be modified + * @param {String} [newPrefsSetName] - [optional] A preferences set name to be selected. + * @return {Object} The modified fullPayload, now including activePrefsSetName + */ + gpii.lifecycleManager.updateActivePrefsSetName = function (that, fullPayload, newPrefsSetName) { + var activeSessionPrefsSetName = that.getSession().model.activePrefsSetName; + fullPayload.activePrefsSetName = ( + newPrefsSetName ? newPrefsSetName : + activeSessionPrefsSetName ? activeSessionPrefsSetName : "gpii-default" + ); + return fullPayload; + }; + + /** + * On changes to the preferences set: + * 1) compare the set against the currently logged in user's preferences set + * 2) if the newly calculated active set of preferences is different from the currently active one, + * the LifecycleManager's update functionality will be called and the new prefsSet and + * configuration will be stored in the active session of the LifecycleManager. + * + * @param {Component} that - The lifecycleManager component holding the current session + * @param {String} [newPrefsSetName] - The name of the preferences set to switch to. If missing, this + * function does nothing. + * @return {Promise} If the preferences set change causes an actual change in the system (i.e. change of + * activePrefsSet and changes to the applied configuration) a promise of an update to the system + * configuration is returned. If the call to prefsSetChanged does not result in a different + * active preferences set or no user is logged in, undefined is returned. + **/ + gpii.lifecycleManager.prefsSetNameChanged = function (that, newPrefsSetName) { + var activeSession = that.getSession(); + + // If no one is logged in, or, a system reserved key is logged in, do nothing + var activeGpiiKey = activeSession.model.gpiiKey; + if (!activeGpiiKey || gpii.isReservedGpiiKey(activeGpiiKey)) { + fluid.log("lifecycleManager: No active GPIIKey, or system reserved GPIIKey: no prefs set change"); + return; + } + var fullPayload = fluid.extend(true, {}, activeSession.model); + var oldPrefsSetName = fullPayload.activePrefsSetName; + that.updateActivePrefsSetName(fullPayload, newPrefsSetName); + if (oldPrefsSetName === fullPayload.activePrefsSetName) { + fluid.log("lifecycleManager: Same preferences set as before (" + newPrefsSetName + "): no prefs set change"); + return; + } + // Update the prefs set name immediately so we don't produce a model update from the lifecycleManager with a + // partially applied set of solutions from the new name, confusing, e.g., the PSPChannel + // This is required because of https://issues.fluidproject.org/browse/FLUID-6208 + activeSession.applier.change("activePrefsSetName", fullPayload.activePrefsSetName); + fluid.log("lifecycleManager: Preferences set changed from current \"", oldPrefsSetName, "\" to new active \"" + fullPayload.activePrefsSetName, "\" for the GPII key: ", activeGpiiKey); + + // Clean up the previous tracked preferences + activeSession.applier.change("currentPreferences", null, "DELETE"); + + // Delete the old currentSettings as the new structure will be updated directly onto activeSession.model + // by lifecycleManager.update() called just after. + delete fullPayload.currentSettings; + + that.addLifecycleInstructionsToPayload(fullPayload); + var response = that.update(fullPayload); + + response.then(function () { + fluid.set(fullPayload, ["currentPreferences"], gpii.lifecycleManager.getPreferencesForSet(fullPayload.preferences, fullPayload.activePrefsSetName)); + // LifecycleInstructions is only used in the "finalPayload" as + // instructions on how to configure the system. Once configured, the + // applied settings are stored in 'activeConfiguration' + activeSession.applier.change("", fullPayload); // TODO: this will need to be a "MERGE" in time + fluid.log("lifecycleManager: Successfully updated configuration triggered by prefsSet changes"); + }, function () { + fluid.log(fluid.logLevel.FAIL, "lifecycleManager: Failed to apply newly preferences set: ", fullPayload.activePrefsSetName); }); - activeSessions[userToken] = sessionState; - callback(true); + return response; }; -}()); + gpii.lifecycleManager.logSnapshotUpdate = function (lifecycleManager, session, originalSettings) { + fluid.log("Settings for session " + session.id + " created at " + session.createTime + " updated to ", originalSettings); + }; +})(); diff --git a/gpii/node_modules/lifecycleManager/src/LifecycleManagerPost.js b/gpii/node_modules/lifecycleManager/src/LifecycleManagerPost.js deleted file mode 100644 index 939dfb2c0..000000000 --- a/gpii/node_modules/lifecycleManager/src/LifecycleManagerPost.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - GPII Lifecycle Manager POST Handler - - Copyright 2013 OCAD University - - Licensed under the New BSD license. You may not use this file except in - compliance with this License. - - You may obtain a copy of the License at - https://github.com/gpii/universal/LICENSE.txt -*/ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - gpii = fluid.registerNamespace("gpii"), - kettle = fluid.require("kettle", require); - - fluid.defaults("kettle.requests.request.handler.lifecycleManagerStart", { - gradeNames: ["autoInit", "fluid.littleComponent"], - invokers: { - handle: { - funcName: "gpii.lifecycleManagerStart", - args: ["{requestProxy}", "{request}.req.params.token", "{gpii.lifecycleManager}.start", "{request}.req.body"], - dynamic: true - } - } - }); - - fluid.defaults("kettle.requests.request.handler.lifecycleManagerStop", { - gradeNames: ["autoInit", "fluid.littleComponent"], - invokers: { - handle: { - funcName: "gpii.lifecycleManagerStop", - args: ["{requestProxy}", "{request}.req.params.token", "{gpii.lifecycleManager}.stop"], - dynamic: true - } - } - }); - - gpii.lifecycleManagerStart = function (requestProxy, token, start, match) { - start({ - userToken: token - }, match, requestProxy.events.onSuccess.fire); - }; - - gpii.lifecycleManagerStop = function (requestProxy, token, stop) { - stop({ - userToken: token - }, requestProxy.events.onSuccess.fire); - }; - -})(); diff --git a/gpii/node_modules/lifecycleManager/src/LifecycleManagerServer.js b/gpii/node_modules/lifecycleManager/src/LifecycleManagerServer.js deleted file mode 100644 index 3865c3f0e..000000000 --- a/gpii/node_modules/lifecycleManager/src/LifecycleManagerServer.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - GPII Lifecycle Manager Server - - Copyright 2013 OCAD University - - Licensed under the New BSD license. You may not use this file except in - compliance with this License. - - You may obtain a copy of the License at - https://github.com/gpii/universal/LICENSE.txt -*/ - -"use strict"; - -var fluid = require("infusion"), - path = require("path"), - gpii = fluid.registerNamespace("gpii"); - -fluid.require("kettle", require); -fluid.require("./LifecycleManagerPost.js", require); - -fluid.defaults("gpii.lifecycleManagerServer", { - gradeNames: ["kettle.app", "gpii.lifecycleManager", "autoInit"], - handlers: { - lifecycleManagerStart: { - route: "/start/:token", - type: "post" - }, - lifecycleManagerStop: { - route: "/stop/:token", - type: "post" - } - }, - root: path.join(__dirname, "..") -}); diff --git a/gpii/node_modules/lifecycleManager/src/LifecycleManagerSession.js b/gpii/node_modules/lifecycleManager/src/LifecycleManagerSession.js new file mode 100644 index 000000000..2967360b8 --- /dev/null +++ b/gpii/node_modules/lifecycleManager/src/LifecycleManagerSession.js @@ -0,0 +1,168 @@ +/*! + * Lifecycle Manager Session + * + * Copyright 2016 Raising the Floor - International + * Copyright 2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = fluid || require("infusion"), + $ = fluid.registerNamespace("jQuery"), + gpii = fluid.registerNamespace("gpii"); + +(function () { + + /** Mediate between updates to the session component's original snapshot and the external `onSessionSnapshotUpdate` fired by the + * LifecycleManager (principally to the journaller) + */ + fluid.defaults("gpii.lifecycleManager.sessionSnapshotUpdater", { + gradeNames: "fluid.component", + modelListeners: { + "originalSettings": { + excludeSource: ["init", "SessionCleanup"], + func: "{gpii.lifecycleManager}.events.onSessionSnapshotUpdate.fire", + args: ["{gpii.lifecycleManager}", "{that}", "{change}.value"] + } + } + }); + + /** Two component of this type are constructed as lifecycleManager subcomponents for key-in sessions: + * 1. The user key in session; + * 2. The session for restoring the system to specific original settings. + */ + fluid.defaults("gpii.lifecycleManager.session", { + gradeNames: ["fluid.modelComponent"], + model: { + // The matchMaker output payload for applied preferences. + gpiiKey: undefined, + /* + preferences: {Object}, + activePrefsSetName: {String}, + solutionsRegistryEntries: {Object}, + activeConfiguration: {Object}, + matchMakerOutput: {Object}, + */ + + // The original solution settings at the key-in in preparation for a restore. + originalSettings: {}, + // In the same structure as "originalSettings" but tracks currently applied solution settings. + currentSettings: {}, + + // Keep track of all preferences applied or read. PSPChannel goes through it to generate output + // for PSPChannel clients. + currentPreferences: {}, + + // Action results of free lifecycle actions. + actionResults: {} + }, + modelListeners: { + preferences: { + listener: "gpii.lifecycleManager.session.savePrefsUpdates", + args: ["{that}", "{change}.value", "{change}.transaction.sources", "{gpii.lifecycleManager}.events.onAutoSaveRequired"] + } + }, + invokers: { + localResolver: "gpii.lifecycleManager.localResolver({gpii.lifecycleManager}, {that}, {arguments}.0)" + } + }); + + /** + * Filter the preferences by metadata defined in the preferences set. Only the preferences that are allowed to be + * autosaved will be returned. + * @param {Object} preferences - The updated preferences. + * @param {Object} metadata - The metadata that defined in the prefs set. + * @return {Object|Undefined} - The preferences to be autosaved. Return undefined when no preferences are found. + */ + gpii.lifecycleManager.session.filterPrefsByAutoSave = function (preferences, metadata) { + var autoSaveScope; + fluid.each(metadata, function (meta) { + if (meta.type === "autosave") { + autoSaveScope = autoSaveScope || {}; + fluid.each(meta.scope, function (oneScope) { + var oneScopeArray = fluid.makeArray(oneScope); + fluid.set(autoSaveScope, oneScopeArray, true); + }); + } + }); + + // Preferences will not be auto saved if the "autosave" metadata is not defined + if (!autoSaveScope) { + return undefined; + } + + var prefsForAutoSave = {}; + fluid.each(preferences, function (prefValue, prefKey) { + var autoSaveValue = autoSaveScope[prefKey]; + if (prefKey.startsWith("http://registry.gpii.net/applications")) { + fluid.each(prefValue, function (commonValue, commonKey) { + var autoSaveCommon = autoSaveValue ? autoSaveValue[commonKey] : null; + if (autoSaveCommon === true) { + fluid.set(prefsForAutoSave, [prefKey, commonKey], commonValue); + } + }); + } else if (autoSaveValue === true) { + fluid.set(prefsForAutoSave, [prefKey], prefValue); + } + }); + + return $.isEmptyObject(prefsForAutoSave) ? undefined : prefsForAutoSave; + }; + + /** + * Auto save prefs to the cloud when the preferences are updated. Note that change requests on session.model.preferences + * are also fired by the lifecycleManager session start and changes in preferences sets, in which cases preferences should + * not be written to the cloud. + * @param {Component} that - An instance of gpii.lifecycleManager.session. + * @param {Object} preferences - The updated preferences that will be saved. + * @param {Object} transactionSources - The transaction sources from the model change request. + * @param {Event} onAutoSaveRequired - Fired to trigger the save action to the cloud. + */ + gpii.lifecycleManager.session.savePrefsUpdates = function (that, preferences, transactionSources, onAutoSaveRequired) { + var autoSavePrefs = fluid.copy(preferences); + if (transactionSources.userUpdate) { + var metadata = fluid.get(that.model, ["preferences", "contexts", that.model.activePrefsSetName, "metadata"]); + var prefsInActivePrefsSet = fluid.get(preferences, ["contexts", that.model.activePrefsSetName, "preferences"]); + var prefsForAutoSave = gpii.lifecycleManager.session.filterPrefsByAutoSave(prefsInActivePrefsSet, metadata); + if (prefsForAutoSave) { + fluid.set(autoSavePrefs, ["contexts", that.model.activePrefsSetName, "preferences"], prefsForAutoSave); + fluid.log("Lifecycle Manager Session: auto save for gpiiKey (", that.model.gpiiKey, "), with preferences: ", autoSavePrefs); + onAutoSaveRequired.fire(that.model.gpiiKey, autoSavePrefs); + } + } + }; + + /** A standard session corresponding to a standard user logon + */ + fluid.defaults("gpii.lifecycleManager.userSession", { + gradeNames: ["gpii.lifecycleManager.session", "gpii.lifecycleManager.sessionSnapshotUpdater"] + }); + + /** A special session type corresponding to a "restore" action by the journaller + */ + fluid.defaults("gpii.lifecycleManager.restoreSession", { + gradeNames: "gpii.lifecycleManager.session" + }); + + gpii.lifecycleManager.localResolver = function (lifecycleManager, session, material) { + return lifecycleManager.variableResolver.resolve(material, session.localFetcher); + }; + + gpii.lifecycleManager.computeLocalFetcher = function (session) { + // let the user's token as well as anything else in the session be resolvable (this latter is + // intended only for use in test cases) + return gpii.resolversToFetcher({ + gpiiKey: session.model.gpiiKey, + session: function (path) { + return fluid.get(session.model, path); + } + }); + }; + +})(); diff --git a/gpii/node_modules/lifecycleManager/src/LifecycleManagerUtils.js b/gpii/node_modules/lifecycleManager/src/LifecycleManagerUtils.js new file mode 100644 index 000000000..7de950fdf --- /dev/null +++ b/gpii/node_modules/lifecycleManager/src/LifecycleManagerUtils.js @@ -0,0 +1,146 @@ +/*! + * Lifecycle Manager Utilities + * + * Copyright 2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = fluid || require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.lifecycleManager"); + +/** + * Remove settings that are not in the matchMaker settings results. This is used by + * lifecycleManager.readPreferences() to filter out applcation setting values that are + * relevant to request preferences. + * @param {Array} settingValues - An array of objects with each object keyed by a solution id with its value being + * an object of setting values that this solution supports. + * An example of an input for "settingValues": + * [{ + * "org.gnome.desktop.a11y.magnifier": [ + * { + * "settings": { + * "mag-factor": 2, + * "show-cross-hairs": false, + * "focus-tracking": false, + * "caret-tracking": false, + * "mouse-tracking": false, + * "screen-position": "full-screen" + * } + * } + * ] + * }] + * @param {Object} matchMakerSolutionRegistryEntries - The solution registry entries in the calculated lifecyle + * instructions. Each solution registry entry contains a list of settings generated by a matchmaking process. + * @return {Object} An object with each element keyed by a solution id with its value only having settings + * generated by a matchmaking process. + * An example of the output: + * [{ + * "org.gnome.desktop.a11y.magnifier": [ + * { + * "settings": { + * "mag-factor": 2, + * "screen-position": "full-screen" + * } + * } + * ] + * }] + */ +gpii.lifecycleManager.getMatchMakerSettings = function (settingValues, matchMakerSolutionRegistryEntries) { + var settingsTogo = {}; + // 1. collect all settings generated from the a matchmaking process + var matchedSettingsForSolutions = {}; + fluid.each(matchMakerSolutionRegistryEntries, function (solutionSpec, solutionId) { + var matchedSettings = []; + fluid.each(solutionSpec.settingsHandlers, function (settingsHandlerSpec) { + matchedSettings = matchedSettings.concat(fluid.keys(settingsHandlerSpec.settings)); + }); + fluid.set(matchedSettingsForSolutions, [solutionId], matchedSettings); + }); + + // 2. remove setting values that are not in matchedSettings + fluid.each(settingValues, function (oneSolutionBlock) { + fluid.each(oneSolutionBlock, function (settingBlocks, solutionId) { + fluid.each(settingBlocks, function (oneSettingBlock) { + fluid.each(oneSettingBlock.settings, function (value, key) { + if (matchedSettingsForSolutions[solutionId].includes(key)) { + fluid.set(settingsTogo, [solutionId, key], value); + }; + }); + }); + }); + }); + return settingsTogo; +}; + +/** + * Invert application settings to common or application terms. This is currently used by + * lifecycleManager.readPreferences() to transform application settings to request preferences. + * @param {Object} settings - Settings keyed by solution ids with values being objects containing setting values. + * An example of an input for "settings": + * { + * "org.gnome.desktop.a11y.magnifier": { + * "mag-factor": 2, + * "screen-position": "full-screen" + * } + * } + * @param {Object} solutionRegistryEntries - The solution registry entries. + * the "settingValues". + * @return {Object} An transformed object keyed by preference URLs. + * An example of the output: + * { + * "http://registry.gpii.net/common/magnification": 2 + * } + */ +gpii.lifecycleManager.transformSettingsToPrefs = function (settings, solutionRegistryEntries) { + var transformed = {}; + fluid.each(settings, function (settingsForOneSolution, solutionId) { + var settingsHandlers = fluid.get(solutionRegistryEntries, [solutionId, "settingsHandlers"]); + fluid.each(settingsHandlers, function (handler) { + transformed = fluid.extend(true, transformed, fluid.model.transformWithRules(settingsForOneSolution, gpii.getInverseRules(handler))); + }); + }); + return transformed; +}; + +/** + * Filter out preferences by matching preference paths. + * @param {Object} preferences - The preferences object to filter from. + * @param {Object} prefsSearchFor - Contain preference paths to match. + * @return {Object} A subset of "preferences" containing preference paths matching paths from "prefsSearchFor". + */ +gpii.lifecycleManager.filterMatchedPrefs = function (preferences, prefsSearchFor) { + var matchedPrefs = {}; + var prefApplicationPrefix = "http://registry.gpii.net/applications/"; + + fluid.each(prefsSearchFor, function (prefsVal, prefsKey) { + if (prefsKey.startsWith(prefApplicationPrefix)) { + // application terms + fluid.each(prefsVal, function (innerPrefsVal, innerPrefsKey) { + var exists = fluid.get(preferences, [prefsKey, innerPrefsKey]); + if (exists !== undefined) { + fluid.set(matchedPrefs, [prefsKey, innerPrefsKey], exists); + } + }); + } else { + // common terms + var exists = fluid.get(preferences, [prefsKey]); + if (exists !== undefined) { + fluid.set(matchedPrefs, [prefsKey], exists); + } + } + }); + return matchedPrefs; +}; diff --git a/gpii/node_modules/lifecycleManager/src/PrivateMatchMaker.js b/gpii/node_modules/lifecycleManager/src/PrivateMatchMaker.js new file mode 100644 index 000000000..93d5ee5a4 --- /dev/null +++ b/gpii/node_modules/lifecycleManager/src/PrivateMatchMaker.js @@ -0,0 +1,77 @@ +/*! + * Private MatchMaker + * + * Copyright 2018 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + + "use strict"; + + var fluid = fluid || require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +// This component is used by the lifecycleManager to perform a local matchmaking at handling requests for applying +// new preferences that don't exist in lifecycleManager.session.model.preferences. + fluid.defaults("gpii.lifecycleManager.privateMatchMaker", { + gradeNames: ["fluid.component", "gpii.flowManager.matchMaking"], + invokers: { + doMatch: { + funcName: "gpii.lifecycleManager.privateMatchMaker.doMatch", + args: [ + "{that}", + "{flowManager}.deviceReporter", + "{arguments}.0", // gpiiKey + "{arguments}.1" // preferences + ] + } + }, + events: { + onSuccess: null, + onError: null + }, + listeners: { + "onGpiiKey.getPreferences": "fluid.identity", + "onMatchDone.matchToSettings": { + listener: "gpii.flowManager.cloudBased.matchToSettings", + args: ["{arguments}.0", "{that}.events.onSuccess", "Private MatchMaker"] + } + } + }); + + gpii.lifecycleManager.privateMatchMaker.doMatch = function (that, deviceReporter, gpiiKey, preferences) { + var requestPromise = fluid.promise(); + + that.events.onSuccess.addListener(function (payload) { + requestPromise.resolve(payload); + // TODO: To work around the issue that a fixed listener always binds to the first request prmoise + // which causes subsequent match making requests throw "promise has been resolved" error. The proper + // solution is to request-scope the private matchMaker by perhaps adding a "UserUpdate" request type + // to go along with other UserLogon request types. + that.events.onSuccess.removeListener("resolvePromise"); + }, "resolvePromise"); + + that.events.onError.addListener(function (error) { + requestPromise.reject(error); + that.events.onError.removeListener("rejectPromise"); + }, "rejectPromise"); + + // Do match making + that.events.onGpiiKey.fire(gpiiKey); + that.events.onPreferences.fire(preferences); + + var devicePromise = deviceReporter.get(); + devicePromise.then(function (deviceData) { + fluid.log("Private MatchMaker, doMatch - received device data: ", deviceData); + that.events.onDeviceContext.fire(deviceData); + }, function (err) { + fluid.log("Private MatchMaker, doMatch - error at receiving device data: " + err.message); + that.events.onError.fire(err); + }); + + return requestPromise; + }; diff --git a/gpii/node_modules/lifecycleManager/src/Resolvers.js b/gpii/node_modules/lifecycleManager/src/Resolvers.js new file mode 100644 index 000000000..bd3f4bf93 --- /dev/null +++ b/gpii/node_modules/lifecycleManager/src/Resolvers.js @@ -0,0 +1,140 @@ +/*! + * Lifecycle Manager Resolvers + * + * Copyright 2012 Antranig Basman + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = fluid || require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +(function () { + // A standard interception point so that the process of resolving names onto + // settings handlers and actions can be mocked for integration tests + + fluid.defaults("gpii.lifecycleManager.nameResolver", { + gradeNames: ["fluid.component"], + invokers: { + resolveName: { + funcName: "fluid.identity" + } + } + }); + + /* The central machinery in the Lifecycle Manager which manages the process + * of resolving contextual expressions such as ${{environment}.WINDIR} onto + * strings by interpolation. + */ + + fluid.defaults("gpii.lifecycleManager.variableResolver", { + gradeNames: ["fluid.component"], + components: { + resolverConfig: { + type: "gpii.lifecycleManager.standardResolverConfig" + } + }, + members: { + resolvers: { + expander: { + func: "gpii.lifecycleManager.variableResolver.computeResolvers", + args: "{that}.resolverConfig.options.resolvers" + } + }, + fetcher: { + expander: { + func: "gpii.resolversToFetcher", + args: "{that}.resolvers" + } + } + }, + invokers: { + /* Resolves interpolated variables within some options material (argument 0). + * This is resolved first against the `builtin fetcher` which is computed by pooling + * environmental fetchers from the platform-specific repositories, together with + * a fetcher resolving onto variables in the user's current session (argument 1) + */ + resolve: { + funcName: "gpii.lifecycleManager.variableResolver.resolve", + args: ["{arguments}.0", "{that}.fetcher", "{arguments}.1"] + } + } + }); + + gpii.lifecycleManager.variableResolver.computeResolvers = function (resolvers) { + return fluid.transform(resolvers, fluid.getGlobalValue); + }; + + gpii.lifecycleManager.variableResolver.resolve = function (material, fetcher, extraFetcher) { + // TODO: This relies on a horribly undocumented and unstable Infusion API + return fluid.expand(material, { + bareContextRefs: false, + // TODO: FLUID-4932 - the framework currently has no wildcard support in mergePolicy. + mergePolicy: { + 0: { + capabilitiesTransformations: { + "*": { + noexpand: true + } + } + } + }, + fetcher: gpii.combineFetchers(fetcher, extraFetcher) + }); + }; + + /* Converts a free hash of "resolvers" (keyed by resolver name) into a + * "fetcher" suitable for being operated by Infusion's expansion machinery resolving + * expressions such as ${{environment}.WINDIR} when they are seen within strings for interpolation. + */ + + gpii.resolversToFetcher = function (resolvers) { + return function (parsed) { + var resolver = resolvers[parsed.context]; + return !resolver ? undefined : ( + typeof(resolver) === "function" ? + resolver(parsed.path) : fluid.get(resolver, parsed.path)); + }; + }; + + /* A hacked and unsatisfactory method to compose two fetchers (the second of which is optional) + * into a single one + */ + gpii.combineFetchers = function (main, fallback) { + return fallback ? function (parsed) { + var fetched = main(parsed); + return fetched === undefined ? fallback(parsed) : fetched; + } : main; + }; + + /* Central standard repository for "resolvers" mapping some environmental material onto + * resolvable expressions. This is targetted, e.g., from the windows repository by a + * grade linkage registering the Windows Registry resolver. + * It contains an option named `resolvers` which maps resolver names (which will act as context names, + * in expressions such as ${{environment}.WINDIR}) onto global names, which are resolvable via + * `fluid.getGlobalValue` to unary functions resolving the path expressions - these unary + * functions mapping Strings to Strings are named (variable) `resolvers`. + */ + + fluid.defaults("gpii.lifecycleManager.standardResolverConfig", { + gradeNames: "fluid.component", + resolvers: { + environment: "gpii.lifecycleManager.environmentResolver" + } + }); + + gpii.lifecycleManager.environmentResolver = function (name) { + return process.env[name]; + }; + +})(); diff --git a/gpii/node_modules/lifecycleManager/src/UserLogonRequest.js b/gpii/node_modules/lifecycleManager/src/UserLogonRequest.js new file mode 100644 index 000000000..9706d77dc --- /dev/null +++ b/gpii/node_modules/lifecycleManager/src/UserLogonRequest.js @@ -0,0 +1,207 @@ +/*! + * User Logon Request + * + * Copyright 2018 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = fluid || require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +// One dynamic component of this type is constructed for each user logon request on the LifecycleManager. +fluid.defaults("gpii.lifecycleManager.userLogonRequest", { + gradeNames: ["fluid.component"], + logonState: null, // "login" or "logout". Supplied at defining inherited request handler grades + gpiiKey: null, // A GPII key. Supplied at creating dynamic request instances + requestPromise: null, // A promise. Supplied at creating dynamic request instances + events: { + onSuccess: null, + onError: null + }, + invokers: { + handleRequest: { + funcName: "gpii.lifecycleManager.userLogonRequest.handleRequest", + args: ["{that}", "{lifecycleManager}", "{arguments}.0"] + // logonState + } + }, + listeners: { + "onCreate.handleRequest": { + listener: "{that}.handleRequest", + args: ["{that}.options.logonState"] + }, + "onSuccess.handleSuccessRequest": { + listener: "gpii.lifecycleManager.userLogonRequest.handleSuccessRequest", + args: ["{that}", "{arguments}.0"] + }, + "onError.handleFailedRequest": { + listener: "gpii.lifecycleManager.userLogonRequest.handleFailedRequest", + args: ["{that}", "{lifecycleManager}.userErrors.events.userError", "{arguments}.0"], + priority: "after:handleErrors" + } + } +}); + +gpii.lifecycleManager.userLogonRequest.handleRequest = function (that, lifecycleManager, logonState) { + var gpiiKey = that.options.gpiiKey; + var activeGpiiKey = lifecycleManager.getActiveSessionGpiiKey(); // find currently logged in user: + + // keep track of the last non-noUser key for /proximityTriggered endpoint to verify its debounce rule + // even when the the current active key is "noUser". + if (gpiiKey && gpiiKey !== "noUser") { + gpii.lifecycleManager.userLogonHandling.updateCurrentUserLogon(lifecycleManager, gpiiKey, logonState); + } + + // handle reset requests + if (gpiiKey === "reset") { + that.handleResetGpiiKey(); + return; + } + + // handle keyin requests + if (logonState === "login") { + that.loginUser(gpiiKey); + } + + // handle keyout requests + if (logonState === "logout") { + if (activeGpiiKey !== gpiiKey) { + var ignoreUserErrors = activeGpiiKey === "noUser" ? true : false; + lifecycleManager.errorResponse(that, "Got logout request from user " + gpiiKey + ", but the user " + + activeGpiiKey + " is logged in. So ignoring the request.", 409, ignoreUserErrors); + } else { + that.logoutUser(gpiiKey); + } + } +}; + +gpii.lifecycleManager.userLogonRequest.handleSuccessRequest = function (that, payload) { + fluid.log("UserLogonRequest completes successfully with the payload: ", payload); + that.options.requestPromise.resolve(payload); + that.destroy(); +}; + +gpii.lifecycleManager.userLogonRequest.handleFailedRequest = function (that, userErrorEvent, err) { + fluid.log("UserLogonRequest fails with the error: ", err); + + if (gpii.userErrors.isConnectionError(err.code)) { + userErrorEvent.fire({ + isError: true, + messageKey: "NoConnection", + originalError: err + }); + } else if (!err.ignoreUserErrors) { + userErrorEvent.fire({ + isError: true, + messageKey: "KeyInFail", + originalError: err.message + }); + } + + that.options.requestPromise.reject(err); + that.destroy(); +}; + +// Define the grade to be used for key-in and key-out +gpii.lifecycleManager.userLogonRequest.handlerGrades = { + login: "gpii.lifecycleManager.loginRequest", + logout: "gpii.lifecycleManager.logoutRequest" +}; + +// A variant of user logon request to handle key-in requests. +// Note: The gradename that handles the logon state change will be distributed down: +// 1. When GPII runs in the all-in-local config, the handler grade +// "gpii.lifecycleManager.userLogonHandling.matchMakingStateChangeHandler" is distributed by "gpii.flowManager.local"; +// 2. When GPII runs in the untrusted config, the handler grade +// "gpii.lifecycleManager.untrusted.stateChangeHandler" is distributed by "gpii.flowManager.untrusted". +fluid.defaults("gpii.lifecycleManager.loginRequest", { + gradeNames: [ + "gpii.lifecycleManager.userLogonRequest" + ], + logonState: "login" +}); + +// A variant of user logon request to handle key-out requests. +fluid.defaults("gpii.lifecycleManager.logoutRequest", { + gradeNames: [ + "gpii.lifecycleManager.userLogonRequest", + "gpii.lifecycleManager.userLogonHandling.stateChangeHandler" + ], + logonState: "logout" +}); + +/* + * The lifecycleManager logon request queue is used to hold the key-in and key-out requests. + * The entries in the action queue are of the format: + * { + * gpiiKey: {String}, + * logonState: {String} // enum: ["login", "logout"] + * } + * The action queue is run sequentially, and an item is considered "done" once the promise returned by the request + * is resolved. + */ +gpii.lifecycleManager.addToUserLogonRequestQueue = function (that, queue, item) { + var newItem = fluid.copy(item); + newItem.requestPromise = fluid.promise(); + queue.push(newItem); + if (queue.length === 1) { + that.processUserLogonRequestQueue(); + } + return newItem.requestPromise; +}; + +gpii.lifecycleManager.processUserLogonRequestQueue = function (that, queue) { + // Pick the first item and process. This item will be removed from the queue once the processing completes. + var item = queue[0]; + var gradeName = gpii.lifecycleManager.userLogonRequest.handlerGrades[item.logonState]; + + if (!gradeName) { + fluid.fail("No handler grade found for user logon action - " + item.logonState); + }; + + that.events.onUserLogonRequestReceived.fire(gradeName, item.gpiiKey, item.requestPromise); + + item.requestPromise.then(that.endProcessOneUserLogonRequest, that.endProcessOneUserLogonRequest); +}; + +gpii.lifecycleManager.endProcessOneUserLogonRequest = function (lifecycleManager) { + // TODO: fluid.invokeLater() is to work around an issue that kettle reports the error "Failing to resume callback for + // request xxx which has already concluded". The request that fails to resume callback is the user logon request that + // the current "endProcessOneUserLogonRequest" callback is attached with. It appears that starting the process of + // "noUser" keyin before the current callback completes triggers this error. fluid.invokeLater() helps to pull the + // "noUser" keyin into a separate process. The request failure needs to be further investigated. + fluid.invokeLater(function () { + var queue = lifecycleManager.userLogonRequestQueue; + + // The processing on the first item completes. Remove it from the queue. + queue.shift(); + + if (queue.length > 0) { + // process next item in the queue if it exists + lifecycleManager.processUserLogonRequestQueue(queue); + } else { + // if no actual user is logged in, login with "noUser" + var activeGpiiKey = lifecycleManager.getActiveSessionGpiiKey(); // find currently logged in user: + + // Checking lifecycleManager.model.logonChange.inProgress is to make sure during the period to finish + // this request handling, there isn't another user logon request being added to the queue and in process. + if (!activeGpiiKey) { + fluid.log("No active keys in GPII. Key in noUser"); + lifecycleManager.addToUserLogonRequestQueue({ + gpiiKey: "noUser", + logonState: "login" + }); + } + else { + lifecycleManager.events.onQueueEmpty.fire(activeGpiiKey); + } + } + }); +}; diff --git a/gpii/node_modules/lifecycleManager/src/UserLogonStateChange.js b/gpii/node_modules/lifecycleManager/src/UserLogonStateChange.js new file mode 100644 index 000000000..3070cca77 --- /dev/null +++ b/gpii/node_modules/lifecycleManager/src/UserLogonStateChange.js @@ -0,0 +1,310 @@ +/* + * GPII User Logon State Change + * + * Copyright 2012 OCAD University + * Copyright 2015, 2017 Raising the Floor - International + * Copyright 2018 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.lifecycleManager.userLogonHandling"); + +gpii.lifecycleManager.userLogonHandling.handleResetGpiiKey = function (that, lifecycleManager) { + var activeGpiiKey = lifecycleManager.getActiveSessionGpiiKey(); // find currently logged in user: + + that.defaultSettingsDataPromise.then(function (defaultSettingsData) { + var resetActions = []; + if (activeGpiiKey) { + fluid.log("Reset: logging out the user with GPII key " + activeGpiiKey); + resetActions.push(that.logoutUserWithoutResponse(activeGpiiKey)); + } + if (defaultSettingsData.defaultSnapshot) { + fluid.log("Reset: restoring the system to the default snapshot: ", defaultSettingsData.defaultSnapshot); + resetActions.push(lifecycleManager.restoreSnapshot(defaultSettingsData.defaultSnapshot)); + } + var promise = fluid.promise.sequence(resetActions); + + // A separate logout of "reset" is not necessary because of the special behavior of restoreSnapshot() that uses a + // special "restore" key and also detroys the restore session once the restoration completes. The final condition + // of using the "reset" key is to have the "noUser" log back in the system. + promise.then(function () { + that.events.onSuccess.fire("Reset successfully."); + }, function (error) { + lifecycleManager.errorResponse(that, "Reset: Error occurred during logout: " + error.message); + }); + }); +}; + +gpii.lifecycleManager.userLogonHandling.startLifecycle = function (that, lifecycleManager, lifecyclePayload, event) { + gpii.logFully("userLogonHandling.startLifecycle got final payload ", gpii.renderMegapayload(lifecyclePayload)); + that.defaultSettingsDataPromise.then(function (defaultSettingsData) { + if (defaultSettingsData.defaultLifecycleInstructions && lifecyclePayload.gpiiKey !== "noUser") { + // Merge the default snapshot with current key's lifecycle instructions to combine the key in action + // with "reset all" functionality + fluid.set(lifecyclePayload, ["activeConfiguration", "lifecycleInstructions"], fluid.extend( + true, {}, defaultSettingsData.defaultLifecycleInstructions, fluid.get(lifecyclePayload, ["activeConfiguration", "lifecycleInstructions"]) + )); + } + var promise = lifecycleManager.start(lifecyclePayload); + promise.then(function () { + gpii.lifecycleManager.userLogonHandling.updateLogonChangeModel(lifecycleManager, lifecyclePayload.gpiiKey, "login", false); + event.fire("User with GPII key " + lifecyclePayload.gpiiKey + " was successfully logged in."); + }, function (error) { + fluid.log(fluid.logLevel.FAIL, "Error returned from lifecycle manager: ", error); + lifecycleManager.errorResponse(that, "Error occurred during login: " + error.message); + }); + }); +}; + +/** + * Updates the `currentUserLogon` portion of the lifecycle manager model. + * + * @param {Object} lifecycleManager - The lifecycleManager component. + * @param {String} gpiiKey - The gpiiKey of the user who is previously logging in or out. + * @param {String} logonType - "login" or "logout" depending on whether its a login or logout. + */ +gpii.lifecycleManager.userLogonHandling.updateCurrentUserLogon = function (lifecycleManager, gpiiKey, logonType) { + lifecycleManager.applier.change("currentUserLogon", { + type: logonType, // "login"/"logout" + gpiiKey: gpiiKey, + timeStamp: Date.now() + }); +}; + +/** + * Updates the `logonChange` portion of the lifecycle manager model. + * + * @param {Object} lifecycleManager - The lifecycleManager component. + * @param {String} gpiiKey - The gpiiKey of the user who is logging in or out. + * @param {String} logonType - "login" or "logout" depending on whether its a login or logout. + * @param {Boolean} inProgress - If this call signifies a login that is (or is about to be) in progress. + */ +gpii.lifecycleManager.userLogonHandling.updateLogonChangeModel = function (lifecycleManager, gpiiKey, logonType, inProgress) { + lifecycleManager.applier.change("logonChange", { + type: logonType, // "login"/"logout" + inProgress: inProgress, // boolean + gpiiKey: gpiiKey, // string with GPII key + timeStamp: Date.now() + }); +}; + +/* + * Resets the logon change model of the lifecycle manager. This means that the + * current GPII key is set to undefined, and inProgress set to false + */ +gpii.lifecycleManager.userLogonHandling.resetLogonChangeModel = function (lifecycleManager) { + fluid.log("Resetting logon change model of lifecycleManager"); + gpii.lifecycleManager.userLogonHandling.updateLogonChangeModel(lifecycleManager, undefined, "logout", false); +}; + +/** + * Does a logout of the user with the given token. No response will be sent to the onError and onSuccess events + * but a promise will be returned for the logout action. + * + * @param {Component} that - An instance of stateChangeHandler component. + * @param {Component} lifecycleManager - An instance of lifecycleManager component. + * @param {String} gpiiKey - The user to logout. + * @return {Promise} A promise with successful or failed user logout action. + */ +gpii.lifecycleManager.userLogonHandling.logoutUserWithoutResponse = function (that, lifecycleManager, gpiiKey) { + gpii.lifecycleManager.userLogonHandling.updateLogonChangeModel(lifecycleManager, gpiiKey, "logout", true); + var promiseTogo = fluid.promise(); + var stopPromise = lifecycleManager.stop({ + gpiiKey: gpiiKey + }); + stopPromise.then(function (response) { + fluid.log("userLogonStateChange.logoutUser: Lifecycle manager returned: ", response); + gpii.lifecycleManager.userLogonHandling.updateLogonChangeModel(lifecycleManager, gpiiKey, "logout", false); + promiseTogo.resolve(response); + }, function (error) { + fluid.log(fluid.logLevel.FAIL, "userLogonStateChange.logoutUser: An error occurred when attempting to log out user: ", error); + gpii.lifecycleManager.userLogonHandling.updateLogonChangeModel(lifecycleManager, gpiiKey, "logout", false); + promiseTogo.reject(error); + }); + return promiseTogo; +}; + +// Logs out user with the logoutUserWithoutResponse function, but calls the relevant events on the logout request +gpii.lifecycleManager.userLogonHandling.logoutUser = function (that, lifecycleManager, gpiiKey) { + var promise = that.logoutUserWithoutResponse(gpiiKey); + promise.then(function () { + that.events.onSuccess.fire("User with GPII key " + gpiiKey + " was successfully logged out."); + }, function (error) { + lifecycleManager.errorResponse(that, "Error occurred during logout: " + error.message); + }); +}; + +gpii.lifecycleManager.userLogonHandling.loginUser = function (that, lifecycleManager, gpiiKey) { + gpii.lifecycleManager.userLogonHandling.updateLogonChangeModel(lifecycleManager, gpiiKey, "login", true); + that.events.onGpiiKey.fire(gpiiKey); +}; + +gpii.lifecycleManager.getDeviceContext = function (deviceReporter, event, errorEvent) { + gpii.lifecycleManager.getDeviceContextPromise(deviceReporter).then( + event.fire, + errorEvent.fire + ); +}; + +gpii.lifecycleManager.getDeviceContextPromise = function (deviceReporter) { + // GPII-4324 TODO: We have a factoring problem here since properly the LifecycleManager has no business + // reaching out into the environment to interact with the DeviceReporter, as shown by the + // awkward-looking reference {flowManager}.deviceReporter which is out of place with the others. + // However, this ended up being a natural place for the implementation since it is scoped to the + // matchmaking workflow which is tied to lifecycleManager's userLogonRequest via containment. + // When we improve this factoring we will better deliver on our goal of "containment without dependency" + var promiseTogo = fluid.promise(); + var promise = deviceReporter.get(); + promise.then(function (deviceData) { + gpii.logFully("getDeviceContext got deviceData ", deviceData); + if (!deviceData) { + promiseTogo.reject({message: "Device reporter returned `undefined`." }); + } else if (deviceData.isError) { + promiseTogo.reject({message: "Error in device reporter data: " + deviceData.message}); + } else { + promiseTogo.resolve(deviceData); + } + }, function (err) { + var error = fluid.extend(err, { + message: "Rejected deviceReporter promise: " + err.message + }); + promiseTogo.reject(error); + }); + + return promiseTogo; +}; + +// A mixin grade for a matchMakingRequest request handler, supporting local user logon +fluid.defaults("gpii.lifecycleManager.userLogonHandling.stateChangeHandler", { + members: { + defaultSettingsDataPromise: null // A promise distributed from "gpii.flowManager.local". containing data for reset actions. + }, + invokers: { + getDeviceContext: { + funcName: "gpii.lifecycleManager.getDeviceContext", + args: ["{flowManager}.deviceReporter", "{that}.events.onDeviceContext", "{that}.events.onError"] + }, + startLifecycle: { + funcName: "gpii.lifecycleManager.userLogonHandling.startLifecycle", + args: [ "{that}", "{lifecycleManager}", "{arguments}.0", "{that}.events.onSuccess"] + // final payload from matchmaking process + }, + logoutUserWithoutResponse: { + funcName: "gpii.lifecycleManager.userLogonHandling.logoutUserWithoutResponse", + args: ["{that}", "{lifecycleManager}", "{arguments}.0"] + }, + logoutUser: { + funcName: "gpii.lifecycleManager.userLogonHandling.logoutUser", + args: ["{that}", "{lifecycleManager}", "{arguments}.0"] + }, + loginUser: { + funcName: "gpii.lifecycleManager.userLogonHandling.loginUser", + args: ["{that}", "{lifecycleManager}", "{arguments}.0"] + }, + handleResetGpiiKey: { + funcName: "gpii.lifecycleManager.userLogonHandling.handleResetGpiiKey", + args: ["{that}", "{lifecycleManager}"] + } + }, + events: { + onMatchDone: null, + onError: null, + onSuccess: null + }, + listeners: { + "onMatchDone": [{ + priority: 100, + namespace: "updsteActivePrefsSetName", + listener: "{lifecycleManager}.updateActivePrefsSetName" + }, { + priority: 90, + namespace: "addLifecycleInstructionsToPayload", + listener: "{lifecycleManager}.addLifecycleInstructionsToPayload" + }, { + priority: 80, + namespace: "startLifecycle", + listener: "{that}.startLifecycle" + }], + "onError.handleErrors": { + listener: "gpii.lifecycleManager.userLogonHandling.resetLogonChangeModel", + args: [ "{lifecycleManager}" ] + } + } +}); + +// The grade used to handle login request for all-in-local GPII deployment +fluid.defaults("gpii.lifecycleManager.userLogonHandling.matchMakingStateChangeHandler", { + gradeNames: [ + "gpii.lifecycleManager.userLogonHandling.stateChangeHandler", + "gpii.flowManager.matchMaking" + ], + listeners: { + "onGpiiKey.getDeviceContext": "{that}.getDeviceContext" + } +}); + +// This grade communicate with GPII Cloud to read settings from the Cloud. +// This grade is distributed to "gpii.lifecycleManager.loginRequest" when GPII is deployed in untrusted settings. +fluid.defaults("gpii.lifecycleManager.untrusted.stateChangeHandler", { + gradeNames: ["fluid.component", "gpii.lifecycleManager.userLogonHandling.stateChangeHandler"], + invokers: { + getSettings: { + funcName: "gpii.lifecycleManager.untrusted.getSettings", + args: [ + "{settingsDataSource}", + "{that}.gpiiKey", + "{arguments}.0", // device reporter data + "{that}.events.onMatchDone", + "{that}.events.onError" + ] + } + }, + events: { + onGpiiKey: null, + onDeviceContext: null + }, + listeners: { + "onGpiiKey.setGpiiKey": { + listener: "gpii.flowManager.setGpiiKey", + args: ["{that}", "{arguments}.0"] + }, + "onGpiiKey.getDeviceContext": { + func: "{that}.getDeviceContext", + priority: "after:setGpiiKey" + }, + "onDeviceContext.getSettings": "{that}.getSettings" + } +}); + +gpii.lifecycleManager.untrusted.getSettings = function (settingsDataSource, gpiiKey, deviceReporterData, onMatchDone, onError) { + // Unnecessary to fetch settings for "noUser" from the cloud + var noUserSettings = { + gpiiKey: "noUser", + preferences: {}, + matchMakerOutput: { + inferredConfiguration: { + "gpii-default": { + applications: {} + } + }, + lifecycleInstructions: {} + } + }; + + var settings = gpiiKey === "noUser" ? fluid.promise().resolve(noUserSettings) : settingsDataSource.get(gpiiKey, deviceReporterData); + + settings.then(onMatchDone.fire, onError.fire); +}; diff --git a/gpii/node_modules/lifecycleManager/test/html/LifecycleManagerResolverTest.html b/gpii/node_modules/lifecycleManager/test/html/LifecycleManagerResolverTest.html new file mode 100644 index 000000000..d70793691 --- /dev/null +++ b/gpii/node_modules/lifecycleManager/test/html/LifecycleManagerResolverTest.html @@ -0,0 +1,51 @@ + + + + + GPII Lifecycle Manager Resolver Tests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

      GPII Lifecycle Manager Resolver Tests

      +

      +
      +

      +
        + + + + diff --git a/gpii/node_modules/lifecycleManager/test/html/LifecycleManagerTest.html b/gpii/node_modules/lifecycleManager/test/html/LifecycleManagerTest.html index 94879b602..ebbf6ef15 100644 --- a/gpii/node_modules/lifecycleManager/test/html/LifecycleManagerTest.html +++ b/gpii/node_modules/lifecycleManager/test/html/LifecycleManagerTest.html @@ -4,19 +4,38 @@ GPII Lifecycle Manager Tests - + - + - + + + + - + + + + + + + + + + + + + + + + + diff --git a/gpii/node_modules/lifecycleManager/test/html/LifecycleManagerUtilsTest.html b/gpii/node_modules/lifecycleManager/test/html/LifecycleManagerUtilsTest.html new file mode 100644 index 000000000..bb79b1b11 --- /dev/null +++ b/gpii/node_modules/lifecycleManager/test/html/LifecycleManagerUtilsTest.html @@ -0,0 +1,44 @@ + + + + + GPII Lifecycle Manager Utilities Tests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

        GPII Lifecycle Manager Utilities Tests

        +

        +
        +

        +
          + + + diff --git a/gpii/node_modules/lifecycleManager/test/js/LifecycleManagerQueueTests.js b/gpii/node_modules/lifecycleManager/test/js/LifecycleManagerQueueTests.js new file mode 100644 index 000000000..454091a5d --- /dev/null +++ b/gpii/node_modules/lifecycleManager/test/js/LifecycleManagerQueueTests.js @@ -0,0 +1,144 @@ +/*! +GPII Lifecycle Manager Queue Tests + +Copyright 2017 Raising The Floor - International + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +/* eslint-env browser */ +/* eslint strict: ["error", "function"] */ + +/* global jqUnit, fluid */ + +(function () { + "use strict"; + fluid.setLogging(true); + + var gpii = fluid.registerNamespace("gpii"); + + fluid.registerNamespace("gpii.tests.lifecycleManager.queue"); + + /** + * Creates (and returns) an asynchronous function that returns a promise. + * The function will wait the given `timeout` ms, the run the provided function and finally resolve (or reject) the promise. + * The promise will be rejected if the `reject` parameter is a truthy. The value passed + * in the rejection or resolving of the promise will be the same value that the created function + * is called with + * + * @param {Integer} timeout - The number of ms the function should wait before resolving (or rejecting) the promise. + * @param {String|Function} func - The function to call after `timeout` ms, immediately before resolving or rejecting the promise. + * @param {Boolean} reject - If true, this will cause the promise to be rejected instead of resolved. + * @return {Promise} A promise that will resolve with the original value func was called with or rejected on error. + */ + gpii.tests.lifecycleManager.queue.createAsyncPromiseFunc = function (timeout, func, reject) { + return function (arg) { + var myPromise = fluid.promise(); + setTimeout(function () { + func(arg); + reject ? myPromise.reject(arg) : myPromise.resolve(arg); + }, timeout); + return myPromise; + }; + }; + + /* + * Creates an item suitable for the lifecycle managers queue. + * The function provided to the queue-item is generated by gpii.tests.lifecycleManager.queue.createAsyncPromiseFunc + * using the timeout and func parameters. + */ + gpii.tests.lifecycleManager.queue.createCustomTask = function (timeout, func, arg) { + return { + func: gpii.tests.lifecycleManager.queue.createAsyncPromiseFunc(timeout, func), + arg: arg + }; + }; + + // ensure that we're processing queue in the correct order: + jqUnit.asyncTest("Tasks are being processed on addition", function () { + var lifecycleManager = gpii.lifecycleManager(); + jqUnit.expect(1); + + var queuePromise = lifecycleManager.addToActionQueue(gpii.tests.lifecycleManager.queue.createCustomTask(1, fluid.identity, 1)); + + queuePromise.then(function (val) { + jqUnit.assertEquals("Task was processed on addition to queue", 1, val); + jqUnit.start(); + }); + }); + + // ensure that we're processing queue in the correct order: + jqUnit.asyncTest("Queue is being processed in the correct order", function () { + var lifecycleManager = gpii.lifecycleManager(); + var results = []; + var pushResultFunc = function (arg) { + results.push(arg); + }; + + jqUnit.expect(1); + + lifecycleManager.addToActionQueue(gpii.tests.lifecycleManager.queue.createCustomTask(300, pushResultFunc, 1)); + lifecycleManager.addToActionQueue(gpii.tests.lifecycleManager.queue.createCustomTask(50, pushResultFunc, 2)); + var queuePromise3 = lifecycleManager.addToActionQueue(gpii.tests.lifecycleManager.queue.createCustomTask(100, pushResultFunc, 3)); + + queuePromise3.then(function () { + jqUnit.assertDeepEq("Task was processed on addition to queue", [1, 2, 3], results); + jqUnit.start(); + }); + }); + + jqUnit.asyncTest("Rejected promises causes the queue promise to be rejected", function () { + var lifecycleManager = gpii.lifecycleManager(); + jqUnit.expect(1); + + var task = { + func: gpii.tests.lifecycleManager.queue.createAsyncPromiseFunc(200, fluid.identity, true), + promise: fluid.promise() + }; + var queuePromise = lifecycleManager.addToActionQueue(task); + + queuePromise.then(function () { + jqUnit.fail("Rejected task should reject promise"); + jqUnit.start(); + }, function () { + jqUnit.assertTrue("Task failed and promise was rejected", true); + jqUnit.start(); + }); + }); + + jqUnit.asyncTest("All promises in queue are rejected when a task fail", function () { + var lifecycleManager = gpii.lifecycleManager(); + jqUnit.expect(4); + + var failTask = { + func: gpii.tests.lifecycleManager.queue.createAsyncPromiseFunc(200, fluid.identity, true), + promise: fluid.promise() + }; + // first task that succeeds + lifecycleManager.addToActionQueue(gpii.tests.lifecycleManager.queue.createCustomTask(300, fluid.identity)).then(function () { + jqUnit.assertTrue("First task should succeed", true); + }); + // next task fails + lifecycleManager.addToActionQueue(failTask).then(function () { + jqUnit.fail("Second task should fail"); + }, function () { + jqUnit.assertTrue("Second task failed as expected", true); + }); + // subsequent tasks fail: + lifecycleManager.addToActionQueue(gpii.tests.lifecycleManager.queue.createCustomTask(50, fluid.identity)).then( + function () {}, + function () { jqUnit.assertTrue("third task failed as expected", true); } + ); + lifecycleManager.addToActionQueue(gpii.tests.lifecycleManager.queue.createCustomTask(50, fluid.identity)).then( + function () {}, + function () { + jqUnit.assertTrue("fourth task failed as expected", true); + jqUnit.start(); + } + ); + }); +})(); diff --git a/gpii/node_modules/lifecycleManager/test/js/LifecycleManagerResolverTests.js b/gpii/node_modules/lifecycleManager/test/js/LifecycleManagerResolverTests.js new file mode 100644 index 000000000..160243835 --- /dev/null +++ b/gpii/node_modules/lifecycleManager/test/js/LifecycleManagerResolverTests.js @@ -0,0 +1,109 @@ +/*! + + GPII Lifecycle Manager Resolver Tests + + Copyright 2018 Raising The Floor - International + + Licensed under the New BSD license. You may not use this file except in + compliance with this License. + + You may obtain a copy of the License at + https://github.com/GPII/universal/blob/master/LICENSE.txt + +*/ +/* eslint-env browser */ +/* eslint strict: ["error", "function"] */ + +/* global jqUnit, fluid */ + +(function () { + "use strict"; + fluid.setLogging(true); + + var gpii = fluid.registerNamespace("gpii"); + + fluid.registerNamespace("gpii.tests.lifecycleManager.resolver"); + + fluid.defaults("gpii.tests.lifecycleManager.resolver.resolverConfig", { + gradeNames: ["fluid.component"], + resolvers: {} + }); + + gpii.tests.lifecycleManager.resolver.testOptions = { + distributeOptions: { + source: "{that}.options.resolverType", + target: "{that resolverConfig}.type" + }, + resolverType: "gpii.tests.lifecycleManager.resolver.resolverConfig" + }; + + gpii.tests.lifecycleManager.resolver.runSingleTest = function (testDef) { + jqUnit.test(testDef.message, function () { + //var lifecycleManager = gpii.lifecycleManager(gpii.tests.lifecycleManager.resolver.testOptions); + var lifecycleManager = gpii.lifecycleManager(); + + lifecycleManager.start(testDef.startPayload).then( + function (success) { + jqUnit.assertTrue("gpii.lifecycleManager.start() succeeds", success); + var session = lifecycleManager.getSession("resolver-tests"); + var expanded = session.localResolver(testDef.toExpand); + jqUnit.assertDeepEq(testDef.message + " - Variables should have been resolved as expected.", testDef.expectedResults, expanded); + }, + function () { + jqUnit.fail("There should not have been any errors."); + } + ); + }); + }; + + fluid.defaults("gpii.tests.lifecycleManager.resolver.testRunner", { + gradeNames: ["fluid.component"], + mergePolicy: { + "testDefs": "nomerge, noexpand" + }, + testDefs: { + sessionResolver: { + message: "Session resolver tests", + startPayload: { + gpiiKey: "resolver-tests", + activeConfiguration: { + lifecycleInstructions: {} + } + }, + toExpand: { + session: "${{session}.gpiiKey}" + }, + expectedResults: { + session: "resolver-tests" + } + }, + gpiiKeyResolver: { + message: "GPII key resolver tests", + startPayload: { + gpiiKey: "resolver-tests", + activeConfiguration: { + lifecycleInstructions: {} + } + }, + toExpand: { + gpiiKey: "${{gpiiKey}}" + }, + expectedResults: { + gpiiKey: "resolver-tests" + } + } + }, + invokers: { + runSingleTest: { + funcName: "gpii.tests.lifecycleManager.resolver.runSingleTest", + args: ["{arguments}.0"] // testDef + } + }, + listeners: { + "onCreate.runTests": { + funcName: "fluid.each", + args: ["{that}.options.testDefs", "{that}.runSingleTest"] + } + } + }); +})(); diff --git a/gpii/node_modules/lifecycleManager/test/js/LifecycleManagerTests.js b/gpii/node_modules/lifecycleManager/test/js/LifecycleManagerTests.js index a16fd92a1..3d1423416 100644 --- a/gpii/node_modules/lifecycleManager/test/js/LifecycleManagerTests.js +++ b/gpii/node_modules/lifecycleManager/test/js/LifecycleManagerTests.js @@ -1,101 +1,210 @@ /*! -GPII Configuration Manager Tests +GPII Lifecycle Manager Tests Copyright 2012 OCAD University -Copyright 2012 Raising The Floor - International +Copyright 2012, 2017 Raising The Floor - International Licensed under the New BSD license. You may not use this file except in compliance with this License. +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt +https://github.com/GPII/universal/blob/master/LICENSE.txt */ -// Declare dependencies -/*global require, fluid, jqUnit, gpii, start*/ +/* eslint-env browser */ +/* eslint strict: ["error", "function"] */ -var fluid = fluid || require("infusion"); +/* global jqUnit, fluid */ (function () { "use strict"; + fluid.setLogging(true); var gpii = fluid.registerNamespace("gpii"); - fluid.staticEnvironment.gpiiTests = fluid.typeTag("gpii.tests.lifecycleManager"); - fluid.registerNamespace("gpii.tests.lifecycleManager"); - var fakeEnvironment = { + gpii.tests.lifecycleManager.fakeEnvironment = { JAWS_DIR: "e:\\Programs and Things\\Jaws\\", WINDIR: "c:\\Windows\\" }; fluid.defaults("gpii.tests.lifecycleManager.resolverConfig", { - gradeNames: ["fluid.littleComponent", "autoInit"], + gradeNames: ["fluid.component"], resolvers: { environment: "gpii.tests.lifecycleManager.environmentResolver" } }); gpii.tests.lifecycleManager.environmentResolver = function (name) { - return fakeEnvironment[name]; + return gpii.tests.lifecycleManager.fakeEnvironment[name]; }; - var standardLifecycle = { - "start": [ - "setSettings", - { - "type": "gpii.tests.lifecycleManager.mockExecHandler", - "name": "exec", - "command": "${{environment}.JAWS_DIR}jaws.exe", - args: ["-settingsDirectory", "${{environment}.WINDIR}"] - }], - "stop": [{ - "type": "gpii.tests.lifecycleManager.mockKillHandler", - pid: "${{exec}.pid}" + /* + * ensure everything is clean and ready for the tests + */ + gpii.tests.lifecycleManager.setup = function () { + gpii.tests.lifecycleManager.staticRepository.execHandler = undefined; + + // To keep track of the number of times that the mock settingsHandler's set() is called + gpii.tests.lifecycleManager.NumOfSettingsHandlerSetCalls = 0; + }; + + /* + * Create a solution entry keyed by solution ID. + * All subsequent arguments will be part of the solution entry + */ + gpii.tests.lifecycleManager.buildLifecycleInstructions = function (solId) { + var togo = {}; + + for (var i = 1; i < arguments.length; ++i) { + togo[solId] = fluid.extend(true, {}, togo[solId], arguments[i]); + } + + return togo; + }; + + /** + * Used by a bunch of the tests to avoid having to write out the almost-identical full set of lifecycle + * instructions for each test. Instead one can have a "base set" of lifecycle instructions + * that can be shared across multiple tests/multiple phases of a single test, and then use + * this function to extend base set to match the specific test/test phase. + * + * The function takes an unlimited number of arguments. The first argument is mandatory and must + * be a set of lifecycle instructions entries keyed by solution ids. While the system wont explode + * if entries for multiple solutions are provided, only the "first" solution entry will be + * extended. The remaining arguments will be fluid.extended onto the first argument. + * + * @param {Object} currentInstructions - A set of lifecycle instructions keyed by a solution id. For example, + * this could be the output of the buildLifecycleInstructions function. + * @param {Object} arguments - The remaining arguments should be valid lifecycleInstructions that will + * be extended onto the original. + * @return {Object} The original lifecycle instructions, but with the remaining arguments fluid.extend'ed + * onto the first solution entry in the currentInstructions. + */ + gpii.tests.lifecycleManager.extendLifecycleInstructions = function (currentInstructions) { + if (Object.keys(currentInstructions).length < 1) { + jqUnit.assertTrue("Instructions passed to extendLifecycleInstructions didn't contain an entry as expected", false); + } + var togo = fluid.copy(currentInstructions); + var solId = Object.keys(currentInstructions)[0]; + + for (var i = 1; i < arguments.length; ++i) { + togo[solId] = fluid.extend(true, {}, togo[solId], arguments[i]); + } + + return togo; + }; + + gpii.tests.lifecycleManager.buildSettingsHandlersEntry = function (settings, setRunning, removeLifecycleHandlerBlock) { + var togo = { + "settingsHandlers": { + "myconf": { + "type": "gpii.tests.lifecycleManager.mockSettingsHandler", + "options": {} + } }, - "restoreSettings"] + "launchHandlers": { + "launcher": { + "type": "gpii.tests.lifecycleManager.mockLaunchHandler", + "options": {} + } + } }; + if (settings !== undefined) { + fluid.set(togo, [ "settingsHandlers", "myconf", "settings" ], settings); + } else { + delete togo.settingsHandlers; + } + if (setRunning !== undefined) { + fluid.set(togo, [ "launchHandlers", "launcher", "settings" ], { running: setRunning }); + } + if (removeLifecycleHandlerBlock) { + delete togo.launchHandlers; + } + return togo; + }; - var configurationSpec = [{ - "id": "org.gnome.desktop.a11y.magnifier", - "settingsHandlers": [{ - "type": "gpii.tests.lifecycleManager.mockSettingsHandler", - "settings": { - "cross-hairs-clip": true, - "cross-hairs-color": "red" - }, - "options": {} - }], - "lifecycleManager": standardLifecycle - }]; + gpii.tests.lifecycleManager.noUpdateLifecycle = { + "active": true, + "configure": [ + "settings.myconf" + ], + "restore": [ + "settings.myconf" + ], + "start": [ + { + "type": "gpii.tests.lifecycleManager.mockExecHandler", + "name": "exec", + "command": "${{environment}.JAWS_DIR}jaws.exe", + args: ["-settingsDirectory", "${{environment}.WINDIR}"] + } + ], + "stop": [ + { + "type": "gpii.tests.lifecycleManager.mockKillHandler", + pid: "${{exec}.pid}" + } + ] + }; - var createPayloadForSettingsHandlerRequest = { - "type": "gpii.gsettings.set", - "settings": { - "cross-hairs-clip": true, - "cross-hairs-color": "red" - }, - "options": {} + gpii.tests.lifecycleManager.standardLifecycle = { + "active": true, + "configure": [ + "settings.myconf" + ], + "restore": [ + "settings.myconf" + ], + "start": [ + "launchers.launcher" + ], + "stop": [ + "launchers.launcher" + ], + "isRunning": [ + "launchers.launcher" + ] }; - var createPayloadForSettingsHandlerResponse = { - "org.gnome.desktop.a11y.magnifier": [{ - "settings": { - "cross-hairs-clip": true, - "cross-hairs-color": "red" + gpii.tests.lifecycleManager.configurationSpec = gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.buildSettingsHandlersEntry({ "cross-hairs-clip": true, "cross-hairs-color": "red" }), + gpii.tests.lifecycleManager.noUpdateLifecycle, + { "update": [ "settings.myconf" ] }); + + + gpii.tests.lifecycleManager.specToSettingsHandlerTests = { + "Basic test for response and conversion": { + request: { + "type": "gpii.gsettings.set", + "settings": { + "cross-hairs-clip": true, + "cross-hairs-color": "red" + }, + "options": {} }, - "options": {} - }] + response: [{ + "settings": { + "cross-hairs-clip": true, + "cross-hairs-color": "red" + }, + "options": {} + }] + } }; - var parseHandlerResponseFunctionRequestNoSettings = { + gpii.tests.lifecycleManager.responseToSnapshotRequestNoSettings = { "org.gnome.desktop.interface": [{ "settings": {} }] }; - var parseHandlerResponseFunctionRequest = { + gpii.tests.lifecycleManager.responseToSnapshotRequest = { "org.gnome.desktop.a11y.magnifier": [{ "settings": { "cross-hairs-clip": { @@ -112,37 +221,66 @@ var fluid = fluid || require("infusion"); }] }; - var parseHandlerResponseFunctionExpectedResponse = { + gpii.tests.lifecycleManager.responseToSnapshotExpectedResponse = { "settings": { "cross-hairs-clip": false, "cross-hairs-color": "red" } }; - var invokeSettingsHandlersRequest = [{ + gpii.tests.lifecycleManager.invokeSettingsHandlersRequest = { "type": "gpii.tests.lifecycleManager.mockSettingsHandler", "settings": { "cross-hairs-clip": true, "cross-hairs-color": "red" }, "options": {} - }]; + }; - var invokeSettingsHandlersExpectedSnapshot = [{ + gpii.tests.lifecycleManager.invokeSettingsHandlersExpectedSnapshot = { "type": "gpii.tests.lifecycleManager.mockSettingsHandler", "settings": { "cross-hairs-clip": false, "cross-hairs-color": "red" }, "options": {} - }]; + }; + + gpii.tests.lifecycleManager.testOptions = { + distributeOptions: { + "resolverConfig": { + record: "gpii.tests.lifecycleManager.resolverConfig", + target: "{that resolverConfig}.type" + } + } + }; + + gpii.tests.lifecycleManager.userOptions = { + gpiiKey: 123 + }; + + gpii.tests.lifecycleManager.buildStartPayload = function (lifecycleInstructions) { + return fluid.extend(true, {}, gpii.tests.lifecycleManager.userOptions, { + activeConfiguration: { + lifecycleInstructions: lifecycleInstructions + } + }); + }; + + gpii.tests.lifecycleManager.startPayload = gpii.tests.lifecycleManager.buildStartPayload(gpii.tests.lifecycleManager.configurationSpec); + + gpii.tests.lifecycleManager.sampleActiveSession = { + 123: { + gpiiKey: 123 + } + }; // Repository for "static" information reported by various kinds of handlers during the lifecycle // cleared to be empty on each test start - gpii.tests.staticRepository = {}; + gpii.tests.lifecycleManager.staticRepository = {}; gpii.tests.lifecycleManager.mockExecHandler = function (command, args) { - gpii.tests.staticRepository.execHandler = { + gpii.tests.lifecycleManager.staticRepository.execHandler = { command: command, args: args }; @@ -161,7 +299,7 @@ var fluid = fluid || require("infusion"); }); gpii.tests.lifecycleManager.mockKillHandler = function (pid) { - gpii.tests.staticRepository.killHandler = { + gpii.tests.lifecycleManager.staticRepository.killHandler = { pid: pid }; }; @@ -169,30 +307,120 @@ var fluid = fluid || require("infusion"); fluid.defaults("gpii.tests.lifecycleManager.mockKillHandler", { gradeNames: "fluid.function", argumentMap: { - pid: 0, + pid: 0 } }); - gpii.tests.lifecycleManager.mockSettingsHandler = function (data) { - gpii.tests.staticRepository.settingsHandler = data; - return parseHandlerResponseFunctionRequest; + gpii.tests.lifecycleManager.backingMockSettingsHandler = null; // initialised in every test that requires it + + gpii.tests.lifecycleManager.initBackingMock = function (origSettings, setRunning) { + gpii.tests.lifecycleManager.backingMockSettingsHandler = gpii.test.integration.mockSettingsHandler({ + optionsPathKey: function (options, solutionId) { + return "settings|" + solutionId; + } + }); + gpii.tests.lifecycleManager.backingMockLaunchHandler = gpii.test.integration.mockSettingsHandler({ + optionsPathKey: function (options, solutionId) { + return "launch|" + solutionId; + } + }); + + var settings = origSettings === undefined ? // use default if no parameter is given + gpii.tests.lifecycleManager.settingsHandlerOriginalSystemSettings : + origSettings; + + setRunning = (setRunning === undefined) ? false : setRunning; // default to false + var launchSettings = gpii.tests.lifecycleManager.launchHandlerOriginalSystemSettingsRunning(setRunning); + // initialise the mock with the initial settings expected at the end of the test + gpii.tests.lifecycleManager.backingMockSettingsHandler.set(settings); + gpii.tests.lifecycleManager.backingMockLaunchHandler.set(launchSettings); + // and ensure our 'local' storage is reset + gpii.tests.lifecycleManager.staticRepository.settingsHandler = undefined; + gpii.tests.lifecycleManager.staticRepository.launchHandler = undefined; + gpii.tests.lifecycleManager.staticRepository.launchHandlerCalls = 0; }; - gpii.tests.lifecycleManager.assertExpectedExec = function () { - jqUnit.assertDeepEq("Exec handler fired with expected arguments", - { - "command": fakeEnvironment.JAWS_DIR+"jaws.exe", - args: ["-settingsDirectory", fakeEnvironment.WINDIR] - }, - gpii.tests.staticRepository.execHandler); + gpii.tests.lifecycleManager.mockSettingsHandler = { + set: function (data) { + gpii.tests.lifecycleManager.NumOfSettingsHandlerSetCalls++; + + var togo = fluid.promise(); + gpii.invokeLater(function () { + gpii.tests.lifecycleManager.staticRepository.settingsHandler = data; + var response = gpii.resolveSync(gpii.tests.lifecycleManager.backingMockSettingsHandler.set(data)); + togo.resolve(response); + }, 100); + return togo; + }, + get: function (data) { + return gpii.resolveSync(gpii.tests.lifecycleManager.backingMockSettingsHandler.get(data)); + } }; - gpii.tests.lifecycleManager.assertExpectedSettingsHandler = function (message, expected) { - jqUnit.assertDeepEq("expected input sent to settingsHandler" + message, expected, gpii.tests.staticRepository.settingsHandler); + gpii.tests.lifecycleManager.mockLaunchHandler = { + set: function (data) { + var togo = fluid.promise(); + gpii.invokeLater(function () { + gpii.tests.lifecycleManager.staticRepository.launchHandlerCalls++; + gpii.tests.lifecycleManager.staticRepository.launchHandler = data; + var response = gpii.resolveSync(gpii.tests.lifecycleManager.backingMockLaunchHandler.set(data)); + togo.resolve(response); + }, 100); + return togo; + }, + get: function (data) { + return gpii.resolveSync(gpii.tests.lifecycleManager.backingMockLaunchHandler.get(data)); + } + }; + + gpii.tests.lifecycleManager.createSettingsHandlerPayload = function (solutionId, settings) { + if (settings === undefined) { + return undefined; + } else { + var togo = {}; + togo[solutionId] = [ + { + options: {}, + settings: settings + } + ]; + return togo; + } + }; + + gpii.tests.lifecycleManager.settingsHandlerOriginalSystemSettings = { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "cross-hairs-clip": false, + "cross-hairs-color": "red", + "iamasetting": 200, + "undefSetting": undefined + }, + "options": { + // "storePath": "settingsBlock1" + } + }], + "other.application": [{ + "settings": { + "mysetting": "Hello World" + }, + "options": { + // "storePath": "settingsBlock1" + } + }] }; + gpii.tests.lifecycleManager.launchHandlerOriginalSystemSettingsRunning = function (setRunning) { + return { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "running": setRunning + } + }] + }; + }; - var settingsHandlerExpectedInputNewSettings = { + gpii.tests.lifecycleManager.settingsHandlerExpectedInputNewSettings = { "org.gnome.desktop.a11y.magnifier": [{ "settings": { "cross-hairs-clip": true, @@ -202,7 +430,7 @@ var fluid = fluid || require("infusion"); }] }; - var settingsHandlerExpectedInputRestoreSettings = { + gpii.tests.lifecycleManager.settingsHandlerExpectedInputRestoreSettings = { "org.gnome.desktop.a11y.magnifier": [{ "settings": { "cross-hairs-clip": false, @@ -212,67 +440,982 @@ var fluid = fluid || require("infusion"); }] }; + gpii.tests.lifecycleManager.startTestDefs = [{ + name: "Solution with all lifecycle blocks", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle + ), + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: false, + appliedSettings: { "cross-hairs-clip": false, "cross-hairs-color": "green" }, + runningAfterLogin: true, + expectedLaunchHandlerCalls: 2 + }, { + name: "Solution with no explicit configure and restore (GPII-2106)", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + { + "active": true, + "start": [ + "launchers.launcher" + ], + "stop": [ + "launchers.launcher" + ], + "isRunning": [ + "launchers.launcher" + ] + } + ), + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: false, + appliedSettings: { "cross-hairs-clip": false, "cross-hairs-color": "green" }, + runningAfterLogin: true, + expectedLaunchHandlerCalls: 2 + }, { + name: "Solution with no configure and restore and no settingsHandler block", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + { + "active": true, + "start": [ + "launchers.launcher" + ], + "stop": [ + "launchers.launcher" + ], + "isRunning": [ + "launchers.launcher" + ] + } + ), + originalSettings: undefined, + runningOnLogin: false, + appliedSettings: undefined, + runningAfterLogin: true, + expectedLaunchHandlerCalls: 2 + }, { + name: "no explicit start, stop, isRunning directives", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + { + "active": true, + "configure": [ + "settings.myconf" + ], + "restore": [ + "settings.myconf" + ] + } + ), + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: false, + appliedSettings: { "cross-hairs-clip": false, "cross-hairs-color": "green" }, + runningAfterLogin: true, + expectedLaunchHandlerCalls: 2 + }, { + name: "no explicit start, stop, isRunning directives and no launchHandlers block", // expect no launch handler calls + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + { + "active": true, + "configure": [ + "settings.myconf" + ], + "restore": [ + "settings.myconf" + ] + } + ), + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: undefined, + appliedSettings: { "cross-hairs-clip": false, "cross-hairs-color": "green" }, + runningAfterLogin: undefined, + expectedLaunchHandlerCalls: 0, + removeLifecycleHandlerBlock: true + }, { + name: "no explicit lifecycle directives (i.e. start,stop,configure,restore,isRunning)", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + { + "active": true + } + ), + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: false, + appliedSettings: { "cross-hairs-clip": false, "cross-hairs-color": "green" }, + runningAfterLogin: true, + expectedLaunchHandlerCalls: 2 + }, { + name: "solution with active: false", // no launch handler calls + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle, + { "active": false } + ), + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: false, + appliedSettings: { "cross-hairs-clip": false, "cross-hairs-color": "green" }, + runningAfterLogin: undefined, + expectedLaunchHandlerCalls: 0 + }, { + name: "solution with active: undefined", // expect no launch handler calls + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + { + "active": undefined, + "configure": [ + "settings.myconf" + ], + "restore": [ + "settings.myconf" + ], + "start": [ + "launchers.launcher" + ], + "stop": [ + "launchers.launcher" + ], + "isRunning": [ + "launchers.launcher" + ] + } + ), + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: false, + appliedSettings: { "cross-hairs-clip": false, "cross-hairs-color": "green" }, + runningAfterLogin: undefined, + expectedLaunchHandlerCalls: 0 + }, { + name: "running on login - 'update' with reference to settings handler block", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle, + { + "update": [ + "settings.myconf" + ] + } + ), + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: true, + appliedSettings: { "cross-hairs-clip": false, "cross-hairs-color": "green" }, + runningAfterLogin: undefined, + expectedLaunchHandlerCalls: 0 + }, { + name: "running on login - 'update' with reference to 'configure' block", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle, + { + "update": [ + "configure" + ] + } + ), + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: true, + appliedSettings: { "cross-hairs-clip": false, "cross-hairs-color": "green" }, + runningAfterLogin: undefined, + expectedLaunchHandlerCalls: 0 + }, { + name: "running on login - 'update' with reference to 'stop', 'configure' and 'start' blocks", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle, + { + "update": [ + "stop", + "configure", + "start" + ] + } + ), + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: true, + appliedSettings: { "cross-hairs-clip": false, "cross-hairs-color": "green" }, + runningAfterLogin: true, + expectedLaunchHandlerCalls: 4 // we run start/stop cycle both on login and logout + }, { + name: "running on login - with active: false", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle, + { + "active": false + } + ), + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: true, + appliedSettings: { "cross-hairs-clip": false, "cross-hairs-color": "green" }, + runningAfterLogin: false, + expectedLaunchHandlerCalls: 2 + }, { + name: "running on login - with active: undefined", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + { + "active": undefined, + "configure": [ + "settings.myconf" + ], + "restore": [ + "settings.myconf" + ], + "start": [ + "launchers.launcher" + ], + "stop": [ + "launchers.launcher" + ], + "isRunning": [ + "launchers.launcher" + ], + "update": [ + "stop", + "configure", + "start" + ] + } + ), + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: true, + appliedSettings: { "cross-hairs-clip": false, "cross-hairs-color": "green" }, + runningAfterLogin: true, + expectedLaunchHandlerCalls: 4 // we run start/stop cycle both on login and logout + }]; + + + gpii.tests.lifecycleManager.updateTestDefs = [{ + name: "Updating with the same prefs and values as already applied with the application not running at the login - the prefs should NOT be re-applied", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle, + { "update": [ "settings.myconf" ] } + ), + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: false, + loginSettings: { "cross-hairs-clip": true, "cross-hairs-color": "red" }, + runningAfterLogin: true, + updateSettings: { "cross-hairs-clip": true, "cross-hairs-color": "red" }, + runningAfterUpdate: true, + expectedSettings: { "cross-hairs-clip": true, "cross-hairs-color": "red" }, + expectedSettingsHandlerSetCalls: 1, + expectedLaunchHandlerCalls: 2 + }, { + name: "single pref changed without 'update' directive in solution registry entry", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle + ), + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: false, + loginSettings: { "cross-hairs-clip": true, "cross-hairs-color": "red" }, + runningAfterLogin: true, + updateSettings: { "cross-hairs-clip": false, "cross-hairs-color": "green" }, // don't expect these to apply + runningAfterUpdate: true, + expectedSettings: { "cross-hairs-clip": true, "cross-hairs-color": "red" }, + expectedSettingsHandlerSetCalls: 1, + expectedLaunchHandlerCalls: 2 + }, { + name: "Updating with normal reference to settingsHandler block, single settings", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle, + { "update": [ "settings.myconf" ] } + ), + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: false, + loginSettings: { "cross-hairs-clip": true, "cross-hairs-color": "red" }, + runningAfterLogin: true, + updateSettings: { "cross-hairs-color": "green" }, + runningAfterUpdate: true, + expectedSettings: { "cross-hairs-clip": true, "cross-hairs-color": "green" }, + expectedSettingsHandlerSetCalls: 2, + expectedLaunchHandlerCalls: 2 + }, { + name: "Updating with normal reference to settingsHandler block, multiple settings", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle, + { "update": [ "settings.myconf" ] } + ), + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: false, + loginSettings: { "cross-hairs-clip": true, "cross-hairs-color": "red" }, + runningAfterLogin: true, + updateSettings: { "cross-hairs-clip": false, "cross-hairs-color": "green" }, + runningAfterUpdate: true, + expectedSettings: { "cross-hairs-clip": false, "cross-hairs-color": "green" }, + expectedSettingsHandlerSetCalls: 2, + expectedLaunchHandlerCalls: 2 + }, { + name: "Updating with reference to 'configure' directive, single settings", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle, + { "update": [ "configure" ] } + ), + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: false, + loginSettings: { "cross-hairs-clip": true, "cross-hairs-color": "red" }, + runningAfterLogin: true, + updateSettings: { "cross-hairs-color": "green" }, + runningAfterUpdate: true, + expectedSettings: { "cross-hairs-clip": true, "cross-hairs-color": "green" }, + expectedSettingsHandlerSetCalls: 2, + expectedLaunchHandlerCalls: 2 + }, { + name: "Updating with normal reference to settingsHandler block, and a setting not in the original settings applied to the system", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle, + { "update": [ "settings.myconf" ] } + ), + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: false, + loginSettings: { "cross-hairs-clip": true, "cross-hairs-color": "red" }, + runningAfterLogin: true, + updateSettings: { "cross-hairs-color": "green", "iamasetting": 100 }, + runningAfterUpdate: true, + expectedSettings: { "cross-hairs-clip": true, "cross-hairs-color": "green", "iamasetting": 100 }, + originalSettingsAfterUpdate: { "cross-hairs-clip": false, "cross-hairs-color": "red", "iamasetting": undefined }, + expectedSettingsHandlerSetCalls: 2, + expectedLaunchHandlerCalls: 2 + }, { + name: "Updating with no 'update' block in solutions registry entry and 'live' liveness", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle + ), + payloadExtras: { + settingsHandlers: { + myconf: { + liveness: "live" + } + } + }, + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: false, + loginSettings: { "cross-hairs-clip": true, "cross-hairs-color": "red" }, + runningAfterLogin: true, + updateSettings: { "cross-hairs-clip": true, "cross-hairs-color": "green" }, + runningAfterUpdate: true, + expectedSettings: { "cross-hairs-clip": true, "cross-hairs-color": "green" }, + expectedSettingsHandlerSetCalls: 2, + expectedLaunchHandlerCalls: 2 + }, { + name: "Updating with no 'update' block in solutions registry entry and no liveness", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle + ), + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: false, + loginSettings: { "cross-hairs-clip": true, "cross-hairs-color": "red" }, + runningAfterLogin: true, + updateSettings: { "cross-hairs-clip": true, "cross-hairs-color": "green" }, + runningAfterUpdate: true, + expectedSettings: { "cross-hairs-clip": true, "cross-hairs-color": "red" }, + expectedSettingsHandlerSetCalls: 1, + expectedLaunchHandlerCalls: 2 + }, { + name: "Updating with no 'update' block in solutions registry entry and 'liveRestart' liveness", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle + ), + payloadExtras: { + settingsHandlers: { + myconf: { + liveness: "liveRestart" + } + } + }, + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: false, + loginSettings: { "cross-hairs-clip": true, "cross-hairs-color": "red" }, + runningAfterLogin: true, + updateSettings: { "cross-hairs-clip": true, "cross-hairs-color": "green" }, + runningAfterUpdate: true, + expectedSettings: { "cross-hairs-clip": true, "cross-hairs-color": "green" }, + expectedSettingsHandlerSetCalls: 2, + expectedLaunchHandlerCalls: 4 + }, { + name: "Updating with no 'update' block in solutions registry entry and 'manualRestart' liveness", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle + ), + payloadExtras: { + settingsHandlers: { + myconf: { + liveness: "manualRestart" + } + } + }, + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: false, + loginSettings: { "cross-hairs-clip": true, "cross-hairs-color": "red" }, + runningAfterLogin: true, + updateSettings: { "cross-hairs-clip": true, "cross-hairs-color": "green" }, + runningAfterUpdate: true, + expectedSettings: { "cross-hairs-clip": true, "cross-hairs-color": "green" }, + expectedSettingsHandlerSetCalls: 2, + expectedLaunchHandlerCalls: 4 + }, { + name: "Updating with no 'update' block in solutions registry entry and 'OSRestart' liveness", + basePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle + ), + payloadExtras: { + settingsHandlers: { + myconf: { + liveness: "OSRestart" + } + } + }, + originalSettings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }, + runningOnLogin: false, + loginSettings: { "cross-hairs-clip": true, "cross-hairs-color": "red" }, + runningAfterLogin: true, + updateSettings: { "cross-hairs-clip": true, "cross-hairs-color": "green" }, + runningAfterUpdate: true, + expectedSettings: { "cross-hairs-clip": true, "cross-hairs-color": "green" }, + expectedSettingsHandlerSetCalls: 2, + expectedLaunchHandlerCalls: 4 + }]; + + gpii.tests.lifecycleManager.completeLifecycleTests = [{ + name: "Updating with normal reference to settingsHandler block, and an application not in the original settings applied to the system", + expect: 10, + expectedLaunchHandlerCalls: 2, + expectedSettingsHandlerSetCalls: 2, + runningOnLogin: false, + originalSettings: { + "org.gnome.desktop.a11y.magnifier": [{ options: {}, settings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }}] + }, + loginPayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle, + { "update": [ "configure" ] }, + gpii.tests.lifecycleManager.buildSettingsHandlersEntry({ "cross-hairs-clip": true, "cross-hairs-color": "red" })), + expectedOriginal: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle, + { "update": [ "configure" ] }, + gpii.tests.lifecycleManager.buildSettingsHandlersEntry({ "cross-hairs-clip": false, "cross-hairs-color": "red" }, false)), + expectedAfterLogin: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle, + { "update": [ "configure" ] }, + gpii.tests.lifecycleManager.buildSettingsHandlersEntry({ "cross-hairs-clip": true, "cross-hairs-color": "red" }, true)), + runningAfterLogin: true, + updatePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions("other.application", + gpii.tests.lifecycleManager.buildSettingsHandlersEntry({ "mysetting": "Hallow World" }), + gpii.tests.lifecycleManager.standardLifecycle, + { "update": [ "configure" ] }), + expectedAfterUpdate: fluid.extend(true, {}, + gpii.tests.lifecycleManager.buildLifecycleInstructions("org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.buildSettingsHandlersEntry({ "cross-hairs-clip": true, "cross-hairs-color": "red" }, true), + gpii.tests.lifecycleManager.standardLifecycle, + { "update": [ "configure" ] }), + gpii.tests.lifecycleManager.buildLifecycleInstructions("other.application", + gpii.tests.lifecycleManager.buildSettingsHandlersEntry({ "mysetting": "Hallow World" }), + gpii.tests.lifecycleManager.standardLifecycle, + { "update": [ "configure" ] })), + expectedOriginalSettingsAfterUpdate: gpii.tests.lifecycleManager.buildLifecycleInstructions("org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.buildSettingsHandlersEntry({ "cross-hairs-clip": false, "cross-hairs-color": "red" }, false), + gpii.tests.lifecycleManager.standardLifecycle, + { "update": [ "configure" ] }), + originalSettingsAfterUpdate: { + "other.application": [{ + "options": {}, + "settings": { + "mysetting": undefined + } + }] + }, + runningAfterUpdate: true + }, { + name: "Updating with normal reference to settingsHandler block, and 'undefined' value stored in snapshot", + expect: 10, + expectedLaunchHandlerCalls: 2, + expectedSettingsHandlerSetCalls: 2, + runningOnLogin: false, + originalSettings: { + "org.gnome.desktop.a11y.magnifier": [{ options: {}, settings: { "cross-hairs-clip": false, "cross-hairs-color": "red" }}] + }, + loginPayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle, + { "update": [ "configure" ] }, + gpii.tests.lifecycleManager.buildSettingsHandlersEntry({ "cross-hairs-clip": true })), + expectedOriginal: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle, + { "update": [ "configure" ] }, + gpii.tests.lifecycleManager.buildSettingsHandlersEntry({ "cross-hairs-clip": false }, false)), + expectedAfterLogin: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.standardLifecycle, + { "update": [ "configure" ] }, + gpii.tests.lifecycleManager.buildSettingsHandlersEntry({ "cross-hairs-clip": true }, true)), + runningAfterLogin: true, + updatePayload: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.buildSettingsHandlersEntry({ "undefSetting": "some other value" }), + gpii.tests.lifecycleManager.standardLifecycle, + { "update": [ "configure" ] }), + expectedAfterUpdate: gpii.tests.lifecycleManager.buildLifecycleInstructions( + "org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.buildSettingsHandlersEntry({ "cross-hairs-clip": true, "undefSetting": "some other value" }, true), + gpii.tests.lifecycleManager.standardLifecycle, + { "update": [ "configure" ] }), + expectedOriginalSettingsAfterUpdate: gpii.tests.lifecycleManager.buildLifecycleInstructions("org.gnome.desktop.a11y.magnifier", + gpii.tests.lifecycleManager.buildSettingsHandlersEntry({ "cross-hairs-clip": false, "undefSetting": undefined }, false), + gpii.tests.lifecycleManager.standardLifecycle, + { "update": [ "configure" ] }), + originalSettingsAfterUpdate: gpii.tests.lifecycleManager.createSettingsHandlerPayload("org.gnome.desktop.a11y.magnifier", { "cross-hairs-clip": false, "undefSetting": undefined }), + runningAfterUpdate: true + }]; + + gpii.tests.lifecycleManager.checkFinalSettingsPayload = function (msg, expSettings, store) { + var expectedLastSettings; + if (expSettings === undefined) { + expectedLastSettings = expSettings; + } else { + // change to the format that is stoed by the mock settingshandler: + expectedLastSettings = { + "org.gnome.desktop.a11y.magnifier": [ + { + "options": {}, + "settings": expSettings + } + ] + }; + } + jqUnit.assertDeepEq(msg, expectedLastSettings, store); + }; + + /* + * expects an input with the following: + * name: name of the test + * expect: number of tests to expect + * expectedLaunchHandlerCalls: number of luanch handler calls to expect (incl. logout) + * expectedSettingsHandlerSetCalls: number of setting handler calls on set() to expect (incl. logout) + * originalSettings: the original settings to apply to the system (ie. before login) + * runningOnLogin: boolean - if the magnifier should be running before login + * loginPayload: payload to send to the start() function + * expectedOriginal: the expected "originalSettings" of the session after login + * expectedAfterLogin: the expected "currentSettings" of the session after login + * runningAfterLogin: boolean - if the magnifier should be running after login + * --- The below keys are optional and will trigger an update cycle if present. --- + * updatePayload: payload to send to the update() function + * expectedAfterUpdate: the expected "currentSettings" of the session after update call + * expectedOriginalSettingsAfterUpdate: the original after a call to update() has been done, + * originalSettingsAfterUpdate: the settings that the system should reset to (ie. last call to settingsHandler) + * runningAfterUpdate: boolean - if the magnifier should be running after update + */ + gpii.tests.lifecycleManager.runSequentialLifecycleTest = function (testDef) { + var originalRunSetting = testDef.expectedLaunchHandlerCalls === 0 ? undefined : testDef.runningOnLogin; + jqUnit.asyncTest("Sequential " + testDef.name, function () { + gpii.tests.lifecycleManager.setup(); + jqUnit.expect(testDef.expect); + var lifecycleManager = gpii.lifecycleManager(gpii.tests.lifecycleManager.testOptions); + gpii.tests.lifecycleManager.initBackingMock(testDef.originalSettings, testDef.runningOnLogin); + + lifecycleManager.start(gpii.tests.lifecycleManager.buildStartPayload(testDef.loginPayload)).then(function (success) { + jqUnit.assertTrue("gpii.lifecycleManager.start() succeeds", success); + var session = lifecycleManager.getSession(123); // always just use the default GPII key + jqUnit.assertDeepEq("Checking currentSettings after start", testDef.expectedAfterLogin, session.model.currentSettings); + + var originalSettings = session.model.originalSettings; + jqUnit.assertDeepEq("Checking originalSettings after start", gpii.lifecycleManager.transformAllSolutionSettings(testDef.expectedOriginal, gpii.settingsHandlers.settingsToChanges), originalSettings); + + var stopCycle = function () { + lifecycleManager.stop(gpii.tests.lifecycleManager.userOptions).then(function () { + jqUnit.assertDeepEq("Session not running after stop", lifecycleManager.getSession(123)); + jqUnit.assertEquals("Number of calls to launch handler", testDef.expectedLaunchHandlerCalls, gpii.tests.lifecycleManager.staticRepository.launchHandlerCalls); + // Check that last calls to launch and settings handlers were actually the ones resetting to expected state + jqUnit.assertDeepEq("checking last payload sent to settingshandler:", testDef.originalSettingsAfterUpdate || testDef.originalSettings, gpii.tests.lifecycleManager.staticRepository.settingsHandler); + + // Check that last calls to launch and settings handlers were actually the ones resetting to expected state + gpii.tests.lifecycleManager.checkFinalSettingsPayload("checking last set launch settings", (originalRunSetting === undefined) ? undefined : { running: originalRunSetting }, gpii.tests.lifecycleManager.staticRepository.launchHandler); + jqUnit.start(); + }); + }; + if (testDef.updatePayload) { + lifecycleManager.update(gpii.tests.lifecycleManager.buildStartPayload(testDef.updatePayload)).then(function () { + jqUnit.assertDeepEq("Checking currentSettings after update", testDef.expectedAfterUpdate, session.model.currentSettings); + jqUnit.assertEquals("Checking the number of times that set() is called", testDef.expectedSettingsHandlerSetCalls, gpii.tests.lifecycleManager.NumOfSettingsHandlerSetCalls); + var expected = gpii.lifecycleManager.transformAllSolutionSettings(testDef.expectedOriginalSettingsAfterUpdate, gpii.settingsHandlers.settingsToChanges); + jqUnit.assertDeepEq("Checking originalSettings after update", expected, originalSettings); + + }); + stopCycle(); + } else { + stopCycle(); + } + }); + }); + }; + + /* + * expects an input with the following: + * name: name of the test + * expect: number of tests to expect + * expectedLaunchHandlerCalls: number of luanch handler calls to expect (incl. logout) + * originalSettings: the original settings to apply to the system (ie. before login) + * runningOnLogin: boolean - if the magnifier should be running before login + * loginPayload: payload to send to the start() function + * expectedOriginal: the expected "originalSettings" of the session after login + * expectedAfterLogin: the expected "currentSettings" of the session after login + * runningAfterLogin: boolean - if the magnifier should be running after login + * --- The below keys are optional and will trigger an update cycle if present. --- + * updatePayload: payload to send to the update() function + * expectedAfterUpdate: the expected "currentSettings" of the session after update call + * expectedOriginalSettingsAfterUpdate: the original after a call to update() has been done, + * originalSettingsAfterUpdate: the settings that the system should reset to (ie. last call to settingsHandler) + * runningAfterUpdate: boolean - if the magnifier should be running after update + */ + gpii.tests.lifecycleManager.runAsyncLifecycleTest = function (testDef) { + var originalRunSetting = testDef.expectedLaunchHandlerCalls === 0 ? undefined : testDef.runningOnLogin; + jqUnit.asyncTest("Async " + testDef.name, function () { + gpii.tests.lifecycleManager.setup(); + jqUnit.expect(testDef.expect); + var lifecycleManager = gpii.lifecycleManager(gpii.tests.lifecycleManager.testOptions); + gpii.tests.lifecycleManager.initBackingMock(testDef.originalSettings, testDef.runningOnLogin); + var session, + originalSettings; + + lifecycleManager.start(gpii.tests.lifecycleManager.buildStartPayload(testDef.loginPayload)).then(function (success) { + session = lifecycleManager.getSession(123); // always just use the default GPII key + jqUnit.assertTrue("gpii.lifecycleManager.start() succeeds", success); + jqUnit.assertDeepEq("Checking currentSettings after start", testDef.expectedAfterLogin, session.model.currentSettings); + originalSettings = session.model.originalSettings; + jqUnit.assertDeepEq("Checking originalSettings after start", gpii.lifecycleManager.transformAllSolutionSettings(testDef.expectedOriginal, gpii.settingsHandlers.settingsToChanges), originalSettings); + }); + + if (testDef.updatePayload) { + lifecycleManager.update(gpii.tests.lifecycleManager.buildStartPayload(testDef.updatePayload)).then(function () { + jqUnit.assertDeepEq("Checking currentSettings after update", testDef.expectedAfterUpdate, session.model.currentSettings); + var expected = gpii.lifecycleManager.transformAllSolutionSettings(testDef.expectedOriginalSettingsAfterUpdate, gpii.settingsHandlers.settingsToChanges); + jqUnit.assertDeepEq("Checking originalSettings after update", expected, originalSettings); + jqUnit.assertEquals("Checking the number of times that set() is called", testDef.expectedSettingsHandlerSetCalls, gpii.tests.lifecycleManager.NumOfSettingsHandlerSetCalls); + }); + } + + lifecycleManager.stop(gpii.tests.lifecycleManager.userOptions).then(function () { + jqUnit.assertUndefined("Session not running after stop", lifecycleManager.getSession(123)); + jqUnit.assertEquals("Number of calls to launch handler", testDef.expectedLaunchHandlerCalls, gpii.tests.lifecycleManager.staticRepository.launchHandlerCalls); + // Check that last calls to launch and settings handlers were actually the ones resetting to expected state + jqUnit.assertDeepEq("checking last payload sent to settingshandler:", testDef.originalSettingsAfterUpdate || testDef.originalSettings, gpii.tests.lifecycleManager.staticRepository.settingsHandler); + + // Check that last calls to launch and settings handlers were actually the ones resetting to expected state + gpii.tests.lifecycleManager.checkFinalSettingsPayload("checking last set launch settings", (originalRunSetting === undefined) ? undefined : { running: originalRunSetting }, gpii.tests.lifecycleManager.staticRepository.launchHandler); + jqUnit.start(); + }); + }); + }; + + gpii.tests.lifecycleManager.buildUpdateTests = function () { + fluid.each(gpii.tests.lifecycleManager.updateTestDefs, function (test) { + var expectedOriginal = gpii.tests.lifecycleManager.extendLifecycleInstructions(test.basePayload, + gpii.tests.lifecycleManager.buildSettingsHandlersEntry(test.originalSettings, test.runningOnLogin), test.payloadExtras); + var loginPayload = gpii.tests.lifecycleManager.extendLifecycleInstructions(test.basePayload, + gpii.tests.lifecycleManager.buildSettingsHandlersEntry(test.loginSettings), test.payloadExtras); + var expectedAfterLogin = gpii.tests.lifecycleManager.extendLifecycleInstructions(test.basePayload, + gpii.tests.lifecycleManager.buildSettingsHandlersEntry(test.loginSettings, test.runningAfterLogin), test.payloadExtras); + var updatePayload = gpii.tests.lifecycleManager.extendLifecycleInstructions(test.basePayload, + gpii.tests.lifecycleManager.buildSettingsHandlersEntry(test.updateSettings), test.payloadExtras); + var expectedAfterUpdate = gpii.tests.lifecycleManager.extendLifecycleInstructions(test.basePayload, + gpii.tests.lifecycleManager.buildSettingsHandlersEntry(test.expectedSettings, test.runningAfterUpdate), test.payloadExtras); + + var originalSettingsAfterUpdate = test.originalSettingsAfterUpdate || test.originalSettings; + var expectedOriginalSettingsAfterUpdate = gpii.tests.lifecycleManager.extendLifecycleInstructions(test.basePayload, + gpii.tests.lifecycleManager.buildSettingsHandlersEntry(originalSettingsAfterUpdate, test.runningOnLogin), test.payloadExtras); + + var testDef = { + name: "gpii.lifecycleManager.update() with active user session: " + test.name, + expect: 10, + expectedLaunchHandlerCalls: test.expectedLaunchHandlerCalls, + expectedSettingsHandlerSetCalls: test.expectedSettingsHandlerSetCalls, + originalSettings: gpii.tests.lifecycleManager.createSettingsHandlerPayload("org.gnome.desktop.a11y.magnifier", test.originalSettings), + runningOnLogin: test.runningOnLogin, + loginPayload: loginPayload, + expectedOriginal: expectedOriginal, + expectedAfterLogin: expectedAfterLogin, + runningAfterLogin: test.runningAfterLogin, + updatePayload: updatePayload, + expectedAfterUpdate: expectedAfterUpdate, + expectedOriginalSettingsAfterUpdate: expectedOriginalSettingsAfterUpdate, + runningAfterUpdate: test.runningAfterUpdate, + originalSettingsAfterUpdate: gpii.tests.lifecycleManager.createSettingsHandlerPayload("org.gnome.desktop.a11y.magnifier", originalSettingsAfterUpdate) + }; + + gpii.tests.lifecycleManager.runSequentialLifecycleTest(testDef); + gpii.tests.lifecycleManager.runAsyncLifecycleTest(testDef); + }); + }; + + gpii.tests.lifecycleManager.buildStartTests = function () { + fluid.each(gpii.tests.lifecycleManager.startTestDefs, function (test) { + var testDef = { + name: "gpii.lifecycleManager.start()/stop(): " + test.name, + expect: 7, + expectedLaunchHandlerCalls: test.expectedLaunchHandlerCalls, + runningOnLogin: test.runningOnLogin, + originalSettings: gpii.tests.lifecycleManager.createSettingsHandlerPayload("org.gnome.desktop.a11y.magnifier", test.originalSettings), + loginPayload: gpii.tests.lifecycleManager.extendLifecycleInstructions(test.basePayload, + gpii.tests.lifecycleManager.buildSettingsHandlersEntry(test.appliedSettings, undefined, test.removeLifecycleHandlerBlock)), + expectedAfterLogin: gpii.tests.lifecycleManager.extendLifecycleInstructions(test.basePayload, + gpii.tests.lifecycleManager.buildSettingsHandlersEntry(test.appliedSettings, test.runningAfterLogin, test.removeLifecycleHandlerBlock)), + runningAfterLogin: test.runningAfterLogin, + // only expect launch handler settings to be in the original snapshot if we've run a launchHandler call during login + expectedOriginal: gpii.tests.lifecycleManager.extendLifecycleInstructions(test.basePayload, + gpii.tests.lifecycleManager.buildSettingsHandlersEntry(test.originalSettings, test.expectedLaunchHandlerCalls === 0 ? undefined : test.runningOnLogin, test.removeLifecycleHandlerBlock)) + }; + + gpii.tests.lifecycleManager.runSequentialLifecycleTest(testDef); + gpii.tests.lifecycleManager.runAsyncLifecycleTest(testDef); + }); + }; + + gpii.tests.lifecycleManager.settingsToChanges = function (expected) { + return gpii.settingsHandlers.transformOneSolutionSettings(expected, gpii.settingsHandlers.settingsToChanges); + }; + + gpii.tests.lifecycleManager.assertNoActiveSessions = function (lifecycleManager, message) { + jqUnit.assertUndefined(message + "- session state should be empty", lifecycleManager.getActiveSessionGpiiKey()); + }; gpii.tests.lifecycleManager.runTests = function () { - jqUnit.module("Lifecycle Manager", function() { + jqUnit.module("Lifecycle Manager", function () { gpii.tests.staticRepository = {}; }); - jqUnit.test("gpii.lifecycleManager.createPayloadForSettingsHandler()", function () { - jqUnit.expect(1); - var response = gpii.lifecycleManager.specToSettingsHandler("org.gnome.desktop.a11y.magnifier", createPayloadForSettingsHandlerRequest); - jqUnit.assertDeepEq("createPayloadForSettingsHandler returning the correct payload", createPayloadForSettingsHandlerResponse, response); + jqUnit.test("gpii.lifecycleManager.specToSettingsHandler()", function () { + fluid.each(gpii.tests.lifecycleManager.specToSettingsHandlerTests, function (spec, name) { + jqUnit.expect(1); + var response = gpii.lifecycleManager.specToSettingsHandler("fake.solution", spec.request); + jqUnit.assertDeepEq(name, { "fake.solution": spec.response }, response); + }); }); - jqUnit.test("gpii.lifecycleManager.parseHandlerResponse() no settings", function () { + jqUnit.test("gpii.lifecycleManager.responseToSnapshot() no settings", function () { jqUnit.expect(1); - var response = gpii.lifecycleManager.responseToSnapshot("org.gnome.desktop.interface", parseHandlerResponseFunctionRequestNoSettings); - jqUnit.assertUndefined("parseHandlerResponse returning the correct payload", response); + var response = gpii.lifecycleManager.responseToSnapshot("org.gnome.desktop.interface", gpii.tests.lifecycleManager.responseToSnapshotRequestNoSettings); + jqUnit.assertDeepEq("responseToSnapshot returning the correct payload", { + settings: {} + }, response); }); - jqUnit.test("gpii.lifecycleManager.parseHandlerResponse()", function () { + jqUnit.test("gpii.lifecycleManager.responseToSnapshot()", function () { jqUnit.expect(1); - var response = gpii.lifecycleManager.responseToSnapshot("org.gnome.desktop.a11y.magnifier", parseHandlerResponseFunctionRequest); - jqUnit.assertDeepEq("parseHandlerResponse returning the correct payload", parseHandlerResponseFunctionExpectedResponse, response); + var response = gpii.lifecycleManager.responseToSnapshot("org.gnome.desktop.a11y.magnifier", gpii.tests.lifecycleManager.responseToSnapshotRequest); + var expected = gpii.tests.lifecycleManager.settingsToChanges(gpii.tests.lifecycleManager.responseToSnapshotExpectedResponse); + jqUnit.assertDeepEq("responseToSnapshot returning the correct payload", expected, response); }); - jqUnit.test("gpii.lifecycleManager.invokeSettingsHandlers()", function () { + jqUnit.asyncTest("gpii.lifecycleManager.invokeSettingsHandlerSet()", function () { jqUnit.expect(2); - var snapshot = gpii.lifecycleManager.invokeSettingsHandlers("org.gnome.desktop.a11y.magnifier", invokeSettingsHandlersRequest); - jqUnit.assertDeepEq("invokeSettingsHandlers produced snapshot", invokeSettingsHandlersExpectedSnapshot, snapshot); - gpii.tests.lifecycleManager.assertExpectedSettingsHandler(" - standalone", settingsHandlerExpectedInputNewSettings); + var lifecycleManager = gpii.lifecycleManager(); + gpii.tests.lifecycleManager.initBackingMock(); + var snapshotPromise = lifecycleManager.invokeSettingsHandlerSet("org.gnome.desktop.a11y.magnifier", gpii.tests.lifecycleManager.invokeSettingsHandlersRequest); + var expected = gpii.tests.lifecycleManager.settingsToChanges(gpii.tests.lifecycleManager.invokeSettingsHandlersExpectedSnapshot); + snapshotPromise.then(function (snapshot) { + jqUnit.assertDeepEq("invokeSettingsHandlers produced snapshot", expected, snapshot); + jqUnit.assertDeepEq("expected input sent to settingsHandler", gpii.tests.lifecycleManager.settingsHandlerExpectedInputNewSettings, gpii.tests.lifecycleManager.staticRepository.settingsHandler); + jqUnit.start(); + }); }); - jqUnit.asyncTest("gpii.lifecycleManager.start() and stop()", function () { - //2 tests for the settingsHandler (see mockSettingsHandler function above) - //2 tests for the launchHandler (see mockLaunchHandler function above) - //and the two asserts below - jqUnit.expect(5); - var lifecycleManager = gpii.lifecycleManager({ - distributeOptions: { - source: "{that}.options.resolverType", - target: "{that resolverConfig}.type" - }, - resolverType: "gpii.tests.lifecycleManager.resolverConfig" - }); - var options = { - userToken: 123 + jqUnit.asyncTest("gpii.lifecycleManager.invokeSettingsHandlerGet()", function () { + jqUnit.expect(1); + var lifecycleManager = gpii.lifecycleManager(); + gpii.tests.lifecycleManager.initBackingMock(); + var snapshotPromise = lifecycleManager.invokeSettingsHandlerGet("org.gnome.desktop.a11y.magnifier", gpii.tests.lifecycleManager.invokeSettingsHandlersRequest); + var expected = { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "cross-hairs-clip": false, + "cross-hairs-color": "red" + } + }] }; + snapshotPromise.then(function (snapshot) { + jqUnit.assertDeepEq("invokeSettingsHandlerGet produced snapshot", expected, snapshot); + jqUnit.start(); + }); + }); - lifecycleManager.start(options, configurationSpec, function (success) { - gpii.tests.lifecycleManager.assertExpectedExec(); - gpii.tests.lifecycleManager.assertExpectedSettingsHandler(" - on start", settingsHandlerExpectedInputNewSettings); - lifecycleManager.stop(options, function (success) { - gpii.tests.lifecycleManager.assertExpectedSettingsHandler(" - on stop", settingsHandlerExpectedInputRestoreSettings); - jqUnit.assertEquals("Expected pid has been sent to kill handler", - 8839, gpii.tests.staticRepository.killHandler.pid); - jqUnit.assertDeepEq("stop: Stop message to lifecycle manager, empty session state", {}, lifecycleManager.activeSessions); - jqUnit.start(); - }); + jqUnit.test("gpii.lifecycleManager.update() without active session", function () { + var lifecycleManager = gpii.lifecycleManager(gpii.tests.lifecycleManager.testOptions); + var ret = lifecycleManager.update(gpii.tests.lifecycleManager.startPayload); + jqUnit.expect(1); + ret.then(function () { + jqUnit.assertTrue("User with GPII key 123 has no active session, so ignoring update request", true); }); + }); + jqUnit.test("Test gpii.lifecycleManager.session.filterPrefsByAutoSave() to filter preferences allowed for auto save", function () { + jqUnit.expect(6); + var testCases = { + caseRegular: { + message: "Only preferences defined in the metadata \"autosave\" section are returned", + preferences: { + "http://registry.gpii.net/common/magnification": 3, + "http://registry.gpii.net/common/volume": 0.5, + "http://registry.gpii.net/common/pitch": 0.7, + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier": { + "http://registry.gpii.net/common/magnification": 3 + } + }, + metadata: [ + { + "type": "autosave", + "scope": [ + "http://registry.gpii.net/common/magnification", + "http://registry.gpii.net/common/volume", + [ + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier", + "http://registry.gpii.net/common/magnification" + ] + ] + } + ], + expected: { + "http://registry.gpii.net/common/magnification": 3, + "http://registry.gpii.net/common/volume": 0.5, + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier": { + "http://registry.gpii.net/common/magnification": 3 + } + } + }, + caseMultipleAutoSaveMetadata: { + message: "Preferences are filtered when there are multiple metadata \"autosave\" sections", + preferences: { + "http://registry.gpii.net/common/magnification": 3, + "http://registry.gpii.net/common/volume": 0.5, + "http://registry.gpii.net/common/pitch": 0.7, + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier": { + "http://registry.gpii.net/common/magnification": 3 + } + }, + metadata: [ + { + "type": "autosave", + "scope": [ + "http://registry.gpii.net/common/magnification", + "http://registry.gpii.net/common/volume" + ] + }, + { + "type": "priority", + "scope": [ + "http://registry.gpii.net/applications/com.yourdolphin.supernova-as", + "http://registry.gpii.net/applications/es.codefactory.android.app.ma" + ], + "value": 1024 + }, + { + "type": "autosave", + "scope": [ + "http://registry.gpii.net/common/volume", + [ + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier", + "http://registry.gpii.net/common/magnification" + ] + ] + } + ], + expected: { + "http://registry.gpii.net/common/magnification": 3, + "http://registry.gpii.net/common/volume": 0.5, + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier": { + "http://registry.gpii.net/common/magnification": 3 + } + } + }, + caseNoAutoSaveMetadata: { + message: "Undefined is returned when no \"autosave\" section defined in the metadata", + preferences: { + "http://registry.gpii.net/common/magnification": 3, + "http://registry.gpii.net/common/volume": 0.5, + "http://registry.gpii.net/common/pitch": 0.7 + }, + metadata: [ + { + "type": "priority", + "scope": [ + "http://registry.gpii.net/common/magnification", + "http://registry.gpii.net/common/volume" + ] + } + ], + expected: undefined + }, + caseNoMetadata: { + message: "Undefined is returned when no metadata is defined", + preferences: { + "http://registry.gpii.net/common/magnification": 3, + "http://registry.gpii.net/common/volume": 0.5, + "http://registry.gpii.net/common/pitch": 0.7 + }, + metadata: undefined, + expected: undefined + }, + caseNoAllowedPrefs: { + message: "Undefined is returned when no preferences are allowed to be autosaved", + preferences: { + "http://registry.gpii.net/common/magnification": 3, + "http://registry.gpii.net/common/volume": 0.5 + }, + metadata: [ + { + "type": "autosave", + "scope": [ + "http://registry.gpii.net/common/pitch", + "http://registry.gpii.net/common/speechRate" + ] + } + ], + expected: undefined + }, + caseNoPrefs: { + message: "Undefined is returned when input preferences are not provided", + preferences: undefined, + metadata: [ + { + "type": "autosave", + "scope": [ + "http://registry.gpii.net/common/pitch", + "http://registry.gpii.net/common/speechRate" + ] + } + ], + expected: undefined + } + }; + fluid.each(testCases, function (testCase) { + var result = gpii.lifecycleManager.session.filterPrefsByAutoSave(testCase.preferences, testCase.metadata); + if (testCase.expected === undefined) { + jqUnit.assertUndefined(testCase.message, result); + } else { + jqUnit.assertDeepEq(testCase.message, testCase.expected, result); + } + }); + }); + gpii.tests.lifecycleManager.buildUpdateTests(); + gpii.tests.lifecycleManager.buildStartTests(); + fluid.each(gpii.tests.lifecycleManager.completeLifecycleTests, function (testDef) { + gpii.tests.lifecycleManager.runSequentialLifecycleTest(testDef); + gpii.tests.lifecycleManager.runAsyncLifecycleTest(testDef); }); }; -}()); +})(); diff --git a/gpii/node_modules/lifecycleManager/test/js/LifecycleManagerUtilsTests.js b/gpii/node_modules/lifecycleManager/test/js/LifecycleManagerUtilsTests.js new file mode 100644 index 000000000..12fa7162f --- /dev/null +++ b/gpii/node_modules/lifecycleManager/test/js/LifecycleManagerUtilsTests.js @@ -0,0 +1,566 @@ +/*! +GPII Lifecycle Manager Tests + +Copyright 2019 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = fluid || require("infusion"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = jqUnit || fluid.require("node-jqunit", require, "jqUnit"); + +fluid.registerNamespace("gpii.tests.lifecycleManager"); + +gpii.tests.lifecycleManager.solutionRegistryEntries = { + magnifier: { + "settingsHandlers": { + "configuration": { + "type": "gpii.gsettings", + "liveness": "live", + "options": { + "schema": "org.gnome.desktop.a11y.magnifier" + }, + "capabilitiesTransformations": { + "mag-factor": "http://registry\\.gpii\\.net/common/magnification", + "show-cross-hairs": "http://registry\\.gpii\\.net/common/showCrosshairs", + "transform": { + "type": "fluid.transforms.arrayToSetMembership", + "inputPath": "http://registry\\.gpii\\.net/common/tracking", + "outputPath": "", + "presentValue": "proportional", + "missingValue": "none", + "options": { + "focus": "focus-tracking", + "caret": "caret-tracking", + "mouse": "mouse-tracking" + } + }, + "screen-position": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/magnifierPosition", + "match": { + "FullScreen": "full-screen", + "Lens": "full-screen", + "LeftHalf": "left-half", + "RightHalf": "right-half", + "TopHalf": "top-half", + "BottomHalf": "bottom-half" + }, + "noMatch": { + "outputValue": "full-screen" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/magnification": "mag-factor", + "http://registry\\.gpii\\.net/common/showCrosshairs": "show-cross-hairs", + "transform": [ + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "screen-position", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/magnifierPosition", + "match": { + "full-screen": "FullScreen", + "left-half": "LeftHalf", + "right-half": "RightHalf", + "top-half": "TopHalf", + "bottom-half": "BottomHalf" + } + }, + { + "type": "fluid.transforms.valueMapper", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/tracking", + "defaultInputPath": "mouse-tracking", + "match": { + "centered": "mouse" + } + } + ] + } + } + } + }, + alsa: { + "name": "ALSA System Volume", + "settingsHandlers": { + "configuration": { + "type": "gpii.alsa", + "liveness": "live", + "supportedSettings": { + "masterVolume": { + + } + }, + "capabilitiesTransformations": { + "masterVolume": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volume", + "factor": 100 + } + } + }, + "settings": { + "masterVolume": 10 + } + } + }, + "isInstalled": [ + { + "type": "gpii.packageKit.find", + "name": "alsa" + }, + { + "type": "gpii.packageKit.find", + "name": "alsa-lib" + } + ], + "active": true + }, + office: { + "settingsHandlers": { + "configure.ribbons": { + "type": "gpii.settingsHandlers.remoteFileSettingsHandler", + "liveness": "live", + "options": { + "settings": { + "word-ribbon": { + "path": "${{environment}.LOCALAPPDATA}\\Microsoft\\Office\\Word.officeUI", + "url": "https://raw.githubusercontent.com/GPII/gpii-assets/master/dataBlocks/officeRibbon/Word%20%value.xml", + "cache": true + }, + "excel-ribbon": { + "path": "${{environment}.LOCALAPPDATA}\\Microsoft\\Office\\Excel.officeUI", + "url": "https://raw.githubusercontent.com/GPII/gpii-assets/master/dataBlocks/officeRibbon/Excel%20%value.xml", + "cache": true + } + } + }, + "capabilitiesTransformations": { + "word-ribbon": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.office.word-ribbon", + "excel-ribbon": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.office.excel-ribbon" + } + } + } + } +}; + +jqUnit.test("Test gpii.lifecycleManager.transformSettingsToPrefs() to transform settings to preferences", function () { + jqUnit.expect(3); + var testCases = { + flat: { + message: "Transform a setting to a flat preference", + settings: { + "org.gnome.desktop.a11y.magnifier": { + "mag-factor": 2 + } + }, + solutionRegistryEntries: { + "org.gnome.desktop.a11y.magnifier": gpii.tests.lifecycleManager.solutionRegistryEntries.magnifier + }, + expected: { + "http://registry.gpii.net/common/magnification": 2 + } + }, + nested: { + message: "Transform a setting to a nested preferences", + settings: { + "com.microsoft.office": { + "word-ribbon": "StandardSet" + } + }, + solutionRegistryEntries: { + "com.microsoft.office": gpii.tests.lifecycleManager.solutionRegistryEntries.office + }, + expected: { + "http://registry.gpii.net/applications/com.microsoft.office": { + "word-ribbon": "StandardSet" + } + } + }, + multiple: { + message: "Transform settings for multiple solutions to a multiple preferences", + settings: { + "org.gnome.desktop.a11y.magnifier": { + "mag-factor": 1.5, + "screen-position": undefined + }, + "org.alsa-project": { + "masterVolume": 50 + } + }, + solutionRegistryEntries: { + "org.gnome.desktop.a11y.magnifier": gpii.tests.lifecycleManager.solutionRegistryEntries.magnifier, + "org.alsa-project": gpii.tests.lifecycleManager.solutionRegistryEntries.alsa + }, + expected: { + "http://registry.gpii.net/common/magnification": 1.5, + "http://registry.gpii.net/common/volume": 0.5 + } + } + }; + + fluid.each(testCases, function (testCase) { + var result = gpii.lifecycleManager.transformSettingsToPrefs(testCase.settings, testCase.solutionRegistryEntries); + jqUnit.assertDeepEq(testCase.message, testCase.expected, result); + }); + + jqUnit.test("Test gpii.lifecycleManager.getMatchMakerSettings() to find the settings that generated from a matchmaking process ", function () { + jqUnit.expect(2); + var testCases = { + common: { + message: "Match for one solution", + settingValues: [{ + "org.gnome.desktop.a11y.magnifier": [ + { + "settings": { + "mag-factor": 2, + "show-cross-hairs": false, + "focus-tracking": false, + "caret-tracking": false, + "mouse-tracking": false, + "screen-position": "full-screen" + } + } + ] + }], + matchMakerSolutionRegistryEntries: { + "org.gnome.desktop.a11y.magnifier": { + "name": "GNOME Shell Magnifier", + "capabilities": [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + "settingsHandlers": { + "configuration": { + "type": "gpii.gsettings", + "liveness": "live", + "options": { + "schema": "org.gnome.desktop.a11y.magnifier" + }, + "supportedSettings": { + "mag-factor": { + + }, + "show-cross-hairs": { + + }, + "focus-tracking": { + + }, + "caret-tracking": { + + }, + "mouse-tracking": { + + }, + "screen-position": { + + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/magnification": "mag-factor", + "http://registry\\.gpii\\.net/common/showCrosshairs": "show-cross-hairs", + "transform": [ + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "screen-position", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/magnifierPosition", + "match": { + "full-screen": "FullScreen", + "left-half": "LeftHalf", + "right-half": "RightHalf", + "top-half": "TopHalf", + "bottom-half": "BottomHalf" + } + }, + { + "type": "fluid.transforms.valueMapper", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/tracking", + "defaultInputPath": "mouse-tracking", + "match": { + "centered": "mouse" + } + } + ] + }, + "settings": { + "mag-factor": 1, + "screen-position": "full-screen" + } + } + }, + "launchHandlers": { + "launcher": { + "type": "gpii.gsettings.launch", + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-magnifier-enabled" + }, + "settings": { + "running": true + } + } + }, + "update": [ + "settings.configuration" + ], + "isInstalled": [ + { + "type": "gpii.packageKit.find", + "name": "gnome-shell" + } + ], + "active": true + } + }, + expected: { + "org.gnome.desktop.a11y.magnifier": { + "mag-factor": 2, + "screen-position": "full-screen" + } + } + }, + multiple: { + message: "Get settings for multiple solutions", + settingValues: [{ + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "mag-factor": 1.5, + "show-cross-hairs": undefined, + "focus-tracking": undefined, + "caret-tracking": undefined, + "mouse-tracking": undefined, + "screen-position": undefined + } + }] + }, { + "org.alsa-project": [{ + "settings": { + "masterVolume": 50 + } + }] + }], + matchMakerSolutionRegistryEntries: { + "org.gnome.desktop.a11y.magnifier": { + "name": "GNOME Shell Magnifier", + "capabilities": [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + "settingsHandlers": { + "configuration": { + "type": "gpii.gsettings", + "liveness": "live", + "options": { + "schema": "org.gnome.desktop.a11y.magnifier" + }, + "supportedSettings": { + "mag-factor": { + + }, + "show-cross-hairs": { + + }, + "focus-tracking": { + + }, + "caret-tracking": { + + }, + "mouse-tracking": { + + }, + "screen-position": { + + } + }, + "capabilitiesTransformations": { + "mag-factor": "http://registry\\.gpii\\.net/common/magnification", + "show-cross-hairs": "http://registry\\.gpii\\.net/common/showCrosshairs", + "transform": { + "type": "fluid.transforms.arrayToSetMembership", + "inputPath": "http://registry\\.gpii\\.net/common/tracking", + "outputPath": "", + "presentValue": "proportional", + "missingValue": "none", + "options": { + "focus": "focus-tracking", + "caret": "caret-tracking", + "mouse": "mouse-tracking" + } + }, + "screen-position": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/magnifierPosition", + "match": { + "FullScreen": "full-screen", + "Lens": "full-screen", + "LeftHalf": "left-half", + "RightHalf": "right-half", + "TopHalf": "top-half", + "BottomHalf": "bottom-half" + }, + "noMatch": { + "outputValue": "full-screen" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/magnification": "mag-factor", + "http://registry\\.gpii\\.net/common/showCrosshairs": "show-cross-hairs", + "transform": [ + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "screen-position", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/magnifierPosition", + "match": { + "full-screen": "FullScreen", + "left-half": "LeftHalf", + "right-half": "RightHalf", + "top-half": "TopHalf", + "bottom-half": "BottomHalf" + } + }, + { + "type": "fluid.transforms.valueMapper", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/tracking", + "defaultInputPath": "mouse-tracking", + "match": { + "centered": "mouse" + } + } + ] + }, + "settings": { + "mag-factor": 1, + "screen-position": "full-screen" + } + } + }, + "launchHandlers": { + "launcher": { + "type": "gpii.gsettings.launch", + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-magnifier-enabled" + }, + "settings": { + "running": true + } + } + }, + "update": [ + "settings.configuration" + ], + "isInstalled": [ + { + "type": "gpii.packageKit.find", + "name": "gnome-shell" + } + ], + "active": true + }, + "org.alsa-project": { + "name": "ALSA System Volume", + "settingsHandlers": { + "configuration": { + "type": "gpii.alsa", + "liveness": "live", + "supportedSettings": { + "masterVolume": { + + } + }, + "capabilitiesTransformations": { + "masterVolume": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volume", + "factor": 100 + } + } + }, + "settings": { + "masterVolume": 10 + } + } + }, + "isInstalled": [ + { + "type": "gpii.packageKit.find", + "name": "alsa" + }, + { + "type": "gpii.packageKit.find", + "name": "alsa-lib" + } + ], + "active": true + } + }, + expected: { + "org.gnome.desktop.a11y.magnifier": { + "mag-factor": 1.5, + "screen-position": undefined + }, + "org.alsa-project": { + "masterVolume": 50 + } + } + } + }; + + fluid.each(testCases, function (testCase) { + var result = gpii.lifecycleManager.getMatchMakerSettings(testCase.settingValues, testCase.matchMakerSolutionRegistryEntries); + jqUnit.assertDeepEq(testCase.message, testCase.expected, result); + }); + }); + + jqUnit.test("Test gpii.lifecycleManager.filterMatchedPrefs() to find matched preferences", function () { + jqUnit.expect(1); + var testCases = { + common: { + message: "Find matched preferences", + preferences: { + "http://registry.gpii.net/applications/com.microsoft.office": { + "word-ribbon": "StandardSet", + "excel-ribbon": "Basics+StandardSet" + }, + "http://registry.gpii.net/common/magnification": 2, + "http://registry.gpii.net/common/volume": 1.5 + }, + prefsSearchFor: { + "http://registry.gpii.net/applications/com.microsoft.office": { + "word-ribbon": "StandardSet" + }, + "http://registry.gpii.net/common/magnification": 1, + "http://registry.gpii.net/common/nonexistent": true + }, + expected: { + "http://registry.gpii.net/applications/com.microsoft.office": { + "word-ribbon": "StandardSet" + }, + "http://registry.gpii.net/common/magnification": 2 + } + } + }; + + fluid.each(testCases, function (testCase) { + var result = gpii.lifecycleManager.filterMatchedPrefs(testCase.preferences, testCase.prefsSearchFor); + jqUnit.assertDeepEq(testCase.message, testCase.expected, result); + }); + }); +}); diff --git a/gpii/node_modules/matchMaker/README.md b/gpii/node_modules/matchMaker/README.md deleted file mode 100644 index 86516dbca..000000000 --- a/gpii/node_modules/matchMaker/README.md +++ /dev/null @@ -1,36 +0,0 @@ -Matchmaker -=== - -A server app built to be deployed with node.js server that matches solution records to user needs and preferences. - -### Matchmaker API - -The app currently supports the following urls: - - {urlToSolutionsRegistryServer}/match // POST - -### Dependencies - -[infusion](https://github.com/fluid-project/infusion) framework. - -### Installation - -Run the following command in your newly checked out universal repository. This -will pull all dependencies that are required by universal. - - npm install - -### Run - -To run matchmaker app simply type: - - [NODE_ENV={environment}] node node_modules/kettle/lib/init.js path/to/matchMaker/configs/folder - -- Default environment is development. -- Path to configs folder can be absolute or relative to the current user directory. - -For example: - - node node_modules/kettle/lib/init.js gpii/node_modules/matchMaker/configs/ - node node_modules/kettle/lib/init.js /Users/{userName}/universal/gpii/node_modules/matchMaker/configs/ - NODE_ENV=production node node_modules/kettle/lib/init.js gpii/node_modules/matchMaker/configs/ \ No newline at end of file diff --git a/gpii/node_modules/matchMaker/configs/base.json b/gpii/node_modules/matchMaker/configs/base.json deleted file mode 100644 index 8976d3bec..000000000 --- a/gpii/node_modules/matchMaker/configs/base.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "modules": [ - "matchMaker" - ] -} diff --git a/gpii/node_modules/matchMaker/configs/development.json b/gpii/node_modules/matchMaker/configs/development.json deleted file mode 100644 index 9ecddcfea..000000000 --- a/gpii/node_modules/matchMaker/configs/development.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "typeName": "matchMaker.development", - "options": { - "gradeNames": ["autoInit", "fluid.littleComponent"], - "components": { - "server": { - "type": "kettle.server", - "options": { - "logging": true, - "components": { - "matchMaker": { - "type": "gpii.matchMaker", - "options": { - "gradeNames": ["gpii.matchMaker.dev"], - "solutionsReporterUrl": "file://%root/../../../testData/solutions/%os.json", - "ontologyServerUrl": "file://%root/../../../testData/ontologies/ontologies.json" - } - } - } - } - } - } - }, - "includes": [ - "./base.json" - ], - "modules": [ - "ontologyServer" - ] -} \ No newline at end of file diff --git a/gpii/node_modules/matchMaker/configs/kettleModuleLoader.js b/gpii/node_modules/matchMaker/configs/kettleModuleLoader.js deleted file mode 100644 index 20c0170a7..000000000 --- a/gpii/node_modules/matchMaker/configs/kettleModuleLoader.js +++ /dev/null @@ -1,16 +0,0 @@ -/*! -Kettle Module Loader. - -Copyright 2012-2013 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/kettle/LICENSE.txt -*/ - -// The purpose of this file is to be copied to a location for which the -// require function is needed. It allows to find node modules relative -// to that location that are otherwise non-resolvable. -module.exports = require; \ No newline at end of file diff --git a/gpii/node_modules/matchMaker/configs/production.json b/gpii/node_modules/matchMaker/configs/production.json deleted file mode 100644 index a401c577a..000000000 --- a/gpii/node_modules/matchMaker/configs/production.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "typeName": "matchMaker.production", - "options": { - "gradeNames": ["autoInit", "fluid.littleComponent"], - "components": { - "server": { - "type": "kettle.server", - "options": { - "logging": false, - "components": { - "matchMaker": { - "type": "gpii.matchMaker", - "options": { - "solutionsReporterUrl": "http://localhost:%port/solution?os=%os", - "ontologyServerUrl": "http://localhost:%port/ontology/convert/ISO24751-2" - } - } - } - } - } - } - }, - "includes": [ - "./base.json" - ] -} \ No newline at end of file diff --git a/gpii/node_modules/matchMaker/index.js b/gpii/node_modules/matchMaker/index.js deleted file mode 100644 index 1c1b2df87..000000000 --- a/gpii/node_modules/matchMaker/index.js +++ /dev/null @@ -1,8 +0,0 @@ -var fluid = require("infusion"); - -var loader = fluid.getLoader(__dirname); - -loader.require("./src/MatchMakerUtilities.js"); -loader.require("./src/MatchMaker.js"); -loader.require("./src/FlatMatchMaker.js"); -loader.require("./src/CanopyMatchMaker.js"); \ No newline at end of file diff --git a/gpii/node_modules/matchMaker/package.json b/gpii/node_modules/matchMaker/package.json deleted file mode 100644 index df9308582..000000000 --- a/gpii/node_modules/matchMaker/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "matchMaker", - "description": "Match Maker module that matches device + user preferences to solutions.", - "version": "0.1", - "author": "GPII", - "bugs": "http://issues.fluidproject.org/browse/FLUID", - "homepage": "http://gpii.net/", - "dependencies": {}, - "licenses": [ - { - "type": "BSD-3-Clause", - "url": "http://www.opensource.org/licenses/BSD-3-Clause" - } - ], - "keywords": ["infusion", "framework", "application", "fluid", "IoC", "Inversion of Control", "MVC", "evented"], - "repository": "git://github.com/GPII/universal.git", - "main": "./index.js", - "engines": { "node" : ">=0.1.9" } -} \ No newline at end of file diff --git a/gpii/node_modules/matchMaker/src/CanopyMatchMaker.js b/gpii/node_modules/matchMaker/src/CanopyMatchMaker.js deleted file mode 100644 index 46f8391e8..000000000 --- a/gpii/node_modules/matchMaker/src/CanopyMatchMaker.js +++ /dev/null @@ -1,96 +0,0 @@ -/*! -GPII Canopy Matchmaker - -Copyright 2012 OCAD University -Copyright 2012 Antranig Basman - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -var fluid = fluid || require("infusion"); - -var matchMaker = fluid.registerNamespace("gpii.matchMaker"); -var canopy = fluid.registerNamespace("gpii.matchMaker.canopy"); - -canopy.computeFitness = function (leaves, solution) { - var vector = []; - for (var i = 0; i < leaves.length; ++ i) { - var leaf = leaves[i]; - vector[i] = matchMaker.prefixLength(leaf, solution); - } - vector = vector.sort(matchMaker.sortDescending); - return vector; -}; - -canopy.compareFitness = function (solA, solB) { - var i = 0, fitA = solA.fitness, fitB = solB.fitness;; - for (; ; ++i) { - var endA = i === fitA.length, endB = i === fitB.length; - if (endA || endB) { - return endA && endB? 0 : (endB? -1 : 1); - } - var diff = fitB[i] - fitA[i]; - if (diff !== 0) { - return diff; - } - } -}; - -canopy.sortSolutions = function (solutions) { - return solutions.sort(canopy.compareFitness); -}; - -canopy.bestCoverage = function (leaves, soln) { - var best = Number.NEGATIVE_INFINITY; - for (var i = 0; i < leaves.length; ++ i) { - var leaf = leaves[i]; - var good = matchMaker.prefixLength(leaf, soln.solution); - best = Math.max(good, best); - } - return best; -}; - - - -canopy.rankSolutions = function (leaves, solrecs) { - fluid.each(solrecs, function (solrec) { - solrec.fitness = canopy.computeFitness(leaves, solrec.skeleton); - }); - solrecs = canopy.sortSolutions(solrecs); - return solrecs; -}; - -canopy.disposeStrategy = function (leaves, solrecs) { - var ranked = canopy.rankSolutions(leaves, solrecs); - var disposed = canopy.disposeSolutionsImpl(leaves, ranked); - return disposed; -}; - -canopy.disposeSolutionsImpl = function (leaves, solrecs) { - var canopy = fluid.transform(leaves, function() { - return Number.NEGATIVE_INFINITY; - }); - - for (var i = 0; i < solrecs.length; ++ i) { - var sol = solrecs[i]; - var inCanopy = false; - for (var j = 0; j < leaves.length; ++ j) { - var depth = matchMaker.prefixLength(leaves[j], sol.skeleton); - if (depth > canopy[j]) { - inCanopy = true; - canopy[j] = depth; - } - } - if (inCanopy) { - solrecs[i].disposition = "accept"; - } - else { - solrecs[i].disposition = "reject"; - } - } - return solrecs; -}; diff --git a/gpii/node_modules/matchMaker/src/FlatMatchMaker.js b/gpii/node_modules/matchMaker/src/FlatMatchMaker.js deleted file mode 100644 index 834caeb79..000000000 --- a/gpii/node_modules/matchMaker/src/FlatMatchMaker.js +++ /dev/null @@ -1,27 +0,0 @@ -/*! -GPII Canopy Matchmaker - -Copyright 2012 OCAD University -Copyright 2012 Antranig Basman - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -var fluid = fluid || require("infusion"); - -var matchMaker = fluid.registerNamespace("gpii.matchMaker"); -var flat = fluid.registerNamespace("gpii.matchMaker.flat"); - -flat.disposeStrategy = function (leaves, solrecs) { - fluid.each(solrecs, function(solrec) { - var accepted = fluid.find(leaves, function (leaf) { - return fluid.get(solrec.skeleton, leaf, fluid.model.escapedGetConfig); - }); - solrec.disposition = accepted? "accept" : "reject"; - }); - return solrecs; -}; diff --git a/gpii/node_modules/matchMaker/src/MatchMaker.js b/gpii/node_modules/matchMaker/src/MatchMaker.js deleted file mode 100644 index e0c0e299c..000000000 --- a/gpii/node_modules/matchMaker/src/MatchMaker.js +++ /dev/null @@ -1,154 +0,0 @@ -/*! -GPII Default Matchmaker - -Copyright 2012 OCAD University -Copyright 2012 Antranig Basman - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - path = require("path"), - semver = require("semver"), - when = require("when"), - gpii = fluid.registerNamespace("gpii"), - $ = fluid.registerNamespace("jQuery"); - - fluid.require("kettle", require); - fluid.require("./MatchPost.js", require); - fluid.require("transformer", require); - - fluid.defaults("gpii.matchMaker", { - gradeNames: ["kettle.app", "autoInit"], - handlers: { - matchPost: { - route: "/match", - type: "post" - } - }, - inverseRules: require(path.resolve(__dirname, "inverseRules.json")), - strategy: "gpii.matchMaker.flat.disposeStrategy", - solutionsReporterUrl: "", - ontologyServerUrl: "", - root: path.join(__dirname, ".."), - components: { - solutionsReporter: { - type: "kettle.dataSource.URL", - options: { - gradeNames: ["kettle.dataSource.simpleCallbackWrapper"], - url: "{gpii.matchMaker}.options.solutionsReporterUrl", - termMap: { - "os": "%os", - "version": "%version" - } - } - }, - ontologyServer: { - type: "kettle.dataSource.URL", - options: { - gradeNames: ["kettle.dataSource.simpleCallbackWrapper"], - url: "{gpii.matchMaker}.options.ontologyServerUrl", - writable: true - } - }, - transformer: { - type: "gpii.transformer" - } - }, - invokers: { - match: { - funcName: "gpii.matchMaker.match", - args: ["{arguments}.0", "{arguments}.1", "{arguments}.2", "{that}.options.inverseRules"] - }, - prefilterSolutions: { - funcName: "gpii.matchMaker.prefilterSolutions", - args: ["{arguments}.0", "{arguments}.1"] - }, - filterSolutionsByVersion: { - funcName: "gpii.matchMaker.filterSolutionsByVersion", - args: ["{arguments}.0", "{arguments}.1", "{arguments}.2"] - } - } - }); - - fluid.defaults("gpii.matchMaker.dev", { - gradeNames: ["autoInit", "fluid.littleComponent"], - components: { - ontologyServer: { - type: "gpii.ontologyServer.development" - } - }, - distributeOptions: { - source: "{that}.options.urlExpanderGradeNames", - target: "{that urlExpander}.options.gradeNames" - }, - urlExpanderGradeNames: ["kettle.urlExpander.development"] - }); - - gpii.matchMaker.prefilterSolutions = function (solutions, device) { - return fluid.remove_if(fluid.copy(solutions), function (solution) { - // Match on device solutions. - var matchesSolutions = fluid.find(device.solutions, function (devSolution) { - if (devSolution.id === solution.id && - (!solution.version || - !devSolution.version || - semver.satisfies(devSolution.version, solution.version) - )) { - return true; - } - }); - if (!matchesSolutions) { - return solutions; - } - }); - }; - - gpii.matchMaker.filterSolutionsByVersion = function (solutions, os, version) { - if (!version) { - return; - } - fluid.remove_if(solutions, function (solution) { - var solutionOSVersion = fluid.find(fluid.get(solution, "contexts.OS"), function(OSEntry) { - return OSEntry.id === os ? OSEntry.version : undefined; - }); - if (!semver.satisfies(version, solutionOSVersion)) { - return true; - } - }); - }; - - gpii.matchMaker.match = function (preferences, solutions, strategy, inverseRules) { - - // Temporary inverse transformation. Should be handled by framework instead of - // manual specification - // see: FLUID-5033 - fluid.each(preferences.applications, function (val, id) { - var trans = inverseRules[id]; - if (!trans) { - return; - } - var new_pref = fluid.model.transformWithRules(val.parameters, trans); - fluid.log(new_pref); - $.extend(true, preferences, new_pref); - }); - - return when(gpii.matchMaker.disposeSolutions(preferences, solutions, strategy), function (disposed) { - var togo = []; - fluid.each(disposed, function(solrec) { - if (solrec.disposition === "accept") { - togo.push(solrec.solution); - } - }); - return togo; - }); - }; - -})(); \ No newline at end of file diff --git a/gpii/node_modules/matchMaker/src/MatchMakerUtilities.js b/gpii/node_modules/matchMaker/src/MatchMakerUtilities.js deleted file mode 100644 index c3e707359..000000000 --- a/gpii/node_modules/matchMaker/src/MatchMakerUtilities.js +++ /dev/null @@ -1,130 +0,0 @@ -/*! -GPII Canopy Matchmaker - -Copyright 2012 OCAD University -Copyright 2012 Antranig Basman - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -var fluid = fluid || require("infusion"), - when = when || require("when"); - -var matchMaker = fluid.registerNamespace("gpii.matchMaker"); - -matchMaker.computeLeavesFromOneSolution = function (solution) { - var leaves = []; - leaves = leaves.concat(fluid.makeArray(solution.capabilities)); - var inputPaths = fluid.model.transform.collectInputPaths(solution.capabilitiesTransformations || {}); - leaves = leaves.concat(inputPaths); - return leaves; -}; - -matchMaker.computeLeavesFromSolution = function (solution) { - var leaves = []; - leaves = leaves.concat.apply(leaves, fluid.transform(solution.settingsHandlers, - function(settingsHandler) { - return matchMaker.computeLeavesFromOneSolution(settingsHandler); - })); - return leaves; -}; - -function computeLeavesImpl (model, path, paths) { - fluid.each(model, function (value, key) { - var newPath = fluid.pathUtil.composePath(path, key.toString()); - if (fluid.isPrimitive(value)) { - paths.push(newPath); - } - else { - computeLeavesImpl(value, newPath, paths); - } - }); -} - -matchMaker.computeLeaves = function (model) { - var paths = []; - computeLeavesImpl(model, "", paths); - return paths; -}; - -/** Returns a non-positive number indicating by how many path segments the supplied - * path fails to index correctly into the supplied model. A return value of 0 - * indicates that the path indexes fully */ - -matchMaker.prefixLength = function (path, model) { - var segs = fluid.pathUtil.parseEL(path); - for (var i = 0; i < segs.length; ++ i) { - var seg = segs[i]; - model = model[seg]; - if (model === undefined) { - return i - segs.length; - } - } - return 0; -}; - -matchMaker.makeComparator = function (leaves) { - return function (solnA, solnB) { - var domA = false, domB = false; - for (var i = 0; i < leaves.length; ++ i) { - var leaf = leaves[i]; - var goodA = matchMaker.prefixLength(leaf, solnA.solution); - var goodB = matchmaker.prefixLength(leaf, solnB.solution); - if (goodA > goodB) { - domA = true; - } - else if (goodB > goodA) { - domB = true; - } - } - return domA ^ domB? (domA? 1 : -1) : 0; - }; -}; - -matchMaker.pathsToSkeleton = function (paths) { - var togo = {}; - for (var i = 0; i < paths.length; ++ i) { - var path = paths[i]; - var existing = fluid.get(togo, path, fluid.model.escapedGetConfig); - if (!existing) { - fluid.set(togo, path, {}, fluid.model.escapedSetConfig); - } - }; - return togo; -}; - -matchMaker.sortDescending = function (numA, numB) { - return numB - numA; -}; - -matchMaker.expandSolutions = function (solutions) { - return fluid.transform(solutions, function (solution, index) { - var paths = matchMaker.computeLeavesFromSolution(solution); - var skeleton = matchMaker.pathsToSkeleton(paths); - return { - solution: solution, - skeleton: skeleton, - index: index - } - }); -}; - -matchMaker.disposeSolutions = function (profile, solutions, strategy) { - var leaves = matchMaker.computeLeaves(profile); - var solrecs = matchMaker.expandSolutions(solutions); - return when(strategy(leaves, solrecs), function (disposed) { - return disposed; - }) -}; - -matchMaker.extractDispositions = function (solrecs) { - var togo = []; - fluid.each(solrecs, function(soln) { - togo[soln.index] = soln.disposition; - }); - return togo; -}; \ No newline at end of file diff --git a/gpii/node_modules/matchMaker/src/MatchPost.js b/gpii/node_modules/matchMaker/src/MatchPost.js deleted file mode 100644 index 62a68ecc0..000000000 --- a/gpii/node_modules/matchMaker/src/MatchPost.js +++ /dev/null @@ -1,105 +0,0 @@ -/*! -GPII Match Maker POST Handler - -Copyright 2012 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - request = fluid.registerNamespace("gpii.request.matchMaker"), - kettle = fluid.require("kettle", require); - - request.match = function (matchMaker, when, solutions, preferences) { - var transform = matchMaker.transformer.transformSettings, - strategy = fluid.getGlobalValue(matchMaker.options.strategy); - - when(matchMaker.match(preferences, solutions, strategy), function (matchedSolutions) { - return transform({ - solutions: matchedSolutions, - preferences: preferences - }); - }); - }; - - request.getSolutions = function (matchMaker, event, model) { - var device = model.device, - os = fluid.get(device, "OS.id"), - version = fluid.get(device, "OS.version"); - matchMaker.solutionsReporter.get({ - os: os, - version: version - }, function (solutions) { - matchMaker.prefilterSolutions(solutions, os, version); - solutions = matchMaker.prefilterSolutions(solutions, device); - event.fire(solutions); - }); - }; - - request.transformPreferences = function (matchMaker, event, model) { - matchMaker.ontologyServer.set(null, { - preferences: model.preferences - }, function (preferences) { - event.fire(preferences); - }); - }; - - request.matchPost = function (that, model) { - that.events.onMatchRequest.fire(model); - }; - - fluid.defaults("kettle.requests.request.handler.matchPost", { - gradeNames: ["autoInit", "fluid.eventedComponent"], - components: { - "matchMaker": "{gpii.matchMaker}" - }, - invokers: { - handle: { - funcName: "gpii.request.matchMaker.matchPost", - args: ["{that}", "{request}.req.body"], - dynamic: true - }, - match: { - funcName: "gpii.request.matchMaker.match", - args: ["{gpii.matchMaker}", "{that}.when", "{arguments}.0", "{arguments}.1"] - }, - getSolutions: { - funcName: "gpii.request.matchMaker.getSolutions", - args: ["{gpii.matchMaker}", "{that}.events.onSolutionsReporter", "{arguments}.0"] - }, - transformPreferences: { - funcName: "gpii.request.matchMaker.transformPreferences", - args: ["{gpii.matchMaker}", "{that}.events.onOntologyServer", "{arguments}.0"] - } - }, - events: { - onMatchRequest: null, - onSolutionsReporter: null, - onOntologyServer: null, - onReadyToMatch: { - events: { - solutions: "onSolutionsReporter", - ontology: "onOntologyServer" - }, - args: ["{arguments}.solutions.0", "{arguments}.ontology.0"] - } - }, - listeners: { - onMatchRequest: [{ - listener: "{that}.getSolutions" - }, { - listener: "{that}.transformPreferences" - }], - onReadyToMatch: "{that}.match" - } - }); - -})(); \ No newline at end of file diff --git a/gpii/node_modules/matchMaker/src/inverseRules.json b/gpii/node_modules/matchMaker/src/inverseRules.json deleted file mode 100644 index bd76d6ab7..000000000 --- a/gpii/node_modules/matchMaker/src/inverseRules.json +++ /dev/null @@ -1,576 +0,0 @@ -{ - "com.microsoft.windows.highContrast": { - "display.screenEnhancement.highContrast": "HighContrastOn.value" - }, - "com.microsoft.windows.cursors": { - "transform": [ - { - "type": "fluid.transforms.valueMapper", - "inputPath": "Arrow.value", - "defaultOutputPath": "display.screenEnhancement.cursorSize", - "options": { - "%SystemRoot%\\cursors\\aero_arrow.cur": { - "outputValue": 0.32 - }, - "%SystemRoot%\\cursors\\aero_arrow_l.cur": { - "outputValue": 0.65 - }, - "%SystemRoot%\\cursors\\aero_arrow_xl.cur": { - "outputValue": 1 - } - } - } - ] - }, - "com.microsoft.windows.magnifier": { - "display.screenEnhancement.invertImages": "Invert.value", - "transform": [ - { - "type": "fluid.transforms.linearScale", - "valuePath": "Magnification.value", - "outputPath": "display.screenEnhancement.magnification", - "factor": 0.01 - }, - { - "type": "fluid.transforms.setMembershipToArray", - "outputPath": "display.screenEnhancement.tracking", - "presentValue": 1, - "missingValue": 0, - "options": { - "FollowFocus.value": "focus", - "FollowMouse.value": "mouse", - "FollowCaret.value": "caret" - } - }, - { - "type": "fluid.transforms.valueMapper", - "inputPath": "MagnificationMode.value", - "defaultOutputPath": "display.screenEnhancement.-provisional-magnifierPosition", - "options": { - "2": { - "outputValue": "FullScreen" - }, - "3": { - "outputValue": "Lens" - }, - "1": { - "outputValue": "TopHalf" - } - } - } - - ] - }, - "org.nvda-project": { - "display.screenReader.-provisional-speakTutorialMessages": "presentation\\.reportHelpBalloons", - "display.screenReader.-provisional-keyEcho": "keyboard\\.speakTypedCharacters", - "display.screenReader.-provisional-wordEcho": "keyboard\\.speakTypedWords", - "display.screenReader.-provisional-announceCapitals": "speech\\.espeak\\.sayCapForCapitals", - - "transform": [ - { - "type": "fluid.transforms.valueMapper", - "inputPath": "virtualBuffers\\.autoSayAllOnPageLoad.value", - "defaultOutputPath": "display.textReadingHighlight.readingUnit", - "options": { - "true": { - "outputValue": "all" - }, - "false": { - "outputValue": "sentence" - } - } - }, - { - "type": "fluid.transforms.setMembershipToArray", - "outputPath": "display.screenEnhancement.trackingTTS", - "presentValue": true, - "missingValue": false, - "options": { - "reviewCursor\\.followFocus": "focus", - "reviewCursor\\.followCaret": "caret", - "reviewCursor\\.followMouse": "mouse" - } - }, - { - "type": "fluid.transforms.valueMapper", - "defaultOutputPath": "display.screenReader.-provisional-punctuationVerbosity", - "inputPath": "speech\\.symbolLevel", - "options": { - "0": { - "outputValue": "none" - }, - "100": { - "outputValue": "some" - }, - "200": { - "outputValue": "most" - }, - "300": { - "outputValue": "all" - } - } - }, - { - "type": "fluid.transforms.condition", - "outputPath": "display.screenReader.-provisional-screenReaderTTSEnabled", - "false": true, - "true": false, - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "left": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "speech\\.synth", - "operator": "===", - "right": "silence" - } - }, - "right": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "speech\\.outputDevice", - "operator": "===", - "right": "Microsoft Sound Mapper" - } - }, - "operator": "&&" - } - } - }, - { - "type": "fluid.transforms.condition", - "outputPath": "display.screenReader.speechRate", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "right": true, - "operator": "===", - "leftPath": "speech\\.espeak\\.rateBoost" - } - }, - "false": { - "transform": { - "type": "fluid.transforms.linearScale", - "valuePath": "speech\\.espeak\\.rate", - "factor": 3.1, - "offset": 80 - } - }, - "true": { - "transform": { - "type": "fluid.transforms.binaryOp", - "left": { - "transform": { - "type": "fluid.transforms.linearScale", - "valuePath": "speech\\.espeak\\.rate", - "factor": 3.1, - "offset": 80 - } - }, - "operator": "*", - "right": 3 - } - } - }, - { - "type": "fluid.transforms.valueMapper", - "inputPath": "speech\\.espeak\\.voice", - "defaultOutputPath": "display.screenReader.-provisional-auditoryOutLanguage", - "options": { - "en\\en": { - "outputValue": "en" - }, - "en\\en-us": { - "outputValue": "en-US" - }, - "en\\en-sc": { - "outputValue": "en-scotland" - }, - "en\\en-wi": { - "outputValue": "en-BZ" - }, - "af": { - "outputValue": "af" - }, - "bg": { - "outputValue": "bg" - }, - "bs": { - "outputValue": "bs" - }, - "ca": { - "outputValue": "ca" - }, - "cs": { - "outputValue": "cs" - }, - "cy": { - "outputValue": "cy" - }, - "da": { - "outputValue": "da" - }, - "de": { - "outputValue": "de" - }, - "el": { - "outputValue": "el" - }, - "test\\grc": { - "outputValue": "grc" - }, - "eo": { - "outputValue": "eo" - }, - "es": { - "outputValue": "es" - }, - "es": { - "outputValue": "es-ES" - }, - "es-la": { - "outputValue": "es-419" - }, - "et": { - "outputValue": "et" - }, - "fi": { - "outputValue": "fi" - }, - "fr": { - "outputValue": "fr" - }, - "fr-be": { - "outputValue": "fr-BE" - }, - "hi": { - "outputValue": "hi" - }, - "hr": { - "outputValue": "hr" - }, - "hu": { - "outputValue": "hu" - }, - "hy": { - "outputValue": "hy" - }, - "hy-west": { - "outputValue": "hy-arevmda" - }, - "id": { - "outputValue": "id" - }, - "is": { - "outputValue": "is" - }, - "it": { - "outputValue": "it" - }, - "test\\jbo": { - "outputValue": "jbo" - }, - "ka": { - "outputValue": "ka" - }, - "kn": { - "outputValue": "kn" - }, - "ku": { - "outputValue": "ku" - }, - "la": { - "outputValue": "la" - }, - "lv": { - "outputValue": "lv" - }, - "mk": { - "outputValue": "mk" - }, - "ml": { - "outputValue": "ml" - }, - "test\\nci": { - "outputValue": "nci" - }, - "nl": { - "outputValue": "nl" - }, - "no": { - "outputValue": "no" - }, - "test\\pap": { - "outputValue": "pap" - }, - "pl": { - "outputValue": "pl" - }, - "pt": { - "outputValue": "pt-BR" - }, - "pt-pt": { - "outputValue": "pt-PT" - }, - "ro": { - "outputValue": "ro" - }, - "ru": { - "outputValue": "ru" - }, - "sk": { - "outputValue": "sk" - }, - "sq": { - "outputValue": "sq" - }, - "sr": { - "outputValue": "sr" - }, - "sv": { - "outputValue": "sv" - }, - "sw": { - "outputValue": "sw" - }, - "ta": { - "outputValue": "ta" - }, - "tr": { - "outputValue": "tr" - }, - "vi": { - "outputValue": "vi" - }, - "zh": { - "outputValue": "zh-cmn" - }, - "zh-yue": { - "outputValue": "zh-yue" - }, - "yue": { - "outputValue": "zh-yue" - } - } - } - ] - }, - "org.gnome.desktop.interface": { - "transform": [ - { - "type": "fluid.transforms.binaryOp", - "right": 12, - "operator": "*", - "leftPath": "text-scaling-factor", - "outputPath": "display.screenEnhancement.fontSize" - }, - { - "type": "fluid.transforms.linearScale", - "valuePath": "cursor-size", - "outputPath": "display.screenEnhancement.cursorSize", - "factor": 0.01 - }, - { - "type": "fluid.transforms.binaryOp", - "outputPath": "display.screenEnhancement.highContrast", - "left": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "gtk-theme", - "operator": "===", - "right": "HighContrast" - } - }, - "right": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "icon-theme", - "operator": "===", - "right": "HighContrast" - } - }, - "operator": "&&" - } - ] - }, - "org.gnome.desktop.a11y.magnifier": { - "display.screenEnhancement.magnification": "mag-factor", - "display.screenEnhancement.-provisional-showCrosshairs": "show-cross-hairs", - "transform": [ - { - "type": "fluid.transforms.valueMapper", - "inputPath": "screen-position", - "defaultOutputPath": "display.screenEnhancement.-provisional-magnifierPosition", - "options": { - "full-screen": { - "outputValue": "FullScreen" - }, - "left-half": { - "outputValue": "LeftHalf" - }, - "right-half": { - "outputValue": "RightHalf" - }, - "top-half": { - "outputValue": "TopHalf" - }, - "bottom-half": { - "outputValue": "BottomHalf" - } - } - }, - { - "type": "fluid.transforms.valueMapper", - "defaultOutputPath": "display.screenEnhancement.tracking", - "inputPath": "mouse-tracking", - "options": { - "centered": { - "outputValue": "mouse" - } - } - } - ] - }, - "org.gnome.orca": { - "display.screenReader.-provisional-speakTutorialMessages": "enableTutorialMessages", - "display.screenReader.-provisional-keyEcho": "enableEchoByCharacter", - "display.screenReader.-provisional-wordEcho": "enableEchoByWord", - "display.screenReader.-provisional-screenReaderBrailleOutput": "enableBraille", - "display.screenReader.-provisional-screenReaderTTSEnabled": "enableSpeech", - "transform": [ - { - "type": "fluid.transforms.valueMapper", - "inputPath": "sayAllStyle", - "defaultOutputPath": "display.textReadingHighlight.readingUnit", - "defaultInputValue": "1", - "options": { - "0": { - "outputValue": "line" - }, - "1": { - "outputValue": "sentence" - } - } - }, - { - "type": "fluid.transforms.valueMapper", - "inputPath": "verbalizePunctuationStyle", - "defaultOutputPath": "display.screenReader.-provisional-punctuationVerbosity", - "defaultInputValue": "1", - "options": { - "3": { - "outputValue": "none" - }, - "2": { - "outputValue": "some" - }, - "1": { - "outputValue": "most" - }, - "0": { - "outputValue": "all" - } - } - }, - { - "type": "fluid.transforms.binaryOp", - "outputPath": "display.screenReader.speechRate", - "right": 170, - "operator": "+", - "left": { - "transform": { - "type": "fluid.transforms.binaryOp", - "operator": "*", - "left": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "voices\\.default\\.rate", - "operator": "-", - "right": 50 - } - }, - "right": { - "transform": { - "type": "fluid.transforms.condition", - "condition": { - "transform" : { - "type": "fluid.transforms.binaryOp", - "leftPath": "voices\\.default\\.rate", - "operator": "<", - "right": 50 - } - }, - "true": 1.8, - "false": 4.4 - } - } - } - } - }, - { - "type": "fluid.transforms.valueMapper", - "inputPath": "voices\\.default\\.family.name", - "defaultOutputPath": "display.screenReader.-provisional-auditoryOutLanguage", - "defaultInputValue": "en", - "options": { - "english": { "outputValue": "en" }, - "english-us": { "outputValue": "en-US" }, - "en-scottish": { "outputValue": "en-scotland" }, - "en-westindies": { "outputValue": "en-BZ" }, - "afrikaans": { "outputValue": "af" }, - "bulgarian-test": { "outputValue": "bg" }, - "bosnian": { "outputValue": "bs" }, - "catalan": { "outputValue": "ca" }, - "czech": { "outputValue": "cs" }, - "welsh-test": { "outputValue": "cy" }, - "danish": { "outputValue": "da" }, - "german": { "outputValue": "de" }, - "greek": { "outputValue": "el" }, - "greek-ancient": { "outputValue": "grc" }, - "esperanto": { "outputValue": "eo" }, - "spanish": { "outputValue": "es" }, - "spanish-latin-american": { "outputValue": "es-419" }, - "estonian": { "outputValue": "et" }, - "finnish": { "outputValue": "fi" }, - "french": { "outputValue": "fr" }, - "hindi": { "outputValue": "hi" }, - "croatian": { "outputValue": "hr" }, - "hungarian": { "outputValue": "hu" }, - "armenian": { "outputValue": "hy" }, - "armenian-west": { "outputValue": "hy-arevmda" }, - "indonesian-test": { "outputValue": "id" }, - "icelandic-test": { "outputValue": "is" }, - "italian": { "outputValue": "it" }, - "lojban": { "outputValue": "jbo" }, - "georgian-test": { "outputValue": "ka" }, - "kannada": { "outputValue": "kn" }, - "kurdish": { "outputValue": "ku" }, - "latin": { "outputValue": "la" }, - "latvian": { "outputValue": "lv" }, - "macedonian-test": { "outputValue": "mk" }, - "malayalam": { "outputValue": "ml" }, - "dutch-test": { "outputValue": "nl" }, - "norwegian": { "outputValue": "no" }, - "papiamento-test": { "outputValue": "pap" }, - "polish": { "outputValue": "pl" }, - "brazil": { "outputValue": "pt-BR" }, - "portugal": { "outputValue": "pt-PT" }, - "romanian": { "outputValue": "ro" }, - "russian_test": { "outputValue": "ru" }, - "slovak": { "outputValue": "sk" }, - "albanian": { "outputValue": "sq" }, - "serbian": { "outputValue": "sr" }, - "swedish": { "outputValue": "sv" }, - "swahili-test": { "outputValue": "sw" }, - "tamil": { "outputValue": "ta" }, - "turkish": { "outputValue": "tr" }, - "vietnam": { "outputValue": "vi" }, - "Mandarin": { "outputValue": "zh-cmn" }, - "cantonese": { "outputValue": "zh-yue" } - } - } - ] - } -} diff --git a/gpii/node_modules/matchMaker/test/html/AsynchronousMatchMakerTest.html b/gpii/node_modules/matchMaker/test/html/AsynchronousMatchMakerTest.html deleted file mode 100644 index 210fe5dfd..000000000 --- a/gpii/node_modules/matchMaker/test/html/AsynchronousMatchMakerTest.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - GPII Asynchronous Strategy MatchMaker tests - - - - - - - - - - - - - - - - - - -

          GPII Asynchronous Strategy MatchMaker tests

          -

          -
          -

          -
            - - - - diff --git a/gpii/node_modules/matchMaker/test/js/AsynchronousMatchMakerTest.js b/gpii/node_modules/matchMaker/test/js/AsynchronousMatchMakerTest.js deleted file mode 100644 index efd214d0e..000000000 --- a/gpii/node_modules/matchMaker/test/js/AsynchronousMatchMakerTest.js +++ /dev/null @@ -1,90 +0,0 @@ -/*! -GPII Async MatchMaker Tests - -Copyright 2012 OCAD University -Copyright 2012 Raising The Floor - International - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -// Declare dependencies -/*global jQuery, require, fluid, jqUnit, gpii, start*/ - -var fluid = fluid || require("infusion"); -var gpii = fluid.registerNamespace("gpii"); - -(function ($) { - "use strict"; - - fluid.registerNamespace("gpii.tests.asyncMatchMaker"); - var solutions = [{ - "settingsHandlers": [{ - "type": "gpii.integrationTesting.mockSettingsHandler", - "capabilities": ["display.screenEnhancement"], - "capabilitiesTransformations": { - "mag-factor": "display.screenEnhancement.magnification", - "show-cross-hairs": "display.screenEnhancement.-provisional-showCrosshairs", - "mouse-tracking": { - "transform": { - "type": "fluid.transforms.valueMapper", - "inputPath": "display.screenEnhancement.tracking", - "options": { - "mouse": { - "outputValue": "centered" - } - } - } - } - } - }] - }]; - var profile = { - "display": { - "screenEnhancement": { - "fontSize": 24, - "foregroundColor": "white", - "backgroundColor": "black", - "fontFace": { - "fontName": ["Comic Sans"], - "genericFontFace": "sans serif" - }, - "magnification": 2.0, - "tracking": ["mouse"], - "invertImages": true, - "-provisional-showCrosshairs": true - } - } - }; - var matchMaker = gpii.matchMaker; - var async = fluid.registerNamespace("gpii.matchMaker.async"); - - // This is a sample strategy that runs asynchronously. - // It does nothing but tries to load a non-existent file - // asynchronously and regardless of outcome, returns an - // accepted match. - async.strategy = function (leaves, solrecs) { - return when($.ajax({ - url: "test", - dataType: "json", - })).always(function () { - return [{disposition: "accept"}] - }); - }; - var runAsync = function(profile, solutions) { - return matchMaker.disposeSolutions(profile, solutions, matchMaker.async.strategy); - }; - gpii.tests.asyncMatchMaker.runTests = function() { - jqUnit.module("MatchMaker"); - jqUnit.asyncTest("Run an async strategy", function() { - runAsync(profile, solutions).then(function (disposed) { - jqUnit.assertEquals("Accepted relevant solution", "accept", disposed[0].disposition); - jqUnit.start(); - }); - }); - }; - -}(jQuery)); \ No newline at end of file diff --git a/gpii/node_modules/matchMaker/test/js/CanopyMatchMakerTests.js b/gpii/node_modules/matchMaker/test/js/CanopyMatchMakerTests.js deleted file mode 100644 index f96606b9e..000000000 --- a/gpii/node_modules/matchMaker/test/js/CanopyMatchMakerTests.js +++ /dev/null @@ -1,201 +0,0 @@ -/*! -GPII Canopy MatchMaker Tests - -Copyright 2012 OCAD University -Copyright 2012 Raising The Floor - International - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -// Declare dependencies -/*global require, fluid, jqUnit, gpii, start*/ - -var fluid = fluid || require("infusion"); -var gpii = fluid.registerNamespace("gpii"); - -(function () { - "use strict"; - - fluid.registerNamespace("gpii.tests.canopyMatchMaker"); - - var magnifier = {"settingsHandlers": [{ - "type": "gpii.integrationTesting.mockSettingsHandler", - "capabilities": ["display.screenEnhancement"], - "capabilitiesTransformations": { - "mag-factor": "display.screenEnhancement.magnification", - "show-cross-hairs": "display.screenEnhancement.-provisional-showCrosshairs", - "mouse-tracking": { - "transform": { - "type": "fluid.transforms.valueMapper", - "inputPath": "display.screenEnhancement.tracking", - "options": { - "mouse": { - "outputValue": "centered" - } - } - } - } - } - }] - }; - - var lesserMagnifier = {"settingsHandlers": [{ - "type": "gpii.integrationTesting.mockSettingsHandler", - "capabilitiesTransformations": { - "mag-factor": "display.screenEnhancement.magnification" - } - }, { - "type": "gpii.integrationTesting.mockSettingsHandler", - "capabilities": "display.screenEnhancement.magnification" - }] - }; - - var magnifierLeaves = [ - "display.screenEnhancement.magnification", - "display.screenEnhancement.-provisional-showCrosshairs", - "display.screenEnhancement.tracking", - "display.screenEnhancement" - ]; - - var magnifierSkeleton = { - display: { - screenEnhancement: { - magnification: {}, - "-provisional-showCrosshairs": {}, - tracking: {} - } - } - }; - - var escapedLeaves = [ - "display.screenReader.applications.nvda\\.screenReader" - ]; - - var escapedSkeleton = { - display: { - screenReader: { - applications: { - "nvda.screenReader": {} - } - } - } - }; - - var sammyProfile = { - "display": { - "screenEnhancement": { - "fontSize": 24, - "foregroundColor": "white", - "backgroundColor": "black", - "fontFace": { - "fontName": ["Comic Sans"], - "genericFontFace": "sans serif" - }, - "magnification": 2.0, - "tracking": ["mouse"], - "invertImages": true, - "-provisional-showCrosshairs": true - } - } - }; - - var sammyLeaves = [ - "display.screenEnhancement.-provisional-showCrosshairs", - "display.screenEnhancement.backgroundColor", - "display.screenEnhancement.fontFace.fontName.0", - "display.screenEnhancement.fontFace.genericFontFace", - "display.screenEnhancement.fontSize", - "display.screenEnhancement.foregroundColor", - "display.screenEnhancement.invertImages", - "display.screenEnhancement.magnification", - "display.screenEnhancement.tracking.0" - ]; - - var expandFitness = function (fitnesses) { - return fluid.transform(fitnesses, function(fit, index) { - return { - fitness: fit, - index: index - }; - }) - }; - - var extractIndices = function (solns) { - return fluid.transform(solns, function (soln) { - return soln.index; - }); - }; - - var extractDispositions = function (solns) { - var togo = []; - fluid.each(solns, function(soln) { - togo[soln.index] = soln.disposition; - }); - return togo; - }; - - var canopy = gpii.matchMaker.canopy; - - gpii.tests.canopyMatchMaker.runTests = function () { - jqUnit.module("Utilities"); - - jqUnit.test("Compute Leaves", function () { - var leaves = gpii.matchMaker.computeLeaves(sammyProfile); - jqUnit.assertDeepEq("Computed Leaves", sammyLeaves, leaves.sort()); - }); - - jqUnit.test("Path Utilities", function() { - jqUnit.assertEquals("Exact depth", 0, gpii.matchMaker.prefixLength("display.screenEnhancement.fontSize", sammyProfile)); - jqUnit.assertEquals("Near depth", 0, gpii.matchMaker.prefixLength("display.screenEnhancement.fontSize", sammyProfile)); - jqUnit.assertEquals("Mid depth", -1, gpii.matchMaker.prefixLength("display.unrecognizable", sammyProfile)); - jqUnit.assertEquals("Far depth", -2, gpii.matchMaker.prefixLength("display.unrecognizable.thing", sammyProfile)); - var skeleton = gpii.matchMaker.pathsToSkeleton(magnifierLeaves); - jqUnit.assertDeepEq("Computed model skeleton", magnifierSkeleton, skeleton); - - var skeleton2 = gpii.matchMaker.pathsToSkeleton(escapedLeaves); - jqUnit.assertDeepEq("Computed model escaped skeleton", escapedSkeleton, skeleton2); - - }); - - - jqUnit.test("Fitness computation", function() { - var fitness = canopy.computeFitness(sammyLeaves, magnifierSkeleton); - var expected = [0, 0, -1, -1, -1, -1, -1, -2, -3]; - jqUnit.assertDeepEq("Computed fitness vector", expected, fitness); - - var fitnesses = [ - [-1, -3, 0], - [0, 0, 0], - [0, 0], - [0, 0, 0, 0], - [-1, -3, -1] - ]; - var solns = canopy.sortSolutions(expandFitness(fitnesses)); - var indices = extractIndices(solns); - var expected = [3, 1, 2, 0, 4]; - jqUnit.assertDeepEq("Ranked fitnesses", expected, indices); - }); - - jqUnit.module("MatchMaker"); - jqUnit.test("Rank and dispose solutions", function() { - var solutions = [lesserMagnifier, magnifier]; - var expanded = matchMaker.expandSolutions(solutions); - var ranked = canopy.rankSolutions(sammyLeaves, expanded); - var indices = extractIndices(ranked); - var expected = [1, 0]; - jqUnit.assertDeepEq("Ranked solutions", expected, indices); - - matchMaker.disposeSolutions(sammyProfile, solutions, canopy.disposeStrategy).then(function (disposed) { - var itions = matchMaker.extractDispositions(disposed); - var expected = ["reject", "accept"]; - jqUnit.assertDeepEq("Disposed solutions", expected, itions); - }); - }); - - }; - -}()); \ No newline at end of file diff --git a/gpii/node_modules/matchMaker/test/js/FlatMatchMakerTests.js b/gpii/node_modules/matchMaker/test/js/FlatMatchMakerTests.js deleted file mode 100644 index f06fb14ba..000000000 --- a/gpii/node_modules/matchMaker/test/js/FlatMatchMakerTests.js +++ /dev/null @@ -1,115 +0,0 @@ -/*! -GPII Canopy MatchMaker Tests - -Copyright 2012 OCAD University -Copyright 2012 Raising The Floor - International - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -// Declare dependencies -/*global require, fluid, jqUnit, gpii, start*/ - -var fluid = fluid || require("infusion"); -var gpii = fluid.registerNamespace("gpii"); - -(function () { - "use strict"; - - fluid.registerNamespace("gpii.tests.flatMatchMaker"); - - var magnifier = {"settingsHandlers": [{ - "type": "gpii.integrationTesting.mockSettingsHandler", - "capabilities": ["display.screenEnhancement"], - "capabilitiesTransformations": { - "mag-factor": "display.screenEnhancement.magnification", - "show-cross-hairs": "display.screenEnhancement.-provisional-showCrosshairs", - "mouse-tracking": { - "transform": { - "type": "fluid.transforms.valueMapper", - "inputPath": "display.screenEnhancement.tracking", - "options": { - "mouse": { - "outputValue": "centered" - } - } - } - } - } - }] - }; - - var lesserMagnifier = {"settingsHandlers": [{ - "type": "gpii.integrationTesting.mockSettingsHandler", - "capabilitiesTransformations": { - "mag-factor": "display.screenEnhancement.magnification" - } - }] - }; - - var sammyProfile = { - "display": { - "screenEnhancement": { - "fontSize": 24, - "foregroundColor": "white", - "backgroundColor": "black", - "fontFace": { - "fontName": ["Comic Sans"], - "genericFontFace": "sans serif" - }, - "magnification": 2.0, - "tracking": ["mouse"], - "invertImages": true, - "-provisional-showCrosshairs": true - } - } - }; - - var carlaBit = { - "content": { - "adaptationPreference": [ - { - "adaptationType": "caption", - "language": "en" - }, - {} - ] - } - }; - - var carlaBit2 = { - "display": { - "applications": [{ - "name": "UI Options", - "id": "fluid.uiOptions.windows" - }] - } - }; - - var matchMaker = gpii.matchMaker; - - var runFlat = function(profile, solutions) { - return matchMaker.disposeSolutions(profile, solutions, matchMaker.flat.disposeStrategy); - }; - - gpii.tests.flatMatchMaker.runTests = function() { - - jqUnit.module("MatchMaker"); - jqUnit.test("Rank and dispose solutions", function() { - runFlat(sammyProfile, [magnifier]).then(function (disposed) { - jqUnit.assertEquals("Accepted relevant solution", "accept", disposed[0].disposition); - }); - runFlat(carlaBit, [magnifier]).then(function (disposed) { - jqUnit.assertEquals("Rejected irrelevant solution", "reject", disposed[0].disposition); - }); - runFlat(carlaBit2, [magnifier]).then(function (disposed) { - jqUnit.assertEquals("Rejected mostly irrelevant solution", "reject", disposed[0].disposition); - }); - }); - }; - -}()); \ No newline at end of file diff --git a/gpii/node_modules/matchMakerFramework/index.js b/gpii/node_modules/matchMakerFramework/index.js new file mode 100644 index 000000000..96ab5f39a --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/index.js @@ -0,0 +1,4 @@ +"use strict"; + +require("./src/MatchMakerFramework.js"); +require("./src/MatchMakerUtilities.js"); diff --git a/gpii/node_modules/matchMakerFramework/package.json b/gpii/node_modules/matchMakerFramework/package.json new file mode 100644 index 000000000..5eb902283 --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/package.json @@ -0,0 +1,13 @@ +{ + "name": "matchMakerFramework", + "description": "MatchMaker framework doing pre- and post processing and calling the appropriate MM.", + "version": "0.3.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } +} diff --git a/gpii/node_modules/matchMakerFramework/src/MatchMakerFramework.js b/gpii/node_modules/matchMakerFramework/src/MatchMakerFramework.js new file mode 100644 index 000000000..6ba03f3df --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/src/MatchMakerFramework.js @@ -0,0 +1,86 @@ +/* +* MatchMaker Framework +* +* Copyright 2014 Raising the Floor - International +* Copyright 2018 OCAD University +* +* Licensed under the New BSD license. You may not use this file except in +* compliance with this License. +* +* The research leading to these results has received funding from the European Union's +* Seventh Framework Programme (FP7/2007-2013) +* under grant agreement no. 289016. +* +* You may obtain a copy of the License at +* https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + semver = require("semver"); + +fluid.registerNamespace("gpii.matchMakerFramework"); + +fluid.defaults("gpii.matchMakerFramework", { + gradeNames: ["fluid.component"], + invokers: { + matchMakerDispatcher: { + funcName: "gpii.matchMakerFramework.matchMakerDispatcher", + args: ["{that}", "{matchMaker}", "{arguments}.0"] + } + }, + events: { + onSolutions: null + } +}); + +/** + * Asynchronous function responsible for, given a matchMaker input payload, deciding which matchmaker is most + * suitable to do the actual matching process and send the payload there. Once the matchmaker + * returns its matched data, the promise will be resolved with both the returned and original data in a combined + * payload. + * + * @param {Object} that - object containing a matchMakerService datasource component which can be used + * when sending the data to the actual MatchMaker. + * @param {Component} matchMaker - A instance of "matchMaker" component. + * @param {Object} matchMakerInput - A valid input payload for the matchmakers. + * @return {Promise} A promise that will be resolved with both the returned and original data in a combined payload, or rejected on error. + */ +gpii.matchMakerFramework.matchMakerDispatcher = function (that, matchMaker, matchMakerInput) { + fluid.log("MatchMaker Framework: dispatching to matchMaker.match() with the input ", matchMakerInput); + var matchMakerResponse = fluid.extend({}, matchMakerInput); + matchMakerResponse.matchMakerOutput = matchMaker.match(matchMakerInput); + return fluid.promise().resolve(matchMakerResponse); +}; + +/** + * Takes a solutions registry object and filters out all solutions that do not match the ones + * reported by the device reporter. + * + * @param {Object} solutions - Solutions registry entries as retrieved from solutions registry. + * @param {Object} device - Device reporter data as retrieved from the device reporter. + * @return {Object} The solutions registry object, but with all the solutions not matching the device filtered out. + * + */ +gpii.matchMakerFramework.filterSolutions = function (solutions, device) { + var filteredSolutions = {}; + fluid.each(solutions, function (solution, solutionId) { + // Match on device solutions. + var matchesSolutions = fluid.find(device.solutions, function (devSolution) { + if (devSolution.id === solutionId && + (!solution.version || + !devSolution.version || + semver.satisfies(devSolution.version, solution.version) + )) { + return true; + } + }); + if (matchesSolutions) { + filteredSolutions[solutionId] = solution; + } + }); + + return filteredSolutions; +}; diff --git a/gpii/node_modules/matchMakerFramework/src/MatchMakerUtilities.js b/gpii/node_modules/matchMakerFramework/src/MatchMakerUtilities.js new file mode 100644 index 000000000..2f56767ef --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/src/MatchMakerUtilities.js @@ -0,0 +1,635 @@ +/*! +GPII Matchmaker Framework - Utilities + +Copyright 2012 OCAD University +Copyright 2012 Antranig Basman +Copyright 2014 Raising the Floor - international + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +/* eslint-env browser */ +/* eslint strict: ["error", "function"] */ + +var fluid = fluid || require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +(function () { + + "use strict"; + fluid.registerNamespace("gpii.matchMakerFramework.utils"); + fluid.registerNamespace("gpii.matchMakerFramework"); + + /* + * APP_SETTING_PRIORITY: the priority that an application is given if the user has application + * specific settings in their preference set for that application, but has no explicit priority for + * that application. * MIN_USER_PRIORITY: If an explicit priority is set by the user for an application, this is + * the minimum value it should have, to avoid conflicting with system generated priorities. + */ + + gpii.matchMakerFramework.APP_SETTING_PRIORITY = 512; + gpii.matchMakerFramework.MIN_USER_PRIORITY = 1024; + + /** + * responsible for building the input payload to the matchmaker, via a bunch of helper functions + * + * @param {Object} initialPayload - The initial payload sent to the matchmaking framework. It should as a + * minimum contain: gpiiKey, preferences, deviceContext, solutionsRegistryEntries. + * The values from this object will become part of the new matchmaker input object returned from this function, + * @return {Object} The matchmaker input. + */ + gpii.matchMakerFramework.utils.preProcess = function (initialPayload) { + var matchMakerInput = fluid.extend({ + specialPreferences: gpii.matchMakerFramework.utils.findSpecialPreferences(initialPayload.preferences) + }, fluid.copy(initialPayload)); + gpii.matchMakerFramework.utils.addCapabilitiesInformation(matchMakerInput); + // remove full solutions registry from the payload, now that we've used it + // to avoid sending a too large payload to the matchmaker (see GPII-1880) + delete matchMakerInput.fullSolutionsRegistry; + + return matchMakerInput; + }; + + /* + * extracts the capabilities from a settingshandler block based on the `capabilities` entry + * and the inputPaths of the capabilitiesTransformations block + */ + gpii.matchMakerFramework.utils.extractCapabilities = function (settingsHandler) { + return fluid.model.transform.collectInputPaths(settingsHandler.capabilitiesTransformations || {}); + }; + + /** + * Takes an array of paths as input and returns them in the ontology provided via the transform spec. + * The result will be an array list of el-paths in the ontology transformed to. + * + * @param {Array} paths - An array of supported capabilities by a solution (as returned by the + * gpii.matchMakerFramework.utils.computeCapabilitiesFromSolution function). + * @param {Object} transformSpec - Transformation rules to transform the input paths from the solution + * registry format into the format desired in the output array. If if one would like the result + * as a list of ISO24751 el-paths and the solutionsregistry transformations are given in + * the flat format, a transformation rules for 'flat' to ISO24751 should be given. + * @return {Array} An array of el-paths to all the input paths of the given capabilities in the + * ontology produced by the transformSpec. + */ + gpii.matchMakerFramework.utils.transformCapabilitiesToOntologizedLeaves = function (paths, transformSpec) { + // Input paths are flat so need to be transformed into hierarchical + var skel = gpii.matchMakerFramework.utils.pathsToSkeleton(paths, true); + var hskeleton = fluid.model.transformWithRules(skel, transformSpec); + + return gpii.matchMakerFramework.utils.computeLeaves(hskeleton); + }; + + /** + * Given a solution entry, all the capabilities of that solution will be calculated based on + * input paths in the transformations (specified in the capabilitiesTransformations block) and + * on the list in the capabilities block of that solution + * + * @param {Object} solution - A single solution entry, as can be found in the solutions registry. + * @return {Array} An array capabilities supported by the given solution. + */ + gpii.matchMakerFramework.utils.computeCapabilitiesFromSolution = function (solution) { + var paths = []; + fluid.each(solution.settingsHandlers, function (handlerBlock) { + paths = paths.concat(gpii.matchMakerFramework.utils.extractCapabilities(handlerBlock)); + }); + // grab the capabilities block from the solution if present: + if (solution.capabilities) { + paths = paths.concat(solution.capabilities); + } + return paths; + }; + + /* + * Helper function for gpii.matchMakerFramework.utils.computeLeaves - see description there + */ + var computeLeavesImpl = function (model, path, paths) { + fluid.each(model, function (value, key) { + var newPath = fluid.pathUtil.composePath(path, key.toString()); + if (fluid.isPrimitive(value)) { + paths.push(newPath); + } + else { + computeLeavesImpl(value, newPath, paths); + } + }); + }; + + /** + * Function to extract all leaves (i.e. primitive end points) from an object. More specifically, + * it takes a object (eg. prefs set) and finds all leaves - for each leaf found the path is + * to an array as an el-path, one path for each leaf. + * + * @param {Object} model - The object from which to subtract leaves. + * @return {Array} An array of el-paths, one entry for each leaf found in the object. + */ + gpii.matchMakerFramework.utils.computeLeaves = function (model) { + var paths = []; + computeLeavesImpl(model, "", paths); + return paths; + }; + + /** + * Given a array of el-paths, this function creates an object with a structure that includes + * all the el-paths in the array. Each of the entries will have the value passed as parameter or + * defaulting to {} (i.e. the empty object); + * + * @param {Array} paths - An array of el-paths that should be present in the resulting object. + * @param {Any} value - The value that should be assigned to each of the keys given in the el-path array, + * if no @value is given, this defaults to the empty object ({}). + * @return {Object} An object with a structure containing all the el-paths, where each path has + * the value given in the parameter. + */ + gpii.matchMakerFramework.utils.pathsToSkeleton = function (paths, value) { + var togo = {}; + for (var i = 0; i < paths.length; ++i) { + var path = paths[i]; + var existing = fluid.get(togo, path, fluid.model.escapedGetConfig); + if (!existing) { + fluid.set(togo, path, value || {}, fluid.model.escapedSetConfig); + } + } + return togo; + }; + + /** + * Given a list of solutions and capabilities, ontologize the capabilities and generate a skeleton based on those. + * + * @param {Object} solutions - List of solution entries in the same format as the solutions registry\. + * @param {Object} capabilities - (array)listings of solution capabilities, keyed by solution ID. + * @param {Object} transformSpec - Transformation rules to translate from the format the input paths are + * given in in the solutions entries' capabilitiesTransformations block, to the ontology + * that the skeleton should be in. + * @return {Object} An object listing the same solutions as the input payload, but where each + * solution block instead contains three keys: 'solution' which contains the original solution + * entry. 'skeleton' which contains the capabilities of the solution. 'index' which contains + * the solution id. + */ + gpii.matchMakerFramework.utils.expandSolutions = function (solutions, capabilities, transformSpec) { + return fluid.transform(solutions, function (solution, solutionId) { + var solutionIdObj = {}; + solutionIdObj["http://registry.gpii.net/applications/" + solutionId] = {}; + var transformedId = fluid.model.transformWithRules(solutionIdObj, transformSpec); + + var paths = gpii.matchMakerFramework.utils.transformCapabilitiesToOntologizedLeaves(capabilities[solutionId], transformSpec); + var skeleton = fluid.extend(true, {}, gpii.matchMakerFramework.utils.pathsToSkeleton(paths), transformedId); + + return { + solution: solution, + skeleton: skeleton, + index: solutionId + }; + }); + }; + + /** + * Based on the /enabled terms from a user's preference set and the /enabled capabilities from the solution, + * this function returns what the 'active' value of the solution should be. + * + * @param {Object} specialPreferences - The specialPreferences value of the relevant prefsSet (from the value of the same name in the MatchMaker input payload). + * @param {Array} specialCapabilities - The solutions specialCapabilities (from the value of the same name in the MatchMaker input payload). + * @param {Object} solution - The solution registry entry of the solution for which to decide the 'active' value. + * @param {String} solutionId - The solution id. + * @return {Boolean} The value of the 'active' flag of the solution. + **/ + gpii.matchMakerFramework.utils.getActiveValueFromEnabledTerms = function (specialPreferences, specialCapabilities, solution, solutionId) { + var active; + + // see if solution has any enabled flags: + fluid.each(specialCapabilities, function (term) { + // if we have an /enabled term and this is not just a settings transformation: + if (term.endsWith("/enabled") && solution.capabilities && solution.capabilities.includes(term)) { + // set the active flag based on the user's preference value (from solution specific pref or general pref) + var applicationBlockKey = gpii.matchMakerFramework.utils.makeApplicationTerm(solutionId); + active = fluid.get(specialPreferences[applicationBlockKey], term, fluid.model.escapedGetConfig); + if (active === undefined) { + active = fluid.get(specialPreferences, term, fluid.model.escapedGetConfig); + } + } + }); + + return active; + }; + + /** + * Function that takes a MM payload set as input that *includes* an entry of + * preferences in a hierarchical format, as well as a strategy for selecting solutions + * and a set of transformations rules between ontologies. + * + * @param {Object} data - A MatchMaker input payload that includes a translation of the + * preferences in an hierarchical ontology, keyed by 'hierarchicalPrefs'. + * @param {Function} strategy - The strategy to use to select which solutions should be configured and + * and launched on the system. + * @param {Object} transformSpec - Transformation rules FROM the format used in the input paths of the + * solutions registry capability transformations TO the format in which the + * settings of hierarchicalPrefs in the data argument are given. + * @return {Object} An object keyed by prefsSets and where the values are arrays of solutions + * to launch given that set of preferences. + */ + gpii.matchMakerFramework.utils.disposeSolutions = function (data, strategy, transformSpec) { + var solrecs = gpii.matchMakerFramework.utils.expandSolutions(data.solutionsRegistryEntries, data.solutionCapabilities, transformSpec); + var togo = {}; + // calculate match for each of the preferences sets + fluid.each(data.hierarchicalPrefs.contexts, function (prefsSet, prefsSetId) { + var tmpSolrecs = fluid.copy(solrecs); + // add implicit priorities based on which applications the user has application + // specific settings for + gpii.matchMakerFramework.utils.addPriorityFromApplicationSettings(prefsSet.preferences, tmpSolrecs); + + // add users explicit application priorities to the solrecs object + gpii.matchMakerFramework.utils.parsePriorities(prefsSet, tmpSolrecs); + + var leaves = gpii.matchMakerFramework.utils.computeLeaves(prefsSet.preferences); + var disposed = strategy(leaves, tmpSolrecs, data.solutionTypeMapping); + + togo[prefsSetId] = {}; + fluid.each(disposed, function (solrec, solid) { + if (solrec.disposition === "accept") { // only output accepted solutions + togo[prefsSetId][solid] = solrec.active; // true if it should be running + } + }); + }); + return togo; + }; + + /** + * Scans the metadata block for application priorities, and for each found, the priority is added + * to the solrecs object. + * + * Given a set of preferences from the "contexts:" block, the function will search its metadata section for + * application priorities. If this is found, the entry for that solution in the solrec object + * will be modified by the addition of a `priority` key with the value of the user priority. + * + * NOTE: this function will modify the solrecs object + * + * @param {Object} prefsSet - The specific set of preferences within a "contexts:" block - i.e. the + * part of the preference set that is keyed by a prefsSet name. + * @param {Object} solrecs - A map of solution information, keyed by solution ids. This object should + * have a format identical to the output of the `gpii.matchMakerFramework.utils.expandSolutions` + * function. For each solution for which the user has an application priority, this solutions' + * entry in solrecs will be modified by the addition of a priority`key. + */ + gpii.matchMakerFramework.utils.parsePriorities = function (prefsSet, solrecs) { + // for each entry in the metadata block: + fluid.each(prefsSet.metadata, function (metadata) { + if (metadata.type === "priority") { + fluid.each(metadata.scope, function (applicationId) { + // set the priority of the application if it's part of our solrecs list: + var solrec = solrecs[gpii.matchMakerFramework.utils.applicationIdFromTerm(applicationId)]; + if (solrec) { + solrec.priority = metadata.value; + } + }); + } + }); + }; + + gpii.matchMakerFramework.utils.addPriorityFromApplicationSettings = function (preferences, solrecs) { + fluid.each(preferences.applications, function (value, applicationId) { + var sol = solrecs[applicationId]; + // If solution is available on the system, and doesn't already have higher priority + if (sol && (!sol.priority || sol.priority < gpii.matchMakerFramework.APP_SETTING_PRIORITY)) { + sol.priority = gpii.matchMakerFramework.APP_SETTING_PRIORITY; + } + }); + }; + + /* + * Filters preferences to contain only common terms and application specific settings for the solutionId + */ + gpii.matchMakerFramework.utils.filterPreferencesForSolution = function (solutionId, allPreferences, solutionEntry) { + var filtered = fluid.copy(allPreferences); + + var applicationString = "http://registry.gpii.net/applications/" + solutionId; + + // find supported common terms: + var supportedCommonArr = gpii.matchMakerFramework.utils.computeCapabilitiesFromSolution(solutionEntry); + var supportedCommon = {}; // create object of keys that are unescaped common term URIs and values of true + fluid.each(supportedCommonArr, function (common) { + fluid.set(supportedCommon, common, true, fluid.model.escapedSetConfig); + }); + + // move any common terms from the application block to the outer level: + if (filtered[applicationString]) { // application block for this solution + fluid.each(filtered[applicationString], function (value, preference) { + // move all common terms outside application block: + if (gpii.matchMakerFramework.utils.isCommonTerm(preference)) { + filtered[preference] = value; + delete filtered[applicationString][preference]; + } + }); + } + + // top level: filter everything that is not the application block and supported common terms + return fluid.remove_if(filtered, function (value, preference) { + // Keep preference if it is a supported common term + if (gpii.matchMakerFramework.utils.isCommonTerm(preference) && supportedCommon[preference] === true) { + return false; + } + // Also leave the applicaiton specific block + if (preference.startsWith(applicationString) === true) { + return false; + } + + return true; + }); + }; + + /** + * Function to build the payload required to be the output from the matchmaker framework + * + * @param {Object} fullModel - The full input sent to the MM. + * @param {Object} disposed - Object with keys of prefsSet-ids. Each value should be an array listing + * the disposed solutions. + * @return {Object} The data arranged to be compatible with the required MM format. + */ + gpii.matchMakerFramework.utils.buildReturnPayload = function (fullModel, disposed) { + var togo = { + inferredConfiguration: {} + }; + fluid.each(disposed, function (solArray, prefsSetId) { + togo.inferredConfiguration[prefsSetId] = { + applications: {} + }; + var appBlock = togo.inferredConfiguration[prefsSetId].applications; + fluid.each(solArray, function (active, solutionId) { + var filteredSettings = gpii.matchMakerFramework.utils.filterPreferencesForSolution(solutionId, fullModel.preferences.contexts[prefsSetId].preferences, fullModel.solutionsRegistryEntries[solutionId]); + appBlock[solutionId] = { + active: active, + settings: filteredSettings + }; + // TODO: add metadata sections + }); + }); + return togo; + }; + + /** + * Simple function that takes a list of capabilities and extract all 'special' capabilities. + * This is currently all terms ending with /enabled + * + * @param {Array} capabilities - An array of term/capabilities. + * @return {Array} An array of all capabilities that are special (i.e. ends with /enabled). + */ + gpii.matchMakerFramework.utils.extractSpecialCapabilities = function (capabilities) { + return fluid.remove_if(fluid.copy(capabilities), function (term) { + return !term.endsWith("/enabled"); + }); + }; + + /** + * Adds information about the supported capabilities for each solution (in the flat format) and special capabilities. + * Two things will be added to the payload: + * (1) A list of inputPaths from the capabilitiesTransformations of the settingshandlers as well as the content of the solutions "capabiliites" entry. + * (2) A list of special capabilities, meaning the (1) list run through the extractSpecialCapabilities function + * + * NOTE: This will modify the supplied payload + * + * @param {Object} payload - An object with the format of a standard matchmaker input payload. At a very + * minimum this payload should have a `solutionsRegistryEntries` key with a hashmap of + * (solutionID => solution registry entry) pairs. Note that this object will be modified by + * addition of two toplevel keys: `solutionTypes` and `solutionTypeMapping. + */ + gpii.matchMakerFramework.utils.addCapabilitiesInformation = function (payload) { + fluid.each(payload.solutionsRegistryEntries, function (solution, solutionId) { + var capabilities = gpii.matchMakerFramework.utils.computeCapabilitiesFromSolution(solution); + fluid.set(payload, [ "solutionCapabilities", solutionId ], capabilities); + fluid.set(payload, [ "specialCapabilities", solutionId ], gpii.matchMakerFramework.utils.extractSpecialCapabilities(capabilities)); + }); + }; + + /** + * Adds information about the solutions types to the payload parameter, which should be in the + * format of an input payload for matchmakers. + * + * Two keys are added to the given payload: + * `solutionTypes` is a hash of solutionId->list of the solution types it is + * `solutionTypeMapping` is a hash of solutionType->array of solutionIds of that type + * + * NOTE: this function will modify the supplied `payload` + * + * @param {Object} payload - An object with the format of a standard matchmaker input payload. At a very + * minimum this payload should have a `solutionsRegistryEntries` key with the solution entries as well + * as a 'solutionCapabilities' key, which contains information about a solutions capabilities (i.e. via + * the addCapabilitiesInformation function). + * @param {Object} transformSpec - ontology transform spec translating from common terms into + * a solution type ontology. + */ + gpii.matchMakerFramework.utils.addSolutionTypeInformation = function (payload, transformSpec) { + fluid.each(payload.solutionCapabilities, function (capabilities, solutionId) { + // first get an array of what solution types the current entry is: + var solutionTypes = gpii.matchMakerFramework.utils.transformCapabilitiesToOntologizedLeaves(capabilities, transformSpec); + fluid.set(payload, [ "solutionTypes", solutionId], solutionTypes); + // now add solution ID to the return array in the appropriate places: + for (var i in solutionTypes) { + // add solution ID to the solution type + fluid.set(payload, ["solutionTypeMapping", solutionTypes[i], solutionId], true); + } + }); + }; + + gpii.matchMakerFramework.utils.isApplicationTerm = function (term) { + return term.startsWith("http://registry.gpii.net/applications/"); + }; + + gpii.matchMakerFramework.utils.isCommonTerm = function (term) { + return term.startsWith("http://registry.gpii.net/common/"); + }; + + gpii.matchMakerFramework.utils.applicationIdFromTerm = function (term) { + return term.substring("http://registry.gpii.net/applications/".length); + }; + + gpii.matchMakerFramework.utils.makeApplicationTerm = function (applicationId) { + return "http://registry.gpii.net/applications/" + applicationId; + }; + + /** + * Given a preference set, return a nested structure of context->appliactionBlock->special setting->value + * and context->special setting->value. + * + * This function will walk an preference set and look for any /enabled terms. Anything that is not an /enabled + * term will not be kept. A structure is returned containing the contexts on the outer level. Each prefsSet + * will have any non application specific /enabled terms, as well as any application block with its contained + * /enabled terms. + * + * @param {Object} preferences - A preference set + * @return {Object} The preference set with only prefsSet names and /enabled terms + */ + gpii.matchMakerFramework.utils.findSpecialPreferences = function (preferences) { + return fluid.transform(fluid.copy(preferences.contexts), function (prefsSet) { + var togo = {}; + fluid.each(prefsSet.preferences, function (prefsBlock, prefsId) { + if (prefsId.endsWith("/enabled")) { + togo[prefsId] = prefsBlock; + } else if (gpii.matchMakerFramework.utils.isApplicationTerm(prefsId)) { + var applicationId = gpii.matchMakerFramework.utils.applicationIdFromTerm(prefsId); + fluid.each(prefsBlock, function (appPrefVal, appPrefId) { + if (appPrefId.endsWith("/enabled")) { + fluid.set(togo, [ applicationId, appPrefId ], appPrefVal); + } + }); + } + }); + return togo; + }); + }; + + gpii.matchMakerFramework.utils.updateSingleInferredConfiguration = function (inferredConfiguration, prefsSet, pref, value, solutionId, upperLevelApplicationTerm) { + if (solutionId) { + var path = gpii.matchMakerFramework.utils.isCommonTerm(pref) ? + [ prefsSet, "applications", solutionId, "settings", pref ] : + [ prefsSet, "applications", solutionId, "settings", upperLevelApplicationTerm, pref ]; + if (fluid.get(inferredConfiguration, path) !== undefined) { + fluid.set(inferredConfiguration, path, value); + } + } else { + // if solutionId is not provided, loop through all solutions and update the preference value + // (if the preference is already set in the solution) + fluid.each(inferredConfiguration[prefsSet].applications, function (sol) { + if (sol.settings[pref] !== undefined) { + sol.settings[pref] = value; + } + }); + } + }; + + /* + * Takes a set of preferences and runs through the inferredConfiguration, updating all instances + * matching preferences/settings within the application with the new values from the preferences + * set. + * + * This feature is required by the PSP to allow updating e.g. a top-level common term and + * ensuring that the relevant applications are affected by it + */ + gpii.matchMakerFramework.utils.updateInferredConfiguration = function (preferences, inferredConfiguration, solutionsRegistryEntries) { + var inferred = fluid.copy(inferredConfiguration); + fluid.each(preferences.contexts, function (prefsSet, prefsSetName) { + fluid.each(prefsSet.preferences, function (val, pref) { + if (gpii.matchMakerFramework.utils.isCommonTerm(pref)) { + gpii.matchMakerFramework.utils.updateSingleInferredConfiguration(inferred, prefsSetName, pref, val); + } else if (gpii.matchMakerFramework.utils.isApplicationTerm(pref)) { + var solutionId = gpii.matchMakerFramework.utils.applicationIdFromTerm(pref); + fluid.each(val, function (solPrefVal, solPref) { + gpii.matchMakerFramework.utils.updateSingleInferredConfiguration(inferred, prefsSetName, solPref, solPrefVal, solutionId, pref); + }); + } + }); + // get active value from */enabled preferences: + fluid.each(inferred[prefsSetName].applications, function (solutionBlock, solutionId) { + var solutionCapabilities = fluid.get(solutionsRegistryEntries, [ solutionId, "capabilities" ]) || {}; + var active = gpii.matchMakerFramework.utils.getActiveValueFromEnabledTerms(prefsSet.preferences, solutionCapabilities, + solutionsRegistryEntries[solutionId], solutionId); + if (active !== undefined) { + solutionBlock.active = active; + } + }); + }); + return inferred; + }; + + /* + * Given an inferred configuration this function creates a listing of "allowed" preferences and settings, + * meaning a list of settings and preferences present in the inferred configuration. Any common term + * encountered in the inferred configuration is added to the list as top level keys with a value of true. Any application + * specific term will be added under a top level key of: "http://registry.gpii.net/applications/". The + * resulting "allowed" list will be built based on preferences and settings from all the preferences sets in the + * inferred configuration (i.e. all the "contexts:" of the preference set) + */ + gpii.matchMakerFramework.utils.extractAllowedPreferences = function (inferredConfiguration) { + var allowed = {}; + fluid.each(inferredConfiguration, function (prefsSet) { + fluid.each(prefsSet.applications, function (app, appId) { + allowed["http://registry.gpii.net/applications/" + appId] = {}; // mark the solution in allowed + fluid.each(app.settings, function (val, term) { + if (term.startsWith("http://registry.gpii.net/applications/")) { + allowed[term] = val; // copy all application specific values to a flat application block key + return; + } + allowed[term] = true; + }); + }); + }); + return allowed; + }; + + /* + * Filter a preference set based on the preferences available in the inferred configuration. This is used to + * ensure that no preferences are passed in the preference set that are not already given via the inferred configuration, + * which is useful for the something like the untrusted flowmanager model. + */ + gpii.matchMakerFramework.utils.filterPreferencesFromInferredConfig = function (preferences, inferredConfiguration) { + // Get the list of allowed terms + var allowed = gpii.matchMakerFramework.utils.extractAllowedPreferences(inferredConfiguration); + + var togo = fluid.censorKeys(preferences, ["contexts"]); + togo.contexts = fluid.transform(fluid.copy(preferences).contexts, function (prefsSet) { + prefsSet.preferences = fluid.remove_if(prefsSet.preferences, function (val, term) { + // if it's an application block + if (term.startsWith("http://registry.gpii.net/applications/")) { + if (allowed[term] === undefined) { // if the application is not in the allowed terms, remove it + return true; + } + + fluid.remove_if(val, function (appVal, appTerm) { + // remove if not found as top level common term and it's not in the application specific allowed + // terms block + return (allowed[appTerm] !== true && (fluid.get(allowed, [ term, appTerm ]) === undefined)); + }); + } else { + return allowed[term] !== true; + } + }); + return prefsSet; + }); + return togo; + }; + + gpii.matchMakerFramework.utils.livenessOrdering = [ + "OSRestart", + "manualRestart", + "liveRestart", + "live" + ]; + + /** + * Given one or more solutions registry entries and optionally a term, this function will walk each + * solution (and its settingshandlers) and find the lowest liveness for any of the solutions. If a term is + * given, the liveliness reported is the lowest of those settingshandlers which supports that term + * (decided via its capabilities and/or capabilitiesTransformations). The lowest or least + * liveness is dictated by gpii.matchMakerFramework.utils.livenessOrdering, where a lower + * index means less/lower liveness. + * + * @param {Array} solutions - A list of the solutions that should be search for support of the term. + * @param {String} [term] - [optional] The term to find the liveness for. + * @return {String} Returns the lowest/least liveness found by any solution provided that supports the term. + */ + gpii.matchMakerFramework.utils.getLeastLiveness = function (solutions, term) { + var leastLiveness; + fluid.each(solutions, function (solution) { + fluid.each(solution.settingsHandlers, function (handler) { + if (term !== undefined) { + // if term is defined, only check liveness of block if the term is within the capabilities + // of the block. If not, simply skip to next block + var capabilities = gpii.matchMakerFramework.utils.extractCapabilities(handler); + if (capabilities && capabilities.indexOf(fluid.pathUtil.composeSegments(term)) === -1) { + return; + } + } + + // if the liveness of term in current solution is lower than any previously checked solutions + var livenessOrder = gpii.matchMakerFramework.utils.livenessOrdering.indexOf(handler.liveness); + if (leastLiveness === undefined || livenessOrder < leastLiveness) { + leastLiveness = livenessOrder; + } + }); + }); + return gpii.matchMakerFramework.utils.livenessOrdering[leastLiveness]; + }; +})(); diff --git a/gpii/node_modules/matchMakerFramework/test/MatchMakerFrameworkTests.js b/gpii/node_modules/matchMakerFramework/test/MatchMakerFrameworkTests.js new file mode 100644 index 000000000..ffad856ba --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/test/MatchMakerFrameworkTests.js @@ -0,0 +1,100 @@ +/* +* MatchMaker Framework Tests +* +* Copyright 2014 Raising the Floor - International +* +* Licensed under the New BSD license. You may not use this file except in +* compliance with this License. +* +* The research leading to these results has received funding from the European Union's +* Seventh Framework Programme (FP7/2007-2013) +* under grant agreement no. 289016. +* +* You may obtain a copy of the License at +* https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +// TODO: This should be converted to a set of browser tests + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.require("node-jqunit", require, "jqUnit"), + kettle = fluid.require("kettle", require); + +kettle.loadTestingSupport(); + +require("matchMakerFramework"); + +fluid.registerNamespace("gpii.tests.matchMakerFramework"); + +fluid.defaults("gpii.tests.matchMakerFramework.testTree", { + gradeNames: ["fluid.test.testEnvironment"], + components: { + matchMakerFrameworkTester: { + type: "gpii.tests.matchMakerFramework.testCaseHolder" + } + } +}); + +fluid.defaults("gpii.tests.matchMakerFramework.testCaseHolder", { + gradeNames: ["fluid.test.testCaseHolder"], + modules: [{ + name: "gpii.matchMakerFramework tests", + tests: [ + { + expect: 1, + name: "filterSolutions tests", + type: "test", + func: "gpii.tests.matchMakerFramework.filterSolutions" + } + ] + }] +}); + +var fullSolutionsRegistry = fluid.require("%gpii-universal/testData/solutions"); + +gpii.tests.matchMakerFramework.filterSolutionsFixture = [ + { + description: "filterSolutions test 1", + device: { + OS: { + id: "win32", + version: "6.8.5" + }, + solutions: [ + { + "id": "com.microsoft.windows.magnifier" + }, { + "id": "com.microsoft.windows.onscreenKeyboard" + }, { + "id": "org.nvda-project" + }, { + "id": "com.microsoft.windows.highContrast" + }, { + "id": "com.microsoft.windows.mouseTrailing" + }, { + "id": "com.microsoft.windows.cursors" + } + ] + }, + expected: { + "com.microsoft.windows.magnifier": fullSolutionsRegistry.win32["com.microsoft.windows.magnifier"], + "com.microsoft.windows.onscreenKeyboard": fullSolutionsRegistry.win32["com.microsoft.windows.onscreenKeyboard"], + "org.nvda-project": fullSolutionsRegistry.win32["org.nvda-project"], + "com.microsoft.windows.highContrast": fullSolutionsRegistry.win32["com.microsoft.windows.highContrast"], + "com.microsoft.windows.mouseTrailing": fullSolutionsRegistry.win32["com.microsoft.windows.mouseTrailing"], + "com.microsoft.windows.cursors": fullSolutionsRegistry.win32["com.microsoft.windows.cursors"] + } + } +]; + +gpii.tests.matchMakerFramework.filterSolutions = function () { + fluid.each(gpii.tests.matchMakerFramework.filterSolutionsFixture, function (fixture) { + var result = gpii.matchMakerFramework.filterSolutions(fullSolutionsRegistry.win32, fixture.device); + jqUnit.assertDeepEq("Expecting the filtered payload", fixture.expected, result); + }); +}; + +module.exports = kettle.test.bootstrap("gpii.tests.matchMakerFramework.testTree"); diff --git a/gpii/node_modules/matchMakerFramework/test/data/jaws_application.json b/gpii/node_modules/matchMakerFramework/test/data/jaws_application.json new file mode 100644 index 000000000..0476694e3 --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/test/data/jaws_application.json @@ -0,0 +1,18 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.freedomscientific.jaws": { + "Options.SayAllIndicateCaps" : 0, + "Options.TypingEcho": 3, + "Options.SayAllMode": 0, + "Braille.BrailleMode": 0, + "Options.SayAllIgnoreShiftKeys": 1 + } + } + } + } + } +} diff --git a/gpii/node_modules/matchMakerFramework/test/data/os_android.json b/gpii/node_modules/matchMakerFramework/test/data/os_android.json new file mode 100644 index 000000000..e2adb3f8a --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/test/data/os_android.json @@ -0,0 +1,25 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.android.settings.system": { + "dim_screen": 1, + "haptic_feedback_enabled": true, + "accelerometer_rotation": false, + "user_rotation": 2, + "screen_off_timeout": 10000 + }, + "http://registry.gpii.net/applications/com.android.audioManager": { + "STREAM_SYSTEM": 7 + }, + "http://registry.gpii.net/applications/com.android.persistentConfiguration": { + "fontScale": 1.5, + "locale": "en" + } + } + } + } + } +} diff --git a/gpii/node_modules/matchMakerFramework/test/data/os_common.json b/gpii/node_modules/matchMakerFramework/test/data/os_common.json new file mode 100644 index 000000000..61c3e577d --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/test/data/os_common.json @@ -0,0 +1,24 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/magnifierPosition": "Lens", + "http://registry.gpii.net/common/magnification": 1.5, + "http://registry.gpii.net/common/tracking": [ "mouse", "caret" ], + "http://registry.gpii.net/common/invertColours": true, + "http://registry.gpii.net/common/cursorSize": 0.9, + "http://registry.gpii.net/common/fontSize": 9, + "http://registry.gpii.net/common/mouseTrailing": 10, + "http://registry.gpii.net/common/highContrast/enabled": true, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/cursorSpeed": 1, + "http://registry.gpii.net/common/stickyKeys": true, + "http://registry.gpii.net/common/debounce/enabled": true, + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true + } + } + } + } +} diff --git a/gpii/node_modules/matchMakerFramework/test/data/os_gnome.json b/gpii/node_modules/matchMakerFramework/test/data/os_gnome.json new file mode 100644 index 000000000..e368055d7 --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/test/data/os_gnome.json @@ -0,0 +1,22 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier": { + "mag-factor": 1.5, + "screen-position": "full-screen" + }, + "http://registry.gpii.net/applications/org.gnome.desktop.interface": { + "cursor-size": 90, + "text-scaling-factor": 0.75 + }, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 50 + } + } + } + } + } +} diff --git a/gpii/node_modules/matchMakerFramework/test/data/os_win.json b/gpii/node_modules/matchMakerFramework/test/data/os_win.json new file mode 100644 index 000000000..ca59b8980 --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/test/data/os_win.json @@ -0,0 +1,114 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.mouseTrailing": { + "MouseTrails": { + "path": { + "get": "pvParam", + "set": "uiParam" + }, + "value": 10 + } + }, + "http://registry.gpii.net/applications/com.microsoft.windows.mouseKeys": { + "MouseKeysOn": { + "path": "pvParam.dwFlags.MKF_MOUSEKEYSON", + "value": true + }, + "MaxSpeed": { + "path": "pvParam.iMaxSpeed", + "value": 100 + }, + "Acceleration": { + "path": "pvParam.iTimeToMaxSpeed", + "value": 1000 + } + }, + "http://registry.gpii.net/applications/com.microsoft.windows.stickyKeys": { + "StickyKeysOn": { + "path": "pvParam.dwFlags.SKF_STICKYKEYSON", + "value": true + } + }, + "http://registry.gpii.net/applications/com.microsoft.windows.filterKeys": { + "FilterKeysEnable": { + "path": "pvParam.dwFlags.FKF_FILTERKEYSON", + "value": true + } + }, + "http://registry.gpii.net/applications/com.microsoft.windows.highContrast": { + "HighContrastOn": { + "path": "pvParam.dwFlags.HCF_HIGHCONTRASTON", + "value": true + } + }, + "http://registry.gpii.net/applications/com.microsoft.windows.cursors": { + "No": "%SystemRoot%\\cursors\\no_l.cur", + "Hand": "%SystemRoot%\\cursors\\aero_link_l.cur", + "Help": "%SystemRoot%\\cursors\\help_l.cur", + "Wait": "%SystemRoot%\\cursors\\busy_l.cur", + "Arrow": "%SystemRoot%\\cursors\\arrow_l.cur", + "NWPen": "%SystemRoot%\\cursors\\pen_l.cur", + "SizeNS": "%SystemRoot%\\cursors\\size4_l.cur", + "SizeWE": "%SystemRoot%\\cursors\\size3_l.cur", + "SizeAll": "%SystemRoot%\\cursors\\move_l.cur", + "UpArrow": "%SystemRoot%\\cursors\\up_l.cur", + "SizeNESW": "%SystemRoot%\\cursors\\size1_l.cur", + "SizeNWSE": "%SystemRoot%\\cursors\\size2_l.cur", + "AppStarting": "%SystemRoot%\\cursors\\wait_l.cur" + }, + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "FollowFocus": 0, + "FollowCaret": 1, + "FollowMouse": 1, + "Invert": 1, + "Magnification": 150, + "MagnificationMode": 3 + }, + "http://registry.gpii.net/applications/com.microsoft.windows.screenResolution": { + "screen-resolution": { + "width": 800, + "height": 600 + } + }, + "http://registry.gpii.net/applications/com.microsoft.windows.screenDPI": { + "screen-dpi": 1 + }, + "http://registry.gpii.net/applications/com.microsoft.windows.nightScreen": { + "SystemSettings_Display_BlueLight_ManualToggleQuickAction": { + "value": false + } + }, + "http://registry.gpii.net/applications/com.microsoft.windows.narrator": { + "SpeechSpeed": 11, + "SpeechPitch": 4, + "CoupleNarratorCursorKeyboard": 1, + "FollowInsertion": 0, + "InteractionMouse": 1, + "EchoWords": 1, + "EchoChars": 0 + }, + "http://registry.gpii.net/applications/com.microsoft.windows.typingEnhancement": { + "EnableAutocorrection": 1, + "EnableSpellchecking": 1, + "EnableTextPrediction": 1, + "EnablePredictionSpaceInsertion": 1, + "EnableDoubleTapSpace": 1, + "EnableKeyAudioFeedback": 1, + "EnableAutoShiftEngage": 1, + "EnableShiftLock": 1, + "EnableCompatibilityKeyboard": 1, + "EnableDesktopModeAutoInvoke": 1 + }, + "http://registry.gpii.net/applications/com.microsoft.windows.language": { + "MachinePreferredUILanguages": "en-US", + "PreferredUILanguages": "en-US" + } + } + } + } + } +} diff --git a/gpii/node_modules/matchMakerFramework/test/data/scoped_common_magnifier.json b/gpii/node_modules/matchMakerFramework/test/data/scoped_common_magnifier.json new file mode 100644 index 000000000..53e3b0780 --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/test/data/scoped_common_magnifier.json @@ -0,0 +1,16 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "MagnificationMode": 3, + "http://registry.gpii.net/common/magnification": 1.5 + }, + "http://registry.gpii.net/common/tracking": [ "mouse", "caret" ] + } + } + } + } +} diff --git a/gpii/node_modules/matchMaker/test/html/FlatMatchMakerTest.html b/gpii/node_modules/matchMakerFramework/test/html/MatchMakerUtilitiesTest.html similarity index 52% rename from gpii/node_modules/matchMaker/test/html/FlatMatchMakerTest.html rename to gpii/node_modules/matchMakerFramework/test/html/MatchMakerUtilitiesTest.html index c05d7a4e3..5954d427d 100644 --- a/gpii/node_modules/matchMaker/test/html/FlatMatchMakerTest.html +++ b/gpii/node_modules/matchMakerFramework/test/html/MatchMakerUtilitiesTest.html @@ -2,34 +2,42 @@ - GPII Flat MatchMaker tests + GPII MatchMaker Utilities tests - + + - - + + + + - - + + + + + - - + + + -

            GPII Flat MatchMaker tests

            +

            GPII MatchMaker Utilities tests

              diff --git a/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/jaws_application_expected.json b/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/jaws_application_expected.json new file mode 100644 index 000000000..176595be7 --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/jaws_application_expected.json @@ -0,0 +1,11 @@ +{ + "gpii-default": { + "com.freedomscientific.jaws": { + "http://registry.gpii.net/common/announceCapitals": false, + "http://registry.gpii.net/common/keyEcho": true, + "http://registry.gpii.net/common/punctuationVerbosity": "most", + "http://registry.gpii.net/common/readingUnit": "line", + "http://registry.gpii.net/common/wordEcho": true + } + } +} diff --git a/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_android_expected.json b/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_android_expected.json new file mode 100644 index 000000000..165c126ea --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_android_expected.json @@ -0,0 +1,7 @@ +{ + "gpii-default": { + "com.android.audioManager": {}, + "com.android.persistentConfiguration": {}, + "com.android.settings.system": {} + } +} diff --git a/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_common_expected.json b/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_common_expected.json new file mode 100644 index 000000000..d5dfc91d7 --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_common_expected.json @@ -0,0 +1,5 @@ +{ + "gpii-default": { + + } +} diff --git a/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_common_expected.txt b/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_common_expected.txt new file mode 100644 index 000000000..27e7bfc2e --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_common_expected.txt @@ -0,0 +1,4 @@ +No output is expected for this test, since the preferences file contains only common terms. +Whilst these are correctly ontologised into hierarchical form by the ontology server, +the resulting preference set contains no "applications" block and so images to nothing +by the inverse capabilities rules. \ No newline at end of file diff --git a/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_gnome_expected.json b/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_gnome_expected.json new file mode 100644 index 000000000..eb67b8268 --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_gnome_expected.json @@ -0,0 +1,15 @@ +{ + "gpii-default": { + "org.gnome.desktop.interface": { + "http://registry.gpii.net/common/fontSize": 9, + "http://registry.gpii.net/common/cursorSize": 0.9 + }, + "org.gnome.desktop.a11y.magnifier": { + "http://registry.gpii.net/common/magnification": 1.5, + "http://registry.gpii.net/common/magnifierPosition": "FullScreen" + }, + "org.alsa-project": { + "http://registry.gpii.net/common/volume": 0.5 + } + } +} diff --git a/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_gnome_expected.txt b/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_gnome_expected.txt new file mode 100644 index 000000000..b8f482088 --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_gnome_expected.txt @@ -0,0 +1,4 @@ +The following settings are not converted from application-specific form, as a result of not having entries in +the inverseCapabilities.json transformer: + +"http://registry.gpii.net/applications/org.alsa-project" - should be mapped to "masterVolume" diff --git a/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_win_expected.json b/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_win_expected.json new file mode 100644 index 000000000..613bbb092 --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_win_expected.json @@ -0,0 +1,62 @@ +{ + "gpii-default": { + "com.microsoft.windows.highContrast": { + "http://registry.gpii.net/common/highContrast/enabled": true + }, + "com.microsoft.windows.cursors": { + "http://registry.gpii.net/common/cursorSize": 1, + "http://registry.gpii.net/common/cursorColor": "White" + }, + "com.microsoft.windows.magnifier": { + "http://registry.gpii.net/common/invertColours": 1, + "http://registry.gpii.net/common/magnifierPosition": "Lens", + "http://registry.gpii.net/common/tracking": [ + "mouse", + "caret" + ], + "http://registry.gpii.net/common/magnification": 1.5 + }, + "com.microsoft.windows.mouseTrailing": { + "http://registry.gpii.net/common/mouseTrailing": 10 + }, + "com.microsoft.windows.stickyKeys": { + "http://registry.gpii.net/common/stickyKeys": true + }, + "com.microsoft.windows.mouseKeys": { + "http://registry.gpii.net/common/cursorSpeed": 0.2571428571426, + "http://registry.gpii.net/common/mouseEmulation/enabled": true + }, + "com.microsoft.windows.filterKeys": { + "http://registry.gpii.net/common/debounce/enabled": true + }, + "com.microsoft.windows.screenResolution": {}, + "com.microsoft.windows.screenDPI": { + "http://registry.gpii.net/common/DPIScale": 1 + }, + "com.microsoft.windows.nightScreen": { + "http://registry.gpii.net/common/nightScreen": false + }, + "com.microsoft.windows.narrator": { + "http://registry.gpii.net/common/speechRate": 180, + "http://registry.gpii.net/common/pitch": 0.2, + "http://registry.gpii.net/common/trackingTTS": ["focus","mouse"], + "http://registry.gpii.net/common/wordEcho": true, + "http://registry.gpii.net/common/keyEcho": false + }, + "com.microsoft.windows.typingEnhancement": { + "http://registry.gpii.net/common/autocorrectionEnabled": true, + "http://registry.gpii.net/common/spellCheckEnabled": true, + "http://registry.gpii.net/common/textPredictionEnabled": true, + "http://registry.gpii.net/common/textPredictionWithSpaceEnabled": true, + "http://registry.gpii.net/common/doubleTabSpaceEnabled": true, + "http://registry.gpii.net/common/keyAudioFeedbackEnabled": true, + "http://registry.gpii.net/common/autoShiftEngageEnabled": true, + "http://registry.gpii.net/common/shiftLockEnabled": true, + "http://registry.gpii.net/common/compatibilityKeyboardEnabled": true, + "http://registry.gpii.net/common/desktopModeAutoInvokeEnabled": true + }, + "com.microsoft.windows.language": { + "http://registry.gpii.net/common/language": "en-US" + } + } +} diff --git a/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_win_expected.txt b/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_win_expected.txt new file mode 100644 index 000000000..1e867a1b5 --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/os_win_expected.txt @@ -0,0 +1,4 @@ +Note that the value of the application "http://registry.gpii.net/applications/com.microsoft.windows.mouseTrailing" +has no inversion rule entered into inverseCapabilities.json and so it does not appear in the output. It should +appear in a block named "display.screenEnhancement.mouseTrailing" + diff --git a/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/scoped_common_magnifier_expected.json b/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/scoped_common_magnifier_expected.json new file mode 100644 index 000000000..970243b20 --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/test/inverseCapabilities/scoped_common_magnifier_expected.json @@ -0,0 +1,9 @@ +{ + "gpii-default": { + "com.microsoft.windows.magnifier": { + "http://registry.gpii.net/common/magnification": 1.5, + "http://registry.gpii.net/common/magnifierPosition": "Lens", + "http://registry.gpii.net/common/tracking": [] + } + } +} diff --git a/gpii/node_modules/matchMakerFramework/test/js/.eslintrc.json b/gpii/node_modules/matchMakerFramework/test/js/.eslintrc.json new file mode 100644 index 000000000..2d48f5bbc --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/test/js/.eslintrc.json @@ -0,0 +1,8 @@ +{ + "env": { + "browser": true + }, + "rules": { + "strict": ["error", "function"] + } +} diff --git a/gpii/node_modules/matchMakerFramework/test/js/FilterPreferencesFromInferredTests.js b/gpii/node_modules/matchMakerFramework/test/js/FilterPreferencesFromInferredTests.js new file mode 100644 index 000000000..20ecedf38 --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/test/js/FilterPreferencesFromInferredTests.js @@ -0,0 +1,250 @@ +/*! +GPII MatchMaker Utilities: Filter preferences from inferred configuration tests + +Copyright 2017 Raising The Floor - International + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +/*global jqUnit */ + +var fluid = fluid || require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +(function () { + "use strict"; + fluid.registerNamespace("gpii.tests.matchMakerUtilities.filterPreferences"); + + gpii.tests.matchMakerUtilities.filterPreferences.testDefs = [{ + name: "Basic common terms filtering", + preferences: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 24, + "http://registry.gpii.net/common/magnification": 2 + } + } + } + }, + inferred: { + "gpii-default": { + "applications": { + "com.microsoft.windows.magnifier": { + "active": true, + "settings": { + "http://registry.gpii.net/common/magnification": 2 + } + } + } + } + }, + expected: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/magnification": 2 + } + } + } + } + },{ + name: "Nested common terms filtering", + preferences: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "http://registry.gpii.net/common/fontSize": 24, + "http://registry.gpii.net/common/magnification": 2 + } + } + } + } + }, + inferred: { + "gpii-default": { + "applications": { + "com.microsoft.windows.magnifier": { + "active": true, + "settings": { + "http://registry.gpii.net/common/magnification": 2 + } + } + } + } + }, + expected: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "http://registry.gpii.net/common/magnification": 2 + } + } + } + } + } + }, { + name: "Filtering with application specific settings", + preferences: { + "contexts": { + "gpii-default": { + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "http://registry.gpii.net/common/fontSize": 24, + "http://registry.gpii.net/common/magnification": 2, + "Invert": 0 + } + } + } + } + }, + inferred: { + "gpii-default": { + "applications": { + "com.microsoft.windows.magnifier": { + "active": true, + "settings": { + "http://registry.gpii.net/common/magnification": 2, + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "Invert": 0 + } + } + } + } + } + }, + expected: { + "contexts": { + "gpii-default": { + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "http://registry.gpii.net/common/magnification": 2, + "Invert": 0 + } + } + } + } + } + }, { + name: "Filtering with different contexts", + preferences: { + "contexts": { + "gpii-default": { + "preferences": { + "http://registry.gpii.net/common/fontSize": 24, + "http://registry.gpii.net/common/magnification": 2 + } + }, + "other-context": { + "preferences": { + "http://registry.gpii.net/common/magnification": 2, + "http://registry.gpii.net/common/magnifierPosition": "Lens" + } + } + } + }, + inferred: { + "gpii-default": { + "applications": { + "com.microsoft.windows.magnifier": { + "active": true, + "settings": { + "http://registry.gpii.net/common/magnification": 2 + } + } + } + }, + "other-context": { + "applications": { + "com.microsoft.windows.magnifier": { + "active": true, + "settings": { + "http://registry.gpii.net/common/magnification": 2, + "http://registry.gpii.net/common/magnifierPosition": "Lens" + } + } + } + } + }, + expected: { + "contexts": { + "gpii-default": { + "preferences": { + "http://registry.gpii.net/common/magnification": 2 + } + }, + "other-context": { + "preferences": { + "http://registry.gpii.net/common/magnification": 2, + "http://registry.gpii.net/common/magnifierPosition": "Lens" + } + } + } + } + }, { + name: "Filter non-matching application block", + preferences: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "http://registry.gpii.net/common/fontSize": 24, + "http://registry.gpii.net/common/magnification": 2 + }, + "http://registry.gpii.net/common/showCrosshairs": true + } + } + } + }, + inferred: { + "gpii-default": { + "applications": { + "org.gnome.desktop.a11y.magnifier": { + "active": true, + "settings": { + "http://registry.gpii.net/common/magnification": 2, + "http://registry.gpii.net/common/showCrosshairs": true + } + } + } + } + }, + expected: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/showCrosshairs": true + } + } + } + } + }]; + + // non-matching application block + // matching application block with no preferences in it + + gpii.tests.matchMakerUtilities.filterPreferences.runTests = function () { + jqUnit.module("MatchMakerUtilities - filtering preferences from inferred configuration"); + fluid.each(gpii.tests.matchMakerUtilities.filterPreferences.testDefs, function (test) { + jqUnit.test(test.name, function () { + var result = gpii.matchMakerFramework.utils.filterPreferencesFromInferredConfig(test.preferences, test.inferred); + jqUnit.assertDeepEq("Expecting the filtered preference set", test.expected, result); + }); + }); + }; +})(); diff --git a/gpii/node_modules/matchMakerFramework/test/js/MatchMakerUtilitiesTests.js b/gpii/node_modules/matchMakerFramework/test/js/MatchMakerUtilitiesTests.js new file mode 100644 index 000000000..a4ed97f20 --- /dev/null +++ b/gpii/node_modules/matchMakerFramework/test/js/MatchMakerUtilitiesTests.js @@ -0,0 +1,1173 @@ +/*! +GPII MatchMaker Utilities Tests + +Copyright 2014 Raising The Floor - International + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +/*global jqUnit, JSON5 */ + +var fluid = fluid || require("infusion"), + gpii = fluid.registerNamespace("gpii"), + $ = fluid.registerNamespace("jQuery"); + +(function () { + "use strict"; + fluid.registerNamespace("gpii.tests.matchMakerUtilities"); + + var transformSpec = JSON5.parse($.ajax({ + type: "GET", + url: "../../../../../testData/ontologies/mappings/ISO24751-flat.json5", + async: false + }).responseText); + + var apptologyTransformSpec = JSON5.parse($.ajax({ + type: "GET", + url: "../../../../../testData/ontologies/mappings/flat-apptology.json5", + async: false + }).responseText); + + var computeLeavesFixtures = [ + { + name: "Nested structure including empty objects", + model: { + "display": { + "screenEnhancement": { + "fontSize": 24, + "foregroundColor": "white", + "backgroundColor": "black", + "fontFace": { + "genericFontFace": "sans serif" + }, + "screenMagnification": { + } + } + }, + "control": { + "structuralNavigation": { + "tableOfContents": false + } + } + }, + expected: [ + "display.screenEnhancement.fontSize", + "display.screenEnhancement.foregroundColor", + "display.screenEnhancement.backgroundColor", + "display.screenEnhancement.fontFace.genericFontFace", + "control.structuralNavigation.tableOfContents" + ] + }, + { + name: "Array parsing disabled", + model: { + "com.microsoft.windows.narrator": { + "http://registry.gpii.net/common/speechRate": 180, + "http://registry.gpii.net/common/pitch": 0.2 + }, + "http://registry.gpii.net/common/trackingTTS": ["focus","mouse"], + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "invertColours": true + } + }, + expected: [ + "com\\.microsoft\\.windows\\.narrator.http://registry\\.gpii\\.net/common/speechRate", + "com\\.microsoft\\.windows\\.narrator.http://registry\\.gpii\\.net/common/pitch", + "http://registry\\.gpii\\.net/common/trackingTTS.0", + "http://registry\\.gpii\\.net/common/trackingTTS.1", + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.magnifier.invertColours" + ] + } + ]; + + gpii.tests.matchMakerUtilities.computeLeaves = function () { + fluid.each(computeLeavesFixtures, function (fixture) { + var result = gpii.matchMakerFramework.utils.computeLeaves(fixture.model); + jqUnit.assertDeepEq(fixture.name, fixture.expected, result); + }); + }; + + var pathsToSkeletonFixtures = [ + { + description: "Object with various levels of nesting", + expected: { + "display": { + "screenEnhancement": { + "fontSize": {}, + "foregroundColor": {}, + "backgroundColor": {}, + "fontFace": { + "genericFontFace": {} + } + }, + "hello": {} + }, + "control": { + "structuralNavigation": { + "tableOfContents": {} + } + }, + "cat": {} + }, + model: [ + "display.screenEnhancement.fontSize", + "display.screenEnhancement.foregroundColor", + "display.screenEnhancement.backgroundColor", + "display.screenEnhancement.fontFace.genericFontFace", + "display.hello", + "control.structuralNavigation.tableOfContents", + "cat" + ] + }, { + description: "Object with various levels of nesting", + expected: { + "display": { + "screenEnhancement": { + "fontSize": true, + "foregroundColor": true, + "backgroundColor": true, + "fontFace": { + "genericFontFace": true + } + }, + "hello": true + }, + "control": { + "structuralNavigation": { + "tableOfContents": true + } + }, + "cat": true + }, + model: [ + "display.screenEnhancement.fontSize", + "display.screenEnhancement.foregroundColor", + "display.screenEnhancement.backgroundColor", + "display.screenEnhancement.fontFace.genericFontFace", + "display.hello", + "control.structuralNavigation.tableOfContents", + "cat" + ], + value: true + } + ]; + + gpii.tests.matchMakerUtilities.pathsToSkeleton = function () { + fluid.each(pathsToSkeletonFixtures, function (fixture) { + var result = gpii.matchMakerFramework.utils.pathsToSkeleton(fixture.model, fixture.value); + jqUnit.assertDeepEq(fixture.name, fixture.expected, result); + }); + }; + + var computeAndOntologizeCapabilitiesFromSolutionFixture = { + description: "solution incl capabilities and transformations", + solution: { + "name": "GNOME Shell Magnifier", + "contexts": {}, + "capabilities": [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + "settingsHandlers": { + "configuration": { + "type": "gpii.gsettings", + "options": { + "schema": "org.gnome.desktop.a11y.magnifier" + }, + "capabilitiesTransformations": { + "mag-factor": "http://registry\\.gpii\\.net/common/magnification", + "show-cross-hairs": "http://registry\\.gpii\\.net/common/showCrosshairs", + "transform": { + "type": "fluid.transforms.arrayToSetMembership", + "inputPath": "http://registry\\.gpii\\.net/common/tracking", + "presentValue": "proportional", + "missingValue": "none", + "options": { + "focus": "focus-tracking", + "caret": "caret-tracking", + "mouse": "mouse-tracking" + } + }, + "screen-position": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/magnifierPosition", + "match": { + "FullScreen": "full-screen" + } + } + }, + "someSetting": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled" + } + } + }, + "isInstalled": [ + { + "type": "gpii.packageKit.find", + "name": "gnome-shell" + } + ] + }, + expectedCapabilities: [ + "http://registry\\.gpii\\.net/common/tracking", + "http://registry\\.gpii\\.net/common/magnification", + "http://registry\\.gpii\\.net/common/showCrosshairs", + "http://registry\\.gpii\\.net/common/magnifierPosition", + "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled", + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + expectedSpecialCapabilities: [ + "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled", + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + expectedISO: [ + "display.screenEnhancement.tracking", + "display.screenEnhancement.magnification", + "display.screenEnhancement.-provisional-magnifierPosition", + "display.screenEnhancement.-provisional-magnification/enabled", + "display.screenReader.-provisional-screenReaderTTS/enabled" + ], + expectedApptology: [ + "http://registry\\.gpii\\.net/common/magnifierApplication", + "http://registry\\.gpii\\.net/common/screenReaderApplication" + ] + }; + + gpii.tests.matchMakerUtilities.computeAndOntologizeCapabilitiesFromSolution = function () { + var fixture = computeAndOntologizeCapabilitiesFromSolutionFixture; + + var capabilities = gpii.matchMakerFramework.utils.computeCapabilitiesFromSolution(fixture.solution); + jqUnit.assertDeepEq("Capabilities computed by gpii.matchMakerFramework.utils.computeCapabilitiesFromSolution", fixture.expectedCapabilities, capabilities); + + var specialCapabilities = gpii.matchMakerFramework.utils.extractSpecialCapabilities(capabilities); + jqUnit.assertDeepEq("Special capabilities computed by gpii.matchMakerFramework.utils.extractSpecialCapabilities", fixture.expectedSpecialCapabilities, specialCapabilities); + + var flatToISOSpec = fluid.model.transform.invertConfiguration(transformSpec); + var resultISO = gpii.matchMakerFramework.utils.transformCapabilitiesToOntologizedLeaves(capabilities, flatToISOSpec); + jqUnit.assertDeepEq("Capabilities to ISO leaves (gpii.matchMakerFramework.utils.transformCapabilitiesToOntologizedLeaves)", fixture.expectedISO, resultISO); + + var resultApptology = gpii.matchMakerFramework.utils.transformCapabilitiesToOntologizedLeaves(capabilities, apptologyTransformSpec); + jqUnit.assertDeepEq("Capabilities to apptology (gpii.matchMakerFramework.utils.transformCapabilitiesToOntologizedLeaves)", fixture.expectedApptology, resultApptology); + }; + + var buildReturnPayloadFixtures = [ + { + description: "Basic payloads", + disposed: { + "gpii-default": { + "mac.dummy": true + } + }, + fullModel: { + "gpiiKey": "mac", + "preferences": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 16, + "http://registry.gpii.net/common/screenReaderTTS/enabled": false, + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + }, + "deviceReporter": { + "solutions": [ + { + "id": "mac.dummy" + }, + { + "id": "mac.dummy_empty" + } + ], + "OS": { + "id": "darwin", + "version": "12.5.0" + } + }, + "solutionsRegistryEntries": { + "mac.dummy": { + "name": "Mac dummy", + "contexts": { + "OS": [ + { + "id": "darwin" + } + ] + }, + "settingsHandlers": { + "myconf": { + "type": "gpii.settingsHandlers.JSONSettingsHandler.set", + "options": { + "path": "/path/to/some/file.json" + }, + "capabilitiesTransformations": { + "setting1": "http://registry\\.gpii\\.net/common/fontSize", + "setting2.path1": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled", + "setting2.path2": "http://registry\\.gpii\\.net/common/magnification/enabled", + "setting2.path3.hello": "http://registry\\.gpii\\.net/common/magnification" + } + } + }, + "configure": [ + "settings.myconf" + ], + "restore": [ + "settings.myconf" + ] + }, + "mac.dummy_empty": { + "name": "Mac dummy empty", + "contexts": { + "OS": [ + { + "id": "darwin" + } + ] + }, + "settingsHandlers": { + "otherconf": { + "type": "gpii.settingsHandlers.noSettings" + } + }, + "start": [], + "stop": [] + } + }, + "hierarchicalPrefs": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "display": { + "screenEnhancement": { + "fontSize": 16, + "magnification": 1, + "-provisional-magnification/enabled": false + }, + "screenReader": { + "-provisional-screenReaderTTS/enabled": false + } + } + } + } + } + } + }, + expected: { + "inferredConfiguration": { + "gpii-default": { + "applications": { + "mac.dummy": { + "active": true, + "settings": { + "http://registry.gpii.net/common/fontSize": 16, + "http://registry.gpii.net/common/screenReaderTTS/enabled": false, + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + } + } + } + } + ]; + + gpii.tests.matchMakerUtilities.buildReturnPayload = function () { + fluid.each(buildReturnPayloadFixtures, function (fixture) { + var result = gpii.matchMakerFramework.utils.buildReturnPayload(fixture.fullModel, fixture.disposed); + jqUnit.assertDeepEq(fixture.description, fixture.expected, result); + }); + }; + + var addSolutionTypeInformationFixtures = [{ + name: "Three solutions, two of same type", + payload: { + solutionCapabilities: { + "fakemag1": [ + "http://registry\\.gpii\\.net/common/magnification/enabled", + "http://registry\\.gpii\\.net/common/magnification" + ], + "fakemag2": [ + "http://registry\\.gpii\\.net/common/magnification/enabled", + "http://registry\\.gpii\\.net/common/magnification", + "http://registry\\.gpii\\.net/common/invertColours" + ], + "fakescreenreader1": [ + "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled", + "http://registry\\.gpii\\.net/common/pitch", + "http://registry\\.gpii\\.net/common/volumeTTS", + "http://registry\\.gpii\\.net/common/speechRate" + ] + } + }, + expectedSolutionTypes: { + "fakemag1": [ + "http://registry\\.gpii\\.net/common/magnifierApplication" + ], + "fakemag2": [ + "http://registry\\.gpii\\.net/common/magnifierApplication" + ], + "fakescreenreader1": [ + "http://registry\\.gpii\\.net/common/screenReaderApplication" + ] + }, + expectedSolutionTypeMapping: { + "http://registry\\.gpii\\.net/common/magnifierApplication": { + "fakemag1": true, + "fakemag2": true + }, + "http://registry\\.gpii\\.net/common/screenReaderApplication": { + "fakescreenreader1": true + } + } + }]; + + gpii.tests.matchMakerUtilities.testAddSolutionTypeInformation = function () { + jqUnit.module("AddSolutionTypeInformation tests"); + fluid.each(addSolutionTypeInformationFixtures, function (fixture) { + var payload = fluid.copy(fixture.payload); + gpii.matchMakerFramework.utils.addSolutionTypeInformation(payload, apptologyTransformSpec); + jqUnit.assertDeepEq(fixture.name + ". Checking solutions types are properly added", fixture.expectedSolutionTypes, payload.solutionTypes); + jqUnit.assertDeepEq(fixture.name + ". Checking solutions Mapping is properly added", fixture.expectedSolutionTypeMapping, payload.solutionTypeMapping); + }); + }; + + + gpii.tests.matchMakerUtilities.updateInferredConfigurationSolutionRegistryEntries = { + "fake.magnifier1": { + "capabilities": [] + }, + "fake.magnifier2": { + "capabilities": [] + }, + "fake.magnifier3": { + "capabilities": [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ] + }, + "fontSizer": { + "capabilities": [] + } + }; + + gpii.tests.matchMakerUtilities.updateInferredConfigurationTestDefs = [{ + name: "single top-level common term, single application", + preferences: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 20 + } + } + } + }, + inferredConfiguration: { + "gpii-default": { + "applications": { + "fake.magnifier1": { + "active": true, + "settings": { + "http://registry.gpii.net/common/fontSize": 16, + "http://registry.gpii.net/common/screenReaderTTS/enabled": false, + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + } + }, + expected: { + "gpii-default": { + "applications": { + "fake.magnifier1": { + "active": true, + "settings": { + "http://registry.gpii.net/common/fontSize": 20, + "http://registry.gpii.net/common/screenReaderTTS/enabled": false, + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + } + } + }, { + name: "multiple top-level common terms in preferences, single application", + preferences: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 20, + "http://registry.gpii.net/common/magnification": 2 + } + } + } + }, + inferredConfiguration: { + "gpii-default": { + "applications": { + "fake.magnifier1": { + "active": true, + "settings": { + "http://registry.gpii.net/common/fontSize": 16, + "http://registry.gpii.net/common/screenReaderTTS/enabled": false, + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + } + }, + expected: { + "gpii-default": { + "applications": { + "fake.magnifier1": { + "active": true, + "settings": { + "http://registry.gpii.net/common/fontSize": 20, + "http://registry.gpii.net/common/screenReaderTTS/enabled": false, + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 2 + } + } + } + } + } + }, { + name: "common terms in preferences not matching applications in inferred", + preferences: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 20 + } + } + } + }, + inferredConfiguration: { + "gpii-default": { + "applications": { + "fake.magnifier1": { + "active": true, + "settings": { + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + } + }, + expected: { + "gpii-default": { + "applications": { + "fake.magnifier1": { + "active": true, + "settings": { + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + } + } + }, { + name: "multiple top-level common terms in preferences, multiple application", + preferences: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 20, + "http://registry.gpii.net/common/magnification": 2 + } + } + } + }, + inferredConfiguration: { + "gpii-default": { + "applications": { + "fake.magnifier1": { + "active": true, + "settings": { + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + }, + "fontSizer": { + "active": true, + "settings": { + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/screenReaderTTS/enabled": false + } + } + } + } + }, + expected: { + "gpii-default": { + "applications": { + "fake.magnifier1": { + "active": true, + "settings": { + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 2 + } + }, + "fontSizer": { + "active": true, + "settings": { + "http://registry.gpii.net/common/fontSize": 20, + "http://registry.gpii.net/common/screenReaderTTS/enabled": false + } + } + } + } + } + }, { + name: "Common terms in preferences scoped to an application", + preferences: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/fake.magnifier1": { + "http://registry.gpii.net/common/magnification": 2 + } + } + } + } + }, + inferredConfiguration: { + "gpii-default": { + "applications": { + "fake.magnifier1": { + "active": true, + "settings": { + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + }, + "fake.magnifier2": { + "active": true, + "settings": { + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + } + }, + expected: { + "gpii-default": { + "applications": { + "fake.magnifier1": { + "active": true, + "settings": { + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 2 + } + }, + "fake.magnifier2": { + "active": true, + "settings": { + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + } + } + }, { + name: "Common terms in preferences, both scoped and not scoped to an application", + preferences: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/fake.magnifier1": { + "http://registry.gpii.net/common/magnification": 2 + }, + "http://registry.gpii.net/common/magnification/enabled": true + } + } + } + }, + inferredConfiguration: { + "gpii-default": { + "applications": { + "fake.magnifier1": { + "active": true, + "settings": { + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + }, + "fake.magnifier2": { + "active": true, + "settings": { + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + } + }, + expected: { + "gpii-default": { + "applications": { + "fake.magnifier1": { + "active": true, + "settings": { + "http://registry.gpii.net/common/magnification/enabled": true, + "http://registry.gpii.net/common/magnification": 2 + } + }, + "fake.magnifier2": { + "active": true, + "settings": { + "http://registry.gpii.net/common/magnification/enabled": true, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + } + } + }, { + name: "'enabled' term in updated preferences, resulting in 'active' change", + preferences: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/magnification/enabled": false + } + } + } + }, + inferredConfiguration: { + "gpii-default": { + "applications": { + "fake.magnifier3": { + "active": true, + "settings": { + "http://registry.gpii.net/common/fontSize": 16, + "http://registry.gpii.net/common/screenReaderTTS/enabled": true, + "http://registry.gpii.net/common/magnification/enabled": true, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + } + }, + expected: { + "gpii-default": { + "applications": { + "fake.magnifier3": { + "active": false, + "settings": { + "http://registry.gpii.net/common/fontSize": 16, + "http://registry.gpii.net/common/screenReaderTTS/enabled": true, + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + } + } + }, { + name: "application scoped 'enabled' term in updated preferences, resulting in 'active' change", + preferences: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/fake.magnifier3": { + "http://registry.gpii.net/common/magnification/enabled": false + } + } + } + } + }, + inferredConfiguration: { + "gpii-default": { + "applications": { + "fake.magnifier3": { + "active": true, + "settings": { + "http://registry.gpii.net/common/fontSize": 16, + "http://registry.gpii.net/common/screenReaderTTS/enabled": true, + "http://registry.gpii.net/common/magnification/enabled": true, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + } + }, + expected: { + "gpii-default": { + "applications": { + "fake.magnifier3": { + "active": false, + "settings": { + "http://registry.gpii.net/common/fontSize": 16, + "http://registry.gpii.net/common/screenReaderTTS/enabled": true, + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + } + } + }, { + name: "application scoped 'enabled' term in updated preferences, but for other application (i.e. no 'active' change)", + preferences: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/fake.magnifier2": { + "http://registry.gpii.net/common/magnification/enabled": true + } + } + } + } + }, + inferredConfiguration: { + "gpii-default": { + "applications": { + "fake.magnifier2": { + "active": false, + "settings": { + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + }, + "fake.magnifier3": { + "active": false, + "settings": { + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + } + }, + expected: { + "gpii-default": { + "applications": { + "fake.magnifier2": { + "active": false, + "settings": { + "http://registry.gpii.net/common/magnification/enabled": true, + "http://registry.gpii.net/common/magnification": 1 + } + }, + "fake.magnifier3": { + "active": false, + "settings": { + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + } + } + }, { + name: "GPII-3995: application terms can have non-common terms nested in it", + preferences: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/fake.magnifier3": { + fontSize: 18 + } + } + } + } + }, + inferredConfiguration: { + "gpii-default": { + "applications": { + "fake.magnifier3": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/fake.magnifier3": { + fontSize: 16 + }, + "http://registry.gpii.net/common/screenReaderTTS/enabled": true, + "http://registry.gpii.net/common/magnification/enabled": true, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + } + }, + expected: { + "gpii-default": { + "applications": { + "fake.magnifier3": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/fake.magnifier3": { + fontSize: 18 + }, + "http://registry.gpii.net/common/screenReaderTTS/enabled": true, + "http://registry.gpii.net/common/magnification/enabled": true, + "http://registry.gpii.net/common/magnification": 1 + } + } + } + } + } + }]; + + gpii.tests.matchMakerUtilities.updateInferredConfiguration = function () { + jqUnit.module("Tests for updateInferredConfiguration"); + fluid.each(gpii.tests.matchMakerUtilities.updateInferredConfigurationTestDefs, function (testDef) { + var result = gpii.matchMakerFramework.utils.updateInferredConfiguration(testDef.preferences, testDef.inferredConfiguration, + gpii.tests.matchMakerUtilities.updateInferredConfigurationSolutionRegistryEntries); + jqUnit.assertDeepEq(testDef.name, testDef.expected, result); + }); + }; + + fluid.registerNamespace("gpii.tests.matchMakerUtilities.getLeastLiveness"); + + gpii.tests.matchMakerUtilities.getLeastLiveness.solutions = { + "fakemag1": { + "capabilities": [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.JSONSettingsHandler", + "options": { + "filename": "/tmp/fakemag1.settings.json" + }, + "capabilitiesTransformations": { + "magnification": "http://registry\\.gpii\\.net/common/magnification" + }, + "liveness": "live" + } + } + }, + "fakemag2": { + "capabilities": [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.JSONSettingsHandler", + "options": { + "filename": "/tmp/fakemag2.settings.json" + }, + "capabilitiesTransformations": { + "magnification": "http://registry\\.gpii\\.net/common/magnification", + "invert": "http://registry\\.gpii\\.net/common/invertColours" + }, + "liveness": "liveRestart" + } + } + }, + "fakescreenreader1": { + "capabilities": [ + "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled" + ], + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.JSONSettingsHandler", + "options": { + "filename": "/tmp/fakescreenreader1.json" + }, + "capabilitiesTransformations": { + "rate": "http://registry\\.gpii\\.net/common/speechRate" + }, + "liveness": "OSRestart" + }, + "configuration2": { + "type": "gpii.settingsHandlers.JSONSettingsHandler", + "options": { + "filename": "/tmp/fakescreenreader1.json" + }, + "capabilitiesTransformations": { + "pitch": "http://registry\\.gpii\\.net/common/pitch", + "volumeTTS": "http://registry\\.gpii\\.net/common/volumeTTS", + "rate": "http://registry\\.gpii\\.net/common/speechRate" + }, + "liveness": "manualRestart" + } + } + } + }; + + gpii.tests.matchMakerUtilities.getLeastLiveness.testData = [ + { + name: "Basic with single solution", + term: "http://registry.gpii.net/common/magnification", + solutions: fluid.filterKeys(gpii.tests.matchMakerUtilities.getLeastLiveness.solutions, ["fakemag1"]), + expected: "live" + }, { + name: "Term present in multiple solutions, least lively wins", + term: "http://registry.gpii.net/common/magnification", + solutions: fluid.filterKeys(gpii.tests.matchMakerUtilities.getLeastLiveness.solutions, ["fakemag1", "fakemag2"]), + expected: "liveRestart" + }, { + name: "Term inferred from capability transformations", + term: "http://registry.gpii.net/common/invertColours", + solutions: fluid.filterKeys(gpii.tests.matchMakerUtilities.getLeastLiveness.solutions, [ "fakemag2"]), + expected: "liveRestart" + }, { + name: "Term in a secondary settingshandler block", + term: "http://registry.gpii.net/common/pitch", + solutions: fluid.filterKeys(gpii.tests.matchMakerUtilities.getLeastLiveness.solutions, [ "fakescreenreader1"]), + expected: "manualRestart" + }, { + name: "No terms provided", + term: undefined, + solutions: fluid.filterKeys(gpii.tests.matchMakerUtilities.getLeastLiveness.solutions, [ "fakescreenreader1"]), + expected: "OSRestart" + } + ]; + + + gpii.tests.matchMakerUtilities.getLeastLiveness.tests = function () { + jqUnit.module("Tests for getLeastLiveness"); + fluid.each(gpii.tests.matchMakerUtilities.getLeastLiveness.testData, function (testDef) { + var liveness = gpii.matchMakerFramework.utils.getLeastLiveness(testDef.solutions, testDef.term); + jqUnit.assertDeepEq(testDef.name, testDef.expected, liveness); + }); + }; + + gpii.tests.matchMakerUtilities.getActiveValueFromEnabledTermsData = [ + { + name: "no enabled terms in the user's preference set", + args: { + specialPreferences: {}, + specialCapabilities: [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + solution: { + capabilities: [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ] + }, + solutionId: "my.solution" + }, + expected: undefined + }, { + name: "no enabled term in solutions capability", + args: { + specialPreferences: { + "http://registry.gpii.net/common/magnification/enabled": true + }, + specialCapabilities: [ + ], + solution: {}, + solutionId: "my.solution" + }, + expected: undefined + }, { + name: "enabled term in outer level of prefs - 'true' value", + args: { + specialPreferences: { + "http://registry.gpii.net/common/magnification/enabled": true + }, + specialCapabilities: [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + solution: { + capabilities: [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ] + }, + solutionId: "my.solution" + }, + expected: true + }, { + name: "enabled term in outer level of prefs - 'false' value", + args: { + specialPreferences: { + "http://registry.gpii.net/common/magnification/enabled": false + }, + specialCapabilities: [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + solution: { + capabilities: [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ] + }, + solutionId: "my.solution" + }, + expected: false + }, { + name: "enabled term in application specific block of prefs", + args: { + specialPreferences: { + "http://registry.gpii.net/applications/my.solution": { + "http://registry.gpii.net/common/magnification/enabled": true + } + }, + specialCapabilities: [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + solution: { + capabilities: [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ] + }, + solutionId: "my.solution" + }, + expected: true + }, { + name: "enabled capability is from capabilities transformations (not from solution's capabilities block)", + args: { + specialPreferences: { + "http://registry.gpii.net/common/magnification/enabled": true + }, + specialCapabilities: [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + solution: { + capabilities: [] + }, + solutionId: "my.solution" + }, + expected: undefined + } + ]; + + gpii.tests.matchMakerUtilities.getActiveValueFromEnabledTerms = function () { + jqUnit.module("Tests for getActiveValueFromEnabledTerms"); + fluid.each(gpii.tests.matchMakerUtilities.getActiveValueFromEnabledTermsData, function (def) { + var result = gpii.matchMakerFramework.utils.getActiveValueFromEnabledTerms(def.args.specialPreferences, def.args.specialCapabilities, def.args.solution, def.args.solutionId); + jqUnit.assertDeepEq(def.name, def.expected, result); + }); + }; + + + gpii.tests.matchMakerUtilities.runTests = function () { + jqUnit.module("MatchMakerUtilities"); + jqUnit.test("Compute Leaves", gpii.tests.matchMakerUtilities.computeLeaves); + jqUnit.test("Paths to Skeleton", gpii.tests.matchMakerUtilities.pathsToSkeleton); + jqUnit.test("Compute Capabilities From Solution", gpii.tests.matchMakerUtilities.computeAndOntologizeCapabilitiesFromSolution); + jqUnit.test("Build Return Payload", gpii.tests.matchMakerUtilities.buildReturnPayload); + jqUnit.test("Adding solution type information to payload", gpii.tests.matchMakerUtilities.testAddSolutionTypeInformation); + jqUnit.test("Updated Inferred Configuration with preferences", gpii.tests.matchMakerUtilities.updateInferredConfiguration); + jqUnit.test("Test getLeastLiveness functionality", gpii.tests.matchMakerUtilities.getLeastLiveness.tests); + jqUnit.test("Test getActiveValueFromEnabledTerms", gpii.tests.matchMakerUtilities.getActiveValueFromEnabledTerms); + }; +})(); diff --git a/gpii/node_modules/ontologyHandler/index.js b/gpii/node_modules/ontologyHandler/index.js new file mode 100644 index 000000000..48cc13a35 --- /dev/null +++ b/gpii/node_modules/ontologyHandler/index.js @@ -0,0 +1,8 @@ +"use strict"; + +var fluid = require("infusion"); + +fluid.module.register("ontologyHandler", __dirname, require); + +require("./src/ontologyHandlerUtilities.js"); +require("./src/ontologyHandler.js"); diff --git a/gpii/node_modules/ontologyHandler/package.json b/gpii/node_modules/ontologyHandler/package.json new file mode 100644 index 000000000..c910e6980 --- /dev/null +++ b/gpii/node_modules/ontologyHandler/package.json @@ -0,0 +1,13 @@ +{ + "name": "ontologyHandler", + "description": "A component for transforming preferences between ontologies", + "version": "0.3.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } +} diff --git a/gpii/node_modules/ontologyHandler/src/ontologyHandler.js b/gpii/node_modules/ontologyHandler/src/ontologyHandler.js new file mode 100644 index 000000000..ff2ed1da0 --- /dev/null +++ b/gpii/node_modules/ontologyHandler/src/ontologyHandler.js @@ -0,0 +1,637 @@ +/*! +GPII Ontology Server + +Copyright 2012 OCAD University +Copyright 2014 Raising The Floor - International + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +The ontologyHandler component is responsible for all the functionality related to ontologization, +etc., of preference sets. It is not implemented as a kettle server and hence does _not_ expose any +URLs - this aspect is taken care of in the preferences server component. +*/ + +/* eslint strict: ["error", "function"] */ + +(function () { + + "use strict"; + + var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + fs = require("fs"), + path = require("path"), + JSON5 = require("json5"); + + fluid.registerNamespace("gpii.ontologyHandler.floydWarshall"); + + // TODO: Create an incarnation of the ontologyHandler that is not tied to node.js and can be tested + // the browser - it will need to be moved to using dataSources for I/O and have an "index" file rather than + // calling fs.readdirSync(). + fluid.defaults("gpii.ontologyHandler", { + gradeNames: ["fluid.component"], + invokers: { + prefsToOntology: { + funcName: "gpii.ontologyHandler.prefsToOntology", + args: [ "{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2" ] + }, + rawPrefsToOntology: { + funcName: "gpii.ontologyHandler.rawPrefsToOntology", + args: [ "{that}", "{arguments}.0", "{arguments}.1" ] + }, + addPrefsToRawPrefs: { + funcName: "gpii.ontologyHandler.addPrefsToRawPrefs", + args: [ "{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2" ] + } + }, + listeners: { + onCreate: "gpii.ontologyHandler.loadOntologies" + }, + ontologyTransformationListFolder: "%gpii-universal/testData/ontologies/mappings", + ontologyMetadataFolder: "%gpii-universal/testData/ontologies", // directory with ontologies + members: { + ontologyTransformSpecs: {}, // will hold the actual ontology transformation definitions after read + ontologyTransformRoutes: {}, // will hold results of Floyd-Warshall for generating paths between specs + ontologyMetadata: {} // will hold the actual ontology metadata after files have been read + } + }); + + gpii.ontologyHandler.loadOntologies = function (that) { + // Check for re-occurence of GPII-1063 + fluid.log("loadOntologies for " + that.id + " path " + fluid.pathForComponent(that)); + + // Load ontology transformations + var transformsFolder = fluid.module.resolvePath(that.options.ontologyTransformationListFolder); + var ontologyTransformsList = fs.readdirSync(transformsFolder); // Read the list of available ontologyTransformations + // Load the ontologies: + gpii.ontologyHandler.loadOntologyTransformSpecs(that, transformsFolder, ontologyTransformsList); + + // Load the ontology metadata: + var metadataFolder = fluid.module.resolvePath(that.options.ontologyMetadataFolder); + gpii.ontologyHandler.loadOntologyMetadata(that, metadataFolder); + }; + + // TODO: Update to pull from a "solutions codex maleficarum" or other globally available single source of truth. + gpii.ontologyHandler.loadOntologyMetadata = function (that, metadataFolder) { + var metadataList = fs.readdirSync(metadataFolder); + fluid.each(metadataList, function (filename) { + var fullPath = metadataFolder + "/" + filename; + + // Skip any directories we encounter: + if (fs.statSync(fullPath).isDirectory()) { + return; + } + fluid.log("ontologyHandler Loading filename " + filename); + var metadata = fs.readFileSync(fullPath, "utf8"); + + // get the ontology name by removing the extension of the filename + var ontologyName = path.parse(filename).name; + that.ontologyMetadata[ontologyName] = JSON5.parse(metadata); + }); + }; + + /** + * Sanitizes the "contexts:" sections and the outer metadata section by removing entries for which + * * Arrays are empty + * * Values are undefined + * + * @param {Object} prefs - The preferences to sanitize. + * @return {Object} The sanitized settings (according to above definition). + */ + gpii.ontologyHandler.sanitizePreferencesSet = function (prefs) { + fluid.each(prefs.contexts, function (prefsSet) { + fluid.each(prefsSet, function (val, key) { + if (val === undefined || (Array.isArray(val) && val.length === 0)) { + delete prefsSet[key]; + } + }); + }); + if (prefs.metadata === undefined || (Array.isArray(prefs.metadata) && prefs.metadata.length === 0)) { + delete prefs.metadata; + } + return prefs; + }; + + /** + * The current format for naming ontology mapping files is source-target. + * + * @param {String} ontologyMapping - Mapping in source-target format. + * @return {Object} Object with source and target keys with ontology names. + */ + gpii.ontologyHandler.parseOntologyMapping = function (ontologyMapping) { + var onts = ontologyMapping.split("-"); + return { + source: onts[0], + target: onts[1] + }; + }; + + /** + * Reads the files (or URLs) that contains the ontology transformation specs and stores it in + * the ontologyTransformSpecs propery of the gpii.ontologyHandler object + * + * @param {Object} that - The gpii.ontologyHandler object. Should at minimum contain ontologySourceURL + * and ontologyTransformSpecs properties. + * @param {String} ontologyDir - Path to folder containing the ontology transformation specs. + * @param {Array} ontologyTransformsList - A list of available ontology transformations. + */ + gpii.ontologyHandler.loadOntologyTransformSpecs = function (that, ontologyDir, ontologyTransformsList) { + var transformSpecs = {}; + fluid.each(ontologyTransformsList, function (filename) { + fluid.log("ontologyHandler loading transformSpec filename " + filename); + var transformSpec = fs.readFileSync(ontologyDir + "/" + filename, "utf8"); + + // get the ontology name by removing the extension of the filename + var ontologyPair = path.parse(filename).name; + transformSpecs[ontologyPair] = JSON5.parse(transformSpec); + }); + that.ontologyTransformSpecs = transformSpecs; + // Generate Ontology Routes + var edges = []; + fluid.each(transformSpecs, function (spec, key) { + // The transforms are bidirectional so we add both directed edges + var mapping = gpii.ontologyHandler.parseOntologyMapping(key); + edges.push(mapping); + edges.push({ + source: mapping.target, + target: mapping.source + }); + }); + that.ontologyTransformRoutes = + gpii.ontologyHandler.floydWarshall.fullAlgorithmWithReconstruction(edges); + }; + + /** + * Implementation of the Floyd-Warshall algorithm to determine the shortest + * paths between a set of named edges. This is mostly generic, but currently + * used in generating a path between multiple linked ontologies. + * https://en.wikipedia.org/wiki/Floyd–Warshall_algorithm#Path_reconstruction + * Currently assumes that all edges are weight 1. + * + * @param {Array} edges - An array of arrays of length 2, consisting of the named + * edges such as [['A','B'], ['B','C']]. + * @return {Object} An object containing the necessary structures to resolve + * paths using the floydWarshallPath function. Contains 4 members: + * vertices (Object) key/value pair where key is the vertex name, and value + * is an integer to be used in the algorithm marking it as a vertex. + * For the ontology path use case, the vertex is the name of the + * ontology. + * numVertices (Integer) Convenience variable for the total number of vertices(ontologies). + * dist (2D Array) dist and next are matrices used in the algorithm, see + * documentation link above for algorithm details. + * next (2D Array) see dist above. + */ + gpii.ontologyHandler.floydWarshall.fullAlgorithmWithReconstruction = function ( edges ) { + var data = {}; + gpii.ontologyHandler.floydWarshall.algorithmInitVertices(data, edges); + gpii.ontologyHandler.floydWarshall.algorithmInitMatrices(data, edges); + gpii.ontologyHandler.floydWarshall.algorithmCore(data.dist, data.next, data.numVertices); + return data; + }; + + /** + * Internal Use only + * Function used in gpii.ontologyHandler.floydWarshall.fullAlgorithmWithReconstruction + * to initialize the mapping of vertices named by ontology to integers used in + * the algorithm. Does not a return a value, operates directly on the data structure. + * + * @param {Object} data - Data structure used in main function. + * @param {Array} edges - List of edges where each is an Object contains a directed source and + * target parameter. + */ + gpii.ontologyHandler.floydWarshall.algorithmInitVertices = function (data, edges) { + data.vertices = {}; + var record = { + hash: data.vertices, + count: 0 + }; + fluid.each(edges, function (edge) { + gpii.ontologyHandler.floydWarshall.incrementingHashAppend(record, edge.source); + gpii.ontologyHandler.floydWarshall.incrementingHashAppend(record, edge.target); + }); + data.numVertices = record.count; + }; + + /** + * Fill a hashmap as if it were a set, assigning each new unique entry a + * sequential integer value. A datastructure for each item is passed in that includes + * the hash we are incrementing and the current size. Does not return a value, operates directly on the hash. + * + * @param {Object} record - A record containing two items, "hash", which is the current hashmap + * we are filling, and count, the current size of hash. + * example: { hash: ourKeyedObject, count: 3 } + * @param {Object} item - The next value to add to the set. If this item is already + * in the set, the existing value for it will be unchanged. + */ + gpii.ontologyHandler.floydWarshall.incrementingHashAppend = function (record, item) { + if (!record.hash[item]) { + record.hash[item] = record.count; + record.count += 1; + } + }; + + /** + * Internal Use only + * Function used in gpii.ontologyHandler.floydWarshall.fullAlgorithmWithReconstruction + * to initialize the core matrices used in the algorithm, next and dist. Does not a return a value, operates + * directly on the data structure. + * + * @param {Object} data - Data structure used in main function. + * @param {Array} edges List of edges where each is an Object contains a directed source and + * target parameter. + */ + gpii.ontologyHandler.floydWarshall.algorithmInitMatrices = function (data, edges) { + // Create |V|x|V| dist and next arrays and initialize to Infinity and null + data.dist = fluid.generate(data.numVertices, function () { + return fluid.generate(data.numVertices, Infinity, false); + }, true); + data.next = fluid.generate(data.numVertices, function () { + return fluid.generate(data.numVertices, null, false); + }, true); + fluid.each(edges, function (edge) { + var u = data.vertices[edge.source], + v = data.vertices[edge.target]; + data.dist[u][v] = 1; // All weights currently 1 + data.next[u][v] = v; + }); + }; + + /** + * Internal Use only + * Function used in gpii.ontologyHandler.floydWarshall.fullAlgorithmWithReconstruction + * to perform the core algorithm of Floyd-Warshall. Does not a return a value, operates directly on the dist and + * next matrices. + * + * @param {Array} dist - See algorithm documentation. + * @param {Array} next - See algorithm documentation. + * @param {Integer} numVertices - Total number of vertices. + */ + gpii.ontologyHandler.floydWarshall.algorithmCore = function (dist, next, numVertices) { + // Standard Floyd-Warshall implementation + for (var k = 0; k < numVertices; k++) { + for (var i = 0; i < numVertices; i++) { + for (var j = 0; j < numVertices; j++) { + if (dist[i][k] + dist[k][j] < dist[i][j]) { + dist[i][j] = dist[i][k] + dist[k][j]; + next[i][j] = next[i][k]; + } + } + } + } + }; + + /** + * Uses the data generated in floydWarshall.fullAlgorithmWithReconstruction to generate the shortest path as an + * array of vertex names. + * + * @param {Object} data - The data structure returned from floydWarshall.fullAlgorithmWithReconstruction. + * @param {String} v1 - The starting vertex. + * @param {String} v2 - The ending vertex. + * @return {Array} An array of vertices for the shortest path, or an empty array if the path does not exist. + */ + gpii.ontologyHandler.floydWarshallPath = function ( data, v1, v2 ) { + if (data.vertices[v1] === undefined || data.vertices[v2] === undefined) { + return []; + } + var path = [], + u = data.vertices[v1], + v = data.vertices[v2]; + if (data.next[u][v] === null) { + return path; + } + path.push(v1); + while (u !== v) { + u = data.next[u][v]; + path.push(fluid.keyForValue(data.vertices, u)); + } + return path; + }; + + /** + * Function for getting a transformation spec translating between ontologies. + * + * If the desired transformation spec from the requested ontology to the requested ontology + * can be produced, return it. Else return undefined. + * + * @param {Object} transformSpecs - With all the available transformationSpecs. It should be keyed by: + * "-" values (where and are replaced by the keys of ontologies) and + * value should be the actual transformationSpec. + * @param {String} from - The key of the ontology to transform from. + * @param {String} to - The key of the ontology to transform to. + * @return {Object} Transformation spec for translating the from ontology defined by param 'from' to + * ontology defined by param 'from'. + */ + gpii.ontologyHandler.getTransformSpec = function (transformSpecs, from, to) { + var fromTo = from + "-" + to; + if (transformSpecs[fromTo] !== undefined) { + return transformSpecs[fromTo]; + } + + var toFrom = to + "-" + from; + return (transformSpecs[toFrom] === undefined) ? undefined : + fluid.model.transform.invertConfiguration(transformSpecs[toFrom]); + }; + + + /** + * Function to collect all paths in the given object that has a certain value and return an + * array of these paths as el-paths (strings). This is useful when transforming metadata and + * we need to translate from an el-path in one ontology into an el-path in another ontology. + * + * The function is recursive and the 'target' parameter should be an array that will be + * populated by the found paths. + * + * @param {Object} obj - To search through. + * @param {Object} search - The value to search for. Whenever this value is found, an el-path string leading to + * this location is saved to the target array. + * @param {Array} target - It will be modified by this function to contain the el-paths that contain the + * 'search' value. + * @param {Array} pathArray - Is used by the function to keep track of the current path. Should generally + * not be supplied to this function unless one wishes to have the paths that populate the + * the target being prefixed by some path. + * @return {undefined} - rather the array of collection will be built in the @target parameter. + */ + gpii.ontologyHandler.collectPaths = function (obj, search, target, pathArray) { + if (!(pathArray instanceof Array)) { + pathArray = []; + } + fluid.each(obj, function (entry, key) { + pathArray.push(key); + if (entry === search) { + var path = fluid.pathUtil.composeSegments.apply(null, pathArray); + target.push(path); + pathArray.pop(key); + return; + } + if (!fluid.isPrimitive(entry)) { + gpii.ontologyHandler.collectPaths(entry, search, target, pathArray); + } + pathArray.pop(key); + }); + }; + + /** + * Function to translate the metadata section of a preferences document - more specifically, + * the scope section of a metadata part of a preferences document. For individual values + * they will be translated as expected (i.e. by a lookup in the transformation rules). In case + * of wildcard characters, these are _only_ supported at the end of a string. A string containing + * only the wildcard ("*") will be kept as it, as this is globally applicable across + * ontologies. For other wildcards (eg. "display.*") they will be exploded when requested in a + * different ontology than they're specified in. So for example, a metadata section with a scope + * of "display.*" would first be exploded to all the preferences under the display object + * and then each of these would be transformed to the requested ontology. + * + * @param {Array} metadata - The metadata section to be transformed + * @param {Object} transformSpec - The transformation rules to be applied to the scopes + * @return {Object} The transformed metadata section + */ + gpii.ontologyHandler.transformMetadata = function (metadata, transformSpec) { + if (metadata === undefined) { + return undefined; + } + var togo = []; + + fluid.each(metadata, function (entry) { + // if metadata section is application priority, we don't want to have the + // scope ontologized. We want to keep the flat-style application ID + if (entry.type === "priority") { + togo.push(entry); + return; + } + + var tmpEntry = fluid.copy(entry), + toTransform = {}, + transformed, + target; + tmpEntry.scope = []; + + + + fluid.each(entry.scope, function (preference) { + if (preference === "*") { // ontology independent wildcard - leave as is + tmpEntry.scope.push(preference); + return; + } + if (fluid.model.transform.hasWildcard(preference)) { + // explode wildcard preference to paths, and set all of these in toTransform + var paths = fluid.model.transform.collectInputPaths(transformSpec); + var prefix = preference.substring(0, preference.indexOf("*")); + for (var i in paths) { + var path = paths[i]; + // each matching path should be added to toTransform for trannsform + if (path.indexOf(prefix) === 0) { + fluid.set(toTransform, path, true, fluid.model.escapedSetConfig); + } + } + return; + } + fluid.set(toTransform, preference, true, fluid.model.escapedSetConfig); + }); + // transform to new ontology and collect all paths with 'true' value - these equal + // the settings from the original scope array + transformed = fluid.model.transformWithRules(toTransform, transformSpec); + target = []; + gpii.ontologyHandler.collectPaths(transformed, true, target); + tmpEntry.scope = tmpEntry.scope.concat(target); + togo.push(tmpEntry); + }); + return togo; + }; + + /** + * Takes a set of preferences in some ontology X and transforms they into some ontology Y. Note + * that the preferences given should NOT be keyed by an ontology ID. + * + * @param {Component} that - An instance of gpii.ontologyHandler component. + * @param {Object} prefs - The preference set to be translated into a different ontology. The preference set + * passed should be in the ontology specified by the 'fromView' parameter and NOT keyed by + * an ontology ID. + * @param {String} fromView - The ontology in which the preference set given in the 'prefs' parameter is formatted. + * @param {String} toView - The ontology to which the preference set should be transformed. + * @return {Object} The NP transformed into the ontology requested via the 'toView' parameter. If no + * valid transformation to that ontology was found, an empty preference set is returned. + */ + gpii.ontologyHandler.prefsToOntology = function (that, prefs, fromView, toView) { + var transformSpec, + transformed = { + name: prefs.name, + contexts: prefs.contexts, + metadata: prefs.metadata + }; + + // if we're in same ontology, return as is + if (fromView === toView) { + return prefs; + } + // else get the appropriate shortest route between ontologies. Return {} if none found + var transformSpecRoute = gpii.ontologyHandler.floydWarshallPath(that.ontologyTransformRoutes, + fromView, toView); + if (transformSpecRoute.length === 0) { + return {}; + } + // We are iterating through the array of specs by creating the dashed filenames. + // e.g transformSpecRoute is ['A', 'B', 'C'] so we would look up spec files + // 'A-B' and 'B-C' + for (var i = 0; i < transformSpecRoute.length - 1; i++) { + transformSpec = gpii.ontologyHandler.getTransformSpec(that.ontologyTransformSpecs, + transformSpecRoute[i], transformSpecRoute[i + 1]); + if (transformSpec === undefined) { + return {}; + } + var prefsSetTransform = gpii.ontologyHandler.makePrefsSetTransform(transformSpec); + transformed.contexts = fluid.transform(transformed.contexts, prefsSetTransform); + // translate the preferences set's independent metadata block: + transformed.metadata = gpii.ontologyHandler.transformMetadata(transformed.metadata, transformSpec); + } + return gpii.ontologyHandler.sanitizePreferencesSet(transformed); + }; + + /** + * Creates a transform function to be used with fluid.transform to take a + * set of preferences from the "contexts:" block and translate them to another + * ontology. + * + * @param {Object} transformSpec - The transform spec used to go between ontologies. + * @return {Function} A transform function for the conversion suitable to + * use in fluid.transform. + */ + gpii.ontologyHandler.makePrefsSetTransform = function (transformSpec) { + return function (prefsSet) { + // copy values directly or transform - depending on key + return fluid.transform(prefsSet, function (val, key) { + var rule = gpii.ontologyHandler.contextBlocks[key]; + return rule === null ? val : fluid.invokeGlobalFunction(rule, [val, transformSpec]); + }); + }; + }; + + // Helper object for prefsToOntology function + gpii.ontologyHandler.contextBlocks = { + name: null, + priority: null, + preferences: "fluid.model.transformWithRules", + metadata: "gpii.ontologyHandler.transformMetadata" + }; + + /** + * Function to merge two preference sets. + * + * Takes two (non-raw) prefs sets, merges them and returns the resulting set. One cannot just + * use fluid.extend, as this would override array entries in the metadata section, so a more + * informed merge is required. The preference sets are expected to be in the same ontology + * and should not be keyed by ontology. + * + * @param {Object} master - The first preference set to be merged. + * @param {Object} secondary - The second preference set to be merged. + * @return {Object} A merged preference set (non-raw). + */ + gpii.ontologyHandler.mergePrefsSets = function (master, secondary) { + master = fluid.copy(master); + secondary = fluid.copy(secondary); + + master.name = master.name || secondary.name; + fluid.each(secondary.contexts, function (prefsSet, prefsSetId) { + var mprefsSet = master.contexts[prefsSetId]; + // if not in master, just copy over from secondary: + if (!mprefsSet) { + master.contexts[prefsSetId] = prefsSet; + } else { + // preferences and metadata should be the only things varying + fluid.extend(true, mprefsSet.preferences, prefsSet.preferences); + mprefsSet.metadata = fluid.makeArray(mprefsSet.metadata).concat(fluid.makeArray(prefsSet.metadata)); + } + }); + + // merge outer metadata block + master.metadata = fluid.makeArray(master.metadata).concat(fluid.makeArray(secondary.metadata)); + return master; + }; + + /** + * Function to take a raw preference set and transform it into a desired ontology (incl. doing + * the required transformations for each of the preferences. metadata, etc). This function does + * not have any side-effects + * + * @param {Object} that - An instance of gpii.ontologyHandler object. + * @param {Object} rawPrefs - The raw preferences as can be found on the preferences server - NOT + * expected keyed by 'preferences'. + * @param {String} toView - The ontology to translate the preference set into. + * @return {Object} Preferences in the ontology given in 'toView' parameter. This includes the preferences, + * metadata, etc., resulting from transforming all compatible preferences from different ontologies. + */ + gpii.ontologyHandler.rawPrefsToOntology = function (that, rawPrefs, toView) { + var togo = { + contexts: {} + }; + + fluid.each(rawPrefs, function (fromContent, fromView) { + var transformed = gpii.ontologyHandler.prefsToOntology(that, fromContent, fromView, toView); + togo = gpii.ontologyHandler.mergePrefsSets(togo, transformed); + }); + return gpii.ontologyHandler.sanitizePreferencesSet(togo); + }; + + /** + * Takes a preference set X in a particular ontology and removes all settings in the various + * ontologies of the raw preference set (Y) that are duplicates of the settings in the X + * preference set. The result is a raw preference set that contains the preferences of X in + * it's ontology, and no duplicates of settings of the X settings in the other ontologies. Note + * that the preferneces given will overwrite the entire entry of that ontology in the raw + * preference set provided (i.e. it wont get merged with any settings already existing in that + * ontology) + * + * In other words: if any of the preferences that are being added are present in another + * ontology, that entry should be removed from the other ontology, to avoid duplication of the + * same term in different ontologies. + * + * @param {Object} that - An instance of gpii.ontologyHandler component. + * @param {Object} prefs - A preference set in some ontology. + * @param {String} prefsView - The format of the preference set given in the 'prefs' parameter. + * @param {Object} rawPrefs - The raw preference set to be modified. + * @return {Object} A raw preference set that contains the settings of 'prefs' in the ontology + * 'prefsView', and has no settings in any of the other ontologies that can be transformed + * (or created/inferred) from the settings of 'prefs'. + */ + gpii.ontologyHandler.addPrefsToRawPrefs = function (that, prefs, prefsView, rawPrefs) { + var togo = fluid.copy(rawPrefs); + + // on an update, the consumer is responsible for sending _ALL_ relevant preferences in an + // ontology, so we can safely replace the existing preferences in that ontology + togo[prefsView] = prefs; + + // for each of the remaining ontologies in the raw preferences, check if there are any + // preferences that are duplicate and remove them if that is the case: + fluid.each(togo, function (existingPrefs, existingView) { + if (existingView === prefsView) { + return; + } + // TODO - GPII-848: The preference merging needs to be improved once the user has the + // ability to specifically 'unset' settings in the PSP/PMT - i.e. decide that they do not + // want eg. font-size defined anymore. + + // transform settings from the new preference set (prefsView) into the ontology + // we're current looking at (i.e. 'existingView') + var transformed = gpii.ontologyHandler.prefsToOntology(that, prefs, prefsView, existingView); + // dig down to each of the prefs sets: + var contexts = transformed.contexts; + fluid.each(contexts, function (prefsSet, prefsSetName) { + if (prefsSet.preferences && existingPrefs.contexts[prefsSetName] && existingPrefs.contexts[prefsSetName].preferences) { + var filteredPrefs = gpii.ontologyHandler.utils.filterPrefs( + existingPrefs.contexts[prefsSetName].preferences, + prefsSet.preferences); + + togo[existingView].contexts[prefsSetName].preferences = filteredPrefs; + } + }); + }); + // clean up prefs sets for each ontology + return fluid.transform(togo, gpii.ontologyHandler.sanitizePreferencesSet); + }; +})(); diff --git a/gpii/node_modules/ontologyHandler/src/ontologyHandlerUtilities.js b/gpii/node_modules/ontologyHandler/src/ontologyHandlerUtilities.js new file mode 100644 index 000000000..12dd82a95 --- /dev/null +++ b/gpii/node_modules/ontologyHandler/src/ontologyHandlerUtilities.js @@ -0,0 +1,127 @@ +/*! +GPII Ontology Server Utilities + +Copyright 2012 OCAD University +Copyright 2014 Raising the Floor - International + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = fluid || require("infusion"), + gpii = fluid.registerNamespace("gpii"), + $ = fluid.registerNamespace("jQuery"); + +fluid.registerNamespace("gpii.ontologyHandler.utils"); +fluid.registerNamespace("gpii.ontologyHandler.transforms"); + +/* + * Function used when PUTting preferences to the preferences server. Filtering a full preferences + * set by removing a subset from it which is needed when ensuring that no preferences are stored + * twice on the server + * + * There are two rules regarding the preference sets that this function helps to adhere to: + * (1) A preference should be stored in the ontology the user saves it in and + * (2) no preference should be stored at one time in multiple ontologies (not taking into account + * contexts). + * + * So if I do a PUT with preference set containing setting Xa in ontology A, and that setting + * already exist in the preference set stored on server as Xb, in ontology B, we need to make + * sure that Xb is deleted from the preference set. That is what this filter function does. + * When storing a preference set in eg. the flat ontology, all the prefs are first translated + * to other known ontologies, and then these settings are removed from the prefs set using + * this filter function. Once this is done, the flat preferences can safely be stored on the + * server without risking duplication. + * + * @source (Object) - the set of preferences from which to remove elements found in the _remove_ + * object + * @remove (Object) - The set of preferences to remove from the _source_ set. + * @return (Object) - The _source_ set of preferences, but with all the elements present in _remove_ + * removed + */ +gpii.ontologyHandler.utils.filter = function (source, remove) { + var modified = false, + sourceElement, + toRemove; + + var ret = fluid.freshContainer(source); + for (var ind in source) { + sourceElement = source[ind]; + + if (ind in remove) { + toRemove = remove[ind]; + + if (fluid.isPrimitive(toRemove) || $.isArray(toRemove)) { + continue; + } else { + var res = gpii.ontologyHandler.utils.filter(sourceElement, toRemove); + if (res === undefined) { + continue; + } + + ret[ind] = res; + modified = true; + } + } else { + ret[ind] = sourceElement; + modified = true; + } + } + return modified ? ret : undefined; +}; + +gpii.ontologyHandler.utils.filterPrefs = function (fullSet, toRemove) { + var cpy = fluid.copy(fullSet); + var result = gpii.ontologyHandler.utils.filter(cpy, toRemove); + return result === undefined ? {} : result; +}; + +// Transforms required for application specific settings +fluid.defaults("gpii.ontologyHandler.transforms.applicationISOToFlat", { + gradeNames: "fluid.standardTransformFunction", + invertConfiguration: "gpii.ontologyHandler.transforms.applicationISOToFlat.invert" +}); + +gpii.ontologyHandler.transforms.applicationISOToFlat = function (preferences, transformSpec, transform) { + fluid.each(preferences, function (data, appId) { + var value, key; + value = data.parameters; + key = "http://registry\\.gpii\\.net/applications/" + fluid.pathUtil.escapeSegment(appId); + fluid.model.transform.setValue(key, value, transform); + }); +}; + +gpii.ontologyHandler.transforms.applicationISOToFlat.invert = function (transformSpec) { + transformSpec.type = "gpii.ontologyHandler.transforms.applicationFlatToISO"; + return transformSpec; +}; + +fluid.defaults("gpii.ontologyHandler.transforms.applicationFlatToISO", { + gradeNames: "fluid.standardTransformFunction" +}); + +gpii.ontologyHandler.transforms.applicationFlatToISO = function (preferences, transformSpec, transform) { + var searchValue = "//registry.gpii.net/applications/"; + fluid.each(preferences, function (preference, uri) { + var appIndex = uri.indexOf(searchValue), + appId; + if (appIndex < 0) { + return; + } + appId = uri.substring(appIndex + searchValue.length); + preference = { + parameters: preference, + id: appId + }; + fluid.model.transform.setValue(fluid.model.composeSegments(transformSpec.outputPath, + fluid.pathUtil.escapeSegment(appId)), preference, transform); + }); +}; diff --git a/gpii/node_modules/matchMaker/test/html/CanopyMatchMakerTest.html b/gpii/node_modules/ontologyHandler/test/html/OntologyHandlerUtilitiesTest.html similarity index 57% rename from gpii/node_modules/matchMaker/test/html/CanopyMatchMakerTest.html rename to gpii/node_modules/ontologyHandler/test/html/OntologyHandlerUtilitiesTest.html index 03b36a4f3..5b34c5063 100644 --- a/gpii/node_modules/matchMaker/test/html/CanopyMatchMakerTest.html +++ b/gpii/node_modules/ontologyHandler/test/html/OntologyHandlerUtilitiesTest.html @@ -2,34 +2,38 @@ - GPII Canopy MatchMaker tests + GPII Ontology Handler Utilities Tests - + - - - + + + - + + + + + + + - - - + -

              GPII Canopy MatchMaker tests

              +

              GPII Ontology Handler Utilities Tests

                diff --git a/gpii/node_modules/ontologyHandler/test/js/.eslintrc.json b/gpii/node_modules/ontologyHandler/test/js/.eslintrc.json new file mode 100644 index 000000000..2d48f5bbc --- /dev/null +++ b/gpii/node_modules/ontologyHandler/test/js/.eslintrc.json @@ -0,0 +1,8 @@ +{ + "env": { + "browser": true + }, + "rules": { + "strict": ["error", "function"] + } +} diff --git a/gpii/node_modules/ontologyHandler/test/js/OntologyHandlerUtilitiesTests.js b/gpii/node_modules/ontologyHandler/test/js/OntologyHandlerUtilitiesTests.js new file mode 100644 index 000000000..9ec58c202 --- /dev/null +++ b/gpii/node_modules/ontologyHandler/test/js/OntologyHandlerUtilitiesTests.js @@ -0,0 +1,218 @@ +/*! +GPII Settings Transformer Tests + +Copyright 2012 OCAD University +Copyright 2013 Raising the Floor + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +var fluid = fluid || require("infusion"), + jqUnit = fluid.registerNamespace("jqUnit"), + gpii = fluid.registerNamespace("gpii"); + +(function () { + "use strict"; + fluid.registerNamespace("gpii.tests.ontologyHandlerUtilities"); + + var full = { + simple: { + a: { + b: 1, + c: 2 + } + }, + medium: { + a: { + a1: { + a11: 14 + }, + a2: { + a21: [ "a", "b" ] + } + } + }, + complex: { + a: 12, + b: {}, + c: { + c1: [ + { hello: "world" } + ] + } + } + }; + + var testFixtures = [ + { + description: "Very simple non-nested removal of single key", + full: full.simple, + remove: { + a: { + b: 3 + } + }, + expected: { + a: { + c: 2 + } + } + }, { + description: "simple value filtering", + full: full.medium, + remove: { + a: { + a1: { + a11: 12 + } + } + }, + expected: { + a: { + a2: { + a21: [ "a", "b" ] + } + } + } + }, { + description: "Higher hierarchy filtering", + full: full.medium, + remove: { + a: 13 + }, + expected: {} + }, { + description: "Filtering simple array value", + full: full.medium, + remove: { + a: { + a2: { + a21: [ "a" ] + } + } + }, + expected: { + a: { + a1: { + a11: 14 + } + } + } + }, { + description: "filtering complementary objects", + full: full.medium, + remove: { + b: 12 + }, + expected: full.medium + }, { + description: "removing multiple keys", + full: full.complex, + remove: { + a: 15, + c: 18 + }, + expected: { + b: {} + } + }, { + description: "Removing all keys", + full: full.complex, + remove: { + a: { + a1: "hello" + }, + b: "stupid", + c: "cat" + }, + expected: {} + }, { + description: "empty object passed as 'full' parameter", + full: {}, + remove: { + a: 15 + }, + expected: {} + }, { + description: "empty object passed as 'remove' parameter", + full: { + a: 15 + }, + remove: {}, + expected: { + a: 15 + } + } + ]; + + gpii.tests.ontologyHandlerUtilities.applicationTransformTests = [ + { + name: "ISOtoFlat", + input: { + "applications": { + "org.alsa-project": { + "id": "org.alsa-project", + "parameters": { + "masterVolume": 14 + } + } + } + }, + rules: { + "": { + "transform": { + "type": "gpii.ontologyHandler.transforms.applicationISOToFlat", + "inputPath": "applications", + "outputPath": "" + } + } + }, + expected: { + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + invertedRules: { + transform: [ + { + "type": "gpii.ontologyHandler.transforms.applicationFlatToISO", + "inputPath": "", + "outputPath": "applications" + } + ] + } + } + ]; + + gpii.tests.ontologyHandlerUtilities.runTests = function () { + jqUnit.test("ontologyServer.filter", function () { + fluid.each(testFixtures, function (test) { + var result = gpii.ontologyHandler.utils.filterPrefs(test.full, test.remove); + jqUnit.assertDeepEq(test.description, test.expected, result); + }); + }); + + jqUnit.test("Testing Application Transformats; applicationFlatToISO and applicationISOToFlat", + function () { + fluid.each(gpii.tests.ontologyHandlerUtilities.applicationTransformTests, function (test) { + var transformed = fluid.model.transform(test.input, test.rules); + jqUnit.assertDeepEq(test.name, test.expected, transformed); + + var inverseRules = fluid.model.transform.invertConfiguration(test.rules); + jqUnit.assertDeepEq("Rules inversion for " + test.name, test.invertedRules, inverseRules); + + var doubleTransformed = fluid.model.transform(transformed, inverseRules); + jqUnit.assertDeepEq(test.name, test.input, doubleTransformed); + }); + } + ); + }; + +})(); diff --git a/gpii/node_modules/ontologyHandler/test/node/OntologyHandlerTests.js b/gpii/node_modules/ontologyHandler/test/node/OntologyHandlerTests.js new file mode 100644 index 000000000..d8440cda1 --- /dev/null +++ b/gpii/node_modules/ontologyHandler/test/node/OntologyHandlerTests.js @@ -0,0 +1,1294 @@ +/* +Ontology Handler Tests + +Copyright 2014 Raising the Floor - International + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/gpii/universal/LICENSE.txt +*/ + +// TODO: Convert these to a set of browser tests + +/* eslint strict: ["error", "function"] */ + +(function () { + + "use strict"; + + var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.registerNamespace("jqUnit"), + kettle = require("kettle"); + + kettle.loadTestingSupport(); + + require("ontologyHandler"); + + fluid.defaults("gpii.ontologyHandler.tests", { + gradeNames: ["fluid.test.testEnvironment"], + components: { + tester: { + type: "gpii.ontologyHandler.tests.testCaseHolder" + } + } + }); + + var getTransformSpecTests = { + specs: { + "A-B": { + "a1": "b1", + "a2": "b2" + }, + "C-B": { + "c1": "b1", + "c2": "b2" + }, + "B-D": { + "b1": "d1", + "b2": "d2" + } + }, + tests: [ + { + from: "B", + to: "D", + expect: { + "b1": "d1", + "b2": "d2" + } + }, { + from: "B", + to: "A", + expect: { + transform: [ + { + type: "fluid.transforms.value", + inputPath: "a1", + outputPath: "b1" + }, { + type: "fluid.transforms.value", + inputPath: "a2", + outputPath: "b2" + } + ] + } + } + ] + }; + + var floydWarshallTestsLength = 19; + var floydWarshallTests = [ + { + name: "Initial 4 vertexes, 3 edges", + edges: [["A","B"], ["B","A"], ["C","B"], ["B", "C"], ["B","D"], ["D", "B"]], + tests: [ + { + desc: "Testing path from A to D through B.", + start: "A", + end: "D", + path: ["A", "B", "D"] + }, { + desc: "Testing path from D to A through B.", + start: "D", + end: "A", + path: ["D", "B", "A"] + }, { + desc: "Testing direct path from A to B.", + start: "A", + end: "B", + path: ["A", "B"] + }, { + desc: "Testing direct path from B to A.", + start: "B", + end: "A", + path: ["B", "A"] + } + ] + }, { + name: "3 vertexes, 3 edges", + edges: [["A", "B"], ["B", "A"], ["A", "C"], ["C", "A"], + ["B", "C"], ["C", "B"]], + tests: [ + { + desc: "3v3e A to B", + start: "A", + end: "B", + path: ["A", "B"] + }, { + desc: "3v3e A to C", + start: "A", + end: "C", + path: ["A", "C"] + } + ] + }, { + name: "3 vertexes, 2 edges", + edges: [["A", "B"], ["B", "A"], ["B", "C"], ["C", "B"]], + tests: [ + { + desc: "3v2e A to B", + start: "A", + end: "B", + path: ["A", "B"] + }, { + desc: "3v2e A to C", + start: "A", + end: "C", + path: ["A", "B", "C"] + } + ] + }, { + name: "Square", + edges: [["A", "B"], ["B", "A"], ["A", "C"], ["C", "A"], + ["C", "D"], ["D", "C"], ["D", "B"], ["B", "D"]], + tests: [ + { + desc: "Square A to B", + start: "A", + end: "B", + path: ["A", "B"] + }, { + desc: "Square A to D", + start: "A", + end: "D", + path: ["A", "B", "D"] + }, { + desc: "Square A to C", + start: "A", + end: "C", + path: ["A", "C"] + } + ] + }, { + name: "Tetrahedron", + edges: [["A", "B"], ["B", "A"], ["A", "C"], ["C", "A"], + ["C", "D"], ["D", "C"], ["D", "B"], ["B", "D"], + ["A", "D"], ["D", "A"], ["B", "C"], ["C", "B"]], + tests: [ + { + desc: "Tetrahedron A to B", + start: "A", + end: "B", + path: ["A", "B"] + }, { + desc: "Tetrahedron A to D", + start: "A", + end: "D", + path: ["A", "D"] + }, { + desc: "Tetrahedron A to C", + start: "A", + end: "C", + path: ["A", "C"] + } + ] + }, { + name: "4 vertexes, 2 disconnected edges", + edges: [["A", "B"], ["B", "A"], ["A", "C"], ["C", "A"], + ["C", "D"], ["D", "C"], ["A", "D"], ["D", "A"]], + tests: [ + { + desc: "4vert2dis A to B", + start: "A", + end: "B", + path: ["A", "B"] + }, { + desc: "4vert2dis A to D", + start: "A", + end: "D", + path: ["A", "D"] + }, { + desc: "4vert2dis A to C", + start: "A", + end: "C", + path: ["A", "C"] + }, { + desc: "4vert2dis B to D", + start: "B", + end: "D", + path: ["B", "A", "D"] + } + ] + }, { + name: "Disconnected test", + edges: [["A", "B"], ["B", "A"], ["C", "D"], ["D", "C"]], + tests: [ + { + desc: "Non-existant path, should be empty.", + start: "A", + end: "D", + path: [] + } + ] + } + ]; + + gpii.ontologyHandler.tests.floydWarshallAlgorithmWithReconstruction = function () { + fluid.each(floydWarshallTests, function (graph) { + var mapped_edges = []; + fluid.each(graph.edges, function (edge) { + mapped_edges.push({ + source: edge[0], + target: edge[1] + }); + }); + var data = gpii.ontologyHandler.floydWarshall.fullAlgorithmWithReconstruction(mapped_edges); + fluid.each(graph.tests, function (test) { + jqUnit.assertDeepEq(test.desc, test.path, + gpii.ontologyHandler.floydWarshallPath(data, test.start, test.end)); + }); + }); + }; + + gpii.ontologyHandler.tests.getTransformSpec = function () { + fluid.each(getTransformSpecTests.tests, function (test) { + var res = gpii.ontologyHandler.getTransformSpec(getTransformSpecTests.specs, test.from, test.to); + jqUnit.assertDeepEq("Testing getTransformSpec from " + test.from + " to " + test.to, test.expect, res); + }); + }; + + var prefsToOntologyTests = [ + { + description: "Basic transform, no inversion", + prefs: { + name: "My simple prefs #1", + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + A: "I am the a-man", + B: "I am the b-man" + } + } + } + }, + from: "capitals", + to: "minuscule", + expect: { + name: "My simple prefs #1", + contexts: { + "gpii-default": { + name: "default", + preferences: { + a: "I am the a-man", + b: "I am the b-man" + } + } + } + } + }, { + description: "Basic single transform with inversion", + prefs: { + name: "My simple prefs #2", + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + a: "I am the a-man", + b: "I am the b-man" + } + } + } + }, + from: "minuscule", + to: "capitals", + expect: { + name: "My simple prefs #2", + contexts: { + "gpii-default": { + name: "default", + preferences: { + A: "I am the a-man", + B: "I am the b-man" + } + } + } + } + }, { + description: "no matching ontology transform specs", + prefs: { + name: "My simple prefs #3", + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + cats: "meow", + dogs: "wruf" + } + } + } + }, + from: "bogus", + to: "minuscule", + expect: {} + }, { + description: "Same ontology requested", + prefs: { + name: "My simple prefs #4", + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + a: "I am the a-man", + b: "I am the b-man" + } + } + } + }, + from: "minuscule", + to: "minuscule", + expect: { + name: "My simple prefs #4", + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + a: "I am the a-man", + b: "I am the b-man" + } + } + } + } + }, { + description: "testing the metadata translations", + prefs: { + name: "My simple prefs #5", + contexts: { + "gpii-default": { + name: "default", + metadata: [{ + type: "test", + scope: [ "a", "b" ] + }], + preferences: { + a: "I am the a-man", + b: "I am the b-man" + } + } + }, + metadata: [{ + type: "outer", + scope: [ "c" ] + }] + }, + from: "minuscule", + to: "capitals", + expect: { + name: "My simple prefs #5", + contexts: { + "gpii-default": { + name: "default", + metadata: [{ + type: "test", + scope: [ "A", "B" ] + }], + preferences: { + A: "I am the a-man", + B: "I am the b-man" + } + } + }, + metadata: [{ + type: "outer", + scope: [ "C" ] + }] + } + }, { + description: "testing the metadata translations", + prefs: { + name: "My simple prefs #6", + contexts: { + "gpii-default": { + name: "default", + metadata: [{ + type: "test", + scope: [ "a", "b" ] + }], + preferences: { + a: "I am the a-man", + b: "I am the b-man" + } + } + }, + metadata: [{ + type: "outer", + scope: [ "c" ] + }] + }, + from: "minuscule", + to: "capitals", + expect: { + name: "My simple prefs #6", + contexts: { + "gpii-default": { + name: "default", + metadata: [{ + type: "test", + scope: [ "A", "B" ] + }], + preferences: { + A: "I am the a-man", + B: "I am the b-man" + } + } + }, + metadata: [{ + type: "outer", + scope: [ "C" ] + }] + } + }, { + description: "testing the metadata wildcard explosions", + prefs: { + name: "My simple prefs #7", + contexts: { + "gpii-default": { + name: "default", + metadata: [{ + type: "test", + scope: [ "x.*" ] + }], + preferences: { + x: { + a: "I was x.a" + } + } + } + }, + metadata: [{ + type: "outer", + scope: [ "y.*" ] + }] + }, + from: "deep", + to: "capitals", + expect: { + name: "My simple prefs #7", + contexts: { + "gpii-default": { + name: "default", + metadata: [{ + type: "test", + scope: [ "A", "B" ] + }], + preferences: { + A: "I was x.a" + } + } + }, + metadata: [{ + type: "outer", + scope: [ "C" ] + }] + } + }, { + description: "testing multiple contexts and non-transformable settings", + prefs: { + name: "My simple prefs #8", + contexts: { + "gpii-default": { + name: "default", + metadata: [{ + type: "test", + scope: [ "x.*" ] + }], + preferences: { + x: { + a: "I was x.a" + } + } + }, + "other": { + name: "the other", + metadata: [], + preferences: { + y: { + q: "I won't survive" + } + } + } + }, + metadata: [{ + type: "outer", + scope: [ "*" ] + }] + }, + from: "deep", + to: "capitals", + expect: { + name: "My simple prefs #8", + contexts: { + "gpii-default": { + name: "default", + metadata: [{ + type: "test", + scope: [ "A", "B" ] + }], + preferences: { + A: "I was x.a" + } + }, + "other": { + name: "the other", + preferences: {} + } + }, + metadata: [{ + type: "outer", + scope: [ "*" ] + }] + } + }, + { + description: "simple test lookup across 3 ontologies", + prefs: { + name: "My simple prefs #9", + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + A: "I am the a-man", + B: "I am the b-man" + } + } + } + }, + from: "capitals", + to: "names", + expect: { + name: "My simple prefs #9", + contexts: { + "gpii-default": { + name: "default", + preferences: { + adam: "I am the a-man", + bassman: "I am the b-man" + } + } + } + } + }, + { + description: "simple test lookup across 3 ontologies, reversed", + prefs: { + name: "My simple prefs #10", + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + adam: "I am the a-man", + bassman: "I am the b-man" + } + } + } + }, + from: "names", + to: "capitals", + expect: { + name: "My simple prefs #10", + contexts: { + "gpii-default": { + name: "default", + preferences: { + A: "I am the a-man", + B: "I am the b-man" + } + } + } + } + }, + { + description: "3-ontologies: testing multiple contexts and non-transformable settings", + prefs: { + name: "My simple prefs #11", + contexts: { + "gpii-default": { + name: "default", + metadata: [{ + type: "test", + scope: [ "x.*" ] + }], + preferences: { + x: { + a: "I was x.a" + } + } + }, + "other": { + name: "the other", + metadata: [], + preferences: { + y: { + q: "I won't survive" + } + } + } + }, + metadata: [{ + type: "outer", + scope: [ "*" ] + }] + }, + from: "deep", + to: "minuscule", + expect: { + name: "My simple prefs #11", + contexts: { + "gpii-default": { + name: "default", + metadata: [{ + type: "test", + scope: [ "a", "b" ] + }], + preferences: { + a: "I was x.a" + } + }, + "other": { + name: "the other", + preferences: {} + } + }, + metadata: [{ + type: "outer", + scope: [ "*" ] + }] + } + }, + { + description: "3-ontologies: Basic single transform with inversion", + prefs: { + name: "My simple prefs #12", + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + a: "I am the a-man", + b: "I am the b-man" + } + } + } + }, + from: "minuscule", + to: "names", + expect: { + name: "My simple prefs #12", + contexts: { + "gpii-default": { + name: "default", + preferences: { + adam: "I am the a-man", + bassman: "I am the b-man" + } + } + } + } + }, + { + description: "3-ontologies: Basic deep transform with inversion", + prefs: { + name: "My simple prefs #13", + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + a: "I am the a-man", + b: "I am the b-man" + } + } + } + }, + from: "minuscule", + to: "deep", + expect: { + name: "My simple prefs #13", + contexts: { + "gpii-default": { + name: "default", + preferences: { + x: { + a: "I am the a-man", + b: "I am the b-man" + } + } + } + } + } + } + ]; + + gpii.ontologyHandler.tests.prefsToOntology = function (ontologyHandler) { + fluid.each(prefsToOntologyTests, function (test) { + var res = ontologyHandler.prefsToOntology(test.prefs, test.from, test.to); + jqUnit.assertDeepEq("gpii.ontologyHandler.prefsToOntology: " + test.description, + test.expect, res); + }); + }; + + var rawPrefsToOntologyTests = [ + { + description: "Basic transform, no inversion", + rawPrefs: { + capitals: { + name: "From caps #1", + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + A: "I am the a-man", + B: "I am the b-man" + } + } + } + } + }, + to: "minuscule", + expect: { + name: "From caps #1", + contexts: { + "gpii-default": { + name: "default", + preferences: { + a: "I am the a-man", + b: "I am the b-man" + } + } + } + } + }, { + description: "Basic single transform with inversion", + rawPrefs: { + minuscule: { + name: "From mini #1", + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + a: "I am the a-man", + b: "I am the b-man" + } + } + } + } + }, + to: "capitals", + expect: { + name: "From mini #1", + contexts: { + "gpii-default": { + name: "default", + preferences: { + A: "I am the a-man", + B: "I am the b-man" + } + } + } + } + }, { + description: "Two transform, normal and with inversion", + rawPrefs: { + capitals: { + name: "From caps #1", + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + A: "I am the a-man" + } + } + } + }, + names: { + name: "From names #1", + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + bassman: "Bum da da dum BASS" + } + } + } + } + }, + to: "minuscule", + expect: { + name: "From caps #1", + contexts: { + "gpii-default": { + name: "default", + preferences: { + a: "I am the a-man", + b: "Bum da da dum BASS" + } + } + } + } + }, { + description: "Some of the preferences already exist in the requested ontology", + rawPrefs: { + minuscule: { + name: "From mini", + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + a: "I am the a-man" + } + } + } + }, + names: { + name: "From names", + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + bassman: "Bum da da dum BASS" + } + } + } + } + }, + to: "minuscule", + expect: { + name: "From mini", + contexts: { + "gpii-default": { + name: "default", + preferences: { + a: "I am the a-man", + b: "Bum da da dum BASS" + } + } + } + } + }, { + description: "no matching ontology transform specs", + rawPrefs: { + bogus: { + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + cats: "meow", + dogs: "wruf" + } + } + } + } + }, + to: "minuscule", + expect: { + name: undefined, + contexts: {} + } + }, { + description: "transforms from two ontologies (normal and with inversion) to third - " + + "with metadata", + rawPrefs: { + capitals: { + name: "From caps", + contexts: { + "gpii-default": { + name: "default", + metadata: [{ + type: "test", + scope: [ "A" ] + }], + preferences: { + A: "I am the a-man" + } + } + } + }, + names: { + name: "From names", + contexts: { + "gpii-default": { + name: "default", + metadata: [{ + type: "test2", + scope: [ "adam" ] + }], + preferences: { + bassman: "Bum da da dum BASS" + } + } + } + } + }, + to: "minuscule", + expect: { + name: "From caps", + contexts: { + "gpii-default": { + name: "default", + metadata: [{ + type: "test", + scope: [ "a" ] + }, { + type: "test2", + scope: [ "a" ] + }], + preferences: { + a: "I am the a-man", + b: "Bum da da dum BASS" + } + } + } + } + } + ]; + + gpii.ontologyHandler.tests.rawPrefsToOntology = function (ontologyHandler) { + fluid.each(rawPrefsToOntologyTests, function (test) { + var res = ontologyHandler.rawPrefsToOntology(test.rawPrefs, test.to); + jqUnit.assertDeepEq("gpii.ontologyHandler.rawPrefsToOntology: " + test.description, + test.expect, res); + }); + }; + + var addPrefsToRawPrefsTests = [ + { + description: "Add prefs to an ontology matching the one in the raw preference set", + prefs: { + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + A: "I am the a-man", + B: "I am the b-man" + } + } + } + }, + prefsView: "capitals", + rawPrefs: { + capitals: { + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + A: "Who am I?" + } + } + } + } + }, + expect: { + capitals: { + contexts: { + "gpii-default": { + name: "default", + preferences: { + A: "I am the a-man", + B: "I am the b-man" + } + } + } + } + } + }, { + description: "Overwriting existing preference which is stored in different view", + prefs: { + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + A: "I am the a-man" + } + } + } + }, + prefsView: "capitals", + rawPrefs: { + minuscule: { + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + a: "Who am I?" + } + } + } + } + }, + expect: { + minuscule: { + contexts: { + "gpii-default": { + name: "default", + preferences: { + } + } + } + }, + capitals: { + contexts: { + "gpii-default": { + name: "default", + preferences: { + A: "I am the a-man" + } + } + } + } + } + }, { + description: "Modifying part of another view and overwriting the same (existing) view", + prefs: { + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + A: "I am the a-man" + } + } + } + }, + prefsView: "capitals", + rawPrefs: { // this is actually theoretically an illegal raw prefs set + minuscule: { + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + a: "Who am I?", + b: "I'm still here" + } + } + } + }, + capitals: { + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + B: "I am a bee" + } + } + } + } + }, + expect: { + capitals: { + contexts: { + "gpii-default": { + name: "default", + preferences: { + A: "I am the a-man" + } + } + } + }, + minuscule: { + contexts: { + "gpii-default": { + name: "default", + preferences: { + b: "I'm still here" + } + } + } + } + } + }, { + description: "All other ontologies unaffected due to lack of transformation file", + prefs: { + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + A: "I am the a-man" + } + } + } + }, + prefsView: "capitals", + rawPrefs: { + bogus: { + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + cat: "I'm very decent" + } + } + } + }, + capitals: { + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + B: "I am a bee" + } + } + } + } + }, + expect: { + capitals: { + contexts: { + "gpii-default": { + name: "default", + preferences: { + A: "I am the a-man" + } + } + } + }, + bogus: { + contexts: { + "gpii-default": { + name: "default", + preferences: { + cat: "I'm very decent" + } + } + } + } + } + }, { + description: "All other ontologies unaffected due to lack of specific setting transformation", + prefs: { + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + C: "you later" + } + } + } + }, + prefsView: "capitals", + rawPrefs: { + minuscule: { + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + d: "secret setting" + } + } + } + }, + capitals: { + contexts: { + "gpii-default": { + name: "default", + metadata: [], + preferences: { + B: "I am a bee" + } + } + } + } + }, + expect: { + capitals: { + contexts: { + "gpii-default": { + name: "default", + preferences: { + C: "you later" + } + } + } + }, + minuscule: { + contexts: { + "gpii-default": { + name: "default", + preferences: { + d: "secret setting" + } + } + } + } + } + } + ]; + + gpii.ontologyHandler.tests.addPrefsToRawPrefs = function (ontologyHandler) { + fluid.each(addPrefsToRawPrefsTests, function (test) { + var res = ontologyHandler.addPrefsToRawPrefs(test.prefs, test.prefsView, test.rawPrefs); + jqUnit.assertDeepEq("gpii.ontologyHandler.addPrefsToRawPrefs: " + test.description, + test.expect, res); + }); + + }; + + fluid.defaults("gpii.ontologyHandler.tests.testCaseHolder", { + gradeNames: ["fluid.test.testCaseHolder"], + components: { + "ontologyHandler": { + type: "gpii.ontologyHandler", + options: { + ontologyTransformationListFolder: __dirname + "/data/ontologies/mappings/" + } + } + }, + modules: [{ + name: "OntologyHandlerTests", + tests: [{ + expect: floydWarshallTestsLength, + name: "gpii.ontologyHandler.tests.floydWarshallAlgorithmWithReconstruction tests", + func: "gpii.ontologyHandler.tests.floydWarshallAlgorithmWithReconstruction" + }, { + expect: 2, + name: "gpii.ontologyHandler.tests.getTransformSpec tests", + func: "gpii.ontologyHandler.tests.getTransformSpec" + }, { + expect: prefsToOntologyTests.length, + name: "gpii.ontologyHandler.prefsToOntology tests", + func: "gpii.ontologyHandler.tests.prefsToOntology", + args: [ "{ontologyHandler}" ] + }, { + expect: rawPrefsToOntologyTests.length, + name: "gpii.ontologyHandler.rawPrefsToOntology tests", + func: "gpii.ontologyHandler.tests.rawPrefsToOntology", + args: [ "{ontologyHandler}" ] + }, { + expect: addPrefsToRawPrefsTests.length, + name: "gpii.ontologyHandler.addPrefsToRawPrefs tests", + func: "gpii.ontologyHandler.tests.addPrefsToRawPrefs", + args: [ "{ontologyHandler}" ] + }] + }] + }); + + module.exports = kettle.test.bootstrap("gpii.ontologyHandler.tests"); + +})(); diff --git a/gpii/node_modules/ontologyHandler/test/node/data/ontologies/mappings/capitals-deep.json b/gpii/node_modules/ontologyHandler/test/node/data/ontologies/mappings/capitals-deep.json new file mode 100644 index 000000000..a4a248c6c --- /dev/null +++ b/gpii/node_modules/ontologyHandler/test/node/data/ontologies/mappings/capitals-deep.json @@ -0,0 +1,5 @@ +{ + "x.a": "A", + "x.b": "B", + "y.c": "C" +} diff --git a/gpii/node_modules/ontologyHandler/test/node/data/ontologies/mappings/capitals-minuscule.json b/gpii/node_modules/ontologyHandler/test/node/data/ontologies/mappings/capitals-minuscule.json new file mode 100644 index 000000000..65c1d7acb --- /dev/null +++ b/gpii/node_modules/ontologyHandler/test/node/data/ontologies/mappings/capitals-minuscule.json @@ -0,0 +1,5 @@ +{ + "a": "A", + "b": "B", + "c": "C" +} diff --git a/gpii/node_modules/ontologyHandler/test/node/data/ontologies/mappings/minuscule-names.json b/gpii/node_modules/ontologyHandler/test/node/data/ontologies/mappings/minuscule-names.json new file mode 100644 index 000000000..6688ef509 --- /dev/null +++ b/gpii/node_modules/ontologyHandler/test/node/data/ontologies/mappings/minuscule-names.json @@ -0,0 +1,4 @@ +{ + "adam": "a", + "bassman": "b" +} diff --git a/gpii/node_modules/ontologyServer/README.md b/gpii/node_modules/ontologyServer/README.md deleted file mode 100644 index 184b5862c..000000000 --- a/gpii/node_modules/ontologyServer/README.md +++ /dev/null @@ -1,32 +0,0 @@ -Ontology Server -=== - -A server app built to be deployed with node.js server that serves as the source for ontology related information. - ---- - -### Dependencies - -[infusion](https://github.com/fluid-project/infusion) framework. - -### Installation - -Run the following command in your newly checked out universal repository. This -will pull all dependencies that are required by universal. - - npm install - -### Run - -To run ontology Server simply type: - - [NODE_ENV={environment}] node node_modules/kettle/lib/init.js path/to/configs/folder - -- Default environment is development. -- Path to configs folder can be absolute or relative to the current user directory. - -For example: - - node node_modules/kettle/lib/init.js gpii/node_modules/ontologyServer/configs/ - node node_modules/kettle/lib/init.js /Users/{userName}/universal/gpii/node_modules/ontologyServer/configs/ - NODE_ENV=production node node_modules/kettle/lib/init.js gpii/node_modules/ontologyServer/configs/ \ No newline at end of file diff --git a/gpii/node_modules/ontologyServer/configs/base.json b/gpii/node_modules/ontologyServer/configs/base.json deleted file mode 100644 index 6a3ee2069..000000000 --- a/gpii/node_modules/ontologyServer/configs/base.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "modules": [ - "ontologyServer" - ] -} diff --git a/gpii/node_modules/ontologyServer/configs/development.json b/gpii/node_modules/ontologyServer/configs/development.json deleted file mode 100644 index e3fa55092..000000000 --- a/gpii/node_modules/ontologyServer/configs/development.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "typeName": "ontologyServer.development", - "options": { - "gradeNames": ["autoInit", "fluid.littleComponent"], - "components": { - "server": { - "type": "kettle.server", - "options": { - "logging": true, - "components": { - "ontologyServer": { - "type": "gpii.ontologyServer", - "options": { - "gradeNames": ["gpii.ontologyServer.dev"], - "ontologySourceUrl": "file://%root/../../../testData/ontologies/%name.json" - } - } - } - } - } - } - }, - "includes": [ - "./base.json" - ] -} \ No newline at end of file diff --git a/gpii/node_modules/ontologyServer/configs/kettleModuleLoader.js b/gpii/node_modules/ontologyServer/configs/kettleModuleLoader.js deleted file mode 100644 index 20c0170a7..000000000 --- a/gpii/node_modules/ontologyServer/configs/kettleModuleLoader.js +++ /dev/null @@ -1,16 +0,0 @@ -/*! -Kettle Module Loader. - -Copyright 2012-2013 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/kettle/LICENSE.txt -*/ - -// The purpose of this file is to be copied to a location for which the -// require function is needed. It allows to find node modules relative -// to that location that are otherwise non-resolvable. -module.exports = require; \ No newline at end of file diff --git a/gpii/node_modules/ontologyServer/configs/production.json b/gpii/node_modules/ontologyServer/configs/production.json deleted file mode 100644 index df7177da2..000000000 --- a/gpii/node_modules/ontologyServer/configs/production.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "typeName": "ontologyServer.production", - "options": { - "gradeNames": ["autoInit", "fluid.littleComponent"], - "components": { - "server": { - "type": "kettle.server", - "options": { - "logging": false, - "components": { - "ontologyServer": { - "type": "gpii.ontologyServer", - "options": { - "ontologySourceUrl": "http://localhost:5984/ontologies/%name" - } - } - } - } - } - } - }, - "includes": [ - "./base.json" - ] -} \ No newline at end of file diff --git a/gpii/node_modules/ontologyServer/index.js b/gpii/node_modules/ontologyServer/index.js deleted file mode 100644 index b035ed430..000000000 --- a/gpii/node_modules/ontologyServer/index.js +++ /dev/null @@ -1,6 +0,0 @@ -var fluid = require("infusion"); - -var loader = fluid.getLoader(__dirname); - -loader.require("./src/ontologyServerUtilities.js"); -loader.require("./src/ontologyServer.js"); \ No newline at end of file diff --git a/gpii/node_modules/ontologyServer/package.json b/gpii/node_modules/ontologyServer/package.json deleted file mode 100644 index 390bd49e3..000000000 --- a/gpii/node_modules/ontologyServer/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "ontologyServer", - "description": "Ontology server provides an API for accessing ontology related information.", - "version": "0.1", - "author": "GPII", - "bugs": "http://issues.fluidproject.org/browse/FLUID", - "homepage": "http://gpii.net/", - "dependencies": {}, - "licenses": [ - { - "type": "BSD-3-Clause", - "url": "http://www.opensource.org/licenses/BSD-3-Clause" - } - ], - "keywords": ["infusion", "framework", "application", "fluid", "IoC", "Inversion of Control", "MVC", "evented"], - "repository": "git://github.com/GPII/universal.git", - "main": "./index.js", - "engines": { "node" : ">=0.1.9" } -} \ No newline at end of file diff --git a/gpii/node_modules/ontologyServer/src/ontologyConvertPost.js b/gpii/node_modules/ontologyServer/src/ontologyConvertPost.js deleted file mode 100644 index ac164ec49..000000000 --- a/gpii/node_modules/ontologyServer/src/ontologyConvertPost.js +++ /dev/null @@ -1,55 +0,0 @@ -/*! -GPII Ontology Server GET Handler - -Copyright 2012 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - gpii = fluid.registerNamespace("gpii"), - kettle = fluid.require("kettle", require); - - fluid.defaults("kettle.requests.request.handler.ontologyConvertPost", { - gradeNames: ["autoInit", "fluid.littleComponent"], - invokers: { - handle: { - funcName: "gpii.ontologyConvertPost", - args: ["{that}", "{requestProxy}", "{request}.req.params.to", "{request}.req.body.preferences", "{ontologyServer}.ontologySource"], - dynamic: true - } - } - }); - - gpii.ontologyConvertPost = function (that, requestProxy, to, preferences, ontologySource) { - if (to !== "ISO24751-2") { - requestProxy.events.onError.fire({ - isError: true, - message: "Ontology server currently support only transformations to ISO24751-2 ontology." - }); - return; - } - var source = gpii.ontologyServer.parsePreferences(preferences, to); - if (source.from === source.to) { - requestProxy.events.onSuccess.fire(preferences); - return; - } - that.when(ontologySource.get({ - name: source.transformation - }), function (transformations) { - var transformed; - transformations = gpii.ontologyServer.expandTransformations(transformations); - transformed = fluid.model.transformWithRules(preferences, transformations); - return transformed; - }); - }; - -})(); \ No newline at end of file diff --git a/gpii/node_modules/ontologyServer/src/ontologyServer.js b/gpii/node_modules/ontologyServer/src/ontologyServer.js deleted file mode 100644 index 9c598bd98..000000000 --- a/gpii/node_modules/ontologyServer/src/ontologyServer.js +++ /dev/null @@ -1,101 +0,0 @@ -/*! -GPII Ontology Server - -Copyright 2012 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - path = require("path"), - gpii = fluid.registerNamespace("gpii"); - - fluid.require("kettle", require); - fluid.require("./ontologyConvertPost.js", require); - - fluid.defaults("gpii.ontologyServer", { - gradeNames: ["kettle.app", "autoInit"], - handlers: { - ontologyConvertPost: { - route: "/ontology/convert/:to", - type: "post" - } - }, - ontologySourceUrl: "", - root: path.join(__dirname, ".."), - components: { - ontologySource: { - type: "kettle.dataSource.CouchDB", - options: { - gradeNames: ["kettle.dataSource.promiseCallbackWrapper"], - url: "{gpii.ontologyServer}.options.ontologySourceUrl", - termMap: { - name: "%name" - }, - responseParser: "gpii.ontologyServer.responseParser" - } - } - } - }); - - fluid.defaults("gpii.ontologyServer.dev", { - gradeNames: ["autoInit", "fluid.littleComponent"], - components: { - ontologySource: { - type: "kettle.dataSource.URL" - } - }, - distributeOptions: { - source: "{that}.options.urlExpanderGradeNames", - target: "{that urlExpander}.options.gradeNames" - }, - urlExpanderGradeNames: ["kettle.urlExpander.development"] - }); - - gpii.ontologyServer.responseParser = function (ontology) { - return ontology.transformations; - }; - - fluid.defaults("gpii.ontologyServer.development", { - gradeNames: ["autoInit", "fluid.littleComponent"], - components: { - ontologySource: { - type: "kettle.dataSource.URL", - options: { - gradeNames: ["kettle.dataSource.promiseCallbackWrapper"], - url: "{gpii.matchMaker}.options.ontologyServerUrl" - } - } - }, - invokers: { - set: { - funcName: "gpii.ontologyServer.development.set", - args: ["{ontologySource}", "{callbackWrapper}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] - } - } - }); - - gpii.ontologyServer.development.set = function (ontologySource, callbackWrapper, directModel, model, callback) { - var preferences = model.preferences, - source = gpii.ontologyServer.parsePreferences(preferences); - if (source.from === source.to) { - callback(preferences); - return; - } - callback = callbackWrapper.wrap(callback); - ontologySource.get(null).then(function (data) { - preferences = fluid.model.transformWithRules(preferences, - gpii.ontologyServer.expandTransformations(data[source.transformation].transformations)); - callback(preferences); - }); - }; - -})(); \ No newline at end of file diff --git a/gpii/node_modules/ontologyServer/src/ontologyServerUtilities.js b/gpii/node_modules/ontologyServer/src/ontologyServerUtilities.js deleted file mode 100644 index b3b46d48d..000000000 --- a/gpii/node_modules/ontologyServer/src/ontologyServerUtilities.js +++ /dev/null @@ -1,71 +0,0 @@ -/*! -GPII Ontology Server Utilities - -Copyright 2012 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -var fluid = fluid || require("infusion"); - -var ontologyServer = fluid.registerNamespace("gpii.ontologyServer"), - transform = fluid.registerNamespace("gpii.ontologyServer.transform"); - -ontologyServer.parsePreferences = function (preferences, to) { - return fluid.find(preferences, function (preference, key) { - var togo = { - to: to || "ISO24751-2" - }; - var ontologyPrefix = "//registry.gpii.org/"; - if (key.indexOf(ontologyPrefix) < 0) { - togo.from = "ISO24751-2"; - } else { - // Currently only assume that we have 2 ontologies: - // ISO24751-2 and the flat one. - togo.from = "flat"; - } - togo.transformation = [togo.to, togo.from].join("/"); - return togo; - }); -}; - -ontologyServer.expandTransformations = function (transformations) { - var expanded = {}; - fluid.each(transformations, function (transformation, elPath) { - var source, sourceURI; - if (typeof transformation !== "string" || transformation.charAt(0) !== "{") { - expanded[elPath] = transformation; - return; - } - source = fluid.parseContextReference(transformation, 0); - sourceURI = fluid.pathUtil.escapeSegment(source.context); - expanded[elPath] = fluid.model.composeSegments(sourceURI, source.path); - }); - return expanded; -}; - -transform.application = function (preferences, expandSpec, expander) { - var searchValue = "//registry.gpii.org/applications/"; - fluid.each(preferences, function (preference, uri) { - var appIndex = uri.indexOf(searchValue), - appId; - if (appIndex < 0) { - return; - } - appId = uri.substring(appIndex + searchValue.length); - preference = { - id: appId, - parameters: preference[0].value - }; - fluid.model.transform.setValue(fluid.model.composeSegments(expandSpec.outputPath, - fluid.pathUtil.escapeSegment(appId)), preference, expander); - }); -}; - -fluid.defaults("gpii.ontologyServer.transform.application", { - gradeNames: "fluid.standardTransformFunction" -}); \ No newline at end of file diff --git a/gpii/node_modules/ontologyServer/test/html/OntologyServerTests.html b/gpii/node_modules/ontologyServer/test/html/OntologyServerTests.html deleted file mode 100644 index a95654c56..000000000 --- a/gpii/node_modules/ontologyServer/test/html/OntologyServerTests.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - GPII Ontology Server tests - - - - - - - - - - - - - - - - - -

                GPII Ontology Server tests

                -

                -
                -

                -
                  - - - - diff --git a/gpii/node_modules/ontologyServer/test/js/OntologyServerTests.js b/gpii/node_modules/ontologyServer/test/js/OntologyServerTests.js deleted file mode 100644 index 4d3f7de42..000000000 --- a/gpii/node_modules/ontologyServer/test/js/OntologyServerTests.js +++ /dev/null @@ -1,134 +0,0 @@ -/*! -GPII Canopy Ontology Server Tests - -Copyright 2012 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -// Declare dependencies -/*global require, fluid, jqUnit, gpii, start*/ - -var fluid = fluid || require("infusion"); -var gpii = fluid.registerNamespace("gpii"); - -(function () { - "use strict"; - - fluid.registerNamespace("gpii.tests.ontologyServer"); - - var preferences = { - "flat": { - "http://registry.gpii.org/common/magnification": [{value: 2.0}], - "http://registry.gpii.org/common/tracking": [{value: ["mouse"]}], - "http://registry.gpii.org/common/invertImages": [{value: true}], - "http://registry.gpii.org/applications/some.application.id": [{ - "value": { - "some setting": true - } - }], - "http://registry.gpii.org/applications/some.other.application.id": [{ - "value": { - "some setting": true - } - }] - } - }; - - var transformations = { - "ISO24751-2/flat": { - "display.screenEnhancement.fontSize": "{http://registry.gpii.org/common/fontSize}.0.value", - "display.screenEnhancement.tracking": "{http://registry.gpii.org/common/tracking}.0.value", - "display.screenEnhancement.invertImages": "{http://registry.gpii.org/common/invertImages}.0.value", - "": { - "transform": { - "type": "gpii.ontologyServer.transform.application", - "inputPath": "", - "outputPath": "applications" - } - } - } - }; - - gpii.tests.ontologyServer.runTests = function () { - jqUnit.module("Utilities"); - - jqUnit.test("ontologyServer.parsePreferences", function () { - var expected = { - "flat": { - to: "ISO24751-2", - from: "flat", - transformation: "ISO24751-2/flat" - } - }; - fluid.each(preferences, function (prefSet, type) { - var actualName = ontologyServer.parsePreferences(prefSet); - jqUnit.assertDeepEq("Ontology related information is parsed correctly", - expected[type], actualName); - }); - }); - - jqUnit.test("ontologyServer.expandTransformations", function () { - var expected = { - "ISO24751-2/flat": { - "display.screenEnhancement.fontSize": "http://registry\\.gpii\\.org/common/fontSize.0.value", - "display.screenEnhancement.tracking": "http://registry\\.gpii\\.org/common/tracking.0.value", - "display.screenEnhancement.invertImages": "http://registry\\.gpii\\.org/common/invertImages.0.value", - "": { - "transform": { - "type": "gpii.ontologyServer.transform.application", - "inputPath": "", - "outputPath": "applications" - } - } - } - }; - fluid.each(expected, function (thisExpected, key) { - jqUnit.assertDeepEq("Ontology transformation rules are expanded correctly.", thisExpected, ontologyServer.expandTransformations(transformations[key])); - }); - }); - - jqUnit.test("test application transformation rule transform", function () { - var transformations = { - "display.screenEnhancement.magnification": "http://registry\\.gpii\\.org/common/magnification.0.value", - "display.screenEnhancement.tracking": "http://registry\\.gpii\\.org/common/tracking.0.value", - "display.screenEnhancement.invertImages": "http://registry\\.gpii\\.org/common/invertImages.0.value", - "": { - "transform": { - "type": "gpii.ontologyServer.transform.application", - "inputPath": "", - "outputPath": "applications" - } - } - }; - jqUnit.assertDeepEq("Ontology transformation rules are expanded correctly.", { - applications: { - "some.application.id": { - "id": "some.application.id", - "parameters": { - "some setting": true - } - }, - "some.other.application.id": { - "id": "some.other.application.id", - "parameters": { - "some setting": true - } - } - }, - display: { - screenEnhancement: { - magnification: 2.0, - tracking: ["mouse"], - invertImages: true - } - } - }, fluid.model.transformWithRules(preferences["flat"], transformations)); - }); - }; - -}()); \ No newline at end of file diff --git a/gpii/node_modules/preferencesServer/README.md b/gpii/node_modules/preferencesServer/README.md index c3d3684b0..af6375331 100644 --- a/gpii/node_modules/preferencesServer/README.md +++ b/gpii/node_modules/preferencesServer/README.md @@ -1,33 +1,29 @@ -PreferencesServer -=== +# PreferencesServer A server app built to be deployed with node.js server that saves and returns user preferences using Apache CouchDB as backend. ---- -Calls to preferences server are made with user credentials. - -### Dependencies +## Dependencies [infusion](https://github.com/fluid-project/infusion) framework. +[kettle](https://github.com/fluid-project/kettle) framework. -### Installation +## Installation Run the following command in your newly checked out universal repository. This will pull all dependencies that are required by universal. npm install -### Run +## Run -To run preferences server simply type: +To run the preferences server type: - [NODE_ENV={environment}] node node_modules/kettle/lib/init.js path/to/configs/folder + [NODE_ENV={environment}] node node_modules/kettle/init.js path/to/configs/folder -- Default environment is development. - Path to configs folder can be absolute or relative to the current user directory. For example: node node_modules/kettle/lib/init.js gpii/node_modules/preferencesServer/configs/ node node_modules/kettle/lib/init.js /Users/{userName}/universal/gpii/node_modules/preferencesServer/configs/ - NODE_ENV=production node node_modules/kettle/lib/init.js gpii/node_modules/preferencesServer/configs/ \ No newline at end of file + NODE_ENV=gpii.preferencesServer.config.base node node_modules/kettle/lib/init.js gpii/node_modules/preferencesServer/configs/ diff --git a/gpii/node_modules/preferencesServer/configs/base.json b/gpii/node_modules/preferencesServer/configs/base.json deleted file mode 100644 index f1621e395..000000000 --- a/gpii/node_modules/preferencesServer/configs/base.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "modules": [ - "preferencesServer" - ] -} diff --git a/gpii/node_modules/preferencesServer/configs/development.json b/gpii/node_modules/preferencesServer/configs/development.json deleted file mode 100644 index ee9469bb0..000000000 --- a/gpii/node_modules/preferencesServer/configs/development.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "typeName": "preferencesServer.development", - "options": { - "gradeNames": ["autoInit", "fluid.littleComponent"], - "components": { - "server": { - "type": "kettle.server", - "options": { - "logging": true, - "components": { - "preferencesServer": { - "type": "gpii.preferencesServer", - "options": { - "gradeNames": ["gpii.preferencesServer.dev"], - "userSourceUrl": "file://%root/../../../testData/preferences/%token.json" - } - } - } - } - } - } - }, - "includes": [ - "./base.json" - ] -} diff --git a/gpii/node_modules/preferencesServer/configs/gpii.preferencesServer.config.base.json5 b/gpii/node_modules/preferencesServer/configs/gpii.preferencesServer.config.base.json5 new file mode 100644 index 000000000..910f2daac --- /dev/null +++ b/gpii/node_modules/preferencesServer/configs/gpii.preferencesServer.config.base.json5 @@ -0,0 +1,36 @@ +{ + "options": { + "gradeNames": ["fluid.component"], + "mainServerPort": 8081, + "dbServerPort": 25984, + "dataSourceConfig": { + "baseUrl": "http://localhost", + "port": "{kettle.config}.options.dbServerPort", + "dbName": "gpii" + }, + "components": { + "server": { + "type": "kettle.server", + "options": { + "port": "{kettle.config}.options.mainServerPort", + "components": { + "preferencesServer": { + "type": "gpii.preferencesServer" + } + } + } + } + }, + "distributeOptions": { + "preferencesServer.base.dataStore": { + "source": "{that}.options.dataSourceConfig", + "target": "{that preferencesServer dataStore}.options.dataSourceConfig" + }, + "preferencesServer.envPort": { + "record": "@expand:kettle.resolvers.env(GPII_PREFERENCESSERVER_LISTEN_PORT)", + "target": "{that kettle.server}.options.port" + } + } + }, + "require": "preferencesServer" +} diff --git a/gpii/node_modules/preferencesServer/configs/kettleModuleLoader.js b/gpii/node_modules/preferencesServer/configs/kettleModuleLoader.js deleted file mode 100644 index 20c0170a7..000000000 --- a/gpii/node_modules/preferencesServer/configs/kettleModuleLoader.js +++ /dev/null @@ -1,16 +0,0 @@ -/*! -Kettle Module Loader. - -Copyright 2012-2013 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/kettle/LICENSE.txt -*/ - -// The purpose of this file is to be copied to a location for which the -// require function is needed. It allows to find node modules relative -// to that location that are otherwise non-resolvable. -module.exports = require; \ No newline at end of file diff --git a/gpii/node_modules/preferencesServer/configs/production.json b/gpii/node_modules/preferencesServer/configs/production.json deleted file mode 100644 index 57a2078d0..000000000 --- a/gpii/node_modules/preferencesServer/configs/production.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "typeName": "preferencesServer.production", - "options": { - "gradeNames": ["autoInit", "fluid.littleComponent"], - "components": { - "server": { - "type": "kettle.server", - "options": { - "logging": false, - "components": { - "preferencesServer": { - "type": "gpii.preferencesServer", - "options": { - "userSourceUrl": "http://localhost:5984/user/%token" - } - } - } - } - } - } - }, - "includes": [ - "./base.json" - ] -} diff --git a/gpii/node_modules/preferencesServer/index.js b/gpii/node_modules/preferencesServer/index.js index d5df77eef..968d4b0ff 100644 --- a/gpii/node_modules/preferencesServer/index.js +++ b/gpii/node_modules/preferencesServer/index.js @@ -1,5 +1,15 @@ +"use strict"; + var fluid = require("infusion"); -var loader = fluid.getLoader(__dirname); +fluid.module.register("preferencesServer", __dirname, require); -loader.require("./src/preferencesServer.js"); \ No newline at end of file +require("gpii-db-operation"); +require("./src/preferencesGetHandler.js"); +require("./src/preferencesPostHandler.js"); +require("./src/preferencesPutHandler.js"); +require("./src/readyGetHandler.js"); +require("../flowManager/src/HealthGetHandler.js"); +require("./src/preferencesService.js"); +require("./src/preferencesServerConst.js"); +require("./src/preferencesServer.js"); diff --git a/gpii/node_modules/preferencesServer/package.json b/gpii/node_modules/preferencesServer/package.json index 68bd16354..b8acac090 100644 --- a/gpii/node_modules/preferencesServer/package.json +++ b/gpii/node_modules/preferencesServer/package.json @@ -1,19 +1,13 @@ { - "name": "preferencesServer", - "description": "Preferences server provides an API for accessing user preferences and settings.", - "version": "0.1", - "author": "GPII", - "bugs": "http://issues.fluidproject.org/browse/FLUID", - "homepage": "http://gpii.net/", - "dependencies": {}, - "licenses": [ - { - "type": "BSD-3-Clause", - "url": "http://www.opensource.org/licenses/BSD-3-Clause" - } - ], - "keywords": ["infusion", "framework", "application", "fluid", "IoC", "Inversion of Control", "MVC", "evented"], - "repository": "git://github.com/GPII/universal.git", - "main": "./index.js", - "engines": { "node" : ">=0.1.9" } -} \ No newline at end of file + "name": "preferencesServer", + "description": "Provides an API for accessing user preferences and settings", + "version": "0.3.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } +} diff --git a/gpii/node_modules/preferencesServer/src/preferencesGetHandler.js b/gpii/node_modules/preferencesServer/src/preferencesGetHandler.js new file mode 100644 index 000000000..a5105cfed --- /dev/null +++ b/gpii/node_modules/preferencesServer/src/preferencesGetHandler.js @@ -0,0 +1,42 @@ +/*! +GPII Preferences Server GET Handler + +Copyright 2014 Raising the Floor - International +Copyright 2018 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.defaults("gpii.preferencesServer.get.handler", { + gradeNames: ["kettle.request.http"], + invokers: { + handleRequest: { + funcName: "gpii.preferencesServer.get.handler.getPreferences", + args: ["{preferencesServer}", "{that}", "{that}.req.params.gpiiKey", "{that}.req.query.view"] + } + } +}); + +gpii.preferencesServer.get.handler.getPreferences = function (preferencesServer, request, gpiiKey, view) { + var prefsPromise = preferencesServer.getPreferences(gpiiKey, view); + prefsPromise.then(request.events.onSuccess.fire, function (error) { + request.events.onError.fire({ + isError: true, + message: error.message, + errorCode: error.errorCode, + statusCode: 404 + }); + }); +}; diff --git a/gpii/node_modules/preferencesServer/src/preferencesPostHandler.js b/gpii/node_modules/preferencesServer/src/preferencesPostHandler.js new file mode 100644 index 000000000..571d283bd --- /dev/null +++ b/gpii/node_modules/preferencesServer/src/preferencesPostHandler.js @@ -0,0 +1,37 @@ +/*! +GPII Preferences Server POST Handler + +Copyright 2014 Raising the Floor - International +Copyright 2018 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +require("../../solutionsRegistry/src/js/sr-validation-middleware.js"); + +fluid.defaults("gpii.preferencesServer.post.handler", { + gradeNames: ["gpii.universal.solutionsRegistry.requestHandlers.preferences"], + invokers: { + handleRequest: { + funcName: "gpii.preferencesServer.post.handler.createPreferences", + args: ["{preferencesServer}", "{that}", "{that}.req.body", "{that}.req.query.view"] + } + } +}); + +gpii.preferencesServer.post.handler.createPreferences = function (preferencesServer, request, preferences, view) { + var prefsPromise = preferencesServer.createPreferences(preferences, view); + prefsPromise.then(request.events.onSuccess.fire, request.events.onError.fire); +}; diff --git a/gpii/node_modules/preferencesServer/src/preferencesPutHandler.js b/gpii/node_modules/preferencesServer/src/preferencesPutHandler.js new file mode 100644 index 000000000..7115ad2fb --- /dev/null +++ b/gpii/node_modules/preferencesServer/src/preferencesPutHandler.js @@ -0,0 +1,60 @@ +/*! +GPII Preferences Server PUT Handler + +Copyright 2014 Raising the Floor - International +Copyright 2018 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +require("../../solutionsRegistry/src/js/sr-validation-middleware.js"); + +fluid.defaults("gpii.preferencesServer.put.handler", { + gradeNames: ["gpii.universal.solutionsRegistry.requestHandlers.preferences"], + invokers: { + handleRequest: { + funcName: "gpii.preferencesServer.post.handler.handlePreferences", + args: ["{preferencesServer}", "{preferencesService}.getPreferencesByGpiiKey", "{that}", "{that}.req.params.gpiiKey", "{that}.req.body", "{that}.req.query.view", "{that}.req.query.merge"] + } + } +}); + +gpii.preferencesServer.post.handler.handlePreferences = function (preferencesServer, getPreferencesByGpiiKeyFunc, request, gpiiKey, preferences, view, merge) { + var gpiiKeyPromise = getPreferencesByGpiiKeyFunc(gpiiKey); + // GPII-3721: if GPII key already exists, update its preferences. Otherwise, create the GPII key and its prefs safe. + gpiiKeyPromise.then(function () { + merge = (merge === "true"); // convert string to boolean + var prefsPromise = preferencesServer.updatePreferences(gpiiKey, preferences, view, merge); + prefsPromise.then(request.events.onSuccess.fire, function (error) { + gpii.preferencesServer.post.handler.fireError(request, error); + }); + }, function (error) { + if (error.errorCode === gpii.preferencesServer.errors.missingGpiiKey.errorCode) { + var prefsPromise = preferencesServer.createPreferences(preferences, view, gpiiKey); + prefsPromise.then(request.events.onSuccess.fire, request.events.onError.fire); + } else { + gpii.preferencesServer.post.handler.fireError(request, error); + } + }); +}; + +gpii.preferencesServer.post.handler.fireError = function (request, error) { + request.events.onError.fire({ + isError: true, + errorCode: error.errorCode, + message: error.message, + statusCode: 404 + }); +}; diff --git a/gpii/node_modules/preferencesServer/src/preferencesServer.js b/gpii/node_modules/preferencesServer/src/preferencesServer.js index a7a9769cc..5668d9168 100644 --- a/gpii/node_modules/preferencesServer/src/preferencesServer.js +++ b/gpii/node_modules/preferencesServer/src/preferencesServer.js @@ -1,68 +1,236 @@ /*! GPII Preferences Server -Copyright 2012 OCAD University +Copyright 2012-2016 OCAD University +Copyright 2014-2015 Raising The Floor - International +Copyright 2017-2019 OCAD University Licensed under the New BSD license. You may not use this file except in compliance with this License. +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt +https://github.com/GPII/universal/blob/master/LICENSE.txt */ -(function () { - - "use strict"; +"use strict"; - var fluid = require("infusion"), - path = require("path"), - gpii = fluid.registerNamespace("gpii"); +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); - fluid.require("kettle", require); - fluid.require("./userGet.js", require); - fluid.require("./userPost.js", require); +require("ontologyHandler"); - fluid.defaults("gpii.preferencesServer", { - gradeNames: ["kettle.app", "autoInit"], - handlers: { - userGet: { - route: "/user/:token", - type: "get" - }, - userPost: { - route: "/user/:token", - type: "post" - } +fluid.defaults("gpii.preferencesServer", { + gradeNames: ["kettle.app"], + requestHandlers: { + // The endpoint to check the readiness of the preferences server: the server is ready but does not check the database connection. + healthGet: { + route: "/health", + method: "get", + type: "gpii.health.handler" }, - root: path.join(__dirname, ".."), - userSourceUrl: "", - components: { - userSource: { - type: "kettle.dataSource.CouchDB", - options: { - gradeNames: ["kettle.dataSource.simpleCallbackWrapper"], - url: "{gpii.preferencesServer}.options.userSourceUrl", - writable: true, - termMap: { - token: "%token" - } - } - } + // The endpoint to check the liveness of the preferences server: both the server and the database connection is ok + readyGet: { + route: "/ready", + method: "get", + type: "gpii.preferencesServer.ready.handler" + }, + preferencesGet: { + route: "/preferences/:gpiiKey", + method: "get", + type: "gpii.preferencesServer.get.handler" + }, + preferencesPost: { + route: "/preferences", + method: "post", + type: "gpii.preferencesServer.post.handler" + }, + preferencesPut: { + route: "/preferences/:gpiiKey", + method: "put", + type: "gpii.preferencesServer.put.handler" } - }); - - fluid.defaults("gpii.preferencesServer.dev", { - gradeNames: ["autoInit", "fluid.littleComponent"], - components: { - userSource: { - type: "kettle.dataSource.URL" - } + }, + invokers: { + getPreferences: { + funcName: "gpii.preferencesServer.getPreferences", + args: ["{preferencesService}", "{ontologyHandler}", "{arguments}.0", "@expand:gpii.preferencesServer.getView({arguments}.1)"] + // gpiiKey, view + }, + createPreferences: { + funcName: "gpii.preferencesServer.createPreferences", + args: ["{preferencesService}", "{ontologyHandler}", "{arguments}.0", "@expand:gpii.preferencesServer.getView({arguments}.1)", "{arguments}.2"] + // preferences, view, gpiiKey + }, + updatePreferences: { + funcName: "gpii.preferencesServer.updatePreferences", + args: ["{preferencesService}", "{ontologyHandler}", "{arguments}.0", "{arguments}.1", "@expand:gpii.preferencesServer.getView({arguments}.2)", "{arguments}.3"] + // gpiiKey, preferences, view, merge + } + }, + components: { + preferencesService: { + type: "gpii.preferencesServer.preferencesService" + }, + ontologyHandler: { + type: "gpii.ontologyHandler" }, - distributeOptions: { - source: "{that}.options.urlExpanderGradeNames", - target: "{that urlExpander}.options.gradeNames" + dataStore: { + type: "gpii.dbOperation.dbDataStore" }, - urlExpanderGradeNames: ["kettle.urlExpander.development"] - }); + // persistent validation component for preferences. + preferencesValidator: { + type: "gpii.universal.solutionsRegistry.validators.preferences" + } + }, + distributeOptions: { + authorizationServiceDataStore: { + "record": "{gpii.preferencesServer}.dataStore", + "target": "{that authorizationService}.options.components.dataStore" + }, + preferencesServiceDataStore: { + "record": "{gpii.preferencesServer}.dataStore", + "target": "{that preferencesService}.options.components.dataStore" + }, + // Censor the last source of DB auth credentials for GPII-3309 + censorDataSourceBaseUrl: { + "record": { + "directModel.baseUrl": true + }, + "target": "{that dataSource}.options.censorRequestOptionsLog" + } + } +}); + +/** + * Simple function to get the 'view'. In case none is given, defaults to the "flat" ontology. + * @param {String} view - A view. + * @return {String} The value of the 'view', or "flat" if none is given. + */ +gpii.preferencesServer.getView = function (view) { + return view ? view : "flat"; +}; + +/** + * The API to get preferences. Note that this API returns the entire value of the prefsSafe.preferences + * rather than the particular prefs set that is associated with the GPII key provided as an argument. + * @param {Component} preferencesService - An instance of gpii.preferencesServer.preferencesService component. + * @param {Component} ontologyHandler - An instance of gpii.ontologyHandler component. + * @param {String} gpiiKey - An GPII key. + * @param {String} view - An view that the preferences should be transformed upon. + * @return {Promise} A promise object whose resolved value is the preferences transformed based on the view. + */ +gpii.preferencesServer.getPreferences = function (preferencesService, ontologyHandler, gpiiKey, view) { + fluid.log("gpii.preferencesServer.getPreferences called - fetching preferences for the GPII key " + gpiiKey); + var rawPrefsPromise = preferencesService.getPreferencesByGpiiKey(gpiiKey); + var mapper = function (rawPrefs) { + var prefs = rawPrefs ? ontologyHandler.rawPrefsToOntology(rawPrefs, view) : undefined; + fluid.log("Preferences Server, getPreferences(), returning preferences: ", prefs); + return prefs; + }; + return fluid.promise.map(rawPrefsPromise, mapper); +}; -})(); +/** + * The API to create preferences. This API does: + * 1. If the GPII key is not provided, first auto generat a GPII key and create it, then create a new prefs safe with the given preferences and associate this prefs safe with the new GPII key; + * 2. If the GPII key is provided but not yet exists, create it first, then create a new prefs safe with the given preferences and associate the prefs safe with the GPII key; + * 3. If the GPII key is provided and already exists, reject with an error; + * 4. If preferences is not provided, reject with an error. + * @param {Component} preferencesService - An instance of gpii.preferencesServer.preferencesService component. + * @param {Component} ontologyHandler - An instance of gpii.ontologyHandler component. + * @param {Object} preferences - A preferences object. + * @param {String} view - An view that the provided preferences is based upon. + * @param {String} [gpiiKey] - [optional] The GPII key to be created. + * @return {Promise} A promise object whose resolved value is: + * { + * gpiiKey: {String}, + * preferences: {Object} + * } + */ +gpii.preferencesServer.createPreferences = function (preferencesService, ontologyHandler, preferences, view, gpiiKey) { + fluid.log("gpii.preferencesServer.createPreferences called for creating preferences: ", preferences); + var promiseTogo = fluid.promise(); + if (!preferences) { + // preferences must be provided + fluid.log("Preferences Server, createPreferences(), creating preferences, rejected due to missing preferences"); + promiseTogo.reject(gpii.preferencesServer.errors.missingPreferences); + } else { + var payload = {}; + payload[view] = preferences; + + fluid.log("Preferences Server, createPreferences(), creating GPII key (", gpiiKey, ") and its preferences: ", payload); + var mapper = function (addPrefsResponse) { + return fluid.extend({}, addPrefsResponse, { + preferences: fluid.get(addPrefsResponse, ["preferences", view]) + }); + }; + promiseTogo = fluid.promise.map(preferencesService.createPreferences(payload, gpiiKey), mapper); + } + return promiseTogo; +}; + +/** + * The API to update an existing preferences safe to the Preferences Server. This API does: + * 1. If the GPII key exists and associates with a prefs safe, update the prefs safe with the new preferences; + * 2. If the GPII key exists but does not associates with a prefs safe, create a new prefs safe with the preferences and associate it with the GPII key; + * 3. If the GPII key does not exist in the db, reject with an error; + * 4. If the GPII key or the preferences value is not provided, reject with an error. + * @param {Component} preferencesService - An instance of gpii.preferencesServer component. + * @param {Object} ontologyHandler - An ontology handler. + * @param {String} gpiiKey - A GPII key. + * @param {Object} preferences - A preferences object. + * @param {String} view - An view that the provided preferences is based upon. + * @param {Boolean} [merge] - [optional] A flag that indicates whether to merge the incoming preferences with the existing preferences. + * @return {Promise} A promise object whose resolved value is: + * { + * gpiiKey: {String}, + * preferences: {Object} + * } + */ +gpii.preferencesServer.updatePreferences = function (preferencesService, ontologyHandler, gpiiKey, preferences, view, merge) { + fluid.log("gpii.preferencesServer.updatePreferences called - updating preferences for the GPII key " + gpiiKey); + var promiseTogo = fluid.promise(); + + if (!gpiiKey) { + // GPII key must be provided + fluid.log("Preferences Server, updatePreferences(), updating preferences, rejected due to the missing GPII key"); + promiseTogo.reject(gpii.preferencesServer.errors.missingGpiiKey); + } else if (!preferences) { + // preferences must be provided + fluid.log("Preferences Server, updatePreferences(), updating preferences, rejected due to the missing preferences"); + promiseTogo.reject(gpii.preferencesServer.errors.missingPreferences); + } else { + var gpiiKeyPromise = preferencesService.getPrefsSafeByGpiiKey(gpiiKey); + + gpiiKeyPromise.then(function (gpiiKeyRelatedData) { + var preferencesPromise = fluid.promise(); + var payload; + if (gpiiKeyRelatedData) { + // If the GPII key exists: + // 1. If an existing prefs safe already associates with the GPII key, update the prefs safe; + // 2. If no prefs safe is associated with the GPII key, create the prefs safe and associate with the GPII key. + var rawPrefs = fluid.get(gpiiKeyRelatedData, ["prefsSafe", "preferences"]); + + payload = ontologyHandler.addPrefsToRawPrefs(preferences, view, rawPrefs || {}); + var updatePromise = preferencesService.updatePreferences(payload, gpiiKey, merge); + + var mapper = function (updateResponse) { + return { + gpiiKey: updateResponse.gpiiKey, + preferences: fluid.get(updateResponse, ["preferences", view]) + }; + }; + preferencesPromise = fluid.promise.map(updatePromise, mapper); + } else { + // Reject if the GPII key does not exist + fluid.log("Preferences Server, updatePreferences(), updating preferences, rejected due to the missing GPII key: " + gpiiKey); + preferencesPromise.reject(gpii.preferencesServer.errors.missingGpiiKey); + } + fluid.promise.follow(preferencesPromise, promiseTogo); + }); + } + return promiseTogo; +}; diff --git a/gpii/node_modules/preferencesServer/src/preferencesServerConst.js b/gpii/node_modules/preferencesServer/src/preferencesServerConst.js new file mode 100644 index 000000000..33146c600 --- /dev/null +++ b/gpii/node_modules/preferencesServer/src/preferencesServerConst.js @@ -0,0 +1,43 @@ +/*! +Copyright 2017-2018 OCAD university + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ +"use strict"; + +var fluid = fluid || require("infusion"); + +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.preferencesServer"); + +// The type of preferences safes +gpii.preferencesServer.prefsSafeType = fluid.freezeRecursive({ + user: "user", + snapset: "snapset" +}); + +gpii.preferencesServer.defaultPrefsSetId = "gpii-default"; + +// Error messages +gpii.preferencesServer.errors = fluid.freezeRecursive({ + missingGpiiKey: { + errorCode: "GPII_ERR_NO_GPIIKEY", + message: "GPII key does not exist" + }, + gpiiKeyExists: { + errorCode: "GPII_ERR_GPIIKEY_EXISTS", + message: "GPII key \"%gpiiKey\" already exists" + }, + readOnly: { + errorCode: "GPII_ERR_READONLY", + message: "Can't update preferences for readOnly GPII key \"%gpiiKey\" (perhaps it is a snapset)" + }, + missingPreferences: { + errorCode: "GPII_ERR_NO_PREFS", + message: "Missing preferences" + } +}); diff --git a/gpii/node_modules/preferencesServer/src/preferencesService.js b/gpii/node_modules/preferencesServer/src/preferencesService.js new file mode 100644 index 000000000..62e1240c5 --- /dev/null +++ b/gpii/node_modules/preferencesServer/src/preferencesService.js @@ -0,0 +1,379 @@ +/*! +Copyright 2017-2018 OCAD university + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +/* eslint-env browser */ +/* eslint strict: ["error", "function"] */ + +var fluid = fluid || require("infusion"); + +(function () { + "use strict"; + + var gpii = fluid.registerNamespace("gpii"); + + fluid.defaults("gpii.preferencesServer.preferencesService", { + gradeNames: ["fluid.component"], + components: { + dataStore: { + type: "gpii.dbOperation.dataStore" + } + }, + invokers: { + getPrefsSafeByGpiiKey: "{dataStore}.findPrefsSafeByGpiiKey", + getPreferencesByGpiiKey: { + funcName: "gpii.preferencesServer.preferencesService.getPreferencesByGpiiKey", + args: ["{dataStore}.findPrefsSafeByGpiiKey", "{arguments}.0"] + // gpiiKey + }, + createPreferences: { + funcName: "gpii.preferencesServer.preferencesService.createPreferences", + args: ["{that}", "{arguments}.0", "{arguments}.1"] + // preferences, gpiiKey + }, + updatePreferences: { + funcName: "gpii.preferencesServer.preferencesService.updatePreferences", + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] + // preferences, gpiiKey, merge + }, + isLive: { + funcName: "gpii.preferencesServer.preferencesService.isLive", + args: ["{dataStore}.findAllViews"] + } + }, + events: { + onCreatePreferences: null, + onAssociatePreferences: null + }, + listeners: { + onCreatePreferences: [{ + listener: "gpii.preferencesServer.preferencesService.verifyGpiiKey", + args: ["{dataStore}.findGpiiKey", "{arguments}.0"], + namespace: "verifyGpiiKey" + }, { + listener: "gpii.preferencesServer.preferencesService.createPrefsSafe", + args: ["{dataStore}.addPrefsSafe", "{arguments}.0"], + namespace: "createPrefsSafe" + }, { + listener: "gpii.preferencesServer.preferencesService.createGpiiKey", + args: ["{dataStore}.addGpiiKey", "{arguments}.0"], + namespace: "createGpiiKey" + }], + onAssociatePreferences: [{ + listener: "gpii.preferencesServer.preferencesService.createPrefsSafe", + args: ["{dataStore}.addPrefsSafe", "{arguments}.0"], + namespace: "createPrefsSafe" + }, { + listener: "gpii.preferencesServer.preferencesService.updateGpiiKey", + args: ["{dataStore}.updateGpiiKey", "{arguments}.0"], + namespace: "updateGpiiKey" + }] + } + }); + + // APIs for Preferences and Preferences Safes + + /** + * Grant an authorization for the give GPII app installation. The gpii key will be verified before the access token is returned. + * @param {Component} findPrefsSafeByGpiiKey - An instance of gpii.preferencesServer.preferencesService. + * @param {String} gpiiKey - A GPII key. + * @return {Promise} A promise object whose resolved value is the entire preferences associated with the GPII key. + * An error will be returned if the GPII key is not found or there is no preferences defined for the GPII key. + */ + gpii.preferencesServer.preferencesService.getPreferencesByGpiiKey = function (findPrefsSafeByGpiiKey, gpiiKey) { + var promiseTogo = fluid.promise(); + var dataPromise = findPrefsSafeByGpiiKey(gpiiKey); + + dataPromise.then(function (data) { + if (data === undefined) { + fluid.log("Preferences Service, getPreferencesByGpiiKey() rejected: missing GPII key: " + gpiiKey); + promiseTogo.reject(gpii.preferencesServer.errors.missingGpiiKey); + } else { + promiseTogo.resolve(data.prefsSafe ? fluid.get(data, ["prefsSafe", "preferences"]) : undefined); + } + }, function (err) { + promiseTogo.reject(err); + }); + return promiseTogo; + }; + + /** + * The entry point of createPreferences() API. Fires the transforming promise workflow by triggering onCreatePreferences event. + * It handles: + * 1. If no GPII key is provided, auto generate and create a GPII key. Also create a new prefs safe to associate with this key; + * 2. If a GPII key is provided and does not exist, create it. Also create a new prefs safe to associate with this key; + * 3. If a GPII key is provided and already exists, reject with an error. + * @param {Component} that - An instance of gpii.preferencesServer.preferencesService component. + * @param {Object} preferences - The prefs safe data + * { + * prefsSafeType: {String} + * name: {String} + * password: {String} + * email: {String} + * preferences: {Object} // must be provided + * } + * @param {String} [gpiiKey] - [optional] The new GPII key, an UUID will be generated if it's not provided + * @return {Promise} A promise that will be resolved with the results of the transform event. + */ + gpii.preferencesServer.preferencesService.createPreferences = function (that, preferences, gpiiKey) { + return fluid.promise.fireTransformEvent(that.events.onCreatePreferences, { + gpiiKey: gpiiKey, + prefsSafeData: { + preferences: preferences + } + }); + }; + + /** + * The first step in the promise transforming chain for implementing createPreferences() API. + * Verify the provided GPII key. Reject the creation if the key already exists. + * @param {Function} findGpiiKey - The findGpiiKey() API provided by preferencesServer.preferencesService. + * @param {Object} input - The data passed on from the entry function gpii.preferencesServer.preferencesService.createPreferences(). Its structure: + * { + * gpiiKey: {String}, // Optional + * prefsSafeData: { + * prefsSafeType: {String} + * name: {String} + * password: {String} + * email: {String} + * preferences: {Object} // must be provided + * } + * } + * @return {Promise} A promise object that carries the given input value with the created prefsSafeId being added. + */ + gpii.preferencesServer.preferencesService.verifyGpiiKey = function (findGpiiKey, input) { + var promiseTogo = fluid.promise(); + var gpiiKey = input.gpiiKey; + + if (gpiiKey) { + // verify the uniqueness of the provided GPII key + var gpiiKeyPromise = findGpiiKey(gpiiKey); + gpiiKeyPromise.then(function (data) { + if (data) { + // reject if the GPII key already exists + var gpiiKeyExistsMsg = fluid.stringTemplate(gpii.preferencesServer.errors.gpiiKeyExists.message, { + gpiiKey: gpiiKey + }); + fluid.log("Preferences Service, verifyGpiiKey() rejected: ", gpiiKeyExistsMsg); + promiseTogo.reject({ + errorCode: gpii.preferencesServer.errors.gpiiKeyExists.errorCode, + message: gpiiKeyExistsMsg + }); + } else { + promiseTogo.resolve(input); + } + }, function (err) { + promiseTogo.reject(err); + }); + } else { + promiseTogo.resolve(input); + } + return promiseTogo; + }; + + /** + * Shared function in the promise transforming chain for implementing createPreferences() and updatePreferences() API. + * Create a prefsSafe record with the provided prefsSafeData. + * @param {Function} addPrefsSafe - The addPrefsSafe() API provided by gpii.oauth2.dbDataStore. + * @param {Object} input - The data passed on from the entry function gpii.preferencesServer.preferencesService.createPreferences(). Its structure: + * { + * gpiiKey: {String} or {Object}, // {String} for createPreferences(), {Object} for updatePreferences() + * prefsSafeData: { + * prefsSafeType: {String}, // optional + * name: {String}, // optional + * password: {String}, // optional + * email: {String}, // optional + * preferences: {Object} // must be provided + * } + * } + * @return {Promise} A promise object that carries the given input value with the created prefsSafeId being added. + */ + gpii.preferencesServer.preferencesService.createPrefsSafe = function (addPrefsSafe, input) { + var promiseTogo = fluid.promise(); + var prefsSafeData = input.prefsSafeData; + var prefsSafeDataAdded = { + prefsSafeType: prefsSafeData.prefsSafeType || gpii.preferencesServer.prefsSafeType.user, + name: prefsSafeData.name || null, + password: prefsSafeData.password || null, + email: prefsSafeData.email || null, + preferences: prefsSafeData.preferences + }; + + var addPrefsSafePromise = addPrefsSafe(prefsSafeDataAdded); + addPrefsSafePromise.then(function (addPrefsSafeResponse) { + fluid.log("Preferences Service, created a new prefs safe with id: ", addPrefsSafeResponse.id); + promiseTogo.resolve({ + gpiiKey: input.gpiiKey, + prefsSafeId: addPrefsSafeResponse.id, + prefsSafeDataAdded: prefsSafeDataAdded + }); + }, function (err) { + promiseTogo.reject(err); + }); + return promiseTogo; + }; + + /** + * The last step in the promise transforming chain for implementing createPreferences() API. + * Create a gpiiKey record with the provided input. + * @param {Function} addGpiiKey - The addGpiiKey() API provided by gpii.oauth2.dbDataStore. + * @param {Object} input - The data passed on from the previous step. Its structure: + * { + * gpiiKey: {String}, + * prefsSafeId: {String}, + * prefsSafeDataAdded: { + * prefsSafeType: {String} + * name: {String} + * password: {String} + * email: {String} + * preferences: {Object} // must be provided + * } + * } + * @return {Promise} A promise object whose resolved value is: + * { + * gpiiKey: {String}, + * preferences: {Object} + * } + */ + gpii.preferencesServer.preferencesService.createGpiiKey = function (addGpiiKey, input) { + var addGpiiKeyPromise = addGpiiKey({ + gpiiKey: input.gpiiKey, + prefsSafeId: input.prefsSafeId, + prefsSetId: gpii.preferencesServer.defaultPrefsSetId + }); + + var mapper = function (addGpiiKeyResponse) { + fluid.log("Preferences Service, created a new GPII key with id: ", addGpiiKeyResponse.id); + return { + gpiiKey: addGpiiKeyResponse.id, + preferences: input.prefsSafeDataAdded.preferences + }; + }; + return fluid.promise.map(addGpiiKeyPromise, mapper); + }; + + /** + * The entry point of updatePreferences() API. + * It handles: + * 1. If an existing prefs safe already associates with the GPII key, update the prefs safe; + * 2. If no prefs safe is associated with the GPII key, create the prefs safe and associate with the GPII key. + * @param {Component} that - An instance of gpii.preferencesServer.preferencesService + * @param {Object} preferences - The preferences to be updated + * @param {String} [gpiiKey] - [optional] The new GPII key, an UUID will be generated if it's not provided + * @param {Boolean} [merge] - [optional] A flag that indicates whether to merge the incoming preferences with the existing preferences + * @return {Promise} A promise object whose resolved value is: + * { + * gpiiKey: {String}, + * preferences: {Object} + * } + */ + gpii.preferencesServer.preferencesService.updatePreferences = function (that, preferences, gpiiKey, merge) { + var promiseTogo = fluid.promise(); + var gpiiKeyPromise = that.dataStore.findPrefsSafeByGpiiKey(gpiiKey); + + gpiiKeyPromise.then(function (data) { + var updatePromise = fluid.promise(); + var prefsSafeId = fluid.get(data, ["gpiiKeyDetails", "prefsSafeId"]); + + if (prefsSafeId) { + if (data.prefsSafe.prefsSafeType !== gpii.preferencesServer.prefsSafeType.user) { + fluid.log("Preferences Service, reject the update request on a snapset key: ", gpiiKey); + var readOnlyMsg = fluid.stringTemplate(gpii.preferencesServer.errors.readOnly.message, { + gpiiKey: gpiiKey + }); + updatePromise.reject({ + errorCode: gpii.preferencesServer.errors.readOnly.errorCode, + message: readOnlyMsg + }); + } else { + // The merge flag determines whether to merge the new preferences with the existing preferences + var prefsSafeData = fluid.extend(merge, {}, data.prefsSafe, {preferences: preferences}); + // Update the associated prefs safe + var updatePrefsSafePromise = that.dataStore.updatePrefsSafe(prefsSafeId, prefsSafeData); + var mapper = function () { + return { + gpiiKey: gpiiKey, + preferences: preferences + }; + }; + updatePromise = fluid.promise.map(updatePrefsSafePromise, mapper); + } + } else { + // Create a prefs safe and associate it with the GPII key + updatePromise = fluid.promise.fireTransformEvent(that.events.onAssociatePreferences, { + gpiiKey: data.gpiiKeyDetails, + prefsSafeData: { + preferences: preferences + } + }); + } + fluid.promise.follow(updatePromise, promiseTogo); + }, function (err) { + promiseTogo.reject(err); + }); + return promiseTogo; + }; + + /** + * The last step in the promise transforming chain for implementing fireTransformEvent onAssociatePreferences event. + * Update gpiiKey record with the created prefs safe id. + * @param {Function} updateGpiiKey - The updateGpiiKey() API provided by gpii.oauth2.dbDataStore + * @param {Object} input - The data passed on from the previous step. Its structure: + * { + * gpiiKey: {String}, + * prefsSafeId: {String}, + * prefsSafeDataAdded: { + * prefsSafeType: {String} + * name: {String} + * password: {String} + * email: {String} + * preferences: {Object} // must be provided + * } + * } + * @return {Promise} A promise object whose resolved value is: + * { + * gpiiKey: {String}, + * preferences: {Object} + * } + */ + gpii.preferencesServer.preferencesService.updateGpiiKey = function (updateGpiiKey, input) { + var gpiiKeyRec = fluid.copy(input.gpiiKey); + fluid.set(gpiiKeyRec, ["prefsSafeId"], input.prefsSafeId); + fluid.set(gpiiKeyRec, ["prefsSetId"], gpii.preferencesServer.defaultPrefsSetId); + var updateGpiiKeyPromise = updateGpiiKey(fluid.get(input, ["gpiiKey", "id"]), gpiiKeyRec); + + var mapper = function (response) { + fluid.log("Preferences Service, updated GPII key with id: ", response.id); + return { + gpiiKey: response.id, + preferences: response.preferences + }; + }; + return fluid.promise.map(updateGpiiKeyPromise, mapper); + }; + + /** + * Verify if the preference server is running and the database connection is + * good. + * @param {Function} findAllViews - The findAllViews() API provided by + * gpii.oauth2.dbDataStore + * @return {Promise} resolves to {isReady: true} if the database connection + * is ok. Otherwise, resolves to {isReady: false}. + */ + gpii.preferencesServer.preferencesService.isLive = function (findAllViews) { + var promiseTogo = fluid.promise(); + var viewsPromise = findAllViews(); + viewsPromise.then(function (views) { + promiseTogo.resolve({isReady: fluid.isPlainObject(views)}); + }, function () { + promiseTogo.resolve({isReady: false}); + }); + return promiseTogo; + }; +})(); diff --git a/gpii/node_modules/preferencesServer/src/readyGetHandler.js b/gpii/node_modules/preferencesServer/src/readyGetHandler.js new file mode 100644 index 000000000..4cf6dd26e --- /dev/null +++ b/gpii/node_modules/preferencesServer/src/readyGetHandler.js @@ -0,0 +1,47 @@ +/*! +GPII Preferences Server Ready Check Handler + +Copyright 2018 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.defaults("gpii.preferencesServer.ready.handler", { + gradeNames: ["kettle.request.http"], + invokers: { + handleRequest: { + funcName: "gpii.preferencesServer.ready.handler.getPreferences", + args: ["{preferencesServer}", "{that}"] + } + } +}); + +gpii.preferencesServer.ready.handler.reportError = function (request) { + request.events.onError.fire({ + isError: true, + message: "Problem with the database connection", + statusCode: 503 + }); +}; + +gpii.preferencesServer.ready.handler.getPreferences = function (preferencesServer, request) { + var isLivePromise = preferencesServer.preferencesService.isLive(); + isLivePromise.then(function (value) { // onResolve + if (value.isReady) { + request.events.onSuccess.fire(value); + } else { + gpii.preferencesServer.ready.handler.reportError(request); + } + }, function () { // onReject + gpii.preferencesServer.ready.handler.reportError(request); + }); +}; diff --git a/gpii/node_modules/preferencesServer/src/userGet.js b/gpii/node_modules/preferencesServer/src/userGet.js deleted file mode 100644 index d98a5d406..000000000 --- a/gpii/node_modules/preferencesServer/src/userGet.js +++ /dev/null @@ -1,38 +0,0 @@ -/*! -GPII Preferences Server GET Handler - -Copyright 2012 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - gpii = fluid.registerNamespace("gpii"), - kettle = fluid.require("kettle", require); - - fluid.defaults("kettle.requests.request.handler.userGet", { - gradeNames: ["autoInit", "fluid.littleComponent"], - invokers: { - handle: { - funcName: "gpii.handleUserGet", - args: ["{requestProxy}", "{request}.req.params.token", "{preferencesServer}.userSource"], - dynamic: true - } - } - }); - - gpii.handleUserGet = function (requestProxy, token, userSource) { - userSource.get({ - token: token - }, requestProxy.events.onSuccess.fire); - }; - -})(); diff --git a/gpii/node_modules/preferencesServer/src/userPost.js b/gpii/node_modules/preferencesServer/src/userPost.js deleted file mode 100644 index 942dd8a1f..000000000 --- a/gpii/node_modules/preferencesServer/src/userPost.js +++ /dev/null @@ -1,39 +0,0 @@ -/*! -GPII Preferences Server POST Handler - -Copyright 2012 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - uuid = require("node-uuid"), - gpii = fluid.registerNamespace("gpii"), - kettle = fluid.require("kettle", require); - - fluid.defaults("kettle.requests.request.handler.userPost", { - gradeNames: ["autoInit", "fluid.littleComponent"], - invokers: { - handle: { - funcName: "gpii.handleUserPost", - args: ["{requestProxy}", "{request}.req", "{preferencesServer}.userSource"], - dynamic: true - } - } - }); - - gpii.handleUserPost = function (requestProxy, req, userSource) { - userSource.set({ - token: req.params.token || uuid.v4() - }, req.body, requestProxy.events.onSuccess.fire); - }; - -})(); diff --git a/gpii/node_modules/preferencesServer/test/configs/gpii.tests.preferencesServer.config.json b/gpii/node_modules/preferencesServer/test/configs/gpii.tests.preferencesServer.config.json new file mode 100644 index 000000000..680ad844e --- /dev/null +++ b/gpii/node_modules/preferencesServer/test/configs/gpii.tests.preferencesServer.config.json @@ -0,0 +1,15 @@ +{ + "type": "gpii.tests.preferencesServer.config", + "options": { + "gradeNames": ["fluid.component"], + "distributeOptions": { + "preferencesServerType": { + "record": "gpii.tests.preferencesServer", + "target": "{that preferencesServer}.type" + } + } + }, + "mergeConfigs": [ + "%preferencesServer/configs/gpii.preferencesServer.config.base.json5" + ] +} diff --git a/gpii/node_modules/preferencesServer/test/configs/gpii.tests.preferencesServer.config.withoutDb.json b/gpii/node_modules/preferencesServer/test/configs/gpii.tests.preferencesServer.config.withoutDb.json new file mode 100644 index 000000000..585023c12 --- /dev/null +++ b/gpii/node_modules/preferencesServer/test/configs/gpii.tests.preferencesServer.config.withoutDb.json @@ -0,0 +1,9 @@ +{ + "type": "gpii.tests.preferencesServer.config.withoutDb", + "options": { + "dbServerPort": 35984 + }, + "mergeConfigs": [ + "%preferencesServer/test/configs/gpii.tests.preferencesServer.config.json" + ] +} diff --git a/gpii/node_modules/preferencesServer/test/data/gpiiKeys-preferenceServiceTests.json b/gpii/node_modules/preferencesServer/test/data/gpiiKeys-preferenceServiceTests.json new file mode 100644 index 000000000..0160df644 --- /dev/null +++ b/gpii/node_modules/preferencesServer/test/data/gpiiKeys-preferenceServiceTests.json @@ -0,0 +1,35 @@ +[ + { + "_id": "alice_gpii_key", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": "prefsSafe-1", + "prefsSetId": "gpii-default", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + }, + { + "_id": "bob_gpii_key", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": null, + "prefsSetId": null, + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + }, + { + "_id": "snapset1", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": "prefsSafe-snapset1", + "prefsSetId": "gpii-default", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + } +] diff --git a/gpii/node_modules/preferencesServer/test/data/gpiiKeys.json b/gpii/node_modules/preferencesServer/test/data/gpiiKeys.json new file mode 100644 index 000000000..99182ad45 --- /dev/null +++ b/gpii/node_modules/preferencesServer/test/data/gpiiKeys.json @@ -0,0 +1,112 @@ +[ + { + "_id": "alice_gpii_key", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": "prefsSafe-1", + "prefsSetId": "gpii-default", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + }, + { + "_id": "bob_gpii_key", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": null, + "prefsSetId": null, + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + }, + { + "_id": "np_ISO24751Only_singleContext", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": "prefsSafe-1", + "prefsSetId": "gpii-default", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-12-14T19:55:11.640Z", + "timestampUpdated": null + }, + { + "_id": "np_ISO24751Only_singleContext_wildcardMetadata", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": "prefsSafe-2", + "prefsSetId": "gpii-default", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-12-14T19:55:11.640Z", + "timestampUpdated": null + }, + { + "_id": "np_flatOnly_multiContext", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": "prefsSafe-3", + "prefsSetId": "gpii-default", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-12-14T19:55:11.641Z", + "timestampUpdated": null + }, + { + "_id": "np_flatOnly_singleContext", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": "prefsSafe-4", + "prefsSetId": "gpii-default", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-12-14T19:55:11.641Z", + "timestampUpdated": null + }, + { + "_id": "np_mixed_multiContext", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": "prefsSafe-5", + "prefsSetId": "gpii-default", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-12-14T19:55:11.641Z", + "timestampUpdated": null + }, + { + "_id": "np_mixed_singleContext", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": "prefsSafe-6", + "prefsSetId": "gpii-default", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-12-14T19:55:11.641Z", + "timestampUpdated": null + }, + { + "_id": "np_tiny", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": "prefsSafe-7", + "prefsSetId": "gpii-default", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-12-14T19:55:11.641Z", + "timestampUpdated": null + }, + { + "_id": "undefined_preferences", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": null, + "prefsSetId": null, + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-12-14T19:55:11.641Z", + "timestampUpdated": null + } +] diff --git a/gpii/node_modules/preferencesServer/test/data/prefsSafes-preferencesServiceTests.json b/gpii/node_modules/preferencesServer/test/data/prefsSafes-preferencesServiceTests.json new file mode 100644 index 000000000..6ac1fe892 --- /dev/null +++ b/gpii/node_modules/preferencesServer/test/data/prefsSafes-preferencesServiceTests.json @@ -0,0 +1,51 @@ +[ + { + "_id": "prefsSafe-1", + "type": "prefsSafe", + "schemaVersion": "0.2", + "prefsSafeType": "user", + "name": null, + "password": null, + "email": null, + "preferences": { + "flat": { + "name": "Default context", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/matchMakerType": "ruleBased", + "http://registry.gpii.net/common/fontSize": 24, + "http://registry.gpii.net/common/foregroundColor": "white" + } + } + } + } + }, + "timestampCreated": "2017-12-01T18:43:32.889Z", + "timestampUpdated": null + }, + { + "_id": "prefsSafe-snapset1", + "type": "prefsSafe", + "schemaVersion": "0.2", + "prefsSafeType": "snapset", + "name": null, + "password": null, + "email": null, + "preferences": { + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/foregroundColor": "white" + } + } + } + } + }, + "timestampCreated": "2017-12-01T18:43:32.889Z", + "timestampUpdated": null + } +] diff --git a/gpii/node_modules/preferencesServer/test/data/prefsSafes.json b/gpii/node_modules/preferencesServer/test/data/prefsSafes.json new file mode 100644 index 000000000..04f7efcfa --- /dev/null +++ b/gpii/node_modules/preferencesServer/test/data/prefsSafes.json @@ -0,0 +1,403 @@ +[ + { + "_id": "prefsSafe-1", + "type": "prefsSafe", + "schemaVersion": "0.2", + "prefsSafeType": "user", + "name": null, + "password": null, + "email": null, + "preferences": { + "ISO24751": { + "name": "ISO24751 set", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "control": { + "onscreenKeyboard": true, + "mouseEmulation": { + "-provisional-initDelay": 120, + "cursorSpeed": 0.850, + "-provisional-mouseEmulation/enabled": true + } + }, + "unknown": true, + "applications": { + "org.alsa-project": { + "id": "org.alsa-project", + "parameters": { + "masterVolume": 14 + } + } + } + }, + "metadata": [ + { + "type": "provenance", + "scope": ["applications.org\\.alsa-project.parameters"], + "source": "snapshotter" + } + ] + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ "display.screenEnhancement.fontSize"] + } + ] + } + }, + "timestampCreated": "2017-12-14T19:55:11.640Z", + "timestampUpdated": null + }, + { + "_id": "prefsSafe-2", + "type": "prefsSafe", + "schemaVersion": "0.2", + "prefsSafeType": "user", + "name": null, + "password": null, + "email": null, + "preferences": { + "ISO24751": { + "name": "ISO24751 set", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "control": { + "onscreenKeyboard": true, + "mouseEmulation": { + "-provisional-initDelay": 120, + "cursorSpeed": 0.850, + "-provisional-mouseEmulation/enabled": true + } + }, + "unknown": true, + "applications": { + "org.alsa-project": { + "id": "org.alsa-project", + "parameters": { + "masterVolume": 14 + } + } + } + }, + "metadata": [ + { + "type": "provenance", + "scope": ["control.*"], + "source": "snapshotter" + } + ] + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ "display.screenEnhancement.*"] + } + ] + } + }, + "timestampCreated": "2017-12-14T19:55:11.640Z", + "timestampUpdated": null + }, + { + "_id": "prefsSafe-3", + "type": "prefsSafe", + "schemaVersion": "0.2", + "prefsSafeType": "user", + "name": null, + "password": null, + "email": null, + "preferences": { + "flat": { + "name": "Multiple Contexts", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/unknown": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [ + { + "type": "provenance", + "scope": ["http://registry\\.gpii\\.net/common/fontSize"], + "source": "snapshotter" + } + ] + }, + "gpii-xyz1234": { + "name": "Nighttime at home", + "preferences": { + "http://registry.gpii.net/common/cursorSpeed": 1 + }, + "metadata": [], + "priority": 100 + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ "http://registry\\.gpii\\.net/common/fontSize"] + } + ] + } + }, + "timestampCreated": "2017-12-14T19:55:11.641Z", + "timestampUpdated": null + }, + { + "_id": "prefsSafe-4", + "type": "prefsSafe", + "schemaVersion": "0.2", + "prefsSafeType": "user", + "name": null, + "password": null, + "email": null, + "preferences": { + "flat": { + "name": "Single set", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true , + "http://registry.gpii.net/common/initDelay": 120 , + "http://registry.gpii.net/common/cursorSpeed": 0.850 , + "http://registry.gpii.net/common/mouseEmulation/enabled": true , + "http://registry.gpii.net/common/unknown": true , + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [ + { + "type": "provenance", + "scope": ["http://registry\\.gpii\\.net/applications/org\\.alsa-project"], + "source": "snapshotter" + }, { + "type": "required", + "scope": ["*"] + } + ] + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ "http://registry\\.gpii\\.net/common/fontSize"] + } + ] + } + }, + "timestampCreated": "2017-12-14T19:55:11.641Z", + "timestampUpdated": null + }, + { + "_id": "prefsSafe-5", + "type": "prefsSafe", + "schemaVersion": "0.2", + "prefsSafeType": "user", + "name": null, + "password": null, + "email": null, + "preferences": { + "flat": { + "name": "lotta stuff", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/flatOnly": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [{ + "type": "provenance", + "scope": ["http://registry\\.gpii\\.net/common/fontSize"], + "source": "snapshotter" + }] + }, + "gpii-xyz1234": { + "name": "Nighttime at home", + "preferences": { + "http://registry.gpii.net/common/cursorSpeed": 1 + }, + "metadata": [], + "priority": 100 + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ "http://registry\\.gpii\\.net/common/fontSize"] + } + ] + }, + "ISO24751": { + "name": "lotta stuff", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "control": { + "onscreenKeyboard": true + }, + "applications": { + "org.gnome.desktop.a11y.magnifier": { + "id": "org.gnome.desktop.a11y.magnifier", + "parameters": { + "show-cross-hairs": true, + "lens-mode": false + } + } + } + }, + "metadata": [] + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ "display.screenEnhancement.magnification"] + } + ] + }, + "bogus": { + "contexts": { + "gpii-default": { + "preferences": { + "foo": "bar" + } + } + } + } + }, + "timestampCreated": "2017-12-14T19:55:11.641Z", + "timestampUpdated": null + }, + { + "_id": "prefsSafe-6", + "type": "prefsSafe", + "schemaVersion": "0.2", + "prefsSafeType": "user", + "name": null, + "password": null, + "email": null, + "preferences": { + "flat": { + "name": "lotta stuff", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true , + "http://registry.gpii.net/common/initDelay": 120 , + "http://registry.gpii.net/common/cursorSpeed": 0.850 , + "http://registry.gpii.net/common/mouseEmulation/enabled": true , + "http://registry.gpii.net/common/flatOnly": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [{ + "type": "required", + "scope": ["*"] + }] + } + } + }, + "ISO24751": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "display": { + "screenEnhancement": { + "fontSize": 24 + } + } + }, + "metadata": [{ + "type": "provenance", + "scope": ["display.screenEnhancement.fontSize"], + "source": "snapshotter" + }] + } + } + }, + "bogus": { + "contexts": { + "gpii-default": { + "preferences": { + "foo": "bar" + } + } + } + } + }, + "timestampCreated": "2017-12-14T19:55:11.641Z", + "timestampUpdated": null + }, + { + "_id": "prefsSafe-7", + "type": "prefsSafe", + "schemaVersion": "0.2", + "prefsSafeType": "user", + "name": null, + "password": null, + "email": null, + "preferences": { + "flat": { + "name": "bit of stuff", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true + }, + "metadata": [] + } + }, + "metadata": [] + } + }, + "timestampCreated": "2017-12-14T19:55:11.641Z", + "timestampUpdated": null + }, + { + "_id": "prefsSafe-snapset1", + "type": "prefsSafe", + "schemaVersion": "0.2", + "prefsSafeType": "snapset", + "name": null, + "password": null, + "email": null, + "preferences": { + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/foregroundColor": "white" + } + } + } + } + }, + "timestampCreated": "2017-12-01T18:43:32.889Z", + "timestampUpdated": null + } +] diff --git a/gpii/node_modules/preferencesServer/test/preferencesServerTests.js b/gpii/node_modules/preferencesServer/test/preferencesServerTests.js new file mode 100644 index 000000000..18fbc3307 --- /dev/null +++ b/gpii/node_modules/preferencesServer/test/preferencesServerTests.js @@ -0,0 +1,1718 @@ +/** +GPII Preferences Server Tests + +Copyright 2014 Raising the Floor - International + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + jqUnit = fluid.require("node-jqunit", require, "jqUnit"), + gpii = fluid.registerNamespace("gpii"); + +require("./preferencesServerTestsUtils.js"); + +fluid.require("%gpii-universal"); +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.preferencesServer"); + +gpii.tests.preferencesServer.config = { + configName: "gpii.tests.preferencesServer.config", + configPath: "%preferencesServer/test/configs" +}; + +gpii.tests.preferencesServer.testResponse = function (response, expected, receivedStatusCode, expectedStatusCode) { + var retrievedData = JSON.parse(response); + jqUnit.assertEquals("Response status code is expected", expectedStatusCode, receivedStatusCode); + jqUnit.assertLeftHand("Response is correct", expected, retrievedData); +}; + +// TESTING preferencesServer GET functionality +fluid.registerNamespace("gpii.tests.preferencesServer.get"); + +// GET success cases +gpii.tests.preferencesServer.get.buildTestDef = function (fixture) { + var expectedStatusCode = fixture.expectedStatusCode || 200; + return { + name: fixture.name, + expect: 2, + components: { + getRequest: { + type: "kettle.test.request.http", + options: { + path: "/preferences/" + fixture.url, + method: "GET", + port: 8081, + termMap: { + gpiiKey: fixture.gpiiKey + } + } + } + }, + sequence: [{ + func: "{getRequest}.send" + }, { + event: "{getRequest}.events.onComplete", + listener: "gpii.tests.preferencesServer.testResponse", + args: ["{arguments}.0", fixture.expected, "{getRequest}.nativeResponse.statusCode", expectedStatusCode] + }] + }; +}; + +gpii.tests.preferencesServer.get.successFixtures = [ + { + name: "GET: Basic retrieval of simple preference set in same ontology (no parameter)", + gpiiKey: "np_flatOnly_singleContext", + url: "%gpiiKey", + expected: { + "name": "Single set", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/unknown": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [{ + "type": "provenance", + "scope": ["http://registry\\.gpii\\.net/applications/org\\.alsa-project"], + "source": "snapshotter" + }, { + "type": "required", + "scope": ["*"] + }] + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ "http://registry\\.gpii\\.net/common/fontSize"] + } + ] + } + }, { + name: "GET: Basic retrieval of simple preference set in same ontology (with parameter)", + gpiiKey: "np_flatOnly_singleContext", + url: "%gpiiKey?view=flat", + expected: { + "name": "Single set", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/unknown": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [{ + "type": "provenance", + "scope": ["http://registry\\.gpii\\.net/applications/org\\.alsa-project"], + "source": "snapshotter" + }, { + "type": "required", + "scope": ["*"] + }] + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ "http://registry\\.gpii\\.net/common/fontSize"] + } + ] + } + }, { + name: "GET: Basic retrieval of simple preference set in different ontology (with parameter)", + gpiiKey: "np_ISO24751Only_singleContext", + url: "%gpiiKey?view=flat", + expected: { + "name": "ISO24751 set", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [{ + "type": "provenance", + "scope": ["http://registry\\.gpii\\.net/applications/org\\.alsa-project"], + "source": "snapshotter" + }] + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ "http://registry\\.gpii\\.net/common/fontSize"] + } + ] + } + }, { + name: "GET: Retrieval of simple preference set in different ontology plus non-translateable preference", + gpiiKey: "np_flatOnly_singleContext", + url: "%gpiiKey?view=ISO24751", + expected: { + "name": "Single set", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "control": { + "onscreenKeyboard": true, + "mouseEmulation": { + "cursorSpeed": 0.85, + "-provisional-initDelay": 120, + "-provisional-mouseEmulation/enabled": true + } + }, + "applications": { + "org.alsa-project": { + "id": "org.alsa-project", + "parameters": { + "masterVolume": 14 + } + } + } + }, + "metadata": [ + { + "type": "provenance", + "scope": ["applications.org\\.alsa-project.parameters"], + "source": "snapshotter" + }, { + "type": "required", + "scope": ["*"] + } + ] + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ "display.screenEnhancement.fontSize"] + } + ] + } + }, { + name: "GET: Retrieval of single ontology, multi-context preference set in same ontology", + gpiiKey: "np_flatOnly_multiContext", + url: "%gpiiKey?view=flat", + expected: { + "name": "Multiple Contexts", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/unknown": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [ + { + "type": "provenance", + "scope": ["http://registry\\.gpii\\.net/common/fontSize"], + "source": "snapshotter" + } + ] + }, + "gpii-xyz1234": { + "name": "Nighttime at home", + "preferences": { + "http://registry.gpii.net/common/cursorSpeed": 1 + }, + "priority": 100 + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ "http://registry\\.gpii\\.net/common/fontSize"] + } + ] + } + }, { + name: "GET: Retrieval of single ontology, multi-context preference set in a different ontology", + gpiiKey: "np_flatOnly_multiContext", + url: "%gpiiKey?view=ISO24751", + expected: { + "name": "Multiple Contexts", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "control": { + "mouseEmulation": { + "-provisional-mouseEmulation/enabled": true + } + }, + "applications": { + "org.alsa-project": { + "id": "org.alsa-project", + "parameters": { + "masterVolume": 14 + } + } + } + }, + "metadata": [ + { + "type": "provenance", + "scope": ["display.screenEnhancement.fontSize"], + "source": "snapshotter" + } + ] + }, + "gpii-xyz1234": { + "name": "Nighttime at home", + "preferences": { + "control": { + "mouseEmulation": { + "cursorSpeed": 1 + } + } + }, + "priority": 100 + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ "display.screenEnhancement.fontSize"] + } + ] + } + }, { + name: "GET: Retrieval of mixed preference set in flat (from multiple ontologies, single context)", + gpiiKey: "np_mixed_singleContext", + url: "%gpiiKey?view=flat", + expected: { + "name": "lotta stuff", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/flatOnly": true, + "http://registry.gpii.net/common/fontSize": 24, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [ + { + "type": "required", + "scope": ["*"] + }, { + "type": "provenance", + "scope": ["http://registry\\.gpii\\.net/common/fontSize"], + "source": "snapshotter" + } + ] + } + } + } + }, { + name: "GET: Retrieval of mixed preference set in flat (from multiple ontologies, multi context)", + gpiiKey: "np_mixed_multiContext", + url: "%gpiiKey?view=flat", + expected: { + "name": "lotta stuff", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/flatOnly": true, + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + }, + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier": { + "show-cross-hairs": true, + "lens-mode": false + } + }, + "metadata": [ + { + "type": "provenance", + "scope": ["http://registry\\.gpii\\.net/common/fontSize"], + "source": "snapshotter" + } + ] + }, + "gpii-xyz1234": { + "name": "Nighttime at home", + "preferences": { + "http://registry.gpii.net/common/cursorSpeed": 1 + }, + "priority": 100 + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ "http://registry\\.gpii\\.net/common/fontSize"] + }, { + "type": "doNotShare", + "scope": [ "http://registry\\.gpii\\.net/common/magnification"] + } + ] + } + }, { + name: "GET: Retrieval of mixed preference set in ISO24751 (from multiple ontologies, multi context)", + gpiiKey: "np_mixed_multiContext", + url: "%gpiiKey?view=ISO24751", + expected: { + "name": "lotta stuff", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "control": { + "onscreenKeyboard": true, + "mouseEmulation": { + "cursorSpeed": 0.85, + "-provisional-initDelay": 120, + "-provisional-mouseEmulation/enabled": true + } + }, + "applications": { + "org.alsa-project": { + "id": "org.alsa-project", + "parameters": { + "masterVolume": 14 + } + }, + "org.gnome.desktop.a11y.magnifier": { + "id": "org.gnome.desktop.a11y.magnifier", + "parameters": { + "show-cross-hairs": true, + "lens-mode": false + } + } + } + }, + "metadata": [ + { + "type": "provenance", + "scope": ["display.screenEnhancement.fontSize"], + "source": "snapshotter" + } + ] + }, + "gpii-xyz1234": { + "name": "Nighttime at home", + "preferences": { + "control": { + "mouseEmulation": { + "cursorSpeed": 1 + } + } + }, + "priority": 100 + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ "display.screenEnhancement.fontSize"] + }, { + "type": "doNotShare", + "scope": [ "display.screenEnhancement.magnification"] + } + ] + } + }, { + name: "GET: Complex retrieval of simple preference set in different ontology (with parameter)", + gpiiKey: "np_ISO24751Only_singleContext_wildcardMetadata", + url: "%gpiiKey?view=flat", + expected: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.85, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + }, + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true + }, + "metadata": [ + { + "type": "provenance", + "scope": [ + "http://registry\\.gpii\\.net/common/initDelay", + "http://registry\\.gpii\\.net/common/stickyKeys", + "http://registry\\.gpii\\.net/common/toggleKeys", + "http://registry\\.gpii\\.net/common/cursorSpeed", + "http://registry\\.gpii\\.net/common/speechControl", + "http://registry\\.gpii\\.net/common/hapticFeedback", + "http://registry\\.gpii\\.net/common/tableOfContents", + "http://registry\\.gpii\\.net/common/slowKeysInterval", + "http://registry\\.gpii\\.net/common/slowKeys/enabled", + "http://registry\\.gpii\\.net/common/debounce/enabled", + "http://registry\\.gpii\\.net/common/mouseEmulation/enabled", + "http://registry\\.gpii\\.net/common/onScreenKeyboard/enabled" + ], + "source": "snapshotter" + } + ] + } + }, + "name": "ISO24751 set", + "metadata": [ + { + "type": "doNotShare", + "scope": [ + "http://registry\\.gpii\\.net/common/fontSize", + "http://registry\\.gpii\\.net/common/tracking", + "http://registry\\.gpii\\.net/common/DPIScale", + "http://registry\\.gpii\\.net/common/lineSpace", + "http://registry\\.gpii\\.net/common/cursorSize", + "http://registry\\.gpii\\.net/common/trackingTTS", + "http://registry\\.gpii\\.net/common/nightScreen", + "http://registry\\.gpii\\.net/common/invertImages", + "http://registry\\.gpii\\.net/common/magnification", + "http://registry\\.gpii\\.net/common/invertColours", + "http://registry\\.gpii\\.net/common/mouseTrailing", + "http://registry\\.gpii\\.net/common/characterSpace", + "http://registry\\.gpii\\.net/common/highlightColor", + "http://registry\\.gpii\\.net/common/foregroundColor", + "http://registry\\.gpii\\.net/common/backgroundColor", + "http://registry\\.gpii\\.net/common/fontFaceFontName", + "http://registry\\.gpii\\.net/common/magnifierPosition", + "http://registry\\.gpii\\.net/common/highContrastTheme", + "http://registry\\.gpii\\.net/common/inputsLarger/enabled", + "http://registry\\.gpii\\.net/common/highContrast/enabled", + "http://registry\\.gpii\\.net/common/magnification/enabled", + "http://registry\\.gpii\\.net/common/fontFaceGenericFontFace" + ] + } + ] + } + } +]; + +// GET fail cases +gpii.tests.preferencesServer.get.failFixtures = [ + { + name: "GET: Retrieving preferences by a nonexistent GPII key returns an error", + gpiiKey: "non-existent-gpii-key", + url: "%gpiiKey", + expectedStatusCode: 404, + expected: { + isError: true, + errorCode: "GPII_ERR_NO_GPIIKEY", + message: "GPII key does not exist" + } + } +]; + +// TESTING preferencesServer POST functionality +fluid.registerNamespace("gpii.tests.preferencesServer.post"); + +// POST Prefs test cases +gpii.tests.preferencesServer.post.testPostResponse = function (response, fixture, receivedStatusCode) { + var retrievedData = JSON.parse(response); + var expectedStatusCode = fixture.expectedStatusCode || 200; + var expected = fixture.expected || { + preferences: fixture.postBody, + gpiiKey: retrievedData.gpiiKey + }; + + jqUnit.assertLeftHand("Response from preferences server is as expected", expected, retrievedData); + jqUnit.assertEquals("Response status code is as expected", expectedStatusCode, receivedStatusCode); +}; + +gpii.tests.preferencesServer.post.buildTestDef = function (fixture) { + return { + name: fixture.name, + expect: 2, + components: { + postRequest: { + type: "kettle.test.request.http", + options: { + path: "/preferences", + method: "POST", + port: 8081 + } + } + }, + sequence: [{ + func: "{postRequest}.send", + args: fixture.postBody + }, { + event: "{postRequest}.events.onComplete", + listener: "gpii.tests.preferencesServer.post.testPostResponse", + args: ["{arguments}.0", fixture, "{postRequest}.nativeResponse.statusCode"] + }] + }; +}; + +gpii.tests.preferencesServer.post.successFixtures = [ + { + name: "POST Prefs: With no gpiiKey specified and no ontology specified", + url: "", + postBody: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/unknown": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [ + { + "type": "provenance", + "scope": ["http://registry.gpii.net/common/fontSize"], + "source": "snapshotter" + } + ] + } + } + } + }, { + name: "POST Prefs: With no gpiiKey specified and ontology specified", + url: "?view=flat", + postBody: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/unknown": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [ + { + "type": "provenance", + "scope": ["http://registry.gpii.net/common/fontSize"], + "source": "snapshotter" + } + ] + } + } + } + } +]; + +gpii.tests.preferencesServer.post.failFixtures = [ + { + name: "POST Prefs: payload with 'shallow' validation error.", + url: "", + postBody: {}, + expectedStatusCode: 400, + expected: { + isValid: false, + message: "Your request was invalid. See the errors for details." + } + }, { + name: "POST Prefs: Payload with 'deep' invalid material.", + url: "", + postBody: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": "true" + } + } + } + }, + expectedStatusCode: 400, + expected: { + isValid: false, + message: "Your request was invalid. See the errors for details." + } + } +]; + + +// TESTING preferencesServer PUT functionality +fluid.registerNamespace("gpii.tests.preferencesServer.put"); + +gpii.tests.preferencesServer.put.wrapPrefs = function (fixture) { + return { + preferences: fixture.putBody, + gpiiKey: fixture.gpiiKey + }; +}; + +gpii.tests.preferencesServer.put.testSuccessResponse = function (response, prefs, gpiiKey) { + var retrievedData = JSON.parse(response); + // expect preferences without ontology info - any generated user token is fine + var expected = { + preferences: prefs, + gpiiKey: gpiiKey + }; + + jqUnit.assertDeepEq("Response from preferences server is as expected", expected, retrievedData); +}; + +gpii.tests.preferencesServer.put.buildSuccessTestDef = function (fixture) { + var expectedStatusCode = fixture.expectedStatusCode || 200; + return { + name: fixture.name, + expect: 3, + config: gpii.tests.preferencesServer.config, + components: { + putRequest: { + type: "kettle.test.request.http", + options: { + path: "/preferences/%gpiiKey" + "?merge=" + fixture.merge + "&view=" + (fixture.view ? fixture.view : ""), + method: "PUT", + port: 8081, + termMap: { + gpiiKey: fixture.gpiiKey + } + } + }, + rawPrefsRequest: { + type: "kettle.test.request.http", + options: { + path: "/rawPreferences/%gpiiKey", + method: "GET", + port: 8081, + termMap: { + gpiiKey: fixture.gpiiKey + } + } + } + }, + sequence: [{ + func: "{putRequest}.send", + args: fixture.putBody + }, { + event: "{putRequest}.events.onComplete", + listener: "gpii.tests.preferencesServer.put.testSuccessResponse", + args: ["{arguments}.0", fixture.putBody, fixture.gpiiKey] + }, { + func: "{rawPrefsRequest}.send" + }, { + event: "{rawPrefsRequest}.events.onComplete", + listener: "gpii.tests.preferencesServer.testResponse", + args: ["{arguments}.0", fixture.expectedRaw, "{rawPrefsRequest}.nativeResponse.statusCode", expectedStatusCode] + }] + }; +}; + +gpii.tests.preferencesServer.put.successFixtures = [ + { + name: "PUT: No view specified, ontology matching preference set", + gpiiKey: "np_flatOnly_singleContext", + putBody: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": false + }, + "metadata": [ + { + "type": "required", + "scope": ["*"] + } + ] + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ "http://registry\\.gpii\\.net/common/fontSize"] + } + ] + }, + expectedRaw: { + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": false + }, + "metadata": [ + { + "type": "required", + "scope": ["*"] + } + ] + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ "http://registry\\.gpii\\.net/common/fontSize"] + } + ] + } + } + }, { + name: "PUT: No view specified, ontology matching preference set, with the merge flag turned on to merge the new preferences with the existing preferences", + gpiiKey: "np_flatOnly_singleContext", + merge: true, + putBody: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": false + }, + "metadata": [ + { + "type": "required", + "scope": ["*"] + } + ] + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ "http://registry\\.gpii\\.net/common/fontSize"] + } + ] + }, + expectedRaw: { + "flat": { + "name": "Single set", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": false, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.85, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/unknown": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [ + { + "type": "required", + "scope": ["*"], + "source": "snapshotter" + }, + { + "type": "required", + "scope": [ + "*" + ] + } + ] + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ + "http://registry\\.gpii\\.net/common/fontSize" + ] + } + ] + } + } + }, { + name: "PUT: To preference set containing same preference in different ontology", + gpiiKey: "np_tiny", + view: "ISO24751", + putBody: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "control": { + "onscreenKeyboard": false + } + } + } + } + }, + expectedRaw: { + "ISO24751": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "control": { + "onscreenKeyboard": false + } + } + } + } + }, + "flat": { + "name": "bit of stuff", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + } + } + } + } + } + }, { + name: "PUT: To preference set containing same preference in different ontology, with the merge flag turned on to merge the new preferences with the existing preferences", + gpiiKey: "np_tiny", + view: "ISO24751", + merge: true, + putBody: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "control": { + "onscreenKeyboard": false + } + } + } + } + }, + expectedRaw: { + "flat": { + "name": "bit of stuff", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true + }, + "metadata": [] + } + }, + "metadata": [] + }, + "ISO24751": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "control": { + "onscreenKeyboard": false + } + } + } + } + } + } + }, { + name: "PUT: Overwriting one view and modifying another", + gpiiKey: "np_mixed_singleContext", + view: "ISO24751", + putBody: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "control": { + "mouseEmulation": { + "cursorSpeed": 1 + } + } + } + } + } + }, + expectedRaw: { + "flat": { + "name": "lotta stuff", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/flatOnly": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [{ + "type": "required", + "scope": ["*"] + }] + } + } + }, + "ISO24751": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "control": { + "mouseEmulation": { + "cursorSpeed": 1 + } + } + } + } + } + }, + "bogus": { + "contexts": { + "gpii-default": { + "preferences": { + "foo": "bar" + } + } + } + } + } + }, { + name: "PUT: Overwriting one view and modifying another, with the merge flag turned on to merge the new preferences with the existing preferences", + gpiiKey: "np_mixed_singleContext", + view: "ISO24751", + merge: true, + putBody: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "control": { + "mouseEmulation": { + "cursorSpeed": 1 + } + } + } + } + } + }, + expectedRaw: { + "flat": { + "name": "lotta stuff", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.85, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/flatOnly": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [ + { + "type": "required", + "scope": [ + "*" + ] + } + ] + } + } + }, + "ISO24751": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "display": { + "screenEnhancement": { + "fontSize": 24 + } + }, + "control": { + "mouseEmulation": { + "cursorSpeed": 1 + } + } + }, + "metadata": [ + { + "type": "provenance", + "scope": [ + "display.screenEnhancement.fontSize" + ], + "source": "snapshotter" + } + ] + } + } + }, + "bogus": { + "contexts": { + "gpii-default": { + "preferences": { + "foo": "bar" + } + } + } + } + } + }, { + name: "PUT: Overwriting app specific block in one view with new ones from another view", + gpiiKey: "np_mixed_singleContext", + view: "ISO24751", + putBody: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "applications": { + "org.alsa-project": { + "parameters": { + "bass": "damn loud" + } + } + } + } + } + } + }, + expectedRaw: { + "flat": { + "name": "lotta stuff", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/flatOnly": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [{ + "type": "required", + "scope": ["*"] + }] + } + } + }, + "ISO24751": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "applications": { + "org.alsa-project": { + "parameters": { + "bass": "damn loud" + } + } + } + } + } + } + }, + "bogus": { + "contexts": { + "gpii-default": { + "preferences": { + "foo": "bar" + } + } + } + } + } + }, { + name: "PUT: Overwriting app specific block in one view with new ones from another view, with the merge flag turned on to merge the new preferences with the existing preferences", + gpiiKey: "np_mixed_singleContext", + view: "ISO24751", + merge: true, + putBody: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "applications": { + "org.alsa-project": { + "parameters": { + "bass": "damn loud" + } + } + } + } + } + } + }, + expectedRaw: { + "flat": { + "name": "lotta stuff", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.85, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/flatOnly": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [ + { + "type": "required", + "scope": [ + "*" + ] + } + ] + } + } + }, + "ISO24751": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "display": { + "screenEnhancement": { + "fontSize": 24 + } + }, + "applications": { + "org.alsa-project": { + "parameters": { + "bass": "damn loud" + } + } + } + }, + "metadata": [ + { + "type": "provenance", + "scope": [ + "display.screenEnhancement.fontSize" + ], + "source": "snapshotter" + } + ] + } + } + }, + "bogus": { + "contexts": { + "gpii-default": { + "preferences": { + "foo": "bar" + } + } + } + } + } + }, { + name: "PUT: System shouldn't act crazy when saving with wildcard metadata", + gpiiKey: "np_flatOnly_singleContext", + view: "ISO24751", + putBody: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "control": { + "onscreenKeyboard": false + } + }, + "metadata": [{ + "type": "required", + "scope": ["control.*"] + }] + } + } + }, + expectedRaw: { + "flat": { + "name": "Single set", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/unknown": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [ + { + "type": "provenance", + "scope": ["http://registry\\.gpii\\.net/applications/org\\.alsa-project"], + "source": "snapshotter" + }, { + "type": "required", + "scope": ["*"] + } + ] + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ "http://registry\\.gpii\\.net/common/fontSize"] + } + ] + }, + "ISO24751": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "control": { + "onscreenKeyboard": false + } + }, + "metadata": [{ + "type": "required", + "scope": ["control.*"] + }] + } + } + } + } + }, { + name: "PUT: System shouldn't act crazy when saving with wildcard metadata, with the merge flag turned on to merge the new preferences with the existing preferences", + gpiiKey: "np_flatOnly_singleContext", + view: "ISO24751", + merge: true, + putBody: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "control": { + "onscreenKeyboard": false + } + }, + "metadata": [{ + "type": "required", + "scope": ["control.*"] + }] + } + } + }, + expectedRaw: { + "flat": { + "name": "Single set", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.85, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/unknown": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [ + { + "type": "provenance", + "scope": [ + "http://registry\\.gpii\\.net/applications/org\\.alsa-project" + ], + "source": "snapshotter" + }, + { + "type": "required", + "scope": [ + "*" + ] + } + ] + } + }, + "metadata": [ + { + "type": "doNotShare", + "scope": [ + "http://registry\\.gpii\\.net/common/fontSize" + ] + } + ] + }, + "ISO24751": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "control": { + "onscreenKeyboard": false + } + }, + "metadata": [{ + "type": "required", + "scope": ["control.*"] + }] + } + } + } + } + }, { + name: "PUT: GPII-3721 - with an nonexistent gpiiKey and no ontology specified", + gpiiKey: "nonexistent", + url: "%gpiiKey", + putBody: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/unknown": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [ + { + "type": "provenance", + "scope": ["http://registry.gpii.net/common/fontSize"], + "source": "snapshotter" + } + ] + } + } + }, + expectedRaw: { + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/unknown": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [ + { + "type": "provenance", + "scope": ["http://registry.gpii.net/common/fontSize"], + "source": "snapshotter" + } + ] + } + } + } + } + }, { + name: "PUT: GPII-3721 - with a nonexistent gpiiKey and ontology specified", + gpiiKey: "nonexistent_another", + url: "%gpiiKey?view=flat", + putBody: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/unknown": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [ + { + "type": "provenance", + "scope": ["http://registry.gpii.net/common/fontSize"], + "source": "snapshotter" + } + ] + } + } + }, + expectedRaw: { + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/unknown": true, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 14 + } + }, + "metadata": [ + { + "type": "provenance", + "scope": ["http://registry.gpii.net/common/fontSize"], + "source": "snapshotter" + } + ] + } + } + } + } + } +]; + +gpii.tests.preferencesServer.put.buildFailureTestDef = function (fixture) { + var expectedStatusCode = fixture.expectedStatusCode || 200; + return { + name: fixture.name, + expect: 2, + config: gpii.tests.preferencesServer.config, + components: { + putRequest: { + type: "kettle.test.request.http", + options: { + path: "/preferences/%gpiiKey" + "?merge=" + fixture.merge + "&view=" + (fixture.view ? fixture.view : ""), + method: "PUT", + port: 8081, + termMap: { + gpiiKey: fixture.gpiiKey + } + } + } + }, + sequence: [{ + func: "{putRequest}.send", + args: fixture.putBody + }, { + event: "{putRequest}.events.onComplete", + listener: "gpii.tests.preferencesServer.testResponse", + args: ["{arguments}.0", fixture.expected, "{putRequest}.nativeResponse.statusCode", expectedStatusCode] + }] + }; +}; + +gpii.tests.preferencesServer.put.failFixtures = [ + { + name: "PUT: A payload that has 'shallow' validation problems should return an error", + url: "%gpiiKey", + gpiiKey: "np_flatOnly_singleContext", + // An empty body should be rejected based on the "outer" schema. + putBody: {}, + expectedStatusCode: 400, + expected: { + isValid: false, + message: "Your request was invalid. See the errors for details." + } + }, + { + name: "PUT: A payload that has 'deep' validation problems should return an error", + url: "%gpiiKey", + gpiiKey: "np_flatOnly_singleContext", + // "deep" failure, should be rejected by the "inner" schema for the specific "common" term. + putBody: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": "false" + } + } + } + }, + expectedStatusCode: 400, + expected: { + isValid: false, + message: "Your request was invalid. See the errors for details." + } + } +]; + +gpii.tests.preferencesServer.testMap = [ { + build: gpii.tests.preferencesServer.get.buildTestDef, + fixtures: gpii.tests.preferencesServer.get.successFixtures +}, { + build: gpii.tests.preferencesServer.get.buildTestDef, + fixtures: gpii.tests.preferencesServer.get.failFixtures +}, { + build: gpii.tests.preferencesServer.post.buildTestDef, + fixtures: gpii.tests.preferencesServer.post.successFixtures +}, { + build: gpii.tests.preferencesServer.post.buildTestDef, + fixtures: gpii.tests.preferencesServer.post.failFixtures +}, { + build: gpii.tests.preferencesServer.put.buildSuccessTestDef, + fixtures: gpii.tests.preferencesServer.put.successFixtures +}, { + build: gpii.tests.preferencesServer.put.buildFailureTestDef, + fixtures: gpii.tests.preferencesServer.put.failFixtures +}]; + +gpii.tests.preferencesServer.testDefs = fluid.flatten(fluid.transform(gpii.tests.preferencesServer.testMap, function (mapEl) { + return fluid.transform(mapEl.fixtures, mapEl.build, function (fixture) { + var common = { + config: gpii.tests.preferencesServer.config + }; + + return fluid.extend({}, common, fixture); + }); +})); + +fluid.defaults("gpii.tests.preferencesServer.testEnvironment", { + gradeNames: ["gpii.test.couchdb.environment.base", "gpii.test.serverEnvironment"], + databases: { + gpii: { + data: [ + "%gpii-universal/gpii/node_modules/preferencesServer/test/data/gpiiKeys.json", + "%gpii-universal/gpii/node_modules/preferencesServer/test/data/prefsSafes.json", + "%gpii-universal/testData/dbData/views.json" + ] + } + } +}); + +gpii.tests.preferencesServer.testDefToEnvironment = function (testDef) { + return gpii.test.testDefToEnvironment(testDef, "gpii.tests.preferencesServer.testEnvironment", "gpii.test.couchSequenceGrade"); +}; + +gpii.test.runTestDefs(gpii.tests.preferencesServer.testDefs, gpii.tests.preferencesServer.testDefToEnvironment); + +// Tests for server status endpoints: /ready and /health +// 1. Test cases when the database connection is up +fluid.registerNamespace("gpii.tests.preferencesServer.serverStatus.testDefsWithDbConnection"); + +gpii.tests.preferencesServer.serverStatus.testDefsWithDbConnection = [{ + name: "With the system running properly, both /health and /ready endpoints return success http status codes", + expect: 2, + config: { + configName: "gpii.tests.preferencesServer.config", + configPath: "%preferencesServer/test/configs" + }, + components: { + readyRequest: { + type: "kettle.test.request.http", + options: { + path: "/ready", + method: "GET", + port: 8081 + } + }, + healthRequest: { + type: "kettle.test.request.http", + options: { + path: "/health", + method: "GET", + port: 8081 + } + } + }, + sequence: [{ + func: "{readyRequest}.send" + }, { + event: "{readyRequest}.events.onComplete", + listener: "jqUnit.assertEquals", + args: ["Receives http status code 200", 200, "{readyRequest}.nativeResponse.statusCode"] + }, { + func: "{healthRequest}.send" + }, { + event: "{healthRequest}.events.onComplete", + listener: "jqUnit.assertEquals", + args: ["Receives http status code 200", 200, "{healthRequest}.nativeResponse.statusCode"] + }] +}]; + +gpii.test.runCouchTestDefs(gpii.tests.preferencesServer.serverStatus.testDefsWithDbConnection); + +// 2. Test cases when there is no database connection +fluid.registerNamespace("gpii.tests.preferencesServer.testDefsWithoutDbConnectionTestDefs"); + +gpii.tests.preferencesServer.testDefsWithoutDbConnectionTestDefs = [{ + name: "Without the database connection, /health returns a success code and /ready returns an error code", + expect: 2, + config: { + configName: "gpii.tests.preferencesServer.config.withoutDb", + configPath: "%preferencesServer/test/configs" + }, + components: { + healthRequest: { + type: "kettle.test.request.http", + options: { + path: "/health", + method: "GET", + port: 8081 + } + }, + readyRequest: { + type: "kettle.test.request.http", + options: { + path: "/ready", + method: "GET", + port: 8081 + } + } + }, + sequence: [{ + func: "{healthRequest}.send" + }, { + event: "{healthRequest}.events.onComplete", + listener: "jqUnit.assertEquals", + args: ["Receives http status code 200", 200, "{healthRequest}.nativeResponse.statusCode"] + }, { + func: "{readyRequest}.send" + }, { + event: "{readyRequest}.events.onComplete", + listener: "jqUnit.assertEquals", + args: ["Receives http status code 503", 503, "{readyRequest}.nativeResponse.statusCode"] + }] +}]; + +gpii.test.runServerTestDefs(gpii.tests.preferencesServer.testDefsWithoutDbConnectionTestDefs); diff --git a/gpii/node_modules/preferencesServer/test/preferencesServerTestsUtils.js b/gpii/node_modules/preferencesServer/test/preferencesServerTestsUtils.js new file mode 100644 index 000000000..9a2a7b095 --- /dev/null +++ b/gpii/node_modules/preferencesServer/test/preferencesServerTestsUtils.js @@ -0,0 +1,43 @@ +/** +GPII Preferences Server Test Utility + +Copyright 2018 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +// Add an endpoint for fetching raw preferences +fluid.defaults("gpii.tests.preferencesServer", { + gradeNames: ["gpii.preferencesServer"], + requestHandlers: { + rawPreferencesGet: { + route: "/rawPreferences/:gpiiKey", + method: "get", + type: "gpii.tests.preferencesServer.rawGet.handler" + } + } +}); + +fluid.defaults("gpii.tests.preferencesServer.rawGet.handler", { + gradeNames: ["kettle.request.http"], + invokers: { + handleRequest: { + funcName: "gpii.tests.preferencesServer.rawGet.handler.getRawPreferences", + args: ["{preferencesServer}", "{that}", "{that}.req.params.gpiiKey"] + } + } +}); + +gpii.tests.preferencesServer.rawGet.handler.getRawPreferences = function (preferencesServer, request, gpiiKey) { + var prefsPromise = preferencesServer.preferencesService.getPreferencesByGpiiKey(gpiiKey); + prefsPromise.then(request.events.onSuccess.fire, request.events.onError.fire); +}; diff --git a/gpii/node_modules/preferencesServer/test/preferencesServiceTests.js b/gpii/node_modules/preferencesServer/test/preferencesServiceTests.js new file mode 100644 index 000000000..6c1a00a6f --- /dev/null +++ b/gpii/node_modules/preferencesServer/test/preferencesServiceTests.js @@ -0,0 +1,436 @@ +/* + Copyright 2017-2018 OCAD University + Copyright 2019 Raising the Floor International + + Licensed under the New BSD license. You may not use this file except in + compliance with this License. + + You may obtain a copy of the License at + https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +var jqUnit = require("node-jqunit"); + +fluid.require("%gpii-universal"); +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.preferencesServer.preferencesService"); + +/** + * + * Convert a function and the follow up retrieval of the prefs safe to a single promise that can be used with the Fluid + * IoC `task` and `resolve` syntax. + * + * @param {Object} preferencesService - The preferences service component. + * @param {String} fnNameToExec - The name of the function to execute. + * @param {Array} fnArgs - The arguments to pass when executing the function. + * @return {Promise} - A `fluid.promise` that will resolve with the updated prefs safe or reject on error. + * + */ +gpii.tests.preferencesServer.preferencesService.executeAndRetrievePrefs = function (preferencesService, fnNameToExec, fnArgs) { + var outerPromise = fluid.promise(); + + var execPromise = preferencesService[fnNameToExec].apply(null, fnArgs); + execPromise.then( + function (results) { + // retrieve the updated prefs safe + preferencesService.getPrefsSafeByGpiiKey(results.gpiiKey).then( + outerPromise.resolve, + outerPromise.reject + ); + }, + outerPromise.reject + ); + + return outerPromise; +}; + +fluid.defaults("gpii.tests.preferencesServer.preferencesService.caseHolder", { + gradeNames: ["fluid.test.testCaseHolder"], + events: { + createPreferencesService: null + }, + components: { + preferencesService: { + type: "gpii.preferencesServer.preferencesService", + createOnEvent: "createPreferencesService", + options: { + gradeNames: ["gpii.tests.dbOperation.dbDataStore.base"], + components: { + dataStore: { + type: "gpii.dbOperation.dbDataStore", + options: { + dataSourceConfig: { + baseUrl: "http://localhost", + port: 25984, + dbName: "gpii" + } + } + } + } + } + } + } +}); + +fluid.defaults("gpii.tests.preferencesServer.preferencesService.createPreferencesService", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [{ + func: "{caseHolder}.events.createPreferencesService.fire" + }] +}); + +fluid.defaults("gpii.tests.preferencesServer.preferencesService.sequenceGrade", { + gradeNames: ["fluid.test.sequence"], + sequenceElements: { + startCouch: { + gradeNames: "gpii.test.startCouchSequence", + priority: "before:sequence" + }, + createPreferencesService: { + gradeNames: "gpii.tests.preferencesServer.preferencesService.createPreferencesService", + priority: "after:startCouch" + }, + stopCouch: { + gradeNames: "gpii.test.stopCouchSequence", + priority: "after:sequence" + } + } +}); + + +// We use the same base grade as the main harness, but avoid merging with that to avoid picking up the wrong test data. +fluid.defaults("gpii.tests.preferencesServer.preferencesService.baseEnvironment", { + gradeNames: ["gpii.test.couchdb.environment.base"], + databases: { + gpii: { + data: [ + "%gpii-universal/gpii/node_modules/preferencesServer/test/data/gpiiKeys-preferenceServiceTests.json", + "%gpii-universal/gpii/node_modules/preferencesServer/test/data/prefsSafes-preferencesServiceTests.json", + "%gpii-universal/testData/dbData/views.json" + ] + } + }, + components: { + caseHolder: { + type: "gpii.tests.preferencesServer.preferencesService.caseHolder" + } + } +}); + +// All input test data +gpii.tests.preferencesServer.preferencesService.testData = { + preferencesToCreate: { + "http://registry.gpii.net/common/matchMakerType": "ruleBased", + "http://registry.gpii.net/common/fontSize": 24 + }, + preferencesToUpdate: { + "flat": { + "contexts": { + "gpii-default": { + "name": "an updated name", + "preferences": { + "http://registry.gpii.net/common/fontSize": 20 + } + } + } + }, + "newKey": { + "nested": "nested-value" + } + } +}; + +// All expected results +gpii.tests.preferencesServer.preferencesService.expected = { + // for testing getPreferencesByGpiiKey() + receivedPrefs: { + "flat": { + "name": "Default context", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/matchMakerType": "ruleBased", + "http://registry.gpii.net/common/fontSize": 24, + "http://registry.gpii.net/common/foregroundColor": "white" + } + } + } + } + }, + updatedPrefs: { + "flat": { + "name": "Default context", + "contexts": { + "gpii-default": { + "name": "an updated name", + "preferences": { + "http://registry.gpii.net/common/fontSize": 20, + "http://registry.gpii.net/common/foregroundColor": "white", + "http://registry.gpii.net/common/matchMakerType": "ruleBased" + } + } + } + }, + "newKey": { + "nested": "nested-value" + } + }, + unauthorized: { + message: "Unauthorized", + statusCode: 401, + isError: true + }, + missingGpiiKey: { + errorCode: "GPII_ERR_NO_GPIIKEY", + message: "GPII key does not exist" + }, + readOnly: { + errorCode: "GPII_ERR_READONLY", + message: "Can't update preferences for readOnly GPII key \"snapset1\" (perhaps it is a snapset)" + }, + gpiiKeyExists: { + errorCode: "GPII_ERR_GPIIKEY_EXISTS", + message: "GPII key \"alice_gpii_key\" already exists" + }, + // for testing createPreferences() + preferencesToCreate_prefsOnly: { + prefsSafeType: "user", + name: null, + password: null, + email: null, + preferences: { + "http://registry.gpii.net/common/cursorSize": 24 + } + } +}; + +fluid.defaults("gpii.tests.preferencesServer.preferencesService.getPreferencesByGpiiKey", { + gradeNames: ["gpii.tests.preferencesServer.preferencesService.baseEnvironment"], + components: { + caseHolder: { + options: { + modules: [{ + name: "Test getPreferencesByGpiiKey()", + tests: [ + { + name: "getPreferencesByGpiiKey() returns preferences - a successful workflow", + sequenceGrade: "gpii.tests.preferencesServer.preferencesService.sequenceGrade", + sequence: [{ + task: "{preferencesService}.getPreferencesByGpiiKey", + args: ["alice_gpii_key"], + resolve: "jqUnit.assertDeepEq", + resolveArgs: ["The access token should be received in an expected format", gpii.tests.preferencesServer.preferencesService.expected.receivedPrefs, "{arguments}.0"] + }] + }, + { + name: "getPreferencesByGpiiKey() returns undefined when the GPII key has no preferences defined", + sequenceGrade: "gpii.tests.preferencesServer.preferencesService.sequenceGrade", + sequence: [{ + task: "{preferencesService}.getPreferencesByGpiiKey", + args: ["bob_gpii_key"], + resolve: "jqUnit.assertUndefined", + resolvArgs: ["undefined is returned when the gpii key has no preferences defined", "{arguments}.0"] + }] + }, + { + name: "getPreferencesByGpiiKey() returns error when a gpii key is not provided in the argument list", + sequenceGrade: "gpii.tests.preferencesServer.preferencesService.sequenceGrade", + sequence: [{ + task: "{preferencesService}.getPreferencesByGpiiKey", + args: ["non-existent-gpii-key"], + reject: "jqUnit.assertDeepEq", + rejectArgs: ["The error is returned when a gpii key is not found", gpii.tests.preferencesServer.preferencesService.expected.missingGpiiKey, "{arguments}.0"] + }] + } + ] + }] + } + } + } +}); + +fluid.defaults("gpii.tests.preferencesServer.preferencesService.createPreferences", { + gradeNames: ["gpii.tests.preferencesServer.preferencesService.baseEnvironment"], + components: { + caseHolder: { + options: { + modules: [{ + name: "Test createPreferences()", + tests: [ + { + name: "createPreferences() creates a prefs safe and an auto generated GPII key - a successful workflow", + sequenceGrade: "gpii.tests.preferencesServer.preferencesService.sequenceGrade", + sequence: [{ + task: "gpii.tests.preferencesServer.preferencesService.executeAndRetrievePrefs", + args: ["{preferencesService}", "createPreferences", [gpii.tests.preferencesServer.preferencesService.testData.preferencesToCreate]], // preferencesServer, fnNameToExec, fnArgs + resolve: "gpii.tests.preferencesServer.preferencesService.verifyFetchedPrefsSafe", + resolveArgs: [gpii.tests.preferencesServer.preferencesService.testData.preferencesToCreate, "{arguments}.0"] + }] + }, + { + name: "createPreferences() creates a prefs safe and an GPII key with the provided key value - a successful workflow", + sequenceGrade: "gpii.tests.preferencesServer.preferencesService.sequenceGrade", + sequence: [{ + task: "gpii.tests.preferencesServer.preferencesService.executeAndRetrievePrefs", + args: ["{preferencesService}", "createPreferences", [gpii.tests.preferencesServer.preferencesService.testData.preferencesToCreate, "a-new-gpii-key-by-preferences-service"]], // preferencesServer, fnNameToExec, fnArgs + resolve: "gpii.tests.preferencesServer.preferencesService.verifyFetchedPrefsSafe", + resolveArgs: [gpii.tests.preferencesServer.preferencesService.testData.preferencesToCreate, "{arguments}.0", "a-new-gpii-key-by-preferences-service"] + }] + }, + { + name: "createPreferences() returns error when the provided GPII key already exists", + sequenceGrade: "gpii.tests.preferencesServer.preferencesService.sequenceGrade", + sequence: [{ + task: "{preferencesService}.createPreferences", + args: [gpii.tests.preferencesServer.preferencesService.testData.preferencesToCreate, "alice_gpii_key"], + reject: "jqUnit.assertDeepEq", + rejectArgs: ["The error is returned when the provided GPII key already exists", gpii.tests.preferencesServer.preferencesService.expected.gpiiKeyExists, "{arguments}.0"] + }] + } + ] + }] + } + } + } +}); + +gpii.tests.preferencesServer.preferencesService.verifyFetchedPrefsSafe = function (preferencesToCreate, response, expectedGpiiKey) { + if (expectedGpiiKey) { + jqUnit.assertEquals("The created GPII key matches the input GPII key", expectedGpiiKey, response.gpiiKey); + jqUnit.assertEquals("The GPII key in the key record matches the input GPII key", expectedGpiiKey, response.gpiiKeyDetails.id); + } else { + jqUnit.assertNotUndefined("The GPII key is auto generated", response.gpiiKey); + } + + var gpiiKeyDetails = response.gpiiKeyDetails; + jqUnit.assertEquals("The value of \"schemaVersion\" has been set correctly", gpii.dbOperation.schemaVersion, gpiiKeyDetails.schemaVersion); + jqUnit.assertNotUndefined("The value of \"prefsSafeId\" has been set to default", gpiiKeyDetails.prefsSafeId); + jqUnit.assertEquals("The value of \"prefsSetId\" has been set to default", gpii.preferencesServer.defaultPrefsSetId, gpiiKeyDetails.prefsSetId); + jqUnit.assertFalse("The value of \"revoked\" has been set to false", gpiiKeyDetails.revoked); + jqUnit.assertNull("The value of \"revokedReason\" has been set to null", gpiiKeyDetails.revokedReason); + jqUnit.assertNotUndefined("The value of \"timestampCreated\" has been set", gpiiKeyDetails.timestampCreated); + jqUnit.assertNull("The value of \"timestampUpdated\" has been set", gpiiKeyDetails.timestampUpdated); + + var prefsSafe = response.prefsSafe; + jqUnit.assertLeftHand("The data is saved successfully", preferencesToCreate, prefsSafe.preferences); +}; + +fluid.defaults("gpii.tests.preferencesServer.preferencesService.updatePreferences", { + gradeNames: ["gpii.tests.preferencesServer.preferencesService.baseEnvironment"], + components: { + caseHolder: { + options: { + modules: [{ + name: "Test updatePreferences()", + tests: [ + { + name: "updatePreferences() updates the associated prefs safe with merged preferences - a successful workflow", + sequenceGrade: "gpii.tests.preferencesServer.preferencesService.sequenceGrade", + sequence: [ + { + task: "{preferencesService}.getPrefsSafeByGpiiKey", + args: ["alice_gpii_key"], + // save the original GPII key and prefs safe records to compare after the update + resolve: "fluid.set", + resolveArgs: ["{that}", ["originalData"], "{arguments}.0"] + }, + { + task: "gpii.tests.preferencesServer.preferencesService.executeAndRetrievePrefs", + args: ["{preferencesService}", "updatePreferences", [gpii.tests.preferencesServer.preferencesService.testData.preferencesToUpdate, "alice_gpii_key", true]], // preferencesServer, fnNameToExec, fnArgs + resolve: "gpii.tests.preferencesServer.preferencesService.verifyUpdatedPrefsSafe", + resolveArgs: ["{that}", gpii.tests.preferencesServer.preferencesService.expected.updatedPrefs, "alice_gpii_key", "{arguments}.0"] + } + ] + }, + { + name: "updatePreferences() updates the associated prefs safe without merging with existing preferences - a successful workflow", + sequenceGrade: "gpii.tests.preferencesServer.preferencesService.sequenceGrade", + sequence: [ + { + task: "{preferencesService}.getPrefsSafeByGpiiKey", + args: ["alice_gpii_key"], + // save the original GPII key and prefs safe records to compare after the update + resolve: "fluid.set", + resolveArgs: ["{that}", ["originalData"], "{arguments}.0"] + }, + { + task: "gpii.tests.preferencesServer.preferencesService.executeAndRetrievePrefs", + args: ["{preferencesService}", "updatePreferences", [gpii.tests.preferencesServer.preferencesService.testData.preferencesToUpdate, "alice_gpii_key"]], // preferencesServer, fnNameToExec, fnArgs + resolve: "gpii.tests.preferencesServer.preferencesService.verifyUpdatedPrefsSafe", + resolveArgs: ["{that}", gpii.tests.preferencesServer.preferencesService.testData.preferencesToUpdate, "alice_gpii_key", "{arguments}.0"] + } + ] + }, + { + name: "updatePreferences() creates a new prefs safe when the GPII key is not associates with a prefs safe - a successful workflow", + sequenceGrade: "gpii.tests.preferencesServer.preferencesService.sequenceGrade", + sequence: [ + { + task: "{preferencesService}.getPrefsSafeByGpiiKey", + args: ["bob_gpii_key"], + // save the original GPII key and prefs safe records to compare after the update + resolve: "fluid.set", + resolveArgs: ["{that}", ["originalData"], "{arguments}.0"] + }, + { + task: "gpii.tests.preferencesServer.preferencesService.executeAndRetrievePrefs", + args: ["{preferencesService}", "updatePreferences", [gpii.tests.preferencesServer.preferencesService.testData.preferencesToUpdate, "bob_gpii_key"]], // preferencesServer, fnNameToExec, fnArgs + resolve: "gpii.tests.preferencesServer.preferencesService.verifyNewPrefsSafeForUpdate", + resolveArgs: ["{that}", gpii.tests.preferencesServer.preferencesService.testData.preferencesToUpdate, "bob_gpii_key", "{arguments}.0"] + } + ] + }, + { + name: "updatePreferences() returns error when updating a snapset", + sequenceGrade: "gpii.tests.preferencesServer.preferencesService.sequenceGrade", + sequence: [{ + task: "{preferencesService}.updatePreferences", + args: [gpii.tests.preferencesServer.preferencesService.testData.preferencesToUpdate, "snapset1"], + reject: "jqUnit.assertDeepEq", + rejectArgs: ["The error is returned when updating a snapset", gpii.tests.preferencesServer.preferencesService.expected.readOnly, "{arguments}.0"] + }] + } + ] + }] + } + } + } +}); + +gpii.tests.preferencesServer.preferencesService.verifyUpdatedPrefsSafe = function (that, updatedPreferences, gpiiKey, response) { + jqUnit.assertDeepEq("The GPII key in the response is expected", gpiiKey, response.gpiiKey); + jqUnit.assertDeepEq("The GPII key record is unchanged", that.originalData.gpiiKeyDetails, response.gpiiKeyDetails); + jqUnit.assertDeepEq("The preferences has been updated", updatedPreferences, response.prefsSafe.preferences); + jqUnit.assertNotEquals("The prefs safe timestampUpdated has been updated", that.originalData.prefsSafe.timestampUpdated, response.prefsSafe.timestampUpdated); + + var unchangedOrigPrefsSafeFields = fluid.censorKeys(that.originalData.prefsSafe, ["preferences", "timestampUpdated"]); + var unchangedPrefsSafeFields = fluid.censorKeys(response.prefsSafe, ["preferences", "timestampUpdated"]); + jqUnit.assertDeepEq("Other prefs safe values are unchanged", unchangedOrigPrefsSafeFields, unchangedPrefsSafeFields); +}; + +gpii.tests.preferencesServer.preferencesService.verifyNewPrefsSafeForUpdate = function (that, updatedPreferences, gpiiKey, response) { + // verify before update status + jqUnit.assertNull("The GPII key originally has no prefs safe associated", that.originalData.gpiiKeyDetails.prefsSafeId); + + // verify after update status + jqUnit.assertDeepEq("The GPII key in the response is expected", gpiiKey, response.gpiiKey); + jqUnit.assertNotUndefined("A prefs safe has been created", response.prefsSafe); + jqUnit.assertNull("The timestampUpdated of the prefs safe is null", response.prefsSafe.timestampUpdated); + jqUnit.assertDeepEq("The preferences in the prefs safe is expected", updatedPreferences, response.prefsSafe.preferences); + jqUnit.assertEquals("The prefs safe has been associated with the GPII key", response.gpiiKeyDetails.prefsSafeId, response.prefsSafe.id); + + var unchangedOrigGpiiKeyFields = fluid.censorKeys(that.originalData.gpiiKeyDetails, ["prefsSafeId", "prefsSetId", "timestampUpdated"]); + var unchangedGpiiKeyFields = fluid.censorKeys(response.gpiiKeyDetails, ["prefsSafeId", "prefsSetId", "timestampUpdated"]); + jqUnit.assertDeepEq("Other GPII key values are unchanged", unchangedOrigGpiiKeyFields, unchangedGpiiKeyFields); +}; + +fluid.test.runTests([ + "gpii.tests.preferencesServer.preferencesService.getPreferencesByGpiiKey", + "gpii.tests.preferencesServer.preferencesService.createPreferences", + "gpii.tests.preferencesServer.preferencesService.updatePreferences" +]); diff --git a/gpii/node_modules/processReporter/README.md b/gpii/node_modules/processReporter/README.md new file mode 100644 index 000000000..b029282f2 --- /dev/null +++ b/gpii/node_modules/processReporter/README.md @@ -0,0 +1,67 @@ +# Process Reporter + +An Infusion component that can be used to acquire information about the processes associated with a GPII solution. In +particular, it can determine the running status of a solution. + +The Process Reporter uses the information stored in the `isRunning` block of a solutions registry entry to decide +whether a solution is running. For example, the `isRunning` block for the Linux screen reader Orca (the solution entry +`org.gnome.orca`) is as follows: + +## Process Reporter API + +Given a solutions registry entry, the Process Reporter uses the information stored in the `isRunning` block to determine +whether a solution has been launched and is currently running. For example, the `isRunning` block for the Orca screen +reader (the solution entry `org.gnome.orca`) is as follows: + +```snippet +"isRunning": [ + { + "type": "gpii.processReporter.find", + "command": "orca" + } +] +``` + +The Process Reporter's invoker `handleIsRunning(entry)` uses the `type` property and `commmand` properties, and returns +either: + +- `true`, if there is a running process corresponding to the `orca` command, +- `false`, if there is no running process corresponding to the `orca` command. + +## Special cases + +If the solutions registry entry does not have an `isRunning` block, then its run-state is not defined, and +`handleIsRunning()` returns `undefined`. + +Most solutions run-state is based on a corresponding entry in the underlying OS's process or tasklist table. For +example, the Linux/GNOME solution `org.gnome.orca` is running if the `orca` screen reader process is running. +Similarly, on Windows, `org.nvda-project` is running if there is an `nvda` process in the tasklist. + +Some solutions, however, are not strictly process-oriented. An example is the GNOME Shell screen magnifier (solution +`org.gnome.desktop.a11y.magnifier`). The magnifier is not a separate process, but is built into GNOME Shell. The +magnifier is running if (1) the `gnome-shell` process is running and (2) the `org.gnome.desktop.a11y.applications +screen-magnifier-enabled` GSetting is `true`. + +To handle such cases, the `isRunning` block contains an array of criteria to check. If all criteria are met, then the +solution is running. The Process Reporter loops through and evaluates all the criteria. Continuing to use the GNOME +Shell magnifier as the example: the Linux/GNOME implementation provides for checking both the status of the gnome-shell +process and the magnfier GSetting: + +```snippet +"isRunning": [ + { + "type": "gpii.processReporter.find", + "command": "gnome-shell" + }, + { + "type": "gpii.processReporter.checkSetting", + "schema": "org.gnome.desktop.a11y.applications", + "setting": "screen-magnifier-enabled", + "value": true + } +] +``` + +## Dependencies + +[infusion](https://github.com/fluid-project/infusion) framework. diff --git a/gpii/node_modules/processReporter/index.js b/gpii/node_modules/processReporter/index.js new file mode 100644 index 000000000..2839dbc9a --- /dev/null +++ b/gpii/node_modules/processReporter/index.js @@ -0,0 +1,8 @@ +"use strict"; + +var fluid = require("infusion"); + +fluid.module.register("processReporter", __dirname, require); + +require("./src/processesBridge.js"); +require("./src/ProcessReporter.js"); diff --git a/gpii/node_modules/processReporter/package.json b/gpii/node_modules/processReporter/package.json new file mode 100644 index 000000000..c8e4fca7d --- /dev/null +++ b/gpii/node_modules/processReporter/package.json @@ -0,0 +1,30 @@ +{ + "name": "processReporter", + "description": "The Process Reporter module provides information about the platform and its running software", + "version": "0.1.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "licenses": [ + { + "type": "BSD-3-Clause", + "url": "http://www.opensource.org/licenses/BSD-3-Clause" + } + ], + "keywords": [ + "gpii", + "accessibility", + "settings", + "fluid", + "IoC", + "Inversion of Control", + "configuration", + "evented" + ], + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { + "node": ">=4.2.18" + } +} diff --git a/gpii/node_modules/processReporter/src/ProcessReporter.js b/gpii/node_modules/processReporter/src/ProcessReporter.js new file mode 100644 index 000000000..05b0e3702 --- /dev/null +++ b/gpii/node_modules/processReporter/src/ProcessReporter.js @@ -0,0 +1,71 @@ +/** + * GPII Process Reporter + * + * Copyright 2015-2017 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = fluid || require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.defaults("gpii.processReporter", { + gradeNames: ["fluid.component"], + components: { + nameResolver: { + type: "gpii.processReporter.nameResolver" + } + }, + invokers: { + handleIsRunning: { + funcName: "gpii.processReporter.handleIsRunning", + args: ["{arguments}.0"] + // entry + } + } +}); + +fluid.defaults("gpii.processReporter.nameResolver", { + gradeNames: ["fluid.component"], + invokers: { + resolveName: { + funcName: "fluid.identity" + } + } +}); + +/* Marker function for use in isRunning sections of a launch handler + * to identify in a meaningful way that the solutions start block should always + * be run. + */ +fluid.defaults("gpii.processReporter.neverRunning", { + gradeNames: "fluid.function", + argumentMap: {} +}); + +gpii.processReporter.neverRunning = function () { + return false; +}; + +/* + * Runs the 'isRunning' entries of the 'entry' argument. If no 'isRunning' block is present + * undefined is returned + */ +gpii.processReporter.handleIsRunning = function (entry) { + var running = undefined; + if (!!entry.isRunning) { + running = true; + + fluid.each (fluid.makeArray(entry.isRunning), function (aMember) { + var partial = fluid.invokeGradedFunction(aMember.type, aMember); + running = running && partial; + }); + } + return running; +}; diff --git a/gpii/node_modules/processReporter/src/processesBridge.js b/gpii/node_modules/processReporter/src/processesBridge.js new file mode 100644 index 000000000..1911fdecb --- /dev/null +++ b/gpii/node_modules/processReporter/src/processesBridge.js @@ -0,0 +1,281 @@ +/*! +GPII Process Reporter processes bridge, a component that encapsulates common +functions for retrieving information about OS processes with no requirement for +native (e.g., linux or windows) OS access. + +Copyright 2014 Inclusive Design Research Centre, OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/gpii/universal/LICENSE.txt +*/ + +"use strict"; + +var fluid = fluid || require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +fluid.defaults("gpii.processes", { + gradeNames: ["fluid.component", "fluid.contextAware"], + contextAwareness: { + platform: { + checks: { + linux: { + contextValue: "{gpii.context.linux}", + gradeNames: "gpii.processes.linux" + }, + windows: { + contextValue: "{gpii.context.windows}", + gradeNames: "gpii.processes.windows" + } + } + } + }, + invokers: { + findSolutionsByCommands: { + funcName: "gpii.processes.findSolutionsByCommands", + args: ["{that}", "{arguments}.0"] + // array of command names + }, + findSolutionsByPids: { + funcName: "gpii.processes.findSolutionsByPids", + args: ["{that}", "{arguments}.0"] + // array of pids (process ids) + }, + findProcessByPid: { + funcName: "gpii.processes.findProcessByPid", + args: ["{that}", "{arguments}.0", "{arguments}.1"] + // pid, procArray [optional] + }, + findProcessesByCommand: { + funcName: "gpii.processes.findProcessesByCommand", + args: ["{that}", "{arguments}.0", "{arguments}.1"] + // command, procArray [optional] + }, + findFirstProcessByCommand: { + funcName: "gpii.processes.findFirstProcessByCommand", + args: ["{that}", "{arguments}.0", "{arguments}.1"] + // command, procArray [optional] + }, + isRunning: { + funcName: "gpii.processes.isRunning", + args: ["{that}", "{arguments}.0"] + // state (string) + }, + updateProcInfo: { + funcName: "gpii.processes.updateProcInfo", + args: ["{that}", "{arguments}.0"] + // process info structure + }, + initProcInfoNotRunning: { + funcName: "gpii.processes.initProcInfoNotRunning", + args: ["{that}", "{arguments}.0"] + // command name (string) + }, + getProcessPath: { + funcName: "gpii.processes.getProcessPath", + args: ["{that}", "{arguments}.0"] + // command name (string) + }, + // Context aware invokers. + getProcessList : { + funcName: "gpii.processes.getProcessList", + args: ["{arguments}.0"] + // optional string or numeric identifier of the process + } + }, + // True if process names are matched with differing case. + ignoreCase: false +}); + +/** + Return a list of process information objects corresponding to the names of each of the passed in commands. If + nothing is found, returns an empty array. + * + @param {Component} that - An instance of a processes component. + @param {Array} commandNames - The names of the processes to inspect. + @return {Array} Array of procInfo objects for each command name. Empty if no processes are found. +*/ +gpii.processes.findSolutionsByCommands = function (that, commandNames) { + return fluid.accumulate(commandNames, function (aCommand, matches) { + var procInfos = that.findProcessesByCommand(aCommand); + matches = matches.concat(procInfos); + return matches; + }, []); +}; + +/** + * Return an list of process information objects corresponding to the pids (process id numbers) passed in. If nothing + * is found, returns an empty array. + * + * @param {Component} that - An instance of a processes component. + * @param {Array} pids - The process ids of the processes to inspect. + * @return {Array} Array of procInfo objects for each pid. Empty if no corresponding processes are found. + */ +gpii.processes.findSolutionsByPids = function (that, pids) { + return fluid.accumulate(pids, function (aPid, matches) { + var found = that.findProcessByPid(aPid); + if (found !== null) { + matches.push(found); + } + return matches; + }, []); +}; + +/** + * Return a process information object corresponding to THE given process + * id (process id number). Returns null if there is no such process. + * + * @param {Component} that - An instance of a processes component. + * @param {Number} pid - The process id of the process to inspect. + * @param {Array} [procArray] - [optional] An array of process information objects to search. + * @return {ProcInfo} A process information object for the process with the given id. Returns null if there is no such process. + */ +gpii.processes.findProcessByPid = function (that, pid, procArray) { + if (!procArray) { + procArray = that.getProcessList(pid); + } + return fluid.find(procArray, function (procInfo) { + if (procInfo.pid === pid) { + return procInfo; + } + }, null); +}; + +/** + * Return the path of a running process. + * + * If the full path is not known (the process is running as another user), only the command is returned. + * + * @param {Component} that - an instance of a processes component. + * @param {Number} pid - the process id of the process. + * @return {String} - The full path of the process, or null if there's no matching process. + */ +gpii.processes.getProcessPath = function (that, pid) { + var proc = that.findProcessByPid(pid); + return proc && (proc.fullPath || proc.command); +}; + +/** + * Return a list of process information objects that match the given + * command name. Returns an empty array if not matching name is found. + * + * @param {Component} that - An instance of a processes component. + * @param {String} commandName - The name of the process to inspect. + * @param {Array} [procArray] - [optional] an array of process information objects to search. + * @return {Array} Array of procInfo objects matching the command name. Empty if no corresponding processes are found. + */ +gpii.processes.findProcessesByCommand = function (that, commandName, procArray) { + if (!procArray) { + procArray = that.getProcessList(commandName); + } + var commandNameLower = that.options.ignoreCase && commandName.toLowerCase(); + return fluid.accumulate(procArray, function (aProcInfo, matchingProcs) { + var match = aProcInfo.command === commandName || + (that.options.ignoreCase && aProcInfo.command.toLowerCase() === commandNameLower); + + if (match) { + matchingProcs.push(aProcInfo); + } + return matchingProcs; + }, []); +}; + +/** + * Return the first process of an array of processes all with the same + * command name. If there are no matching names, return null. + * + * @param {Component} that - An instance of a processes component. + * @param {String} commandName - The name of the processe to inspect. + * @param {Array} [procArray] - [optional] An array of process information objects to search. + * @return {Object} A process information object for the process with the given id, null if there is no such process. + */ +gpii.processes.findFirstProcessByCommand = function (that, commandName, procArray) { + var commands = that.findProcessesByCommand(commandName, procArray); + if (commands.length > 0) { + return commands[0]; + } + else { + return null; + } +}; + +// Map to reduce process state values into "Running" (= true) vs. "Not Running" +// (= false). +gpii.processes.stateToRunning = fluid.freezeRecursive({ + "Running": true, + "Uninterruptible": true, + "Sleeping": true, + "Stopped": true, + "Zombie": false, + "NoSuchProcess": false +}); + +/** + * Determine if a process is running based on its native state. + * + * @param {Component} that - An instance of a processes component. + * @param {String} state - Native state of the process. + * @return {Boolean} Returns true if the process is running, false otherwise. + */ +gpii.processes.isRunning = function (that, state) { + return gpii.processes.stateToRunning[state]; +}; + +/** + * Renew the information about a process, or create a new "no such process" + * information object. + * + * @param {Component} that - An instance of a processes component. + * @param {ProcInfo} procInfo - Latest information about a process. + * @return {ProcInfo} - Returns a new process information object about the process. + */ +gpii.processes.updateProcInfo = function (that, procInfo) { + var newProcInfo = null; + if (procInfo.state === "NoSuchProcess") { + newProcInfo = + that.findFirstProcessByCommand(procInfo.command); + } + else { + newProcInfo = that.findProcessByPid(procInfo.pid); + } + if (newProcInfo === null) { + newProcInfo = + that.initProcInfoNotRunning(procInfo.command); + } + return newProcInfo; +}; + +/** + * Create process information object for a not-running process. + * + * @param {Component} that - An instance of a processes component. + * @param {String} command - Name of the process. + * @return {ProcInfo} Returns a new process information object initialized as + * if the process is not running. + */ +gpii.processes.initProcInfoNotRunning = function (that, command) { + return fluid.freezeRecursive({ + "command": command, + "pid": -1, + "ppid": -1, + "uid": -1, + "gid": -1, + "fullPath": "", + "argv": "", + "state": "NoSuchProcess" + }); +}; + +/** + * Default function for the base 'gpii.processes' grade to return an empty list + * of process information objects. + * + * @param {Number|String} identifier - Optional process id number or command name of the process. + * @return {Array} Returns an empty list. + */ +gpii.processes.getProcessList = function (/* identifier */) { + return []; +}; diff --git a/gpii/node_modules/processReporter/test/web/html/ProcessReporterTest.html b/gpii/node_modules/processReporter/test/web/html/ProcessReporterTest.html new file mode 100644 index 000000000..f51117edd --- /dev/null +++ b/gpii/node_modules/processReporter/test/web/html/ProcessReporterTest.html @@ -0,0 +1,38 @@ + + + + + GPII Process Reporter Tests + + + + + + + + + + + + + + + + + + + + + + + +

                  GPII Process Reporter Tests

                  +

                  +
                  +

                  +
                    + + + + diff --git a/gpii/node_modules/processReporter/test/web/html/ProcessesBridgeTest.html b/gpii/node_modules/processReporter/test/web/html/ProcessesBridgeTest.html new file mode 100644 index 000000000..1752099b4 --- /dev/null +++ b/gpii/node_modules/processReporter/test/web/html/ProcessesBridgeTest.html @@ -0,0 +1,39 @@ + + + + + GPII Processes Bridge Tests + + + + + + + + + + + + + + + + + + + + + + + +

                    GPII Processes Bridge Tests

                    +

                    +
                    +

                    +
                      + + + + diff --git a/gpii/node_modules/processReporter/test/web/js/ProcessReporterTests.js b/gpii/node_modules/processReporter/test/web/js/ProcessReporterTests.js new file mode 100644 index 000000000..d7d28c92a --- /dev/null +++ b/gpii/node_modules/processReporter/test/web/js/ProcessReporterTests.js @@ -0,0 +1,112 @@ +/* + * Process Reporter Tests + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +/* global fluid, jqUnit, gpii */ + +"use strict"; + +(function () { + fluid.registerNamespace("gpii.tests.processReporter"); + + fluid.defaults("gpii.tests.processReporter.alwaysTrue", { + gradeNames: "fluid.function", + argumentMap: {} + }); + + gpii.tests.processReporter.alwaysTrue = function () { + return true; + }; + + fluid.defaults("gpii.tests.processReporter.alwaysFalse", { + gradeNames: "fluid.function", + argumentMap: {} + }); + + gpii.tests.processReporter.alwaysFalse = function () { + return false; + }; + + gpii.tests.processReporter.handleIsRunningSpecs = { + "No isRunning block": { + input: {}, + expected: undefined + }, + "Single true entry as object": { + input: { + isRunning: { + "type": "gpii.tests.processReporter.alwaysTrue" + } + }, + expected: true + }, + "Single true entry as array": { + input: { + isRunning: [{ + "type": "gpii.tests.processReporter.alwaysTrue" + }] + }, + expected: true + }, + "Single false entry": { + input: { + isRunning: [{ + "type": "gpii.tests.processReporter.alwaysFalse" + }] + }, + expected: false + }, + "true + false => false": { + input: { + isRunning: [{ + "type": "gpii.tests.processReporter.alwaysTrue" + }, { + "type": "gpii.tests.processReporter.alwaysFalse" + }] + }, + expected: false + }, + "false + true => false": { + input: { + isRunning: [{ + "type": "gpii.tests.processReporter.alwaysFalse" + }, { + "type": "gpii.tests.processReporter.alwaysTrue" + }] + }, + expected: false + }, + "true + true => true": { + input: { + isRunning: [{ + "type": "gpii.tests.processReporter.alwaysTrue" + }, { + "type": "gpii.tests.processReporter.alwaysTrue" + }] + }, + expected: true + } + }; + + jqUnit.test("gpii.processReporter.handleIsRunning tests", function () { + fluid.each(gpii.tests.processReporter.handleIsRunningSpecs, function (spec, title) { + var actual = gpii.processReporter.handleIsRunning(spec.input); + jqUnit.assertEquals(title, spec.expected, actual); + }); + }); + + jqUnit.test("gpii.processReporter.neverRunning test", function () { + jqUnit.assertFalse( + "Process Reporter neverRunning()", + gpii.processReporter.neverRunning() + ); + }); +})(); diff --git a/gpii/node_modules/processReporter/test/web/js/ProcessesBridgeTests.js b/gpii/node_modules/processReporter/test/web/js/ProcessesBridgeTests.js new file mode 100644 index 000000000..0e5a96637 --- /dev/null +++ b/gpii/node_modules/processReporter/test/web/js/ProcessesBridgeTests.js @@ -0,0 +1,383 @@ +/* +GPII Web Processes Bridge Unit Tests + +Copyright 2014-2017 Inclusive Design Research Centre, OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/gpii/universal/LICENSE.txt +*/ + +/* global fluid, jqUnit, require, gpii */ + +"use strict"; + +fluid.registerNamespace("gpii.tests.processes"); + +// Mock 'node' process. +gpii.tests.processes.mockNode = fluid.freezeRecursive({ + command: "node", + pid: 2345, + ppid: 968, + uid: 501, + gid: 20, + fullPath: "/usr/local/bin/node", + argv: ["/usr/local/bin/node"], + state: "Running" +}); + +// mock 'firefox' process +gpii.tests.processes.mockFirefox = fluid.freezeRecursive({ + command: "firefox", + pid: 2563, + ppid: 1, + uid: 501, + gid: -1, + fullPath: "/Applications/Firefox8.app/Contents/MacOS/firefox", + argv: ["/Applications/Firefox8.app/Contents/MacOS/firefox", "-foreground"], + state: "Sleeping" +}); + +// Mock list of running processes. +gpii.tests.processes.mockProcessesList = [ + gpii.tests.processes.mockNode, + gpii.tests.processes.mockFirefox, + { + command: "node", // second mock node process -- different user + pid: 666, + ppid: 968, + uid: 505, + gid: 20, + fullPath: "/usr/local/bin/node", + argv: ["/usr/local/bin/node"], + state: "Running" + }, { + command: "BBEdit", + pid: 2725, + ppid: 1, + uid: 501, + gid: 20, + fullPath: "Applications/BBEdit.app/Contents/MacOS/BBEdit", + argv: ["Applications/BBEdit.app/Contents/MacOS/BBEdit", "file.txt"], + state: "Stopped" + } +]; + +fluid.defaults("gpii.tests.processes.mock", { + gradeNames: ["gpii.processes", "gpii.contexts.test"], + members: { + mockNode: gpii.tests.processes.mockNode, + mockFirefox: gpii.tests.processes.mockFirefox + }, + invokers: { + getProcessList: { + funcName: "gpii.tests.getProcessList", + args: ["{arguments}.0"] + } + } +}); + +gpii.tests.getProcessList = function (identifier) { + var togo; + var aProcInfo = null; + if (identifier) { + aProcInfo = fluid.find( + gpii.tests.processes.mockProcessesList, + function (procInfo) { + if (procInfo.pid === identifier) { + return procInfo; + } + }, null + ); + } + if (aProcInfo) { + togo = [aProcInfo]; + } else { + togo = gpii.tests.processes.mockProcessesList; + } + return togo; +}; + +/** + * "Start" firefox if not alreday running. Simulated by adding the mock firefox + * process to the list of processes member of the given proceesesBridge, if not + * already present in the list. + * + * @param {Component} processesBridge - An instance of processesBridge component. + */ +gpii.tests.processes.startFireFox = function (processesBridge) { + var procInfo = processesBridge.findProcessByPid(processesBridge.mockFirefox.pid); + // Start only if not running. + if (procInfo === null) { + processesBridge.getProcessList().push(processesBridge.mockFirefox); + } +}; + +/** + * "Stop" firefox if not alreday running. Simulated by removing the mock + * firefox process from the list of processes member of the given + * proceesesBridge, if present in the list. + * + * @param {Component} processesBridge - An instance of processesBridge component. + */ +gpii.tests.processes.stopFireFox = function (processesBridge) { + var procInfo = processesBridge.findProcessByPid(processesBridge.mockFirefox.pid); + // Stop only if running. + if (procInfo !== null) { + var index = processesBridge.getProcessList().indexOf(procInfo); + processesBridge.getProcessList().splice(index, 1); + } +}; + +gpii.tests.processes.runTests = function () { + var processesBridge = gpii.tests.processes.mock(); + jqUnit.module("Processes Bridge"); + jqUnit.test( + "Test getProcessList()/findProcessByPid() with specified node process", + function () { + var procInfos = processesBridge.getProcessList(); + jqUnit.assertNotEquals( + "Listing all processes", 0, procInfos.length + ); + // Check for the presence of "node". + var nodeProc = processesBridge.findProcessByPid(processesBridge.mockNode.pid, procInfos); + jqUnit.assertNotNull("Searching for 'node' process", nodeProc); + } + ); + + jqUnit.test( + "Test findProcessByPid() with non-running process id", + function () { + jqUnit.assertNull( + "Search negative process id value", processesBridge.findProcessByPid(-1) + ); + } + ); + + jqUnit.test( + "Test findProcessByPid() against mock node process object.", + function () { + var nodeProcInfo = processesBridge.findProcessByPid(processesBridge.mockNode.pid); + + jqUnit.assertEquals("Node process 'name'", + processesBridge.mockNode.command, nodeProcInfo.command); + + jqUnit.assertEquals("Node process 'pid'", + processesBridge.mockNode.pid, nodeProcInfo.pid); + + jqUnit.assertEquals("Node process 'uid'", + processesBridge.mockNode.uid, nodeProcInfo.uid); + + jqUnit.assertEquals("Node process 'gid'", + processesBridge.mockNode.gid, nodeProcInfo.gid); + + jqUnit.assertEquals("Node process 'argv' length'", + processesBridge.mockNode.argv.length, nodeProcInfo.argv.length); + + jqUnit.assertEquals("Node process status", + "Running", processesBridge.mockNode.state); + + jqUnit.assertEquals("Node process fullPath", + processesBridge.mockNode.fullPath, nodeProcInfo.fullPath); + + jqUnit.assertDeepEq("Node process argv", + processesBridge.mockNode.argv, nodeProcInfo.argv); + } + ); + + jqUnit.test( + "Test getProcessPath() with non-running process id", + function () { + jqUnit.assertNull( + "Path of negative process id value", processesBridge.getProcessPath(-1) + ); + } + ); + + jqUnit.test( + "Test getProcessPath() against mock node process object.", + function () { + var fullPath = processesBridge.getProcessPath(processesBridge.mockNode.pid); + + jqUnit.assertEquals("Node process fullPath", + processesBridge.mockNode.fullPath, fullPath); + } + ); + + jqUnit.test( + "Test findProcessesByCmd()/findFirstProcessByCmd()", + function () { + // Test without the processes array optional argument. + var procInfos = processesBridge.findProcessesByCommand("node"); + jqUnit.assertNotEquals( + "Getting all 'node' processes (no 'procArray' option)", + 0, + procInfos.length + ); + procInfos.forEach(function (aProcInfo) { + jqUnit.assertEquals( + "Node commmand name (no 'procArray' option)", + "node", + aProcInfo.command + ); + }); + var procInfo = processesBridge.findFirstProcessByCommand("node"); + jqUnit.assertNotNull( + "Looking for first 'node' processes", procInfo); + jqUnit.assertEquals("Node commmand name", "node", procInfo.command); + + // unicorns are mythical in the mock processes list. + procInfos = processesBridge.findProcessesByCommand("unicorn"); + jqUnit.assertEquals( + "Looking for non-existent 'unicorn' processes (no 'procArray' option)", + 0, + procInfos.length + ); + procInfo = processesBridge.findFirstProcessByCommand("unicorn"); + jqUnit.assertNull( + "Looking for non-existent 'unicorn' process", procInfo); + + // Test with the optional processes array argument. + var procArray = gpii.tests.processes.mockProcessesList; + procInfos = processesBridge.findProcessesByCommand("node", procArray); + jqUnit.assertNotEquals( + "Getting all 'node' processes (using 'procArray' option)", + 0, + procInfos.length + ); + procInfos.forEach(function (aProcInfo) { + jqUnit.assertEquals( + "Node commmand name (using 'procArray' option)", + "node", + aProcInfo.command + ); + }); + + // unicorns are mythical in the mock processes list. + procInfos = processesBridge.findProcessesByCommand("unicorn", procArray); + jqUnit.assertEquals( + "Looking for non-existent 'unicorn' processes (using 'procArray' option)", + 0, + procInfos.length + ); + } + ); + + jqUnit.test( + "Test initProcInfoNotRunning()", + function () { + var notRunning = processesBridge.initProcInfoNotRunning("grep"); + jqUnit.assertEquals("Command name", notRunning.command, "grep"); + jqUnit.assertEquals("Negative process id", notRunning.pid, -1); + jqUnit.assertEquals( + "'NoSuchProcess' state", notRunning.state, "NoSuchProcess" + ); + jqUnit.assertNull( + "Search negative process id value", + processesBridge.findProcessByPid(notRunning.pid) + ); + } + ); + + jqUnit.test( + "Test isRunning() with mock node process, and nonexistent process", + function () { + var procInfo = processesBridge.findProcessByPid(processesBridge.mockNode.pid); + jqUnit.assertNotNull("Searching for 'node' process", procInfo); + jqUnit.assertTrue( + "Check node is running", + processesBridge.isRunning(procInfo.state) + ); + procInfo = processesBridge.initProcInfoNotRunning("grep"); + jqUnit.assertFalse( + "Check nonexistent process running", + processesBridge.isRunning(procInfo.state) + ); + } + ); + + jqUnit.test( + "Test updateProcInfo() against non-changing process", + function () { + var procInfo = processesBridge.findProcessByPid(processesBridge.mockNode.pid); + jqUnit.assertNotNull("Looking for 'node' processes", procInfo); + var newProcInfo = processesBridge.updateProcInfo(procInfo); + jqUnit.assertDeepEq( + "Check change in process info", procInfo, newProcInfo + ); + } + ); + + jqUnit.test( + "Test updateProcInfo() against changing process", + function () { + // "Start" firefox by adding to the process list. + gpii.tests.processes.startFireFox(processesBridge); + var fireFoxInfo = processesBridge.findProcessByPid(processesBridge.mockFirefox.pid); + jqUnit.assertNotNull("Search 'firefox' process", fireFoxInfo); + + // "Kill" firefox by removing it from the process list. + gpii.tests.processes.stopFireFox(processesBridge); + var newFireFoxInfo = processesBridge.updateProcInfo(fireFoxInfo); + jqUnit.assertNotEquals( + "Update process state", newFireFoxInfo.state, fireFoxInfo.state + ); + } + ); + + jqUnit.test( + "Test updateProcInfo() against non-existent process", + function () { + var procInfo = processesBridge.initProcInfoNotRunning("unicorn"); + var updatedProcInfo = processesBridge.updateProcInfo(procInfo); + jqUnit.assertDeepEq( + "Updated non-existent process (still non-existent)", + procInfo, updatedProcInfo + ); + } + ); + + jqUnit.test( + "Test findSolutionsByCommands()", + function () { + // Node is running. Add a running firefox. No such command as T6y7u8i9. + gpii.tests.processes.startFireFox(processesBridge); + var solutions = ["node", "firefox", "T6y7u8i9"]; + var procInfos = processesBridge.findSolutionsByCommands(solutions); + jqUnit.assertTrue("Node and Firefox processes", procInfos.length >= 2); + procInfos.forEach(function (item) { + var isNode = item.command === "node"; + var isFirefox = item.command === "firefox"; + jqUnit.assertTrue("Process name 'node' or 'firefox'", isNode || isFirefox); + }); + } + ); + + jqUnit.test( + "Test findSolutionsByPids()", + function () { + // Node is running. Add a running Firefox process. Use -1 as the + // pid for a non-existent process. + gpii.tests.processes.startFireFox(processesBridge); + var pids = [processesBridge.mockNode.pid, processesBridge.mockFirefox.pid, -1]; + var procInfos = processesBridge.findSolutionsByPids(pids); + jqUnit.assertEquals("Node and Firefox processes", 2, procInfos.length); + procInfos.forEach(function (item) { + var isNode = item.pid === processesBridge.mockNode.pid; + var isFirefox = item.pid === processesBridge.mockFirefox.pid; + jqUnit.assertTrue("Process pid 'node' or 'firefox'", isNode || isFirefox); + }); + } + ); + + // TODO: consider making base grade getProcessList() fluid.notImplemented. + jqUnit.test( + "Test base grade getProcessList()", + function () { + var empty = gpii.processes.getProcessList(); + jqUnit.assertEquals("Base grade getProcessList() empty", 0, empty.length); + } + ); +}; diff --git a/gpii/node_modules/settingsHandlers/index.js b/gpii/node_modules/settingsHandlers/index.js index 2c2775cf9..38f25bbc4 100644 --- a/gpii/node_modules/settingsHandlers/index.js +++ b/gpii/node_modules/settingsHandlers/index.js @@ -1,10 +1,15 @@ -/*global require, __dirname */ +"use strict"; + var fluid = require("infusion"); -var loader = fluid.getLoader(__dirname); +fluid.module.register("settingsHandlers", __dirname, require); -loader.require("./src/js/settingsHandlerUtilities.js"); -loader.require("./src/js/INISettingsHandler.js"); -loader.require("./src/js/XMLSettingsHandler.js"); -loader.require("./src/js/JSONSettingsHandler.js"); -loader.require("./src/js/NoSettingsHandler.js"); +require("./src/INISettingsHandler.js"); +require("./src/JawsSettingsHandler.js"); +require("./src/XMLSettingsHandler.js"); +require("./src/JSONSettingsHandler.js"); +require("./src/WebSocketsSettingsHandler.js"); +require("./src/NoSettingsHandler.js"); +require("./src/ExplodingSettingsHandler.js"); +require("./src/LaunchHandlers.js"); +require("./src/RemoteFileSettingsHandler.js"); diff --git a/gpii/node_modules/settingsHandlers/package.json b/gpii/node_modules/settingsHandlers/package.json index 9bf5508d2..e46a4c25f 100644 --- a/gpii/node_modules/settingsHandlers/package.json +++ b/gpii/node_modules/settingsHandlers/package.json @@ -1,19 +1,14 @@ { - "name": "settingsHandlers", - "description": "Cross Platform settingsHandlers to use in the GPII personalization framework. SettingsHandlers are responsible for writing the settings of an application - where/how to write them depends on the settingsHandler in question (it could be XML, JSON, etc).", - "version": "0.1", - "author": "GPII", - "bugs": "http://wiki.gpii.net/index.php/Main_Page", - "homepage": "http://gpii.net/", - "dependencies": {}, - "licenses": [ - { - "type": "BSD-3-Clause", - "url": "http://www.opensource.org/licenses/BSD-3-Clause" - } - ], - "keywords": ["gpii", "accessibility", "settings", "fluid", "IoC", "Inversion of Control", "configuration", "evented"], - "repository": "git://github.com/GPII/universal.git", - "main": "./index.js", - "engines": { "node" : ">=0.1.9" } + "name": "settingsHandlers", + "description": "Cross Platform settingsHandlers to use in the GPII personalization framework. SettingsHandlers are responsible for writing the settings of an application - where/how to write them depends on the settingsHandler in question (it could be XML, JSON, etc).", + "version": "0.3.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "keywords": ["gpii", "accessibility", "settings", "fluid", "IoC", "Inversion of Control", "configuration", "evented"], + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } } diff --git a/gpii/node_modules/settingsHandlers/src/ExplodingSettingsHandler.js b/gpii/node_modules/settingsHandlers/src/ExplodingSettingsHandler.js new file mode 100644 index 000000000..e9143e63d --- /dev/null +++ b/gpii/node_modules/settingsHandlers/src/ExplodingSettingsHandler.js @@ -0,0 +1,118 @@ +/*! + GPII Exploding Settings Handler + + Copyright 2018 Raising the Floor - International + + Licensed under the New BSD license. You may not use this file except in + compliance with this License. + + You may obtain a copy of the License at + https://github.com/gpii/universal/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.settingsHandlers.exploding"); + +/** The exploding settings handler is used in both automated and manual tests in order to verify that errors which occur + * when reading or writing system settings are appropriately signalled to the user. To aid in doing this, it accepts + * various material in its settings payload encoding the method to be used when failing, and special preference sets + * have been drawn up to be used in manual testing which make use of these payloads + */ + +/** + * The payload type sent to some exploding settings handler methods. + * @typedef {Object} {ExplodingPayload} + * @property {String} operation - Either "get" or "set" depending on which settings handler method was requested + * @property {String} explodeOn - Either "get" or "set" depending on which settings handler method was required to explode + * @property {Object} errorPayload - [optional] An error payload (with member `isError: true`) which should be sent to either the + * "reject" or "throw" operations if either of these was requested in `explodeMethod` + * @property {String} explodeMethod - Either: + * "reject", if the explosion should be signalled via returning a rejected promise + * "fail", if the explosion should be signalled via calling `fluid.fail` + * "throw", if the explosion should be signalled by throwing a bare execption + */ + +/** The implementation for the exploding settings handler. + * @param {String} operation - Either "get" or "set" + * @param {ExplodingPayload} explodeSettings - Settings payload describing whether the settings handler should explode + * @return {Promise|None} A rejected promise if the operation should explode and the payload requested that it should do so by returning a rejected promise + */ + +gpii.settingsHandlers.exploding.explode = function (operation, explodeSettings) { + var errorMessage = "Exploding settings handler has failed on " + operation + " method"; + var errorPayload = explodeSettings.errorPayload || { + isError: true, + message: errorMessage + }; + if (explodeSettings.explodeMethod === "reject") { + return fluid.promise().reject(errorPayload); + } else if (explodeSettings.explodeMethod === "fail") { + fluid.fail(errorMessage); + } else if (explodeSettings.explodeMethod === "throw") { + throw new Error(errorMessage); + } +}; + +/** Global store for mock solution running state **/ +gpii.settingsHandlers.exploding.runningState = { + running: false +}; + +/** Return an explosion or settings, depending on whether the operation in progress matches the one designated to explode + * @param {String} operation - Either "get", "set", "launch" or "stop" + * @param {ExplodingPayload} explodeSettings - Settings payload describing whether the settings handler should explode + * @param {Object} settings - Settings handler options which may include a member + * {Boolean} options.launchHandler Set to `true` if this settings handler is being invoked as a launch handler + * {Object} options.preferences The user's entire preference set (for tunnelling launch settings) + * @return {Promise|Value} A rejected promise if an error should have been signalled, and should have been signalled by that method, or otherwise the + * appropriate get/set settingsHandler payload. + */ +gpii.settingsHandlers.exploding.determineExplode = function (operation, explodeSettings, settings) { + if (explodeSettings.explodeOn === operation) { + return gpii.settingsHandlers.exploding.explode(operation, explodeSettings); + } else { + if (operation === "get") { + return "running" in settings ? gpii.settingsHandlers.exploding.runningState : settings; + } else { + return "running" in settings ? {running: {newValue: settings.running, oldValue: !settings.running}} : + fluid.transform(settings, function (value) { + return {newValue: value}; + }); + } + } +}; + +/** Core implementation of the exploding settings handler. + * @param {String} operation - Either "get" or "set" + * @param {ExplodingPayload|Object} settings - Settings payload describing whether the settings handler should explode, or standard settings payload + * @param {Object} options - Settings handler options which may include a member + * {Boolean} options.launchHandler Set to `true` if this settings handler is being invoked as a launch handler + * {Object} options.preferences The user's entire preference set (for tunnelling launch settings) + * @return {Promise|Value} A rejected promise if an error should have been signalled, and should have been signalled by that method, or the + * original input payload if no error should be signalled. + */ +// In the case of "launch"/"stop" interception we use hard-coded knowledge of the exact structure of the preference set as +// being explodeLaunchHandlerXxxxx.json +gpii.settingsHandlers.exploding.impl = function (operation, settings, options) { + if (options && options.launchHandler && operation === "set") { + gpii.settingsHandlers.exploding.runningState.running = settings.running; + var launchOperation = settings.running ? "launch" : "stop"; + var launchSettings = fluid.get(options, ["preferences", "contexts", "gpii-default", "preferences", "http://registry.gpii.net/applications/net.gpii.explode"]); + return gpii.settingsHandlers.exploding.determineExplode(launchOperation, launchSettings, settings); + } else { + return gpii.settingsHandlers.exploding.determineExplode(operation, settings, settings); + } +}; + +// Top-level driver methods for SettingsHandler +["get", "set"].forEach(function (operation) { + gpii.settingsHandlers.exploding[operation] = function (payload) { + return gpii.settingsHandlers.invokeSettingsHandler(function (payload) { + return gpii.settingsHandlers.exploding.impl(operation, payload.settings, payload.options); + }, payload); + }; +}); diff --git a/gpii/node_modules/settingsHandlers/src/INISettingsHandler.js b/gpii/node_modules/settingsHandlers/src/INISettingsHandler.js new file mode 100644 index 000000000..7e6f92bb1 --- /dev/null +++ b/gpii/node_modules/settingsHandlers/src/INISettingsHandler.js @@ -0,0 +1,38 @@ +/*! +GPII INI Settings Handler + +Copyright 2012 Raising the Floor - International + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +require("./settingsHandlerUtilities.js"); + +fluid.registerNamespace("gpii.settingsHandlers.INISettingsHandler.parser"); + +gpii.settingsHandlers.INISettingsHandler.parser.parse = function (content, options) { + return gpii.iniFile.read(content, options); +}; + +gpii.settingsHandlers.INISettingsHandler.parser.stringify = function (content, options) { + return gpii.iniFile.writeFromFile(options.filename, content, options); +}; + +/* PUBLIC API FUNCTIONS */ +gpii.settingsHandlers.INISettingsHandler.get = + gpii.settingsHandlers.makeFileGet(gpii.settingsHandlers.INISettingsHandler.parser); +gpii.settingsHandlers.INISettingsHandler.set = + gpii.settingsHandlers.makeFileSet(gpii.settingsHandlers.INISettingsHandler.parser); diff --git a/gpii/node_modules/settingsHandlers/src/JSONSettingsHandler.js b/gpii/node_modules/settingsHandlers/src/JSONSettingsHandler.js new file mode 100644 index 000000000..6a0e98c98 --- /dev/null +++ b/gpii/node_modules/settingsHandlers/src/JSONSettingsHandler.js @@ -0,0 +1,33 @@ +/* + * GPII JSON Settings Handler + * + * Copyright 2012 OpenDirective and Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + settingsHandlers = fluid.registerNamespace("gpii.settingsHandlers"), + parser = fluid.registerNamespace("gpii.settingsHandlers.JSONSettingsHandler.parser"); + +parser.parse = function (content) { + return JSON.parse(content); +}; + +parser.stringify = function (content) { + return fluid.prettyPrintJSON(content); +}; + +/* PUBLIC API FUNCTIONS */ +settingsHandlers.JSONSettingsHandler.get = settingsHandlers.makeFileGet(parser); +settingsHandlers.JSONSettingsHandler.set = settingsHandlers.makeFileSet(parser); diff --git a/gpii/node_modules/settingsHandlers/src/JawsSettingsHandler.js b/gpii/node_modules/settingsHandlers/src/JawsSettingsHandler.js new file mode 100644 index 000000000..afcc0e0f5 --- /dev/null +++ b/gpii/node_modules/settingsHandlers/src/JawsSettingsHandler.js @@ -0,0 +1,69 @@ +/*! +* JAWS Settings Handler +* +* Copyright 2012 Raising the Floor - International +* +* Licensed under the New BSD license. You may not use this file except in +* compliance with this License. +* +* The research leading to these results has received funding from the European Union's +* Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. +* +* You may obtain a copy of the License at +* https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + path = require("path"), + gpii = fluid.registerNamespace("gpii"); + +require("./INISettingsHandler.js"); + +fluid.registerNamespace("gpii.settingsHandlers.JAWSSettingsHandler.parser"); + +gpii.settingsHandlers.JAWSSettingsHandler.getVoiceProfileFileName = function (options) { + var iniPayload = { + "com.freedomscientific.jaws": [{ + options: { + "defaultSettingsFilePath": options.defaultSettingsFilePath + }, + settings: { + "Voice Profiles.ActiveVoiceProfileName": null + } + }] + }; + var response = gpii.settingsHandlers.INISettingsHandler.get(iniPayload); + var solutions = response["com.freedomscientific.jaws"]; + var voiceProfileFileName = undefined; + + if (Array.isArray(solutions) && solutions.length) { + var solution = solutions[0]; + var settingValue = fluid.get(solution, ["settings", "Voice Profiles.ActiveVoiceProfileName"]); + + if (settingValue) { + voiceProfileFileName = path.join(options.voiceProfilesDirPath, settingValue + ".VPF"); + } else { + voiceProfileFileName = path.join(options.voiceProfilesDirPath, "Eloquence.VPF"); + } + } + + return voiceProfileFileName; +}; + +gpii.settingsHandlers.JAWSSettingsHandler.parser.parse = function (content, options) { + return gpii.iniFile.read(content, options); +}; + +gpii.settingsHandlers.JAWSSettingsHandler.parser.stringify = function (content, options) { + return gpii.iniFile.writeFromFile(options.defaultSettingsFilePath, content, options); +}; + +/* PUBLIC API FUNCTIONS */ +gpii.settingsHandlers.JAWSSettingsHandler.get = + gpii.settingsHandlers.makeFileGet(gpii.settingsHandlers.JAWSSettingsHandler.parser, + gpii.settingsHandlers.JAWSSettingsHandler.getProfileFileName); +gpii.settingsHandlers.JAWSSettingsHandler.set = + gpii.settingsHandlers.makeFileSet(gpii.settingsHandlers.JAWSSettingsHandler.parser, + gpii.settingsHandlers.JAWSSettingsHandler.getProfileFileName); diff --git a/gpii/node_modules/settingsHandlers/src/LaunchHandlers.js b/gpii/node_modules/settingsHandlers/src/LaunchHandlers.js new file mode 100644 index 000000000..85d895235 --- /dev/null +++ b/gpii/node_modules/settingsHandlers/src/LaunchHandlers.js @@ -0,0 +1,79 @@ +/* + * GPII Launch Handlers + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = fluid || require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +/** + * The flexibleHandler is a highly customizable launch handler that is implemented to handle all + * the cases in which a solution's launch-cycle is too complex or specific to efficiently be handled + * in one of the standard launchHandlers. + * + * Besides the normal launch handler options like retryOptions and verifySettings, the flexible handler + * supports three directives: `getState`, `setTrue` and `setFalse`. + * + * `getState`: equals the launchHandler's "get" functionality, and should return a boolean value indicating + * whether the application is running or not. The content of this directive will be sent to the + * processReporter's handleIsRunning function + * `setTrue`: Is run when a "true" value is sent to the launch handler. The content of this block should + * be an array of objects that can be run as graded functions in infusion. The result of runnings these + * blocks should be that the application is running + * `setFalse`: Identical to `setTrue` but is run when a "false" is sent to the launch handler. The result from + * running the blocks in this directive should be that the application is closed/not running. + */ +fluid.registerNamespace("gpii.launchHandlers.flexibleHandler"); + +gpii.launchHandlers.flexibleHandler.set = function (payload) { + return fluid.transform(payload, function (allSettingsBlocks, solutionId) { + return fluid.transform(allSettingsBlocks, function (handlerData) { + // find desired state { running: X } + var desiredState = handlerData.settings.running; + if (desiredState !== true && desiredState !== false) { + fluid.fail("Unable to set the launch state of ", solutionId, " to ", desiredState); + } + + // get current state + var currentState = gpii.launchHandlers.flexibleHandler.executeGetBlock(handlerData.options, solutionId); + + // if not in desired state + if (currentState !== desiredState) { + desiredState === true ? + gpii.launchHandlers.flexibleHandler.executeSetBlock(handlerData, "setTrue") : + gpii.launchHandlers.flexibleHandler.executeSetBlock(handlerData, "setFalse"); + } + + return gpii.settingsHandlers.setSettings(handlerData, { running: currentState }); + }); + }); +}; + +gpii.launchHandlers.flexibleHandler.get = function (payload) { + return fluid.transform(payload, function (allSettingsBlocks, solutionId) { + return fluid.transform(allSettingsBlocks, function (handlerData) { + var currentState = gpii.launchHandlers.flexibleHandler.executeGetBlock(handlerData.options, solutionId); + return { settings: { running: currentState }}; + }); + }); +}; + +gpii.launchHandlers.flexibleHandler.executeSetBlock = function (mainEntry, blockName) { + var setBlocks = fluid.get(mainEntry, [ "options", blockName ]); + fluid.each(fluid.makeArray(setBlocks), function (setBlock) { + fluid.invokeGradedFunction(setBlock.type, setBlock); + }); +}; + +gpii.launchHandlers.flexibleHandler.executeGetBlock = function (options) { + return gpii.processReporter.handleIsRunning({ isRunning: options.getState }); +}; diff --git a/gpii/node_modules/settingsHandlers/src/NoSettingsHandler.js b/gpii/node_modules/settingsHandlers/src/NoSettingsHandler.js new file mode 100644 index 000000000..b396dd31b --- /dev/null +++ b/gpii/node_modules/settingsHandlers/src/NoSettingsHandler.js @@ -0,0 +1,38 @@ +/* + * GPII No Settings Settings Handler + * + * Copyright 2012 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.settingsHandlers.noSettings"); + +// Get/Set settings handlers for "gpii.settingsHandlers.noSettings" +gpii.settingsHandlers.noSettingsGet = function () { + return function (payload) { + return gpii.settingsHandlers.transformPayload(payload, gpii.settingsHandlers.getSettings); + }; +}; + +gpii.settingsHandlers.noSettingsSet = function () { + return function (payload) { + return gpii.settingsHandlers.transformPayload(payload, gpii.settingsHandlers.setSettings); + }; +}; + +gpii.settingsHandlers.noSettings.get = gpii.settingsHandlers.noSettingsGet(); +gpii.settingsHandlers.noSettings.set = gpii.settingsHandlers.noSettingsSet(); diff --git a/gpii/node_modules/settingsHandlers/src/RemoteFileSettingsHandler.js b/gpii/node_modules/settingsHandlers/src/RemoteFileSettingsHandler.js new file mode 100644 index 000000000..edc5a4286 --- /dev/null +++ b/gpii/node_modules/settingsHandlers/src/RemoteFileSettingsHandler.js @@ -0,0 +1,485 @@ +/* Remote file settings handler. + * + * Copyright 2019 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +/* + * The process of applying a setting is as follows: + * 1. Download the file + * 2. Move the original target file out of the way, into another location. + * 3. Copy the downloaded file onto the target path. + * + * + * For restoring a setting: + * 1. Remove the file in the target path. + * 2. Move the original file from stash onto the target path. + * + * A payload for this setting handler would look something like this: + * + * { + * "settings": { + * "first-setting": "first-value", + * "another-setting": "another-value", + * }, + * "options": { + * "settings": { + * "first-setting": { + * // Location of the target file. + * "path": "c:\\somewhere\\file.xyz", + * + * // From where the new file is downloaded, %value will expand to "first-value" + * "url": "https://example.com/gpii-files/%value", + * }, + * "another-setting": { + * "path": "/etc/passwd", + * "url": "https://example.com/gpii-files/%value" + * } + * } + * } + * } + * + */ + +var path = require("path"), + request = require("request"), + fs = require("fs"), + crypto = require("crypto"), + fluid = require("infusion"), + mkdirp = require("mkdirp"), + gpii = fluid.registerNamespace("gpii"); + +var remoteFileSettingsHandler = fluid.registerNamespace("gpii.settingsHandlers.remoteFileSettingsHandler"); + +// Handles a single file download or restore. +fluid.defaults("gpii.settingsHandlers.remoteFileDownload", { + gradeNames: ["fluid.component"], + components: { + settingsDir: { + type: "gpii.settingsDir" + } + }, + members: { + // The URL for the download + url: null, + // The path of the target file. + path: null, + // The setting value. + value: null, + // Where the download is stored. + downloadPath: null, + // Where the original file is kept. + stashPath: null, + cache: null, + gpiiSettingsDir: "@expand:{settingsDir}.getGpiiSettingsDir()" + }, + invokers: { + applyFile: { + funcName: "gpii.settingsHandlers.remoteFileSettingsHandler.applyFile", + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] // path, url, value + }, + restoreFile: { + funcName: "gpii.settingsHandlers.remoteFileSettingsHandler.restoreFile", + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] // path, stashPath, value + } + }, + events: { + applyFile: null, + restoreFile: null + }, + listeners: { + "applyFile.download": { + priority: "first", + funcName: "gpii.settingsHandlers.remoteFileSettingsHandler.downloadFile", + args: ["{that}.url", "{that}.downloadPath", "{that}.cache"] + }, + "applyFile.backup": { + priority: "after:download", + funcName: "gpii.settingsHandlers.remoteFileSettingsHandler.createBackup", + args: ["{that}.path", "{that}.gpiiSettingsDir"] + }, + "applyFile.stash": { + priority: "after:backup", + funcName: "gpii.settingsHandlers.remoteFileSettingsHandler.transferFile", + args: ["{that}.path", "{that}.stashPath"] + }, + "applyFile.write": { + priority: "after:stash", + funcName: "gpii.settingsHandlers.remoteFileSettingsHandler.transferFile", + args: [ + // source, destination, keepSource + "{that}.downloadPath", + "{that}.path", + "{that}.cache" + ] + }, + "applyFile.end": { + priority: "last", + funcName: "fluid.identity", + args: ["{that}.stashPath"] + }, + + "restoreFile.write": { + priority: "first", + funcName: "gpii.settingsHandlers.remoteFileSettingsHandler.transferFile", + args: ["{that}.stashPath", "{that}.path"] + } + } +}); + +/** + * Hash of urls that have been downloaded and cached. This is to tell if a download has occurred in this instance, and + * to use the cached download. Otherwise, download a fresh copy. + */ +gpii.settingsHandlers.remoteFileSettingsHandler.cached = {}; + +/** + * Applies a file setting. + * + * This downloads the new file, moves the old one out the way, and copies the new file in its place. + * + * @param {Component} that The gpii.settingsHandlers.remoteFileDownload instance. + * @param {String} targetPath The path to the target file. + * @param {String} url The URL (a string template accepting a "%value" token) of the new file. + * @param {String} value The setting value. + * @return {Promise} Resolves when complete, with a string value of the stashed file. + */ +gpii.settingsHandlers.remoteFileSettingsHandler.applyFile = function (that, targetPath, url, value) { + that.path = targetPath; + that.value = value; + that.url = fluid.stringTemplate(url, { + value: encodeURIComponent(that.value) + }); + + // Generate the download destination file based on the url + var sha1 = crypto.createHash("sha1"); + sha1.update(that.url); + that.downloadPath = path.join(that.gpiiSettingsDir, "download", sha1.digest("hex")); + + var stashFile = path.basename(that.path) + "." + (new Date().toISOString().replace(/:/g, "")) + ".gpii-stashed"; + that.stashPath = path.join(that.gpiiSettingsDir, "stash", stashFile); + + var existed = fs.existsSync(that.path); + + var promise = fluid.promise(); + fluid.promise.fireTransformEvent(that.events.applyFile).then(function (value) { + fluid.log("remoteFileSettingsHandler applied " + that.path + " from " + that.url); + promise.resolve(value); + }, function (reason) { + fluid.log("remoteFileSettingsHandler failed " + that.path + " from " + that.url, ": ", reason); + var restorePromise; + // try to put things back if it fails. + if (fs.existsSync(that.stashPath) === existed) { + // Copy the stashed file back to the original place + restorePromise = remoteFileSettingsHandler.transferFile(that.stashPath, that.path); + } else { + restorePromise = fluid.promise().resolve(); + } + + restorePromise.then(function () { + promise.reject(reason); + }, function (innerReason) { + fluid.log("remoteFileSettingsHandler failed to restore: ", innerReason); + promise.reject(reason); + }); + }); + + return promise; +}; + + +/** + * Restores a file. + * + * @param {Component} that The gpii.settingsHandlers.remoteFileDownload instance. + * @param {String} targetPath The path to the setting file. + * @param {String} stashPath The path to the original copy of the setting file. + * @param {String} value The setting value. + * @return {Promise} Resolves when complete. + */ +gpii.settingsHandlers.remoteFileSettingsHandler.restoreFile = function (that, targetPath, stashPath, value) { + that.path = targetPath; + that.stashPath = stashPath; + that.value = value; + return fluid.promise.fireTransformEvent(that.events.restoreFile); +}; + + +/** + * Returns a function which can be used as a callback to asynchronous functions, which will resolve or reject + * the given promise based on the existence of the single parameter. + * + * In other words, if the async function fails, the promise will reject (with the error passed to the callback), + * otherwise the promise will resolve with the given value. + * + * @param {Promise} promise The promise to reject or resolve. + * @param {Object} value [optional] The value with which the promise is resolved. + * @return {Function} A callback function that can be used for standard async functions. + */ +gpii.settingsHandlers.remoteFileSettingsHandler.callbackToPromise = function (promise, value) { + return function (err) { + if (err) { + promise.reject(err); + } else { + promise.resolve(value); + } + }; +}; + +/** + * Downloads a file. + * + * @param {String} url The remote file location. + * @param {String} downloadTo The path to the local file to which the download is saved. + * @param {Boolean} cache true to use the existing file at downloadTo, if it exists. + * @return {Promise} Resolves, with the path of the new file. + */ +gpii.settingsHandlers.remoteFileSettingsHandler.downloadFile = function (url, downloadTo, cache) { + var promise = fluid.promise(); + + var downloadExists = fs.existsSync(downloadTo); + if (downloadExists && cache && gpii.settingsHandlers.remoteFileSettingsHandler.cached[url]) { + promise.resolve(downloadTo); + } else { + + // The intermediate filename (named differently to signify the download is incomplete). + var tempFile = downloadTo + ".downloading"; + var outStream; + + fluid.log("remoteFileSettingsHandler: Downloading " + url + " to " + downloadTo); + + promise.then(function () { + gpii.settingsHandlers.remoteFileSettingsHandler.cached[url] = cache; + }, function () { + // Always close the output stream after an error. + if (outStream) { + outStream.close(); + } + }); + + var req = request.get({ + uri: url + }); + + req.on("error", promise.reject); + + req.on("response", function (response) { + if (response.statusCode === 200) { + mkdirp.sync(path.dirname(tempFile)); + outStream = fs.createWriteStream(tempFile); + response.pipe(outStream); + response.on("end", function () { + outStream.close(); + outStream = null; + fs.rename(tempFile, downloadTo, + remoteFileSettingsHandler.callbackToPromise(promise, downloadTo)); + }); + } else if (downloadExists) { + // Resolve with the last download. + promise.resolve(downloadTo); + } else { + promise.reject({ + isError: true, + message: "Unable to download " + url + ": " + response.statusCode + }); + } + }); + } + return promise; +}; + +/** + * Create a single backup of a file. Target files are copied to the backups subdirectory of the settings directory, the + * first time they are updated by this settings handler. This is to allow someone to recover their data from some + * known-good point (that is, before Morphic touched it). + * + * @param {String} originalFile The file to backup. + * @param {String} settingsDir The settings directory. + * @return {Promise} Resolves when complete. + */ +gpii.settingsHandlers.remoteFileSettingsHandler.createBackup = function (originalFile, settingsDir) { + var backupPath = path.join(settingsDir, "backups", path.basename(originalFile)); + + var promise; + if (!fs.existsSync(originalFile) || fs.existsSync(backupPath)) { + promise = fluid.promise().resolve(); + } else { + promise = gpii.settingsHandlers.remoteFileSettingsHandler.transferFile(originalFile, backupPath, true); + } + return promise; +}; + +/** + * Transfers a file from one place to another, by either copying or moving it. The destination will be overwritten. + * + * If the source file does not exist, then the destination will be removed (if it exists) + * + * @param {String} source Path to the file. + * @param {String} destination Path to where the new file should be placed. + * @param {Boolean} keepSource true if the source file needs to be kept (copy the file, rather than move). + * @return {Promise} Resolves when complete. + */ +gpii.settingsHandlers.remoteFileSettingsHandler.transferFile = function (source, destination, keepSource) { + var promise = fluid.promise(); + + if (fs.existsSync(source)) { + var destDir = path.dirname(destination); + if (!fs.existsSync(destDir)) { + mkdirp.sync(destDir); + } + + if (keepSource) { + fs.copyFile(source, destination, remoteFileSettingsHandler.callbackToPromise(promise)); + } else { + fs.rename(source, destination, function (err) { + if (err) { + if (err.code === "EXDEV") { + // Tried to move a file over different filesystems - copy + delete instead. + remoteFileSettingsHandler.transferFile(source, destination, true).then(function () { + fs.unlink(source, remoteFileSettingsHandler.callbackToPromise(promise)); + }, promise.reject); + } else { + promise.reject(err); + } + } else { + promise.resolve(); + } + }); + } + } else { + fs.unlink(destination, function (err) { + if (err && err.code !== "ENOENT") { + promise.reject(err); + } else { + promise.resolve(); + } + }); + } + return promise; +}; + +/** + * Setter for the remote file settings handler. + * + * { + * "settings": { + * "word-ribbon": "file1" + * }, + * "options": { + * "settings": { + * "word-ribbon": { + * "path": "c:\\ ...", + * "url": "https://example.com/" + * }, + * "excel-ribbon": { + * "path": "c:\\ ...", + * "url": "https://example.com/" + * } + * } + * } + * } + * + * @param {Object} payload The payload. + * @param {String} solutionID The solution ID. + * @param {Function} componentCreator [optional] The component creator [default: remoteFileSettingsHandler()] + * @return {Promise} Resolves with the response. + */ +gpii.settingsHandlers.remoteFileSettingsHandler.setImpl = function (payload, solutionID, componentCreator) { + var promiseTogo = fluid.promise(); + + var results = {}; + var promises = []; + fluid.each(payload.settings, function (settingValue, key) { + if (settingValue) { + var value = settingValue.value || settingValue; + + var settingPromise = fluid.promise(); + promises.push(settingPromise); + settingPromise.then(null, function (reason) { + fluid.fail("remoteFileSettingsHandler failed for setting '" + key + "' in payload ", payload, reason); + }); + + var options = payload.options.settings[key]; + + var remoteFileDownload = (componentCreator || gpii.settingsHandlers.remoteFileDownload)({ + members: { + cache: options.cache + } + }); + + var p; + if (settingValue.restore) { + p = remoteFileDownload.restoreFile(options.path, settingValue.stashPath, value); + } else { + p = remoteFileDownload.applyFile(options.path, options.url, value); + } + + p.then(function (result) { + results[key] = { + oldValue: { + value: value, + stashPath: result, + restore: true + }, + newValue: value + }; + settingPromise.resolve(); + remoteFileDownload.destroy(); + }, function (reason) { + fluid.log("remoteFileDownload failed", reason); + results[key] = {failed: true}; + settingPromise.resolve(); + remoteFileDownload.destroy(); + }); + } + }); + + // Resolve the return promise when the files have been processed. + fluid.promise.sequence(promises).then(function () { + promiseTogo.resolve(results); + }); + + return promiseTogo; +}; + +/** + * Getter for the remote file settings handler. + * + * @return {Object} An empty object. + */ +gpii.settingsHandlers.remoteFileSettingsHandler.getImpl = function () { + return {}; +}; + +/** + * Invoke the settings handler. + * + * @param {Object} payload The payload + * @return {Promise} Resolves with the response. + */ +gpii.settingsHandlers.remoteFileSettingsHandler.get = function (payload) { + return gpii.settingsHandlers.invokeSettingsHandler(remoteFileSettingsHandler.getImpl, payload); +}; + +/** + * Invoke the settings handler. + * + * @param {Object} payload The payload + * @return {Promise} Resolves with the response. + */ +gpii.settingsHandlers.remoteFileSettingsHandler.set = function (payload) { + return gpii.settingsHandlers.invokeSettingsHandler(remoteFileSettingsHandler.setImpl, payload); +}; diff --git a/gpii/node_modules/settingsHandlers/src/WebSocketsSettingsHandler.js b/gpii/node_modules/settingsHandlers/src/WebSocketsSettingsHandler.js new file mode 100644 index 000000000..74442e456 --- /dev/null +++ b/gpii/node_modules/settingsHandlers/src/WebSocketsSettingsHandler.js @@ -0,0 +1,254 @@ +/*! + GPII Web Sockets Settings Handler + + Copyright 2014, 2015 Emergya + Copyright 2015 Raising the Floor - International + Copyright 2020 OCAD University + + Licensed under the New BSD license. You may not use this file except in + compliance with this License. + + You may obtain a copy of the License at + https://github.com/gpii/universal/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + $ = fluid.registerNamespace("jQuery"), + gpii = fluid.registerNamespace("gpii"); + +// For debugging -- see gpii.settingsHandlers.webSockets.settingsChanged() +// below. +// var util = require("util"); + +fluid.defaults("gpii.settingsHandlers.webSockets.component", { + gradeNames: ["fluid.modelComponent"], + changeApplierOptions: { + resolverGetConfig: fluid.model.escapedGetConfig, + resolverSetConfig: fluid.model.escapedSetConfig + }, + members: { + clients: {} + }, + model: { + settings: {} + }, + modelListeners: { + "settings.*": { + funcName: "gpii.settingsHandlers.webSockets.settingsChanged", + args: "{change}" + } + }, + invokers: { + addClient: { + funcName: "gpii.settingsHandlers.webSockets.addClient", + args: ["{that}", "{arguments}.0", "{arguments}.1"] + }, + removeClient: { + funcName: "gpii.settingsHandlers.webSockets.removeClient", + args: ["{that}", "{arguments}.0"] + }, + getSettingsForId: { + funcName: "gpii.settingsHandlers.webSockets.getSettingsForId", + args: ["{that}", "{arguments}.0"] + }, + getImpl: { + funcName: "gpii.settingsHandlers.webSockets.getImpl", + args: ["{arguments}.0", "{that}"] + }, + setImpl: { + funcName: "gpii.settingsHandlers.webSockets.setImpl", + args: ["{arguments}.0", "{that}"] + } + } +}); + +///////////////// Clients ///////////////////// + +/** + * Add a client, a web sockets message handler, to the set of clients that this + * settings handler services. This also: + * - adds a listener to the client so thatit relays "changeSettings" message + * payloads to this settings handler. + * - sends a reply back to the client containing the current settings value for + * that client's solution. + * @param {Component} that - An instance of gpii.settingsHandlers.webSockets.component. + * @param {String} solutionId - The solution that the client handles. + * @param {Component} client - The client request handler to add. + */ +gpii.settingsHandlers.webSockets.addClient = function (that, solutionId, client) { + client.events.onReceiveMessage.addListener(gpii.flowManager.browserChannel.receiveChangeSettingsMsg); + var initialSettings = fluid.get(that.model.settings, [solutionId]); + var currentValue = fluid.get(that.clients, [solutionId, client.id]); + if (!currentValue) { + fluid.set(that.clients, [solutionId, client.id], client); + client.sendTypedMessage("connectionSucceeded", initialSettings); + client.events.onDestroy.addListener(function () { + that.removeClient(client); + }); + } +}; + +/** + * Remove a client, a web sockets message handler, from the set of clients that + * this settings handler services. + * @param {Component} that - An instance of gpii.settingsHandlers.webSockets.component. + * @param {Component} client - The client request handler to remove. + */ +gpii.settingsHandlers.webSockets.removeClient = function (that, client) { + for (var solutionId in that.clients) { + client.events.onReceiveMessage.removeListener(gpii.flowManager.browserChannel.receiveChangeSettingsMsg); + delete that.clients[solutionId][client.id]; + if ($.isEmptyObject(that.clients[solutionId])) { + delete that.clients[solutionId]; + } + } +}; + +///////////////// Settings ///////////////////// + +/** + * For debugging: log changes to settings. + * @param {Object} change - The modified setting. + */ +gpii.settingsHandlers.webSockets.settingsChanged = function (/*change*/) { + // Function retained for debugging purposes + // Warning: the 'change' argument can be a circular JSON structure; + // Using util.inspect() instead of JSON.stringify() + // console.log("A change in settings has been registered: " + util.inspect(change)); +}; + +/** + * Get the current settings associated with the given solution persisted by this + * settings handler. + * @param {Component} that - An instance of gpii.settingsHandlers.webSockets.component. + * @param {String} solutionId - The solution whose setting are sought. + * @return {Object} - The current values of the settings for the solution. + */ +gpii.settingsHandlers.webSockets.getSettingsForId = function (that, solutionId) { + return fluid.get(that.model.settings, [solutionId]); +}; + +/** + * Get the current value of specified settings associated with the given + * solution. + * @param {Object} payload - The solution and its setting whose current values + * are to be retrieved. + * @param {String} payload.options.path - The relevant solution. + * @param {Object} payload.settings - The settings whose current values are + * sought. + * @param {Component} that - An instance of gpii.settingsHandlers.webSockets.component. + * @return {Object} - the current values of the settings passed in. + */ +gpii.settingsHandlers.webSockets.getImpl = function (payload, that) { + var path = payload.options.path; + var results = fluid.transform(payload.settings, function (value, key) { + var currentValue = fluid.get(that.model.settings, [path, key]); + return currentValue; + }); + return results; +}; + +/** + * Modify the persistent settings based on the values passed in, and notify + * relevant clients of the changes. In this regard, the client that requested + * the settings is sent an acknowledgement using a "changeSettingsReceived" + * message type. The other clients are sent a "onSettingsChanged" message. + * @param {Object} payload - Information about which settings to modify. + * @param {Object} payload.settings - The new values of the settings. + * @param {String} payload.options.path - Solution id whose settings are to change. + * @param {String} payload.options.source - The client requesting the changes. + * @param {Component} that - An instance of gpii.settingsHandlers.webSockets.component. + * @return {Object} - a set of old value, new value pairs showing the changes + * for each setting. + */ +gpii.settingsHandlers.webSockets.setImpl = function (payload, that) { + var path = payload.options.path; + var results = fluid.transform(payload.settings, function (value, key) { + var oldValue = fluid.get(that.model.settings, [path, key]); + var type; + + if (oldValue === undefined) { + type = "ADD"; + } else if (value === undefined) { + type = "DELETE"; + } else { + type = null; + } + + that.applier.change(["settings", path, key], value, type); + + var newValue = fluid.get(that.model.settings, [path, key]); + + return { + oldValue: oldValue, + newValue: newValue + }; + }); + + if ($.isEmptyObject(that.model.settings[path])) { + delete that.model.settings[path]; + } + + gpii.settingsHandlers.webSockets.notifySettings(payload.options, that); + return results; +}; + +/** + * Notify relevant clients about changes to their solution's settings with + * either a "onSettingsChanged" or "changeSettingsReceived" message type. The + * client that requested the changes is sent the "changeSettingsReceived" + * response. All others are notified of the changes ("onSettingsChanged"). + * @param {Object} clientOptions - Information about which solutions and clients + * to notify. + * @param {String} clientOptions.path - The relevant solution id. + * @param {Component} clientOptions.source - The client who requested the + * changes. + * @param {Component} that - An instance of gpii.settingsHandlers.webSockets.component. + * managing clients and their solution's settings. + */ +gpii.settingsHandlers.webSockets.notifySettings = function (clientOptions, that) { + var solutionId = clientOptions.path; + if (solutionId in that.clients) { + var newSettings = gpii.settingsHandlers.webSockets.getSettingsForId(that, solutionId); + for (var clientId in that.clients[solutionId]) { + var client = that.clients[solutionId][clientId]; + if (client === clientOptions.source) { + client.sendTypedMessage("changeSettingsReceived", newSettings); + } else { + client.sendTypedMessage("onSettingsChanged", newSettings); + } + } + } +}; + +/////////// Top-level driver methods for SettingsHandler /////////////// + +/** + * Retrieve a solution's settings based on the given information. + * @param {Object} payload - The solution's settings to retreive. + * @param {String} payload.options.path - Solution identifier. + * @param {Object} payload.settings - The settings whose current values are + * sought. + * @return {Promise} - A promise whose value, when resolved, is the settings. + */ +gpii.settingsHandlers.webSockets.get = function (payload) { + var instance = gpii.settingsHandlers.webSockets.instance; // this is placed there by FlowManager's mountInstance + return gpii.settingsHandlers.invokeSettingsHandler(instance.getImpl, payload); +}; + +/** + * Modify a solution's settings based on the given information. + * @param {Object} payload - Array of solutions, their clients and settings to + * modify. + * @param {Component} payload.solutionId[i] - The client requesting the change. + * @param {Object} payload.solutionId[i].settings - The new values for the settings. + * @param {String} payload.solutionId[i].options.path - Solution identifier. + * @param {Component} payload.solutionId[i].options.source - The client requesting the change. + * @return {Promise} - A promise whose value, when resolved, is the modified settings. + */ +gpii.settingsHandlers.webSockets.set = function (payload) { + var instance = gpii.settingsHandlers.webSockets.instance; // this is placed there by FlowManager's mountInstance + return gpii.settingsHandlers.invokeSettingsHandler(instance.setImpl, payload); +}; diff --git a/gpii/node_modules/settingsHandlers/src/XMLSettingsHandler.js b/gpii/node_modules/settingsHandlers/src/XMLSettingsHandler.js new file mode 100644 index 000000000..a01bb2b77 --- /dev/null +++ b/gpii/node_modules/settingsHandlers/src/XMLSettingsHandler.js @@ -0,0 +1,113 @@ +/* + * GPII XML Settings Handler + * + * Copyright 2012 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + xm = require("xml-mapping"), + gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.settingsHandlers.XMLHandler"); + +/** + * "Pretty-prints" an XML string. + * + * This function is derived from sample code posted by Dan Brooks on Stack Overflow: + * http://stackoverflow.com/questions/376373/pretty-printing-xml-with-javascript + * + * @param {String} xml - The XML string to reformat. + * @return {String} The reformatted XML document. + */ +gpii.settingsHandlers.XMLHandler.formatXml = function (xml) { + var formatted = ""; + var reg = /(>)(<)(\/*)/g; + xml = xml.replace(reg, "$1\r\n$2$3"); + var pad = 0; + fluid.each(xml.split("\r\n"), function (node) { + var indent = 0; + if (node.match(/.+<\/\w[^>]*>$/)) { + indent = 0; + } else if (node.match(/^<\/\w/)) { + if (pad !== 0) { + pad -= 1; + } + } else if (node.match(/^<\w[^>]*[^\/]>.*$/)) { + indent = 1; + } else { + indent = 0; + } + + var padding = ""; + for (var i = 0; i < pad; i++) { + padding += " "; + } + + formatted += padding + node + "\r\n"; + pad += indent; + }); + return formatted; +}; + +fluid.registerNamespace("gpii.settingsHandlers.XMLHandler.parser"); + +/* + * Convert XML into JSON. + * In case options.rules is present, trasform with those rules. + * NOTE: array transformed to objects if given in knownArrays or detected + * as using 'name' as pivot element. + */ +gpii.settingsHandlers.XMLHandler.parser.parse = function (content, options) { + // Parse XML to JSON. + var json = xm.tojson(content); + // Apply the settings + return (options && options.rules) ? + fluid.model.transformWithRules(json, options.rules) : + json; +}; + +/* + * Convert JSON to XML. + * In case options.rules is present, first tranform JSON with invert rules. + */ +gpii.settingsHandlers.XMLHandler.parser.stringify = function (content, options) { + options = options || {}; + // Create XML - first line is the xml-tag from options if set. + var xml = options["xml-tag"] || ""; + // Get inverse transformation rules. + var invertedJSON; + if (options.rules) { + var inverseRules = fluid.model.transform.invertConfiguration( + options.rules); + // Transform back. + invertedJSON = fluid.model.transformWithRules(content, + inverseRules); + } else { + invertedJSON = content; + } + + xml += xm.toxml(invertedJSON); + // Fix indentation/newlines so it's readable. + xml = gpii.settingsHandlers.XMLHandler.formatXml(xml); + return xml; +}; + +/* PUBLIC API FUNCTIONS */ + +// TODO: For testing purposes, accept additional arguments in +// "solution.options" to produce data via a callback rather than +// hardcoded to hit the filesystem (see GPII-99) +gpii.settingsHandlers.XMLHandler.get = gpii.settingsHandlers.makeFileGet(gpii.settingsHandlers.XMLHandler.parser); +gpii.settingsHandlers.XMLHandler.set = gpii.settingsHandlers.makeFileSet(gpii.settingsHandlers.XMLHandler.parser); diff --git a/gpii/node_modules/settingsHandlers/src/js/INISettingsHandler.js b/gpii/node_modules/settingsHandlers/src/js/INISettingsHandler.js deleted file mode 100644 index c3c4d7502..000000000 --- a/gpii/node_modules/settingsHandlers/src/js/INISettingsHandler.js +++ /dev/null @@ -1,36 +0,0 @@ -/*! - GPII INI Settings Handler - - Copyright 2012 Raising the Floor - International - - Licensed under the New BSD license. You may not use this file except in - compliance with this License. - - You may obtain a copy of the License at - https://github.com/gpii/universal/LICENSE.txt -*/ - -/*global require*/ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - settingsHandlers = fluid.registerNamespace("gpii.settingsHandlers"), - parser = fluid.registerNamespace( - "gpii.settingsHandlers.INISettingsHandler.parser"), - iniparser = require("ini"); - - parser.parse = function (content, options) { - return iniparser.parse(content, options); - }; - - parser.stringify = function (content, options) { - return iniparser.stringify(content, null, options); - }; - - /* PUBLIC API FUNCTIONS */ - settingsHandlers.INISettingsHandler.get = settingsHandlers.makeGet(parser); - settingsHandlers.INISettingsHandler.set = settingsHandlers.makeSet(parser); -}()); diff --git a/gpii/node_modules/settingsHandlers/src/js/JSONSettingsHandler.js b/gpii/node_modules/settingsHandlers/src/js/JSONSettingsHandler.js deleted file mode 100644 index 19513b4a3..000000000 --- a/gpii/node_modules/settingsHandlers/src/js/JSONSettingsHandler.js +++ /dev/null @@ -1,35 +0,0 @@ -/*! - GPII JSON Settings Handler - - Copyright 2012 OpenDirective and Raising the Floor - International - - Licensed under the New BSD license. You may not use this file except in - compliance with this License. - - You may obtain a copy of the License at - https://github.com/gpii/universal/LICENSE.txt -*/ - -/*global require*/ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - settingsHandlers = fluid.registerNamespace("gpii.settingsHandlers"), - parser = fluid.registerNamespace( - "gpii.settingsHandlers.JSONSettingsHandler.parser"); - - parser.parse = function (content) { - return JSON.parse(content); - }; - - parser.stringify = function (content) { - return fluid.prettyPrintJSON(content); - }; - - /* PUBLIC API FUNCTIONS */ - settingsHandlers.JSONSettingsHandler.get = settingsHandlers.makeGet(parser); - settingsHandlers.JSONSettingsHandler.set = settingsHandlers.makeSet(parser); -}()); diff --git a/gpii/node_modules/settingsHandlers/src/js/NoSettingsHandler.js b/gpii/node_modules/settingsHandlers/src/js/NoSettingsHandler.js deleted file mode 100644 index d2003587d..000000000 --- a/gpii/node_modules/settingsHandlers/src/js/NoSettingsHandler.js +++ /dev/null @@ -1,24 +0,0 @@ -/* -GPII No Settings Settings Handler - -Copyright 2012 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -/*global require */ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - settingsHandlers = fluid.registerNamespace("gpii.settingsHandlers"); - - settingsHandlers.noSettings = fluid.identity; - -}()); \ No newline at end of file diff --git a/gpii/node_modules/settingsHandlers/src/js/XMLSettingsHandler.js b/gpii/node_modules/settingsHandlers/src/js/XMLSettingsHandler.js deleted file mode 100644 index ff329b868..000000000 --- a/gpii/node_modules/settingsHandlers/src/js/XMLSettingsHandler.js +++ /dev/null @@ -1,113 +0,0 @@ -/*! -GPII XML Settings Handler - -Copyright 2012 Raising the Floor - International - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -/*global require, $ */ - - -(function () { - - "use strict"; - - var fluid = require("infusion"), - xm = require("xml-mapping"), - settingsHandlers = fluid.registerNamespace("gpii.settingsHandlers"), - parser = fluid.registerNamespace( - "gpii.settingsHandlers.XMLHandler.parser"); - - /** - * "Pretty-prints" an XML string. - * - * This function is derived from sample code posted by Dan Brooks on Stack Overflow: - * http://stackoverflow.com/questions/376373/pretty-printing-xml-with-javascript - * - * @param {String} xml the XML string to reformat - * @return {String} the reformatted XML document - */ - settingsHandlers.XMLHandler.formatXml = function (xml) { - var formatted = ""; - var reg = /(>)(<)(\/*)/g; - xml = xml.replace(reg, "$1\r\n$2$3"); - var pad = 0; - fluid.each(xml.split("\r\n"), function(node) { - var indent = 0; - if (node.match( /.+<\/\w[^>]*>$/ )) { - indent = 0; - } else if (node.match( /^<\/\w/ )) { - if (pad !== 0) { - pad -= 1; - } - } else if (node.match( /^<\w[^>]*[^\/]>.*$/ )) { - indent = 1; - } else { - indent = 0; - } - - var padding = ""; - for (var i = 0; i < pad; i++) { - padding += " "; - } - - formatted += padding + node + "\r\n"; - pad += indent; - }); - return formatted; - }; - - /** - * Convert XML into JSON. - * In case options.rules is present, trasform with those rules. - * NOTE: array transformed to objects if given in knownArrays or detected - * as using 'name' as pivot element. - */ - parser.parse = function (content, options) { - // Parse XML to JSON. - var json = xm.tojson(content); - // Apply the settings - return (options && options.rules) ? - fluid.model.transformWithRules(json, options.rules) : - json; - }; - - /** - * Convert JSON to XML. - * In case options.rules is present, first tranform JSON with invert rules. - */ - parser.stringify = function (content, options) { - options = options || {}; - // Create XML - first line is the xml-tag from options if set. - var xml = options["xml-tag"] || ""; - // Get inverse transformation rules. - var invertedJSON; - if (options.rules) { - var inverseRules = fluid.model.transform.invertConfiguration( - options.rules); - // Transform back. - invertedJSON = fluid.model.transformWithRules(content, - inverseRules); - } else { - invertedJSON = content; - } - - xml += xm.toxml(invertedJSON); - // Fix indentation/newlines so it's readable. - xml = settingsHandlers.XMLHandler.formatXml(xml); - return xml; - }; - - /* PUBLIC API FUNCTIONS */ - - // TODO: For testing purposes, accept additional arguments in - // "solution.options" to produce data via a callback rather than - // hardcoded to hit the filesystem (see GPII-99) - settingsHandlers.XMLHandler.get = settingsHandlers.makeGet(parser); - settingsHandlers.XMLHandler.set = settingsHandlers.makeSet(parser); -}()); diff --git a/gpii/node_modules/settingsHandlers/src/js/settingsHandlerUtilities.js b/gpii/node_modules/settingsHandlers/src/js/settingsHandlerUtilities.js deleted file mode 100644 index ff18c0be0..000000000 --- a/gpii/node_modules/settingsHandlers/src/js/settingsHandlerUtilities.js +++ /dev/null @@ -1,129 +0,0 @@ -/*! -GPII Settings Handler Utilities - -Copyright 2013 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -var fluid = fluid || require("infusion"), - fs = require("fs"), - settingsHandlers = fluid.registerNamespace("gpii.settingsHandlers"); - -/** - * Reads file. - */ - settingsHandlers.readFile = function (options) { - if (options === undefined || - (options.path === undefined && options.filename === undefined)) { - fluid.fail("readFile: expected an options block defining filename and encoding"); - } - // TODO check for and handle read errors - var content = fs.readFileSync(options.path || options.filename, - options.encoding || "utf-8"); - return content; -}; - -/** - * Write file. - */ - settingsHandlers.writeFile = function (content, options) { - // TODO check for and handler write errors. - fs.writeFileSync(options.path || options.filename, content, - options.encoding || "utf-8"); -}; - -settingsHandlers.getSettings = function (solutionEntry, currentSettings) { - var newSettingsResponse = {}; - var userRequestedSettings = solutionEntry.settings; - - fluid.each(userRequestedSettings, function (settingVal, settingKey) { - var value = fluid.get(currentSettings, settingKey, - fluid.model.escapedGetConfig); - newSettingsResponse[settingKey] = value; - }); - - return { - settings: newSettingsResponse - }; -}; - -settingsHandlers.setSettings = function (solutionEntry, currentSettings) { - var newSettingsResponse = {}; - var userRequestedSettings = solutionEntry.settings; - var options = solutionEntry.options; - - // record differences between required and default settings - // so that they default settings can be restored - fluid.each(userRequestedSettings, function (settingVal, settingKey) { - var oldValue = fluid.get(currentSettings, settingKey, - fluid.model.escapedGetConfig); - fluid.set(currentSettings, settingKey, settingVal, - fluid.model.escapedSetConfig); - newSettingsResponse[settingKey] = { - "oldValue": oldValue, - "newValue": settingVal - }; - }); - return { options: options, settings: newSettingsResponse }; -}; - -/** - * Handle individual solution entry data. The 'modifier' function passed as - * parameter is called to allow editing of the solutionEntry based on the - * current settings (both passed as parameters to the modifier function) - */ -settingsHandlers.handleSolutionEntrry = function (solutionEntry, modifier, parser, isWrite) { - var options = solutionEntry.options; - - if (options && (options.path || options.filename)) { - // read file - var content = settingsHandlers.readFile(options); - var currentSettings = parser.parse(content, options); - // modification of the entry - var settings = modifier(solutionEntry, currentSettings); - if (isWrite) { - solutionEntry.settings = currentSettings; - var settingsData = parser.stringify(currentSettings, options); - settingsHandlers.writeFile(settingsData, options); - fluid.log("Settings explicitly configured in GPII profile:", settingsData); - } - return settings; - } - - return solutionEntry; -}; - -/** - * Function for looping through the payload sent to get/set calls. Looping - * is done by fluid.transform calls, and at the level of each solution - * entry. - */ -settingsHandlers.transformPayload = function (payload, modifier, parser, isWrite) { - var returnObj = fluid.copy(payload); - - return fluid.transform(returnObj, function (solution) { - return fluid.transform(solution, function(solutionEntry) { - return settingsHandlers.handleSolutionEntrry(solutionEntry, - modifier, parser, isWrite); - }); - }); -}; - -settingsHandlers.makeGet = function (parser) { - return function (payload) { - return settingsHandlers.transformPayload(payload, settingsHandlers.getSettings, - parser); - }; -}; - -settingsHandlers.makeSet = function (parser) { - return function (payload) { - return settingsHandlers.transformPayload(payload, settingsHandlers.setSettings, - parser, true); - }; -}; diff --git a/gpii/node_modules/settingsHandlers/src/settingsHandlerUtilities.js b/gpii/node_modules/settingsHandlers/src/settingsHandlerUtilities.js new file mode 100644 index 000000000..321a2804e --- /dev/null +++ b/gpii/node_modules/settingsHandlers/src/settingsHandlerUtilities.js @@ -0,0 +1,565 @@ +/*! +GPII Settings Handler Utilities + +Copyright 2013 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = fluid || require("infusion"), + fs = typeof(require) === "undefined" ? null : require("fs"), // courtesy to run web-based tests for partial file contents + kettle = fluid.registerNamespace("kettle"), + gpii = fluid.registerNamespace("gpii"); + +/*************************************************************************************************** + * Low-level settings handler utilities, of use to practically anyone dealing with settings handlers + */ + +// A useful utility to defer an activity by a fixed time, together with the necessary callback wrapping +// This or an equivalent MUST be used by anyone within the GPII who wishes to defer an activity +gpii.invokeLater = function (callback, time) { + var wrapped = kettle.wrapCallback ? kettle.wrapCallback(callback) : callback; + return time ? setTimeout(wrapped, time) : fluid.invokeLater(wrapped); +}; + +// Low-quality utility to force resolution of a value which is known to be either +// a plain value or a synchronous promise +gpii.resolveSync = function (response) { + if (fluid.isPromise(response)) { + response.then(function (resolved) { + response = resolved; + }); + } + return response; +}; + +// Coerce a value or promise to a promise - this should really go in the core framework +gpii.toPromise = function (value) { + if (fluid.isPromise(value)) { + return value; + } else { + var togo = fluid.promise(); + togo.resolve(value); + return togo; + } +}; + +fluid.registerNamespace("gpii.settingsHandlers"); + +/** + * Filters out all the settings from the `settings` argument which are + * not in the `supportedSettings` argument + * + * @param {Object} settings - Object in which each key is a setting name that is to be filtered. This + * object will be modified. + * @param {Object} supportedSettings - Object in which each key is a setting name that will be filtered by. + * @return {Object} The `settings`, filtered by the `supportedSettings` entries, meaning that + * any setting that is not in supportedSettings will not be in the return object. + */ +gpii.settingsHandlers.filterSupportedSettings = function (settings, supportedSettings) { + if (supportedSettings === undefined || settings === undefined) { + return settings; + } else { + var filteredSettings = {}; + // we cant simply use fluid.filterKeys because that wont handle the cases where + // there are 'undefined' values for the keys in handlerSpec.settings + for (var settingName in supportedSettings) { + if (settingName in settings) { + filteredSettings[settingName] = settings[settingName]; + } + } + return filteredSettings; + } +}; + +// TODO: These utilities all clearly form part of some wider idiom of "payload handling" without some underpinnings +// for which they are rather hard to follow. For example, they could benefit from some form of "JSON type system" +// (whether provided via JSON schema or otherwise) in order to provide landmarks within the payloads, as well +// as actually validating arguments to these functions and payloads in general. +// cf. some vaguely related art in F# "Type Providers": http://fsharp.github.io/FSharp.Data/library/JsonProvider.html + +/** A general utility function which helps in transforming settings handler payloads - these algorithms generally + * must iterate over the two levels of containment (per-solution, per-settings) and then apply some transform + * to the nested values + * @param {Object} payload - A settings handler payload (either GET/SET response or return). The top-level keys will be solution ids. + * @param {Function} handler - A function whose signature is (oneSetting, [path], oneSolution) where - + * - oneSetting is the nested payload (i.e. the level containing "settings"/"options") + * - path is an array holding the path to this payload i.e. [solutionId, index] + * - oneSolution is the payload at the outer level of containment - i.e. payload[solutionId] + * @return {Object} The transformed payload-structured value. + */ +gpii.settingsHandlers.transformPayload = function (payload, handler) { + return fluid.transform(payload, function (oneSolution, solutionId) { + return fluid.transform(oneSolution, function (oneSetting, settingPath) { + var path = [solutionId, settingPath]; + return handler(oneSetting, path, oneSolution); + }); + }); +}; + +/** Transform the settings for one solution's worth of settings by a supplied transform. + * @param {Object} oneSolution - One solution's worth of settings, with a member named `settings` containing a free hash of the settings indexed by settingsHandlerBlock name. + * @param {Function} handler - A transformer whose signature is the same as the 2nd argument to gpii.settingsHandlers.transformPayload. + * @param {Array of String} [path] - [optional] An array of path segments holding the path of oneSolution within an overall payload - this will be used to form the 2nd argument of `handler`. +*/ + +gpii.settingsHandlers.transformOneSolutionSettings = function (oneSolution, handler, path) { + if (!fluid.isPlainObject(oneSolution)) { + return oneSolution; + } + path = path || []; + var togo = fluid.censorKeys(oneSolution, "settings"); + var newSettings = fluid.transform(oneSolution.settings, function (oneSetting, settingKey) { + var innerPath = path.concat(["settings", settingKey]); + return handler(oneSetting, innerPath, oneSolution); + }); + if (newSettings !== undefined) { // ignore undefined settings blocks + togo.settings = newSettings; + } + + return togo; +}; + +/* As for gpii.settingsHandlers.transformPayload, only the transform runs one level deeper and iterates over any + * "settings" block held within the payload. + */ + +gpii.settingsHandlers.transformPayloadSettings = function (payload, handler) { + return gpii.settingsHandlers.transformPayload(payload, function (oneSolution, path) { + return gpii.settingsHandlers.transformOneSolutionSettings(oneSolution, handler, path); + }); +}; + +/* Extract just the settings (eliminating "options") from the nested settingsHandler blocks for a full payload (top-level keys are solution ids) + * - this is used from gpii.settingsHandlers.comparePayloads and gpii.test.checkConfiguration + */ +gpii.settingsHandlers.extractSettingsBlocks = function (payload) { + return gpii.settingsHandlers.transformPayload(payload, function (oneSolution) { + return fluid.filterKeys(oneSolution, "settings"); + }); +}; + +// A utility which will accept a payload and convert all leaf values to numbers which may validly be. This +// is useful for settings handlers such as the INI settings handler which only really have a concept of strings +// as leaf values - without this postprocessing, roundtripping data through the handler is harder. This is +// primarly just a courtesy to make mock settings handlers easier to write. +gpii.settingsHandlers.numberify = function (payload) { + if (typeof(payload) === "string") { + return isFinite(payload) ? Number(payload) : payload; + } else if (fluid.isPrimitive(payload)) { + return payload; + } else { + return fluid.transform(payload, function (value) { + return gpii.settingsHandlers.numberify(value); + }); + } +}; + +/** A general utility for invoking a settings handler, given a function for handling a single setting + * @param {Function} handler - A function processing a single solution's entries. + * @param {Object} payload - The full payload as supplied to the settings handler's top-level method + * @return {Promise} A promise yielding the combined payload expected from a top-level settings handler - this + * will resolve synchronously if the supplied handler function is synchronous ("ZALGO" notwithstanding). + **/ +gpii.settingsHandlers.invokeSettingsHandler = function (handler, payload) { + var worklist = []; + var response = gpii.settingsHandlers.transformPayload(payload, function (element, path) { + var solutionId = path[0]; // first entry of path will always be solution ID + // Note shallow copy performed within filterKeys + var directLoad = fluid.filterKeys(element, ["settings", "options"]); + var others = fluid.censorKeys(element, ["settings", "options"]); + worklist.push({ + path: path, + result: handler(directLoad, solutionId), + others: others + }); + return {}; // construct isomorphic skeleton of response + }); + var sequence = fluid.promise.sequence(fluid.getMembers(worklist, "result")); + var togo = fluid.promise(); + sequence.then(function (resolved) { + fluid.each(resolved, function (oneResolve, i) { + // shallow copy to avoid trashing any "undefined" values in returned payload + var combined = fluid.extend({settings: oneResolve}, worklist[i].others); + fluid.set(response, worklist[i].path, combined); + }); + togo.resolve(response); + }, togo.reject); + return togo; +}; + +/** Utilities for converting to and from concrete settings to ChangeApplier requests + * to enact those settings. On the "get" or "set" settingsHandler returns, we receive + * concrete settings, which need to be modelised so that they can safely be stored and persisted. + * In future, the "set" action will accept these requests as they are, but for now we need to + * convert them back using "changesToSettings" to adapt to the historical API. + */ + +// Firstly two utilities which transform an individual settings value from one format to the other + +gpii.settingsHandlers.settingsToChanges = function (element) { + return element === undefined ? { + type: "DELETE" + } : { + type: "ADD", + value: element + }; +}; + +gpii.settingsHandlers.changesToSettings = function (element) { + // Preserve element.path is to satisfy the SPI settings handler requirment, particularly the high contrast use of it: + // https://issues.gpii.net/browse/GPII-3452. The plan is to get rid of this logic once there aren't any more examples + // of this in the solution registry. + return element.type === "DELETE" ? undefined : element.path ? { + path: element.path, + value: element.value + } : element.value; +}; + +// Secondly two utilities which do the same for an entire solution's worth of payloads as used in the LifecycleManager + +gpii.settingsHandlers.settingsPayloadToChanges = function (response) { + return gpii.settingsHandlers.transformPayloadSettings(response, gpii.settingsHandlers.settingsToChanges); +}; + +gpii.settingsHandlers.changesPayloadToSettings = function (response) { + return gpii.settingsHandlers.transformPayloadSettings(response, gpii.settingsHandlers.changesToSettings); +}; + + +/** Invoked by, e.g., the LifecycleManager to convert the SET response from a settingsHandler + * to a form where it can be immediately relayed back to another SET handler to + * restore the original settings values + */ + +gpii.settingsHandlers.setResponseToSnapshot = function (response) { + return gpii.settingsHandlers.transformPayloadSettings(response, function (element) { + return element.oldValue; + }); +}; + + +/************************************************************************ + * Utilities for automatically retrying settings handlers. + */ + +gpii.settingsHandlers.comparePayloads = function (payload1, payload2) { + var settings1 = gpii.settingsHandlers.extractSettingsBlocks(payload1); + var settings2 = gpii.settingsHandlers.extractSettingsBlocks(payload2); + var equal = fluid.model.diff(settings1, settings2); + if (!equal) { + fluid.log(fluid.logLevel.WARN, "Comparing payload ", JSON.stringify(settings1, null, 2), " with ", JSON.stringify(settings2, null, 2), " equal: " + equal); + } + return equal; +}; + +gpii.settingsHandlers.checkRereadSettings = function (that) { + if (that.retryOptions.rewriteEvery && !fluid.resolveContext("gpii.contexts.test", that)) { + fluid.fail("rewriteEvery is set to " + that.retryOptions.rewriteEvery + + ": this facility should only be used in integration or unit tests"); + } + // Note that this feature is now configured away as described in GPII-2522, although it is still (partially) tested + var writeAttempt = that.retryOptions.rewriteEvery && (that.retries % that.retryOptions.rewriteEvery === 0); + if (writeAttempt) { + fluid.log(fluid.logLevel.WARN, + "Stored settings have not settled to required values at attempt " + that.retries + + ": attempting to rewrite"); + } + // TODO: This will currently corrupt the SET response of a genuinely async set handler + var response = gpii.resolveSync(fluid.invokeGlobalFunction(that.resolvedName + (writeAttempt ? ".set" : ".get"), [that.payload])); + var getResponse = writeAttempt ? gpii.settingsHandlers.setResponseToSnapshot(response) : response; + var equal = gpii.settingsHandlers.comparePayloads(getResponse, that.payload); + if (equal) { + that.togo.resolve(that.originalSetPayload); + } else { + ++that.retries; + if (that.retries === that.retryOptions.numRetries) { + fluid.log(fluid.logLevel.WARN, "Maximum retry count exceeded in settings handler " + + that.resolvedName + " at retry " + that.retries + ": rejecting settings action"); + that.togo.reject({ + isError: true, + message: "Failure to read settings for handler " + that.resolvedName + " as written after " + that.retries + " retries" + }); + } else { + fluid.log(fluid.logLevel.WARN, "Settings have not settled to required values - retrying read at attempt " + + that.retries + " of " + that.retryOptions.numRetries + " in " + that.retryOptions.retryInterval + "ms"); + gpii.invokeLater(that.checkRereadSettings, that.retryOptions.retryInterval); + } + } +}; + +gpii.settingsHandlers.dispatchSettingsHandler = function (resolvedName, payload, operation, retryOptions) { + return (operation === "get") ? + gpii.settingsHandlers.dispatchSettingsHandlerGet(resolvedName, payload) : + gpii.settingsHandlers.dispatchSettingsHandlerSet(resolvedName, payload, retryOptions); +}; + +/** + * Called to invoke settings handlers' get functionality. + + * Structure of 'payload' argument is: + * { + * "some.app.id": [{ + * "settings": { ... }, + * "options": { + * ... + * } + * }] + * } + * @param {String} resolvedName - The resolved "trunk name" of the settings handler to be invoked - e.g. gpii.windows.registrySettingsHandler. + * @param {Object} payload - The full payload that would be sent to the SET method of the settings handler + * @return {Promise} A promise that will yield the original payload of the invoked GET method. + */ +gpii.settingsHandlers.dispatchSettingsHandlerGet = function (resolvedName, payload) { + // TODO "gpii.lifecycleManager.specToSettingsHandler" is the one responsible for this awkward + // layout of the settings handler payload - all of this infrastructure will have to be updated + // and cleaned up at some point + try { + return gpii.settingsHandlers.invokeGetHandler(resolvedName, payload); + } catch (e) { + fluid.log(fluid.logLevel.WARN, "Error received when dispatching settingsHandler.get " + resolvedName + " with payload ", + payload, ": " + e); + return fluid.promise().reject(e); + } +}; + +/** + * Called to invoke settings handlers' set function. + * @param {String} resolvedName - The resolved "trunk name" of the settings handler to be invoked - e.g. gpii.windows.registrySettingsHandler. + * @param {Object} payload - The full payload that would be sent to the SET method of the settings handler. + * @return {Promise} a promise that will yield the original payload of the invoked SET method. In the case + * that retrying is enabled, a rejection occurs if the GET payloads never match after the end of the nominated retry period + */ +gpii.settingsHandlers.dispatchSettingsHandlerSet = function (resolvedName, payload) { + // TODO "gpii.lifecycleManager.specToSettingsHandler" is the one responsible for this awkward + // layout of the settings handler payload - all of this infrastructure will have to be updated + // and cleaned up at some point + try { + return gpii.settingsHandlers.invokeSetHandler(resolvedName, payload); + } catch (e) { + fluid.log(fluid.logLevel.WARN, "Error received when dispatching settingsHandler.set " + resolvedName + " with payload ", + payload, ": " + e); + return fluid.promise().reject(e); + } +}; + +gpii.settingsHandlers.invokeGetHandler = function (resolvedName, payload) { + return gpii.toPromise(fluid.invokeGlobalFunction(resolvedName + ".get", [payload])); +}; + +gpii.settingsHandlers.invokeSetHandler = function (resolvedName, payload) { + return gpii.toPromise(fluid.invokeGlobalFunction(resolvedName + ".set", [payload])); +}; + +/**************************************************************** + * Two moderately generic utilities for handling "standard" settings handlers whose + * data is backed by some kind of free-form in-memory JSON-like structure. These are called + * called by the file-based getters and setters. + */ + +gpii.settingsHandlers.getSettings = function (solutionEntry, currentSettings) { + var newSettingsResponse = {}; + var userRequestedSettings = solutionEntry.settings; + + fluid.each(userRequestedSettings, function (settingVal, settingKey) { + var value = fluid.get(currentSettings, settingKey, fluid.model.escapedGetConfig); + newSettingsResponse[settingKey] = value; + }); + + return { + settings: newSettingsResponse + }; +}; + +gpii.settingsHandlers.setSettings = function (solutionEntry, currentSettings) { + var newSettingsResponse = {}; + var userRequestedSettings = solutionEntry.settings; + var options = solutionEntry.options; + var holder = { + model: currentSettings + }; + var applier = fluid.makeHolderChangeApplier(holder, { + resolverGetConfig: fluid.model.escapedGetConfig, + resolverSetConfig: fluid.model.escapedSetConfig + }); + + // record differences between required and default settings + // so that the default settings can be restored + fluid.each(userRequestedSettings, function (settingVal, settingKey) { + var oldValue = fluid.get(holder.model, settingKey, fluid.model.escapedGetConfig); + var change = { + path: settingKey, + value: settingVal + }; + change.type = settingVal === undefined ? "DELETE" : "ADD"; + applier.fireChangeRequest(change); + newSettingsResponse[settingKey] = { + "oldValue": oldValue, + "newValue": settingVal + }; + }); + if (currentSettings !== holder.model) { + // TODO: Re-understand again exactly why this is, and eliminate it + fluid.clear(currentSettings); // We have a silly model based on object reference identity + fluid.extend(currentSettings, holder.model); + } + return { options: options, settings: newSettingsResponse }; +}; + +/**************** FILE DEPENDENCE BELOW THIS POINT *********************/ + +gpii.settingsHandlers.readFile = function (options) { + if (!options || !options.filename) { + fluid.fail("readFile: expected an options block defining filename and encoding"); + } else if (!fs.existsSync(options.filename)) { + fluid.log("readFile: No settingsfile '", options.filename, "'' exists. reporting " + + "'undefined' as current settings"); + return undefined; + } + + // TODO check for and handle read errors + var content = fs.readFileSync(options.filename, options.encoding || "utf-8"); + return content; +}; + +gpii.settingsHandlers.writeFile = function (content, options) { + if (!options || !options.filename) { + fluid.fail("writeFile: expected an options block defining filename and encoding"); + } + // TODO check for and handle write errors. + fs.writeFileSync(options.filename, content, options.encoding || "utf-8"); +}; + +/** + * A parser for settings handler types that includes `parse` and `stringify` functions to allow reading and + * serialization for each filetype. For example `JSON`, `INI`, and `XML` formats would each have their own parsers for + * this. + * + * @typedef {Object} gpii.settingsHandlers.parser + * @property {gpii.settinghandlers.parser.parse} parse - Parses the data read from a file on disk or other source. + * @property {gpii.settinghandlers.parser.stringify} stringify - Serializes the settings data back to a string to be + * written to disk. + */ + +/** + * Parses the data read from a file on disk or other source. + * + * @typedef {Function} gpii.settinghandlers.parser.parse + * @param {Object} parse.content - The file contents. + * @param {Object} parse.options - The `options` block from the settings handler configuration for these settings. + */ + +/** + * Serializes the settings data back to a string to be written to disk. + * + * @typedef {Function} gpii.settinghandlers.parser.stringify + * @param {Object} stringify.settings - The solution settings for this configuration. + * @param {Object} stringify.options - The `options` block from the settings handler configuration for these settings. + */ + +/** + * `fetcher` is a function that returns a path to the file containing the data for this settings handler. This allows + * dynamic resolution of the file during the life cycle. The original purpose and example for this was the current user + * profile in JAWS, which is stored in another system settings file. This allows the current user profile setting and + * file to be looked up and used to find the settings for the current handler. + * + * @typedef {Function} gpii.settingsHandlers.fetcher + * @param {Object} options - The `options` block from the settings handler configuration for these settings. + */ + +/* + * Utility for file-based settings handler. Handle individual solution entry data. The 'modifier' function passed as + * parameter is called to allow editing of the solutionEntry based on the + * current settings (both passed as parameters to the modifier function) + */ +gpii.settingsHandlers.handleFileSolutionEntry = function (solutionEntry, modifier, parser, isWrite, fetcher) { + var options = fluid.copy(solutionEntry.options); + + if (options && (options.path || options.filename)) { + // fetch the target file to read + if (fetcher) { + options.filename = fetcher(options); + } + // read file - returns undefined if it doesn't exist + var content = gpii.settingsHandlers.readFile(options); + // if the file is non-existing, current settings are {}, else they're the parsed content of the file + var currentSettings = (content === undefined) ? {} : parser.parse(content, options); + + // modification of the entry + var settings = modifier(solutionEntry, currentSettings); + if (isWrite) { + solutionEntry.settings = currentSettings; + var settingsData = parser.stringify(currentSettings, options); + gpii.settingsHandlers.writeFile(settingsData, options); + } + return settings; + } + + return solutionEntry; +}; + +/* + * Function for looping through the payload sent to get/set calls. Looping + * is done by fluid.transform calls, and at the level of each solution + * entry. + */ +gpii.settingsHandlers.transformFilePayload = function (payload, modifier, parser, isWrite, fetcher) { + return gpii.settingsHandlers.transformPayload(payload, function (solutionEntry) { + return gpii.settingsHandlers.handleFileSolutionEntry(solutionEntry, modifier, parser, isWrite, fetcher); + }); +}; + +/** + * Used for defining settings handler `get` functions supporting different file types. The supplied `parser` and + * `fetcher` will be specific to the file format and settings handler. + * + * @param {gpii.settingsHandlers.parser} parser - Parser for the file contents. + * @param {gpii.settingsHandlers.fetcher} fetcher - Optional fetcher to adjust the filename dynamically. + * @return {Object} GPII Settings from the file. + */ +gpii.settingsHandlers.makeFileGet = function (parser, fetcher) { + return function (payload) { + return gpii.settingsHandlers.transformFilePayload(payload, gpii.settingsHandlers.getSettings, parser, false, fetcher); + }; +}; + +/** + * Used for defining settings handler `set` functions supporting different file types. The supplied `parser` and + * `fetcher` will be specific to the file format and settings handler. + * + * @param {gpii.settingsHandlers.parser} parser - Parser for the file contents. + * @param {gpii.settingsHandlers.fetcher} fetcher - Optional fetcher to adjust the filename dynamically. + * @return {String} Serialized GPII Settings to write to file. + */ +gpii.settingsHandlers.makeFileSet = function (parser, fetcher) { + return function (payload) { + return gpii.settingsHandlers.transformFilePayload(payload, gpii.settingsHandlers.setSettings, parser, true, fetcher); + }; +}; + +/**************** END OF FILE DEPENDENCE *********************/ + +/* + * Given two settingsHandler blocks, copy the settings (even those with a value of undefined) from + * the source to the target - overwriting the target settings. Note that the keys/values immediately + * within the source settingshandler block (i.e. the named settinghandler sub blocks) must be present + * in the target block structure + * Also note that the target object *will be modified* + */ +gpii.settingsHandlers.copySettings = function (target, source) { + for (var shName in source) { + var shBlock = source[shName]; + for (var setting in shBlock.settings) { // will loop over undefined vals + target[shName].settings[setting] = shBlock.settings[setting]; + } + } +}; diff --git a/gpii/node_modules/settingsHandlers/test/INISettingsHandlerTests.js b/gpii/node_modules/settingsHandlers/test/INISettingsHandlerTests.js index 44a5d2922..158db7fbf 100644 --- a/gpii/node_modules/settingsHandlers/test/INISettingsHandlerTests.js +++ b/gpii/node_modules/settingsHandlers/test/INISettingsHandlerTests.js @@ -1,234 +1,254 @@ /* -INI Settings Handler Tests - -Copyright 2012 Raising the Floor - International - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -/*global require, __dirname */ - -(function () { - - "use strict"; - - // This loads universal. - var fluid = require("infusion"), - gpii = fluid.registerNamespace("gpii"), - jqUnit = fluid.require("jqUnit"), - fs = require("fs"); - - require("settingsHandlers"); - - var testTempFilePaths = [ - __dirname + "/data/INISettingsHandler/test1_tmp.ini", - __dirname + "/data/INISettingsHandler/test2_tmp.ini" - ]; - - var deleteTestFiles = function () { - fluid.each(testTempFilePaths, function (filePath) { - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - } - }); - }; - - jqUnit.module("INI Settings Handler tests", { - teardown: deleteTestFiles + * INI Settings Handler Tests + * + * Copyright 2012 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.registerNamespace("jqUnit"), + fs = require("fs"); + +fluid.registerNamespace("gpii.tests"); + +fluid.require("%gpii-universal"); +gpii.loadTestingSupport(); + +require("settingsHandlers"); + +var testTempFilePaths = [ + __dirname + "/data/INISettingsHandler/test1_tmp.ini", + __dirname + "/data/INISettingsHandler/test2_tmp.ini" +]; + +var deleteTestFiles = function () { + fluid.each(testTempFilePaths, function (filePath) { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } }); - - var tests = { - basicTest: { - defaultFile: __dirname + - "/data/INISettingsHandler/test1_default.ini", - tmpFile: testTempFilePaths[0], - expectedFile: __dirname + - "/data/INISettingsHandler/test1_expectedOutput.ini", - set: { - payload: { - "nvda.screenReader": [{ - options: { - "path": testTempFilePaths[0], - }, - settings: { - "general.askToExit": false, - "general.showWelcomeDialogAtStartup": false, - "speech.symbolLevel": 90 - } - }] - }, - expectedResult: { - "nvda.screenReader": [{ - options: { - "path": testTempFilePaths[0], - }, - settings: { - "general.askToExit": { - newValue: false, - oldValue: "True" - }, - "general.showWelcomeDialogAtStartup": { - newValue: false, - oldValue: "True" - }, - "speech.symbolLevel": { - newValue: 90, - oldValue: "100" - } - } - }] - } +}; + +var tests = { + basicTest: { + defaultFile: __dirname + "/data/INISettingsHandler/test1_default.ini", + tmpFile: testTempFilePaths[0], + expectedFile: __dirname + "/data/INISettingsHandler/test1_expectedOutput.ini", + set: { + payload: { + "nvda.screenReader": [{ + options: { + "filename": testTempFilePaths[0] + }, + settings: { + "general.askToExit": false, + "general.showWelcomeDialogAtStartup": false, + "speech.symbolLevel": 90 + } + }] }, - get: { - payload: { - "nvda.screenReader": [{ - options: { - "path": __dirname + - "/data/INISettingsHandler/test1_default.ini" + expectedResult: { + "nvda.screenReader": [{ + options: { + "filename": testTempFilePaths[0] + }, + settings: { + "general.askToExit": { + newValue: false, + oldValue: "True" }, - settings: { - "general.askToExit": null, - "general.showWelcomeDialogAtStartup": null, - "speech.symbolLevel": null - } - }] - }, - expectedResult: { - "nvda.screenReader": [{ - settings: { - "general.askToExit": "True", - "general.showWelcomeDialogAtStartup": "True" , - "speech.symbolLevel": "100" + "general.showWelcomeDialogAtStartup": { + newValue: false, + oldValue: "True" + }, + "speech.symbolLevel": { + newValue: 90, + oldValue: 100 } - }] - } + } + }] } }, - complexTest: { - defaultFile: __dirname + - "/data/INISettingsHandler/test2_default.ini", - tmpFile: testTempFilePaths[1], - expectedFile: __dirname + - "/data/INISettingsHandler/test2_expectedOutput.ini", - set: { - payload: { - "nvda.screenReader": [{ - options: { - "path": testTempFilePaths[1], - "allowHashComments": true, - "allowSubSections": true + get: { + payload: { + "nvda.screenReader": [{ + options: { + "filename": __dirname + + "/data/INISettingsHandler/test1_default.ini" + }, + settings: { + "general.askToExit": null, + "general.showWelcomeDialogAtStartup": null, + "speech.symbolLevel": null + } + }] + }, + expectedResult: { + "nvda.screenReader": [{ + settings: { + "general.askToExit": "True", + "general.showWelcomeDialogAtStartup": "True", + "speech.symbolLevel": 100 + } + }] + } + } + }, + complexTest: { + defaultFile: __dirname + + "/data/INISettingsHandler/test2_default.ini", + tmpFile: testTempFilePaths[1], + expectedFile: __dirname + + "/data/INISettingsHandler/test2_expectedOutput.ini", + set: { + payload: { + "nvda.screenReader": [{ + options: { + "filename": testTempFilePaths[1] + }, + settings: { + "speech.symbolLevel": 90, + "speech.espeak.rate": 40, + "speech.espeak.pitch": 30, + "speech.espeak2.newEntry": 30 + } + }] + }, + expectedResult: { + "nvda.screenReader": [{ + options: { + "filename": testTempFilePaths[1] + }, + settings: { + "speech.symbolLevel": { + newValue: 90, + oldValue: 100 }, - settings: { - "speech.symbolLevel": 90, - "speech.espeak.rate": 40, - "speech.espeak.pitch": 30, - "speech.espeak2.newEntry": 30 - } - }] - }, - expectedResult: { - "nvda.screenReader": [{ - options: { - "path": testTempFilePaths[1], - "allowHashComments": true, - "allowSubSections": true + "speech.espeak.rate": { + newValue: 40, + oldValue: 30 }, - settings: { - "speech.symbolLevel": { - newValue: 90, - oldValue: "100" - }, - "speech.espeak.rate": { - newValue: 40, - oldValue: "30" - }, - "speech.espeak.pitch": { - newValue: 30, - oldValue: "40" - }, - "speech.espeak2.newEntry": { - newValue: 30, - oldValue: undefined - } - } - }] - } - }, - get: { - payload: { - "nvda.screenReader": [{ - options: { - "path": __dirname + - "/data/INISettingsHandler/test2_default.ini", - "allowHashComments": true, - "allowSubSections": true + "speech.espeak.pitch": { + newValue: 30, + oldValue: 40 }, - settings: { - "speech.symbolLevel": null, - "speech.espeak.rate": null, - "speech.espeak.pitch": null, - "speech.espeak2.newEntry": null + "speech.espeak2.newEntry": { + newValue: 30, + oldValue: undefined } - }] - }, - expectedResult: { - "nvda.screenReader": [{ - settings: { - "speech.symbolLevel": "100", - "speech.espeak.rate": "30", - "speech.espeak.pitch": "40", - "speech.espeak2.newEntry": undefined - } - }] - } + } + }] + } + }, + get: { + payload: { + "nvda.screenReader": [{ + options: { + "filename": __dirname + "/data/INISettingsHandler/test2_default.ini" + }, + settings: { + "speech.symbolLevel": null, + "speech.espeak.rate": null, + "speech.espeak.pitch": null, + "speech.espeak2.newEntry": null + } + }] + }, + expectedResult: { + "nvda.screenReader": [{ + settings: { + "speech.symbolLevel": 100, + "speech.espeak.rate": 30, + "speech.espeak.pitch": 40, + "speech.espeak2.newEntry": undefined + } + }] } } - }; - - // avoid issue with windows using \r\n and unix using \n - var unixLinebreak = function (str) { - return str.replace(/\r\n/g, "\n"); - }; - - var setTester = function (name, test) { - // read original INI and copy to a file we can mess up: - var origINI = fs.readFileSync(test.defaultFile, "utf-8"); - fs.writeFileSync(test.tmpFile, origINI, "utf-8"); - // test return payload - var returnPayload = gpii.settingsHandlers.INISettingsHandler.set( - test.set.payload); - jqUnit.assertDeepEq("Checking return payload for " + name + - " INI file: ", test.set.expectedResult, returnPayload); - - // next check that the written INI file is as expected: - var savedINI = fs.readFileSync(test.tmpFile, "utf-8"); - savedINI = unixLinebreak(savedINI); - var expectedINI = fs.readFileSync(test.expectedFile, "utf-8"); - expectedINI = unixLinebreak(expectedINI); - jqUnit.assertEquals("checking INI file for " + name + ": ", - expectedINI, savedINI); - }; - - jqUnit.test("INIHandler.set", function () { - fluid.each(tests, function (v, k) { - setTester(k, v); - }); + } +}; + +// avoid issue with windows using \r\n and unix using \n +var unixLinebreak = function (str) { + return str.replace(/\r\n/g, "\n"); +}; + +var setTester = function (name, test) { + // read original INI and copy to a file we can mess up: + var origINI = fs.readFileSync(test.defaultFile, "utf-8"); + fs.writeFileSync(test.tmpFile, origINI, "utf-8"); + // test return payload + var returnPayload = gpii.settingsHandlers.INISettingsHandler.set( + test.set.payload); + jqUnit.assertDeepEq("Checking return payload for " + name + + " INI file: ", test.set.expectedResult, returnPayload); + + // next check that the written INI file is as expected: + var savedINI = fs.readFileSync(test.tmpFile, "utf-8"); + savedINI = unixLinebreak(savedINI); + var expectedINI = fs.readFileSync(test.expectedFile, "utf-8"); + expectedINI = unixLinebreak(expectedINI); + jqUnit.assertEquals("checking INI file for " + name + ": ", + expectedINI, savedINI); +}; + +var getTester = function (name, test) { + var returnPayload = gpii.settingsHandlers.INISettingsHandler.get( + test.get.payload); + jqUnit.assertDeepEq("Checking return payload for " + name + + " INI file: ", test.get.expectedResult, returnPayload); +}; + +gpii.tests.INIHandlerSet = function () { + fluid.each(tests, function (v, k) { + setTester(k, v); }); +}; - var getTester = function (name, test) { - var returnPayload = gpii.settingsHandlers.INISettingsHandler.get( - test.get.payload); - jqUnit.assertDeepEq("Checking return payload for " + name + - " INI file: ", test.get.expectedResult, returnPayload); - }; - - jqUnit.test("INIHandler.get", function () { - fluid.each(tests, function (v, k) { - getTester(k, v); - }); +gpii.tests.INIHandlerGet = function () { + fluid.each(tests, function (v, k) { + getTester(k, v); }); -}()); +}; + +fluid.defaults("gpii.tests.INISettingsHandler", { + gradeNames: ["fluid.test.testEnvironment"], + components: { + tester: { + type: "gpii.testsINISettingsHandlerTester" + } + } +}); + +fluid.defaults("gpii.testsINISettingsHandlerTester", { + gradeNames: ["fluid.test.testCaseHolder"], + listeners: { + onDestroy: deleteTestFiles + }, + modules: [{ + name: "INISettingsHandler", + tests: [{ + expect: 4, + name: "INIHandler.set", + func: "gpii.tests.INIHandlerSet" + }, { + expect: 2, + name: "INIHandler.get", + func: "gpii.tests.INIHandlerGet" + }] + }] +}); + +module.exports = gpii.test.runCouchTestDefs("gpii.tests.INISettingsHandler"); diff --git a/gpii/node_modules/settingsHandlers/test/JSONSettingsHandlerTests.js b/gpii/node_modules/settingsHandlers/test/JSONSettingsHandlerTests.js index 92a1e8d92..7210ac6bc 100644 --- a/gpii/node_modules/settingsHandlers/test/JSONSettingsHandlerTests.js +++ b/gpii/node_modules/settingsHandlers/test/JSONSettingsHandlerTests.js @@ -1,206 +1,272 @@ /* -JSON Settings Handler Tests + * JSON Settings Handler Tests + * + * Copyright 2012 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ -Copyright 2012 Raising the Floor - International +"use strict"; -Licensed under the New BSD license. You may not use this file except in -compliance with this License. +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.registerNamespace("jqUnit"), + kettle = require("kettle"), + fs = require("fs"); -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ +fluid.registerNamespace("gpii.tests.JSONSettingsHandler"); +kettle.loadTestingSupport(); -/*global require, __dirname*/ +require("settingsHandlers"); -(function () { +gpii.tests.JSONSettingsHandler.testTempFilePaths = [ + __dirname + "/data/JSONSettingsHandler/test1_tmp.json", + __dirname + "/data/JSONSettingsHandler/test2_tmp.json", + __dirname + "/data/JSONSettingsHandler/test3_tmp.json" +]; - "use strict"; - - // This loads universal. - var fluid = require("infusion"), - gpii = fluid.registerNamespace("gpii"), - jqUnit = fluid.require("jqUnit"), - fs = require("fs"); - - require("settingsHandlers"); - - var testTempFilePaths = [ - __dirname + "/data/JSONSettingsHandler/test1_tmp.json", - __dirname + "/data/JSONSettingsHandler/test2_tmp.json" - ]; - - var deleteTestFiles = function () { - fluid.each(testTempFilePaths, function (filePath) { - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - } - }); - }; - - jqUnit.module("JSONSettingsHandler Tests", { - teardown: deleteTestFiles +// TODO: consolidate this shared utility across our three file-based handlers +gpii.tests.JSONSettingsHandler.deleteTestFiles = function () { + fluid.each(gpii.tests.JSONSettingsHandler.testTempFilePaths, function (filePath) { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } }); +}; - var tests = { - basicTest: { - defaultFile: __dirname + - "/data/JSONSettingsHandler/test1_default.json", - tmpFile: testTempFilePaths[0], - expectedFile: __dirname + - "/data/JSONSettingsHandler/test1_expectedOutput.json", - set: { - payload: { - "net.opendirective.maavis": [{ - options: { - "path": testTempFilePaths[0] - }, - settings: { - "theme": "bw", - "userType": "scan", - "name": "Test" - } - }] - }, - expectedResult: { - "net.opendirective.maavis": [{ - options: { - "path": testTempFilePaths[0] - }, - settings: { - "userType": { newValue: "scan", oldValue: "touch" }, - "theme": { newValue: "bw", oldValue: "colour" }, - "name": { newValue: "Test", oldValue: "Maavis" } - } - }] - } +gpii.tests.JSONSettingsHandler.tests = { + basicTest: { + defaultFile: __dirname + + "/data/JSONSettingsHandler/test1_default.json", + tmpFile: gpii.tests.JSONSettingsHandler.testTempFilePaths[0], + expectedFile: __dirname + + "/data/JSONSettingsHandler/test1_expectedOutput.json", + set: { + payload: { + "net.gpii.test.jsonSettings": [{ + options: { + "filename": gpii.tests.JSONSettingsHandler.testTempFilePaths[0] + }, + settings: { + "fish": "battered and fried", + "lemon": "sliced", + "peas": "mushy", + "potatoes": "fried" + } + }] }, - get: { - payload: { - "net.opendirective.maavis": [{ - options: { - "path": __dirname + - "/data/JSONSettingsHandler/test1_default.json" - }, - settings: { - "theme": null, - "userType": null, - "name": null - } - }] - }, - expectedResult: { - "net.opendirective.maavis": [{ - settings: { - "userType": "touch", - "theme": "colour", - "name": "Maavis" - } - }] - } + expectedResult: { + "net.gpii.test.jsonSettings": [{ + options: { + "filename": gpii.tests.JSONSettingsHandler.testTempFilePaths[0] + }, + settings: { + "fish": { newValue: "battered and fried", oldValue: "raw" }, + "lemon": { newValue: "sliced", oldValue: "whole" }, + "peas": { newValue: "mushy", oldValue: "frozen" }, + "potatoes": { newValue: "fried", oldValue: "raw"} + } + }] } }, - complexTest: { - defaultFile: __dirname + - "/data/JSONSettingsHandler/test2_default.json", - tmpFile: testTempFilePaths[1], - expectedFile: __dirname + - "/data/JSONSettingsHandler/test2_expectedOutput.json", - set: { - payload: { - "org.gnome.orca": [{ - options: { - "path": testTempFilePaths[1] + get: { + payload: { + "net.gpii.test.jsonSettings": [{ + options: { + "filename": __dirname + + "/data/JSONSettingsHandler/test1_default.json" + }, + settings: { + "fish": null, + "potatoes": null + } + }] + }, + expectedResult: { + "net.gpii.test.jsonSettings": [{ + settings: { + "fish": "raw", + "potatoes": "raw" + } + }] + } + } + }, + complexTest: { + defaultFile: __dirname + "/data/JSONSettingsHandler/test2_default.json", + tmpFile: gpii.tests.JSONSettingsHandler.testTempFilePaths[1], + expectedFile: __dirname + "/data/JSONSettingsHandler/test2_expectedOutput.json", + set: { + payload: { + "org.gnome.orca": [{ + options: { + "filename": gpii.tests.JSONSettingsHandler.testTempFilePaths[1] + }, + settings: { + "profiles.default.profile.0": "undefault", + "profiles.default.voices.default.rate": 666, + "profiles.default.voices.foo\\.bar.boo": "far out, man!!" + } + }] + }, + expectedResult: { + "org.gnome.orca": [{ + options: { + "filename": gpii.tests.JSONSettingsHandler.testTempFilePaths[1] + }, + settings: { + "profiles.default.profile.0": { + newValue: "undefault", + oldValue: "Default" }, - settings: { - "profiles.default.profile.0": "undefault", - "profiles.default.voices.default.rate": 666, - "profiles.default.voices.foo\\.bar.boo": "far out, man!!" - } - }] - }, - expectedResult: { - "org.gnome.orca": [{ - options: { - "path": testTempFilePaths[1] + "profiles.default.voices.default.rate": { + newValue: 666, + oldValue: 50 }, - settings: { - "profiles.default.profile.0": { - newValue: "undefault", - oldValue: "Default" - }, - "profiles.default.voices.default.rate": { - newValue: 666, - oldValue: 50 - }, - "profiles.default.voices.foo\\.bar.boo": { - newValue: "far out, man!!", - oldValue: "far" - } + "profiles.default.voices.foo\\.bar.boo": { + newValue: "far out, man!!", + oldValue: "far" } - }] - } + } + }] + } + }, + get: { + payload: { + "org.gnome.orca": [{ + options: { + "filename": __dirname + "/data/JSONSettingsHandler/test2_default.json" + }, + settings: { + "profiles.default.profile.0": null, + "profiles.default.voices.default.rate": null, + "profiles.default.voices.foo\\.bar.boo": null + } + }] + }, + expectedResult: { + "org.gnome.orca": [{ + settings: { + "profiles.default.profile.0": "Default", + "profiles.default.voices.default.rate": 50, + "profiles.default.voices.foo\\.bar.boo": "far" + } + }] + } + } + }, + removalTest: { + defaultFile: __dirname + "/data/JSONSettingsHandler/test2_default.json", + tmpFile: gpii.tests.JSONSettingsHandler.testTempFilePaths[2], + expectedFile: __dirname + "/data/JSONSettingsHandler/test3_expectedOutput.json", + set: { + payload: { + "org.gnome.orca": [{ + options: { + "filename": gpii.tests.JSONSettingsHandler.testTempFilePaths[2] + }, + settings: { + // TODO: we have no way to express removal from arrays. SettingsHandler API will have to be + // upgraded to support ChangeRequest objects, and ChangeApplier will have to be upgraded too (FLUID-3504) + "profiles.default.profile.0": "undefault", + "profiles.default.voices.default.rate": undefined, + "profiles.default.voices.foo\\.bar.boo": undefined + } + }] }, - get: { - payload: { - "org.gnome.orca": [{ - options: { - "path": __dirname + - "/data/JSONSettingsHandler/test2_default.json" + expectedResult: { + "org.gnome.orca": [{ + options: { + "filename": gpii.tests.JSONSettingsHandler.testTempFilePaths[2] + }, + settings: { + "profiles.default.profile.0": { + newValue: "undefault", + oldValue: "Default" }, - settings: { - "profiles.default.profile.0": null, - "profiles.default.voices.default.rate": null, - "profiles.default.voices.foo\\.bar.boo": null - } - }] - }, - expectedResult: { - "org.gnome.orca": [{ - settings: { - "profiles.default.profile.0": "Default", - "profiles.default.voices.default.rate": 50, - "profiles.default.voices.foo\\.bar.boo": "far" + "profiles.default.voices.default.rate": { + newValue: undefined, + oldValue: 50 + }, + "profiles.default.voices.foo\\.bar.boo": { + newValue: undefined, + oldValue: "far" } - }] - } + } + }] } } - }; + } +}; - var setTester = function (name, test) { - //read original JSON and copy to a file we can mess up: - var origJSON = fs.readFileSync(test.defaultFile, "utf-8"); - fs.writeFileSync(test.tmpFile, origJSON, "utf-8"); - //test return payload - var returnPayload = gpii.settingsHandlers.JSONSettingsHandler.set( - test.set.payload); - jqUnit.assertDeepEq("Checking return payload for " + name + - " JSON file: ", test.set.expectedResult, returnPayload); +// TODO: consolidate all this duplicated code across our three file-based handlers +gpii.tests.JSONSettingsHandler.setTester = function (name, test) { + // read original JSON and copy to a file we can mess up: + var origJSON = fs.readFileSync(test.defaultFile, "utf-8"); + fs.writeFileSync(test.tmpFile, origJSON, "utf-8"); + // test return payload + var returnPayload = gpii.settingsHandlers.JSONSettingsHandler.set(test.set.payload); + jqUnit.assertDeepEq("Checking return payload for " + name + " JSON file: ", test.set.expectedResult, returnPayload); - //next check that the written JSON file is as expected: - var savedJSON = JSON.parse(fs.readFileSync(test.tmpFile, "utf-8")); - var expectedJSON = JSON.parse(fs.readFileSync(test.expectedFile, - "utf-8")); - jqUnit.assertDeepEq("checking JSON file for " + name + ": ", - expectedJSON, savedJSON); - }; + // next check that the written JSON file is as expected: + var savedJSON = JSON.parse(fs.readFileSync(test.tmpFile, "utf-8")); + var expectedJSON = JSON.parse(fs.readFileSync(test.expectedFile, "utf-8")); + jqUnit.assertDeepEq("checking JSON file for " + name + ": ", expectedJSON, savedJSON); +}; - jqUnit.test("JSONHandler.set", function () { - fluid.each(tests, function (v, k) { - setTester(k, v); - }); - }); +gpii.tests.JSONSettingsHandler.getTester = function (name, test) { + if (test.get) { + var returnPayload = gpii.settingsHandlers.JSONSettingsHandler.get(test.get.payload); + jqUnit.assertDeepEq("Checking return payload for " + name + " JSON file: ", test.get.expectedResult, returnPayload); + } +}; - var getTester = function (name, test) { - var returnPayload = gpii.settingsHandlers.JSONSettingsHandler.get( - test.get.payload); - jqUnit.assertDeepEq("Checking return payload for " + name + - " JSON file: ", test.get.expectedResult, returnPayload); - }; +gpii.tests.JSONSettingsHandler.testSet = function () { + fluid.each(gpii.tests.JSONSettingsHandler.tests, function (v, k) { + gpii.tests.JSONSettingsHandler.setTester(k, v); + }); +}; - jqUnit.test("JSONHandler.get", function () { - fluid.each(tests, function (v, k) { - getTester(k, v); - }); +gpii.tests.JSONSettingsHandler.testGet = function () { + fluid.each(gpii.tests.JSONSettingsHandler.tests, function (v, k) { + gpii.tests.JSONSettingsHandler.getTester(k, v); }); -}()); +}; + +fluid.defaults("gpii.tests.JSONSettingsHandler.environment", { + gradeNames: ["fluid.test.testEnvironment"], + components: { + tester: { + type: "gpii.tests.JSONSettingsHandler.holder" + } + } +}); + +fluid.defaults("gpii.tests.JSONSettingsHandler.holder", { + gradeNames: ["fluid.test.testCaseHolder"], + listeners: { + onDestroy: "gpii.tests.JSONSettingsHandler.deleteTestFiles" + }, + modules: [{ + name: "JSONSettingsHandler", + tests: [{ + expect: 6, + name: "JSONHandler.set", + func: "gpii.tests.JSONSettingsHandler.testSet" + }, { + expect: 2, + name: "JSONHandler.get", + func: "gpii.tests.JSONSettingsHandler.testGet" + }] + }] +}); + +module.exports = kettle.test.bootstrap("gpii.tests.JSONSettingsHandler.environment"); diff --git a/gpii/node_modules/settingsHandlers/test/JawsSettingsHandlerTests.js b/gpii/node_modules/settingsHandlers/test/JawsSettingsHandlerTests.js new file mode 100644 index 000000000..7cdeeeec6 --- /dev/null +++ b/gpii/node_modules/settingsHandlers/test/JawsSettingsHandlerTests.js @@ -0,0 +1,45 @@ +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.tests"); + +fluid.require("%gpii-universal"); +gpii.loadTestingSupport(); + +require("settingsHandlers"); + +fluid.logObjectRenderChars = 30000; + +var test = { + payload: { + "com.freedomscientific.jaws": [{ + options: { + "defaultSettingsFilePath": "C:\\Users\\vagrant\\AppData\\Roaming\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", + "voiceProfilesDirPath": "C:\\Users\\vagrant\\AppData\\Roaming\\Freedom Scientific\\JAWS\\2019\\Settings\\VoiceProfiles" + }, + settings: { + "ENU-Global.Pitch": 90 + } + }] + }, + expectedResult: { + "com.freedomscientific.jaws": [{ + options: { + "defaultSettingsFilePath": "C:\\Users\\vagrant\\AppData\\Roaming\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", + "voiceProfilesDirPath": "C:\\Users\\vagrant\\AppData\\Roaming\\Freedom Scientific\\JAWS\\2019\\Settings\\VoiceProfiles" + }, + settings: { + "ENU-Global.Pitch": { + newValue: 90, + oldValue: 65 + } + } + }] + } +}; + +var output = gpii.settingsHandlers.JAWSSettingsHandler.get(test.payload); +output = gpii.settingsHandlers.JAWSSettingsHandler.set(test.payload); +fluid.log(output); diff --git a/gpii/node_modules/settingsHandlers/test/NoSettingsHandlerTests.js b/gpii/node_modules/settingsHandlers/test/NoSettingsHandlerTests.js new file mode 100644 index 000000000..abe4efbfd --- /dev/null +++ b/gpii/node_modules/settingsHandlers/test/NoSettingsHandlerTests.js @@ -0,0 +1,98 @@ +/* + * INI Settings Handler Tests + * + * Copyright 2018 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.registerNamespace("jqUnit"), + kettle = require("kettle"); + +fluid.registerNamespace("gpii.tests.noSettingsHandler"); + +kettle.loadTestingSupport(); + +require("settingsHandlers"); + +gpii.tests.noSettingsHandler.tests = { + basicTest: { + set: { + payload: { + "com.microsoft.windows.volumeControl": [{ + settings: { + "volume": 3 + } + }] + }, + expected: { + "com.microsoft.windows.volumeControl": [{ + options: undefined, + settings: { + "volume": { + newValue: 3, + oldValue: undefined + } + } + }] + } + }, + get: { + payload: { + "com.microsoft.windows.volumeControl": [{ + settings: { + "volume": {} + } + }] + }, + expected: { + "com.microsoft.windows.volumeControl": [{ + settings: { + "volume": undefined + } + }] + } + } + } +}; + +gpii.tests.noSettingsHandler.doTest = function () { + fluid.each(gpii.tests.noSettingsHandler.tests, function (oneTest) { + var getResult = gpii.settingsHandlers.noSettings.get(oneTest.get.payload); + jqUnit.assertDeepEq("The get result is expected", oneTest.get.expected, getResult); + + var setResult = gpii.settingsHandlers.noSettings.set(oneTest.set.payload); + jqUnit.assertDeepEq("The set result is expected", oneTest.set.expected, setResult); + }); +}; + +fluid.defaults("gpii.tests.noSettingsHandler", { + gradeNames: ["fluid.test.testEnvironment"], + components: { + tester: { + type: "gpii.tests.noSettingsHandler.testCaseHolder" + } + } +}); + +fluid.defaults("gpii.tests.noSettingsHandler.testCaseHolder", { + gradeNames: ["fluid.test.testCaseHolder"], + modules: [{ + name: "NoSettingsHandler", + tests: [{ + // expect: 2, + name: "Tests for gpii.settingsHandler.noSettingsHandler", + func: "gpii.tests.noSettingsHandler.doTest" + }] + }] +}); + +module.exports = kettle.test.bootstrap("gpii.tests.noSettingsHandler"); diff --git a/gpii/node_modules/settingsHandlers/test/RemoteFileSettingsHandlerTests.js b/gpii/node_modules/settingsHandlers/test/RemoteFileSettingsHandlerTests.js new file mode 100644 index 000000000..08f8a26af --- /dev/null +++ b/gpii/node_modules/settingsHandlers/test/RemoteFileSettingsHandlerTests.js @@ -0,0 +1,1052 @@ +/* Tests for the remote file settings handler. + * + * Copyright 2019 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + path = require("path"), + fs = require("fs"), + os = require("os"), + rimraf = require("rimraf"), + mkdirp = require("mkdirp"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.registerNamespace("jqUnit"), + kettle = require("kettle"); + +fluid.require("%gpii-universal/gpii/node_modules/journal/src/SettingsDir.js"); +fluid.require("%gpii-universal/gpii/node_modules/lifecycleManager"); +fluid.registerNamespace("gpii.tests.remoteFileSettingsHandler"); + +kettle.loadTestingSupport(); +fluid.contextAware.makeChecks({ + "gpii.contexts.test": { + value: true + } +}); + +require("settingsHandlers"); + +fluid.defaults("gpii.tests.remoteFileSettingsHandler", { + gradeNames: ["fluid.test.testEnvironment"], + + components: { + server: { + type: "gpii.tests.remoteFileSettingsHandler.server" + }, + tester: { + type: "gpii.tests.remoteFileSettingsHandler.testCaseHolder" + } + } +}); + +// HTTP server serving the files in this directory +fluid.defaults("gpii.tests.remoteFileSettingsHandler.server", { + gradeNames: ["kettle.server"], + port: 28531, + components: { + app: { + type: "kettle.app", + options: { + requestHandlers: { + main: { + type: "gpii.tests.remoteFileSettingsHandler.requestHandler", + route: "/:type/:arg", + method: "get" + } + } + } + } + } +}); + +fluid.defaults("gpii.tests.remoteFileSettingsHandler.requestHandler", { + gradeNames: ["kettle.request.http"], + invokers: { + handleRequest: { + funcName: "gpii.tests.remoteFileSettingsHandler.handleRequest", + args: [ "{that}", "{that}.req.params.type", "{that}.req.params.arg" ] + } + } +}); + +/** + * Handles a http request. + * + * @param {Object} request The request object. + * @param {String} type The type of request: file, error, content, or delay-content. + * @param {String} arg Additional data for the request. + */ +gpii.tests.remoteFileSettingsHandler.handleRequest = function (request, type, arg) { + switch (type) { + case "file": + // Send a file + try { + var string = fs.readFileSync(path.join(__dirname, path.basename(arg)), "utf8"); + request.events.onSuccess.fire(string); + } catch (e) { + request.events.onError.fire({ + message: "Not found", + statusCode: 404 + }); + } + break; + case "error": + // Respond with an error. + request.events.onError.fire({ + message: "Error response", + statusCode: arg + }); + break; + case "content": + // Return the request data + request.events.onSuccess.fire(arg); + break; + case "delay-content": + // Return the request data, after a random delay. + setTimeout(request.events.onSuccess.fire, Math.random() * 1000, arg); + break; + } +}; + +gpii.tests.remoteFileSettingsHandler.testDir = os.tmpdir() + "/gpii-test-remoteFile" + Math.random(); + +/** Creates the temporary test directory */ +gpii.tests.remoteFileSettingsHandler.createDir = function () { + mkdirp.sync(gpii.tests.remoteFileSettingsHandler.testDir); +}; +/** Removes the temporary test directory */ +gpii.tests.remoteFileSettingsHandler.cleanDir = function () { + rimraf.sync(gpii.tests.remoteFileSettingsHandler.testDir); +}; + +/** + * Tests for the remoteFileSettingsHandler. + */ +fluid.defaults("gpii.tests.remoteFileSettingsHandler.testCaseHolder", { + gradeNames: ["fluid.test.testCaseHolder"], + components: { + settingsDir: { + type: "gpii.settingsDir" + } + }, + members: { + downloadTo: path.join(gpii.tests.remoteFileSettingsHandler.testDir, "downloads", "download"), + stashDir: path.join(gpii.tests.remoteFileSettingsHandler.testDir, "stash"), + gpiiSettingsDir: "@expand:{settingsDir}.getGpiiSettingsDir()" + }, + listeners: { + "onCreate.createDir": { + funcName: "gpii.tests.remoteFileSettingsHandler.createDir" + }, + "onDestroy.clearDir": { + funcName: "gpii.tests.remoteFileSettingsHandler.cleanDir" + } + }, + modules: [{ + name: "remoteFileSettingsHandler transferFile tests", + tests: [{ + name: "transferFile Existing to non-existing, remove source (move)", + task: "gpii.tests.remoteFileSettingsHandler.testTransferFile", + args: [ + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "existing-file-1"), + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "destination-file-1"), + false + ], + + expect: 3, + resolve: "gpii.tests.remoteFileSettingsHandler.assertTransferFile", + resolveArgs: [ + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "existing-file-1"), + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "destination-file-1"), + false + ] + }, { + name: "transferFile Existing to non-existing, keep source (copy)", + task: "gpii.tests.remoteFileSettingsHandler.testTransferFile", + args: [ + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "existing-file-2"), + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "destination-file-2"), + true + ], + + expect: 3, + resolve: "gpii.tests.remoteFileSettingsHandler.assertTransferFile", + resolveArgs: [ + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "existing-file-2"), + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "destination-file-2"), + true + ] + }, { + name: "transferFile Existing to existing, remove source (move)", + task: "gpii.tests.remoteFileSettingsHandler.testTransferFile", + args: [ + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "existing-file-3"), + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "existing-file-dest-3"), + false + ], + + expect: 3, + resolve: "gpii.tests.remoteFileSettingsHandler.assertTransferFile", + resolveArgs: [ + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "existing-file-3"), + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "existing-file-dest-3"), + false + ] + }, { + name: "transferFile Existing to existing, keep source (copy)", + task: "gpii.tests.remoteFileSettingsHandler.testTransferFile", + args: [ + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "existing-file-4"), + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "existing-file-dest-4"), + true + ], + + expect: 3, + resolve: "gpii.tests.remoteFileSettingsHandler.assertTransferFile", + resolveArgs: [ + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "existing-file-4"), + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "existing-file-dest-4"), + true + ] + }, { + name: "transferFile non-existing to existing, remove source (move)", + task: "gpii.tests.remoteFileSettingsHandler.testTransferFile", + args: [ + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "non-existing-file-5"), + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "existing-file-dest-5"), + false + ], + + expect: 3, + resolve: "gpii.tests.remoteFileSettingsHandler.assertTransferFile", + resolveArgs: [ + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "non-existing-file-5"), + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "existing-file-dest-5"), + false + ] + }, { + name: "transferFile non-existing to existing, keep source (copy)", + task: "gpii.tests.remoteFileSettingsHandler.testTransferFile", + args: [ + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "non-existing-file-6"), + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "existing-file-dest-6"), + false + ], + + expect: 3, + resolve: "gpii.tests.remoteFileSettingsHandler.assertTransferFile", + resolveArgs: [ + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "non-existing-file-6"), + path.join(gpii.tests.remoteFileSettingsHandler.testDir, "existing-file-dest-6"), + false + ] + }] + }, { + name: "remoteFileSettingsHandler createBackup", + tests: [{ + name: "createBackup", + args: [ gpii.tests.remoteFileSettingsHandler.testDir ], + expect: 1, + + task: "gpii.tests.remoteFileSettingsHandler.testBackup", + resolve: "gpii.tests.remoteFileSettingsHandler.assertBackup", + resolveArgs: [ gpii.tests.remoteFileSettingsHandler.testDir ] + + }] + }, { + name: "remoteFileSettingsHandler download", + tests: [{ + // First download ensures it's downloaded. + name: "remoteFileSettingsHandler.downloadFile (initial)", + expect: 2, + + task: "gpii.settingsHandlers.remoteFileSettingsHandler.downloadFile", + args: ["http://localhost:28531/file/" + path.basename(__filename), "{that}.downloadTo"], + + resolve: "gpii.tests.remoteFileSettingsHandler.assertDownload", + resolveArgs: [__filename, "{that}.downloadTo", false] + }, { + // Download again to test overwriting the existing. + name: "remoteFileSettingsHandler.downloadFile (overwrite)", + expect: 2, + + task: "gpii.settingsHandlers.remoteFileSettingsHandler.downloadFile", + args: ["http://localhost:28531/file/" + path.basename(__filename), "{that}.downloadTo"], + + resolve: "gpii.tests.remoteFileSettingsHandler.assertDownload", + resolveArgs: [__filename, "{that}.downloadTo", true] + }, { + // The download was deleted in the previous test to check downloading a non-existing file. + name: "remoteFileSettingsHandler.downloadFile (new)", + expect: 2, + + task: "gpii.settingsHandlers.remoteFileSettingsHandler.downloadFile", + args: ["http://localhost:28531/file/" + path.basename(__filename), "{that}.downloadTo"], + + resolve: "gpii.tests.remoteFileSettingsHandler.assertDownload", + resolveArgs: [__filename, "{that}.downloadTo", true] + }, { + // Try a failing download + name: "remoteFileSettingsHandler.downloadFile (new)", + expect: 1, + + task: "gpii.settingsHandlers.remoteFileSettingsHandler.downloadFile", + args: ["http://localhost:28531/error/404", "{that}.downloadTo"], + + reject: "jqUnit.assertTrue", + rejectArgs: [ + "Should reject with an error", + "{arguments}.0.isError" + ] + }] + }, { + name: "remoteFileSettings handler Settings", + tests: [{ + name: "setting single file", + task: "gpii.tests.remoteFileSettingsHandler.testSetting", + args: [{ + payload: { + "gpii.test.remoteFileSetting": [{ + settings: { + f1: "value1" + }, + options: { + settings: { + f1: {path: "~file1", url: "http://localhost:28531/content/file-one.%value"} + } + } + }] + }, + result: { + "gpii.test.remoteFileSetting": [{ + settings: { + f1: { + oldValue: { + value: "value1", + stashPath: "file1.gpii-stashed", + restore: true + }, + newValue: "value1" + } + } + }] + }, + initialFiles: {}, + expectedFiles: { + file1: "file-one.value1" + } + }, "{that}.gpiiSettingsDir"], + resolve: "fluid.identity" + }, { + name: "setting single file, existing", + task: "gpii.tests.remoteFileSettingsHandler.testSetting", + args: [{ + payload: { + "gpii.test.remoteFileSetting": [{ + settings: { + f1: "value1" + }, + options: { + settings: { + f1: {path: "~file1", url: "http://localhost:28531/content/new-content.%value"} + } + } + }] + }, + result: { + "gpii.test.remoteFileSetting": [{ + settings: { + f1: { + oldValue: { + value: "value1", + stashPath: "file1.gpii-stashed", + restore: true + }, + newValue: "value1" + } + } + }] + }, + initialFiles: { + file1: "original-content" + }, + expectedFiles: { + file1: "new-content.value1" + } + }, "{that}.gpiiSettingsDir"], + resolve: "fluid.identity" + }, { + name: "setting two files", + task: "gpii.tests.remoteFileSettingsHandler.testSetting", + args: [{ + payload: { + "gpii.test.remoteFileSetting": [{ + settings: { + f1: "value1", + f2: "value2" + }, + options: { + settings: { + f1: {path: "~file1", url: "http://localhost:28531/content/new-content1.%value"}, + f2: {path: "~file2", url: "http://localhost:28531/content/new-content2.%value"} + } + } + }] + }, + result: { + "gpii.test.remoteFileSetting": [{ + settings: { + f1: { + oldValue: { + value: "value1", + stashPath: "file1.gpii-stashed", + restore: true + }, + newValue: "value1" + }, + f2: { + oldValue: { + value: "value2", + stashPath: "file2.gpii-stashed", + restore: true + }, + newValue: "value2" + } + } + }] + }, + initialFiles: {}, + expectedFiles: { + file1: "new-content1.value1", + file2: "new-content2.value2" + } + }, "{that}.gpiiSettingsDir"], + resolve: "fluid.identity" + }, { + name: "setting two files, 1 exists", + task: "gpii.tests.remoteFileSettingsHandler.testSetting", + args: [{ + payload: { + "gpii.test.remoteFileSetting": [{ + settings: { + f1: "value1", + f2: "value2" + }, + options: { + settings: { + f1: {path: "~file1", url: "http://localhost:28531/content/new-content1.%value"}, + f2: {path: "~file2", url: "http://localhost:28531/content/new-content2.%value"} + } + } + }] + }, + result: { + "gpii.test.remoteFileSetting": [{ + settings: { + f1: { + oldValue: { + value: "value1", + stashPath: "file1.gpii-stashed", + restore: true + }, + newValue: "value1" + }, + f2: { + oldValue: { + value: "value2", + stashPath: "file2.gpii-stashed", + restore: true + }, + newValue: "value2" + } + } + }] + }, + initialFiles: { + file1: "original-content1" + }, + expectedFiles: { + file1: "new-content1.value1", + file2: "new-content2.value2" + } + }, "{that}.gpiiSettingsDir"], + resolve: "fluid.identity" + }, { + name: "setting ten files, half exist, different response times", + task: "gpii.tests.remoteFileSettingsHandler.testSetting", + args: [{ + payload: { + "gpii.test.remoteFileSetting": [{ + settings: { + f1: "value1", + f2: "value2", + f3: "value3", + f4: "value4", + f5: "value5", + f6: "value6", + f7: "value7", + f8: "value8", + f9: "value9", + f10: "value10" + }, + options: { + settings: { + f1: {path: "~file1", url: "http://localhost:28531/delay-content/new-content1.%value"}, + f2: {path: "~file2", url: "http://localhost:28531/delay-content/new-content2.%value"}, + f3: {path: "~file3", url: "http://localhost:28531/delay-content/new-content3.%value"}, + f4: {path: "~file4", url: "http://localhost:28531/delay-content/new-content4.%value"}, + f5: {path: "~file5", url: "http://localhost:28531/delay-content/new-content5.%value"}, + f6: {path: "~file6", url: "http://localhost:28531/delay-content/new-content6.%value"}, + f7: {path: "~file7", url: "http://localhost:28531/delay-content/new-content7.%value"}, + f8: {path: "~file8", url: "http://localhost:28531/delay-content/new-content8.%value"}, + f9: {path: "~file9", url: "http://localhost:28531/delay-content/new-content9.%value"}, + f10: {path: "~file10", url: "http://localhost:28531/delay-content/new-content10.%value"} + } + } + }] + }, + result: { + "gpii.test.remoteFileSetting": [{ + settings: { + f1: { + oldValue: { + value: "value1", + stashPath: "file1.gpii-stashed", + restore: true + }, + newValue: "value1" + }, + f2: { + oldValue: { + value: "value2", + stashPath: "file2.gpii-stashed", + restore: true + }, + newValue: "value2" + }, + f3: { + oldValue: { + value: "value3", + stashPath: "file3.gpii-stashed", + restore: true + }, + newValue: "value3" + }, + f4: { + oldValue: { + value: "value4", + stashPath: "file4.gpii-stashed", + restore: true + }, + newValue: "value4" + }, + f5: { + oldValue: { + value: "value5", + stashPath: "file5.gpii-stashed", + restore: true + }, + newValue: "value5" + }, + f6: { + oldValue: { + value: "value6", + stashPath: "file6.gpii-stashed", + restore: true + }, + newValue: "value6" + }, + f7: { + oldValue: { + value: "value7", + stashPath: "file7.gpii-stashed", + restore: true + }, + newValue: "value7" + }, + f8: { + oldValue: { + value: "value8", + stashPath: "file8.gpii-stashed", + restore: true + }, + newValue: "value8" + }, + f9: { + oldValue: { + value: "value9", + stashPath: "file9.gpii-stashed", + restore: true + }, + newValue: "value9" + }, + f10: { + oldValue: { + value: "value10", + stashPath: "file10.gpii-stashed", + restore: true + }, + newValue: "value10" + } + } + }] + }, + initialFiles: { + file4: "original-content4", + file5: "original-content5", + file7: "original-content7", + file8: "original-content8", + file9: "original-content9" + }, + expectedFiles: { + file1: "new-content1.value1", + file2: "new-content2.value2", + file3: "new-content3.value3", + file4: "new-content4.value4", + file5: "new-content5.value5", + file6: "new-content6.value6", + file7: "new-content7.value7", + file8: "new-content8.value8", + file9: "new-content9.value9", + file10: "new-content10.value10" + } + }, "{that}.gpiiSettingsDir"], + resolve: "fluid.identity" + }, { + name: "setting 1 file, 1 error", + task: "gpii.tests.remoteFileSettingsHandler.testSetting", + args: [{ + payload: { + "gpii.test.remoteFileSetting": [{ + settings: { + f1: "value1" + }, + options: { + settings: { + f1: {path: "~file1", url: "http://localhost:28531/error/404"} + } + } + }] + }, + result: { + "gpii.test.remoteFileSetting": [{ + settings: { + f1: { + failed: true + } + } + }] + }, + initialFiles: { + file1: "original-content1" + }, + expectedFiles: { + file1: "original-content1" + }, + expectedBackups: { + } + }, "{that}.gpiiSettingsDir"], + resolve: "fluid.identity" + }, { + name: "setting 3 files, 1 error", + task: "gpii.tests.remoteFileSettingsHandler.testSetting", + args: [{ + payload: { + "gpii.test.remoteFileSetting": [{ + settings: { + f1: "value1", + f2: "value2", + f3: "value3" + }, + options: { + settings: { + f1: {path: "~file1", url: "http://localhost:28531/content/new-content1.%value"}, + f2: {path: "~file2", url: "http://localhost:28531/error/404"}, + f3: {path: "~file3", url: "http://localhost:28531/error/404"} + } + } + }] + }, + result: { + "gpii.test.remoteFileSetting": [{ + settings: { + f1: { + oldValue: { + value: "value1", + stashPath: "file1.gpii-stashed", + restore: true + }, + newValue: "value1" + }, + f2: { + failed: true + }, + f3: { + failed: true + } + } + }] + }, + initialFiles: { + file1: "original-content1", + file3: "original-content3" + }, + expectedFiles: { + file1: "new-content1.value1", + file3: "original-content3" + }, + expectedBackups: { + file1: "original-content1" + } + }, "{that}.gpiiSettingsDir"], + resolve: "fluid.identity" + }, { + name: "setting 3 files, 1 error", + task: "gpii.tests.remoteFileSettingsHandler.testSetting", + args: [{ + payload: { + "gpii.test.remoteFileSetting": [{ + settings: { + f1: "value1", + f2: "value2", + f3: "value3" + }, + options: { + settings: { + f1: {path: "~file1", url: "http://localhost:28531/content/new-content1.%value"}, + f2: {path: "~file2", url: "http://localhost:28531/error/404"}, + f3: {path: "~file3", url: "http://localhost:28531/content/new-content3.%value"} + } + } + }] + }, + result: { + "gpii.test.remoteFileSetting": [{ + settings: { + f1: { + oldValue: { + value: "value1", + stashPath: "file1.gpii-stashed", + restore: true + }, + newValue: "value1" + }, + f2: { + failed: true + }, + f3: { + oldValue: { + value: "value3", + stashPath: "file3.gpii-stashed", + restore: true + }, + newValue: "value3" + } + } + }] + }, + initialFiles: { + file1: "original-content1", + file2: "original-content2", + file3: "original-content3" + }, + expectedFiles: { + file1: "new-content1.value1", + file2: "original-content2", + file3: "new-content3.value3" + }, + expectedBackups: { + file1: "original-content1", + file3: "original-content3" + } + }, "{that}.gpiiSettingsDir"], + resolve: "fluid.identity" + }] + }] +}); + +/** + * Tests for createBackup. + * @param {String} testDir Directory for testing. + * @return {Promise} Resolves when complete. + */ +gpii.tests.remoteFileSettingsHandler.testBackup = function (testDir) { + + var testFileNotExist = path.join(testDir, "backup-test.not-exist"); + + // Create the initial content of the file to back-up. + var testFile = path.join(testDir, "backup-test"); + fs.writeFileSync(testFile, "initial content"); + + return fluid.promise.sequence([ + // No backup should be made + gpii.settingsHandlers.remoteFileSettingsHandler.createBackup(testFileNotExist, testDir), + // Backup the file + gpii.settingsHandlers.remoteFileSettingsHandler.createBackup(testFile, testDir), + function () { + // Change the content - to check if the original backup gets overwritten. + fs.writeFileSync(testFile, "updated content"); + return gpii.settingsHandlers.remoteFileSettingsHandler.createBackup(testFile, testDir); + } + ]); +}; + +gpii.tests.remoteFileSettingsHandler.assertBackup = function (testDir) { + jqUnit.expect(2); + + // Backup of a non existing file should not be made. + var testFileNotExist = path.join(testDir, "backups", "backup-test.not-exist"); + jqUnit.assertFalse("non existing back up file should not exist", fs.existsSync(testFileNotExist)); + + // The exising file backup should exist. + var testFile = path.join(testDir, "backups", "backup-test"); + jqUnit.assertTrue("back up file should exist", fs.existsSync(testFile)); + + // The content should be the original content, and not overwritten. + jqUnit.assertEquals("content of the backup file should be the original", + "initial content", fs.readFileSync(testFile, "utf8")); +}; + +/** + * Check that a downloaded file is correct. + * + * @param {String} origFile The original file that was downloaded. + * @param {String} downloadedFile The resulting file of the download. + * @param {Boolean} deleteDownload true to delete the downloaded file. + */ +gpii.tests.remoteFileSettingsHandler.assertDownload = function (origFile, downloadedFile, deleteDownload) { + // Sanity check + jqUnit.assertNotEquals("The downloaded file and original file should be different file names", + origFile, downloadedFile); + + var origContent = fs.readFileSync(origFile, "utf8"); + var downloadContent = fs.readFileSync(downloadedFile, "utf8"); + + jqUnit.assertEquals("Downloaded content should match the original", origContent, downloadContent); + + if (deleteDownload) { + fs.unlinkSync(downloadedFile); + fs.rmdirSync(path.dirname(downloadedFile)); + } +}; + +/** + * Runs transferFile, after creating a test file if required. + * + * @param {String} source The path of the file to move. + * @param {String} destination The destination path. + * @return {Promise} Resolves when complete. + */ +gpii.tests.remoteFileSettingsHandler.testTransferFile = function (source, destination) { + if (path.basename(source).startsWith("existing-file")) { + fs.writeFileSync(source, "existing" + source); + } + return gpii.settingsHandlers.remoteFileSettingsHandler.transferFile(source, destination); +}; + +/** + * Calls transferFile, after creating some files if required. + * + * @param {String} source Path to the new file. + * @param {String} destination Path to where the new file should be placed. + * @param {Boolean} keepSource true if the source file needs to be kept (ie, the download should be cached). + * @return {Promise} Resolves when complete. + */ +gpii.tests.remoteFileSettingsHandler.testTransferFile = function (source, destination, keepSource) { + // Create the existing files as required. + if (path.basename(source).startsWith("existing-file")) { + fs.writeFileSync(source, "transferFile source" + source); + } + if (path.basename(destination).startsWith("existing-file")) { + fs.writeFileSync(destination, "transferFile destination" + source); + } + + return gpii.settingsHandlers.remoteFileSettingsHandler.transferFile(source, destination, keepSource); +}; + +/** + * Checks the result of testTransferFile + * @param {String} source Path to the new file. + * @param {String} destination Path to where the new file should be placed. + * @param {Boolean} keepSource true if the source file needs to be kept (ie, the download should be cached). + */ +gpii.tests.remoteFileSettingsHandler.assertTransferFile = function (source, destination, keepSource) { + + jqUnit.assertEquals("The source file should only exist if keepSource is true", keepSource, fs.existsSync(source)); + + var existingSource = path.basename(source).startsWith("existing-file"); + jqUnit.assertEquals("The destination file should exist if the source did", + existingSource, fs.existsSync(destination)); + + if (existingSource) { + jqUnit.assertEquals("The destination file should have the correct content", + "transferFile source" + source, fs.readFileSync(destination, "utf8")); + } else { + jqUnit.assert("Destination does not exist"); + } +}; + +/** + * Checks if a directory contains only the files described in the expectedFiles object. + * + * @param {String} dir The directory to inspect. + * @param {Object} expectedFiles Map of filename => content. + */ +gpii.tests.remoteFileSettingsHandler.checkFiles = function (dir, expectedFiles) { + // Check the new files are correct. + var actualFiles = fs.readdirSync(dir); + jqUnit.assertDeepEq("Only the expected files should exist", + Object.keys(expectedFiles).sort(), actualFiles.sort()); + + fluid.each(expectedFiles, function (content, file) { + var filepath = path.join(dir, file); + jqUnit.assertTrue("File '" + file + "' should exist after applying the setting.", fs.existsSync(filepath)); + + var actualContent = fs.readFileSync(filepath, "utf8"); + jqUnit.assertEquals("Content for file '" + file + "' should be the expected content", content, actualContent); + }); +}; + +/** + * Fixes all "path" members deep within an object to be child of the given directory. + * @param {Object} obj The object to work on. + * @param {String} dir The directory to prepend to the paths. + * @return {Object} The modified copy of obj. + */ +gpii.tests.remoteFileSettingsHandler.fixPath = function (obj, dir) { + return fluid.transform(obj, function (value, key) { + if (key === "path" && value.startsWith("~")) { + return path.join(dir, value.substr(1)); + } else if (fluid.isPlainObject(value)) { + return gpii.tests.remoteFileSettingsHandler.fixPath(value, dir); + } else { + return value; + } + }); +}; + +/** + * Removes the timestamp from the files identified in the "stashPath" members deep within an object, making the names + * more predictable. + * @param {Object} obj The object to work on. + * @return {Object} The modified copy of obj. + */ +gpii.tests.remoteFileSettingsHandler.fixStashPath = function (obj) { + return fluid.transform(obj, function (value, key) { + if (key === "stashPath") { + return path.basename(value).replace(/\.20..-.*(?=\.gpii-stashed)/, ""); + } else if (fluid.isPlainObject(value)) { + return gpii.tests.remoteFileSettingsHandler.fixStashPath(value); + } else { + return value; + } + }); +}; + +/** + * Recording of which custom invokers where invoked. + */ +gpii.tests.remoteFileSettingsHandler.invokedValues = null; + +/** + * Performs an end-to-end test of the remote file settings handler. + * @param {Object} testData The test data. + * @param {String} settingsDir The settings directory. + * @return {Promise} Resolves when complete. + */ +gpii.tests.remoteFileSettingsHandler.testSetting = function (testData, settingsDir) { + + var promise = fluid.promise(); + + var dir = path.join(gpii.tests.remoteFileSettingsHandler.testDir, "setting" + Math.random()); + mkdirp.sync(dir); + + // Remove backups from a previous run + var backupDir = path.join(settingsDir, "backups"); + rimraf.sync(backupDir); + + // Fix the paths in the payload + var test = gpii.tests.remoteFileSettingsHandler.fixPath(testData, dir); + + // Create the initial files. + fluid.each(test.initialFiles, function (content, file) { + fs.writeFileSync(path.join(dir, file), content); + }); + + // Initial files should be backed up, unless specified otherwise. + var expectedBackups = test.expectedBackups || test.initialFiles; + + // post-apply/restore file checks + jqUnit.expect(2 + + (Object.keys(test.expectedFiles).length * 2 + 1) + + (Object.keys(test.initialFiles).length * 2 + 1) + + (Object.keys(expectedBackups).length * 2)); + + // Record the before/after invokes + gpii.tests.remoteFileSettingsHandler.invokedValues = {}; + + fluid.log("Applying setting"); + + // Apply the setting. + var settingPromise = gpii.settingsHandlers.remoteFileSettingsHandler.set(test.payload); + + settingPromise.then(function (result) { + + fluid.logObjectRenderChars = 0xffffff; + fluid.log(result); + + + var checkResult = gpii.tests.remoteFileSettingsHandler.fixStashPath(result); + + jqUnit.assertDeepEq("Result should be what's expected", test.result, checkResult); + + // Check the files have been correctly applied. + gpii.tests.remoteFileSettingsHandler.checkFiles(dir, test.expectedFiles); + + // Restore the setting. + var restore = gpii.settingsHandlers.transformPayload(gpii.settingsHandlers.setResponseToSnapshot(result), function (setting, settingPath) { + var testSetting = fluid.get(test.payload, settingPath); + return { + settings: setting.settings, + options: testSetting.options + }; + }); + + fluid.log("Restoring setting"); + gpii.settingsHandlers.remoteFileSettingsHandler.set(restore).then(function () { + // Check the files have been correctly restored. + gpii.tests.remoteFileSettingsHandler.checkFiles(dir, test.initialFiles); + // Check the custom invokers were hit + jqUnit.assertDeepEq("custom invokers should have been invoked as expected", + test.expectedInvokes || {}, gpii.tests.remoteFileSettingsHandler.invokedValues); + + // Ensure file backups had been made + fluid.each(expectedBackups, function (content, file) { + var backupFile = path.join(backupDir, file); + jqUnit.assertTrue("Backup file for " + file + " should exist.", fs.existsSync(backupFile)); + jqUnit.assertEquals("Backup file for " + file + " should be the correct content.", + content, fs.readFileSync(backupFile, "utf8")); + }); + + promise.resolve(); + }); + }); + + return promise; +}; + +module.exports = kettle.test.bootstrap("gpii.tests.remoteFileSettingsHandler"); diff --git a/gpii/node_modules/settingsHandlers/test/SettingsHandlerUtilitiesTests.js b/gpii/node_modules/settingsHandlers/test/SettingsHandlerUtilitiesTests.js index 19d662bba..28ddc11cc 100644 --- a/gpii/node_modules/settingsHandlers/test/SettingsHandlerUtilitiesTests.js +++ b/gpii/node_modules/settingsHandlers/test/SettingsHandlerUtilitiesTests.js @@ -1,76 +1,108 @@ -/*! -Settings Handler Utilities Tests +/* + * Settings Handler Utilities Tests + * + * Copyright 2016 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ -Copyright 2012 Raising the Floor - International -Copyright 2013 OCAD University +"use strict"; -Licensed under the New BSD license. You may not use this file except in -compliance with this License. +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.registerNamespace("jqUnit"), + kettle = require("kettle"); -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ +fluid.registerNamespace("gpii.tests.settingsHandlers"); -/*global require, __dirname, setTimeout*/ +kettle.loadTestingSupport(); -(function () { - // TODO: Rewrite all these tests into browser-style jqUnit tests, leaving just - // a stub tester here to test filesystem integration +require("settingsHandlers"); - var fluid = require("infusion"), - gpii = fluid.registerNamespace("gpii"), - jqUnit = fluid.require("jqUnit"), - fs = require("fs"); +gpii.tests.settingsHandlers.readFile = function () { + // check that we're able to read existing file: + var result = gpii.settingsHandlers.readFile({ + filename: __dirname + "/data/basic.json" + }); + jqUnit.assertDeepEq("Expecting to have read the file properly", { Hello: "World" }, JSON.parse(result)); - require("settingsHandlers"); + // check reading non-existing file + var result2 = gpii.settingsHandlers.readFile({ + filename: __dirname + "/data/bogus.json" + }); + jqUnit.assertEquals("Expecting undefined on reading non-existing file", undefined, result2); +}; - jqUnit.module("SettingsHandlerUtilities Tests"); +var handleFileSolutionEntryData = { + existingFileSolutionEntry: { + options: { + "filename": __dirname + "/data/basic.json" + }, + settings: { + "Hello": "Mars" + } + }, + missingFileSolutionEntry: { + options: { + "filename": __dirname + "/data/bogus.json" + }, + settings: { + "Hello": "Mars" + } + } +}; - jqUnit.test("settingsHandlers.setSettings", function () { - // check simple json structure - var result = gpii.settingsHandlers.setSettings( - {settings: { "key": "val1" }}, { "key": "val2"}); - jqUnit.assertDeepEq("Simple setting", - { key: { oldValue: 'val2', newValue: 'val1' } }, result.settings); +gpii.tests.settingsHandlers.handleFileSolutionEntry = function () { + // an existing file should be parsed and the current value returned: + var result = gpii.settingsHandlers.handleFileSolutionEntry(handleFileSolutionEntryData.existingFileSolutionEntry, + gpii.settingsHandlers.getSettings, gpii.settingsHandlers.JSONSettingsHandler.parser, false); + jqUnit.assertDeepEq("Expecting to have read the file properly", { + "settings": { + "Hello": "World" + } + }, result); - // complex json structure - var json = { - firstentry: { - lvl2: { - key1: "vala", - key2: "valb" - } - }, - nextentry: { - key3: "valc" - } - }; - var settings = { - "firstentry.lvl2.key1": "newValue", - "nextentry.key3": "otherNewValue" - }; - var exp = { - "firstentry.lvl2.key1": { oldValue: "vala", newValue: "newValue" }, - "nextentry.key3": { oldValue: "valc", newValue: "otherNewValue" } - }; - result = gpii.settingsHandlers.setSettings({settings: settings}, json); - jqUnit.assertDeepEq("Multiple and nested settings", exp, - result.settings); + // a non-existing file should return undefined for the requested settings: + result = gpii.settingsHandlers.handleFileSolutionEntry(handleFileSolutionEntryData.missingFileSolutionEntry, + gpii.settingsHandlers.getSettings, gpii.settingsHandlers.JSONSettingsHandler.parser, false); + jqUnit.assertDeepEq("Expecting to get 'undefined' values for settings requested", { + "settings": { + "Hello": undefined + } + }, result); +}; - // check setSettings for unset values - json = { - somekey: { - somekey2: { - key1: "vala" - } - } - }; - settings = { - "newkey1.newkey2": "someval" - }; - exp = { 'newkey1.newkey2': { oldValue: undefined, newValue: 'someval' }}; - result = gpii.settingsHandlers.setSettings({settings: settings}, json); - jqUnit.assertDeepEq("Non existing settings (multilevel)", exp, - result.settings); - }); -}()); +fluid.defaults("gpii.tests.settingsHandlers.env", { + gradeNames: ["fluid.test.testEnvironment"], + components: { + tester: { + type: "gpii.tests.settingsHandlers.caseHolder" + } + } +}); + +fluid.defaults("gpii.tests.settingsHandlers.caseHolder", { + gradeNames: ["fluid.test.testCaseHolder"], + modules: [{ + name: "File Dependency Methods tests", + tests: [{ + expect: 2, + name: "gpii.settingsHandlers.readFile tests", + func: "gpii.tests.settingsHandlers.readFile" + }, { + expect: 2, + name: "gpii.settingsHandlers.handleFileSolutionEntry tests", + func: "gpii.tests.settingsHandlers.handleFileSolutionEntry" + }] + }] +}); + +module.exports = kettle.test.bootstrap("gpii.tests.settingsHandlers.env"); diff --git a/gpii/node_modules/settingsHandlers/test/WebSocketsSettingsHandlerTests.js b/gpii/node_modules/settingsHandlers/test/WebSocketsSettingsHandlerTests.js new file mode 100644 index 000000000..44a197ef8 --- /dev/null +++ b/gpii/node_modules/settingsHandlers/test/WebSocketsSettingsHandlerTests.js @@ -0,0 +1,160 @@ +/* + * WebSockets Settings Handler Tests + * + * Copyright 2013, 2014 Emergya + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.require("node-jqunit", require, "jqUnit"); + +fluid.require("%gpii-universal"); + +var webSocketsTests = fluid.registerNamespace("gpii.tests.settingsHandlers.webSockets"); + +webSocketsTests.payloads = { + getPayload: { + "net.gpii.test": [ + { + settings: { + dog: null, + cat: null, + amount: null, + cheese: null, + "beers.trappiste": null, + "beers.blanche": null, + "beers.scotch": null + }, + options: { + path: "net.gpii.test" + } + } + ] + }, + expectedGetPayload: { + "net.gpii.test": [ + { + settings: { + dog: undefined, + cat: undefined, + amount: undefined, + cheese: undefined, + "beers.trappiste": undefined, + "beers.blanche": undefined, + "beers.scotch": undefined + } + } + ] + + }, + setPayload: { + "net.gpii.test": [ + { + settings: { + dog: "woof", + cat: "meow", + amount: 3, + cheese: ["gouda", "brugge"], + "beers.trappiste": "Chimay", + "beers.blanche": "Hoegaarden", + "beers.scotch": "Achouffe McChouffe" + }, + options: { + path: "net.gpii.test" + } + } + ] + }, + expectedSetReturnPayload: { + "net.gpii.test": [ + { + settings: { + dog: { oldValue: undefined, newValue: "woof" }, + cat: { oldValue: undefined, newValue: "meow" }, + amount: { oldValue: undefined, newValue: 3 }, + cheese: { oldValue: undefined, newValue: ["gouda", "brugge"] }, + "beers.trappiste": {oldValue: undefined, newValue: "Chimay" }, + "beers.blanche": {oldValue: undefined, newValue: "Hoegaarden" }, + "beers.scotch": {oldValue: undefined, newValue: "Achouffe McChouffe" } + } + } + ] + }, + restorePayload: { + "net.gpii.test": [ + { + settings: { + dog: undefined, + cat: undefined, + amount: undefined, + cheese: undefined, + "beers.trappiste": undefined, + "beers.blanche": undefined, + "beers.scotch": undefined + }, + options: { + path: "net.gpii.test" + } + } + ] + }, + expectedRestoreReturnPayload: { + "net.gpii.test": [ + { + settings: { + dog: { newValue: undefined, oldValue: "woof" }, + cat: { newValue: undefined, oldValue: "meow" }, + amount: { newValue: undefined, oldValue: 3 }, + cheese: { newValue: undefined, oldValue: ["gouda", "brugge"] }, + "beers.trappiste": {newValue: undefined, oldValue: "Chimay" }, + "beers.blanche": {newValue: undefined, oldValue: "Hoegaarden" }, + "beers.scotch": {newValue: undefined, oldValue: "Achouffe McChouffe" } + } + } + ] + } +}; + +jqUnit.module("WebSockets settings handler test module"); + +jqUnit.test("Checking wsSettingsHandler object before doing anything", function () { + var that = gpii.settingsHandlers.webSockets.component(); + jqUnit.assertTrue("There are an object to store the settings", that.model.settings); + jqUnit.assertTrue("There are an object to store the clients", that.clients); + + jqUnit.assertDeepEq("wsSettingsHandler.settings is empty", {}, that.model.settings); + jqUnit.assertDeepEq("wsSettingsHandler.clients is empty", {}, that.clients); +}); + +jqUnit.test("Checking wsSettingsHandler's get/set methods", function () { + gpii.settingsHandlers.webSockets.instance = gpii.settingsHandlers.webSockets.component(); + jqUnit.assertDeepEq("There aren't settings yet for 'net.gpii.test'", + webSocketsTests.payloads.expectedGetPayload, + gpii.resolveSync(gpii.settingsHandlers.webSockets.get(webSocketsTests.payloads.getPayload))); + + var setReturnPayload = gpii.resolveSync(gpii.settingsHandlers.webSockets.set(webSocketsTests.payloads.setPayload)); + jqUnit.assertDeepEq("Set function returns the expected payload", + webSocketsTests.payloads.expectedSetReturnPayload, + setReturnPayload); + + var restoreReturnPayload = gpii.resolveSync(gpii.settingsHandlers.webSockets.set(webSocketsTests.payloads.restorePayload)); + jqUnit.assertDeepEq("Restore returns the expected payload", + webSocketsTests.payloads.expectedRestoreReturnPayload, + restoreReturnPayload); + + jqUnit.assertDeepEq("After restoring, model.settings should be {}", + {}, gpii.settingsHandlers.webSockets.instance.model.settings); + delete gpii.settingsHandlers.webSockets.instance; +}); diff --git a/gpii/node_modules/settingsHandlers/test/XMLSettingsHandlerTests.js b/gpii/node_modules/settingsHandlers/test/XMLSettingsHandlerTests.js index 616b0eae5..c77d8552c 100644 --- a/gpii/node_modules/settingsHandlers/test/XMLSettingsHandlerTests.js +++ b/gpii/node_modules/settingsHandlers/test/XMLSettingsHandlerTests.js @@ -1,534 +1,481 @@ -/*! -XML Settings Handler Tests +/* + * XML Settings Handler Tests + * + * Copyright 2012 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ -Copyright 2012 Raising the Floor - International +"use strict"; -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -/*global require, __dirname, setTimeout*/ - -(function () { // TODO: Rewrite all these tests into browser-style jqUnit tests, leaving just // a stub tester here to test filesystem integration - var fluid = require("infusion"), - gpii = fluid.registerNamespace("gpii"), - jqUnit = fluid.require("jqUnit"), - fs = require("fs"); +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.registerNamespace("jqUnit"), + kettle = require("kettle"), + fs = require("fs"); - require("settingsHandlers"); +fluid.registerNamespace("gpii.tests"); - var testTempFilePaths = [ - __dirname + "/data/XMLSettingsHandler/tmp1.xml", - __dirname + "/data/XMLSettingsHandler/tmp2.xml" - ]; +kettle.loadTestingSupport(); - var deleteTestFiles = function () { - fluid.each(testTempFilePaths, function (filePath) { - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - } - }); - }; +require("settingsHandlers"); - jqUnit.module("XMLSettingsHandler Tests", { - teardown: deleteTestFiles - }); +var testTempFilePaths = [ + __dirname + "/data/XMLSettingsHandler/tmp1.xml", + __dirname + "/data/XMLSettingsHandler/tmp2.xml" +]; - // TODO: Improve naming. - var payload2Options = { - "filename": testTempFilePaths[1], - "encoding": "utf-8", - "xml-tag": "", - "rules": { - "testing.simpleArray.settings": { - "transform": { - type: "fluid.transforms.arrayToObject", - inputPath: "testing.simpleArray.settings", - key: "unique" - } - }, - "testing.Sociable\\.Launcher\\.Properties\\.Settings.setting": { - "transform": { - type: "fluid.transforms.arrayToObject", - inputPath: "testing.Sociable\\.Launcher\\.Properties\\.Settings.setting", - key: "name" - } - }, - "testing": "testing" +var deleteTestFiles = function () { + fluid.each(testTempFilePaths, function (filePath) { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); } - }; + }); +}; - var xmlHandlerXMLtoJSONSpec = [{ - xml: 'worldanother worldme as wellhello indexed worldhello other indexed worldhello third indexed worldC:\\Sociable\\Games\\ResourcesC:\\Sociable\\Games\\Resources\\Launcher0', - tests: [{ - expected: { - testing: { - hello: { - $t: "world" - }, - "hello.to": { - $t: "another world" - }, - "with-params": { - foo: "bar" +// TODO: Improve naming. +var payload2Options = { + "filename": testTempFilePaths[1], + "encoding": "utf-8", + "xml-tag": "", + "rules": { + "testing.simpleArray.settings": { + "transform": { + type: "fluid.transforms.indexArrayByKey", + inputPath: "testing.simpleArray.settings", + key: "unique" + } + }, + "testing": "testing" + } +}; + +var xmlHandlerXMLtoJSONSpec = [{ + xml: "worldanother worldme as wellhello indexed worldhello other indexed worldhello third indexed world", + tests: [{ + expected: { + testing: { + hello: { + $t: "world" + }, + "hello.to": { + $t: "another world" + }, + "with-params": { + foo: "bar" + }, + "double": { + hello1: { + iam: "here" }, - "double": { - hello1: { - iam: "here" - }, - hello2: { - iam: "alsohere", - and: { - $t: "me as well" - } + hello2: { + iam: "alsohere", + and: { + $t: "me as well" } - }, - simpleArray: { - settings: [{ - unique: "ind1", - $t: "hello indexed world" - }, { - unique: "ind2", - $t: "hello other indexed world" - }, { - unique: "ind3", - $t: "hello third indexed world" - }] - }, - "Sociable.Launcher.Properties.Settings": { - setting: [{ - name: "GameResources", - serializeAs: "String", - value: { - $t: "C:\\Sociable\\Games\\Resources" - } - }, { - name: "Resources", - serializeAs: "String", - value: { - $t: "C:\\Sociable\\Games\\Resources\\Launcher" - } - }, { - name: "AutoLogin", - serializeAs: "String", - value: { - $t: "0" - } - }] } + }, + simpleArray: { + settings: [{ + unique: "ind1", + $t: "hello indexed world" + }, { + unique: "ind2", + $t: "hello other indexed world" + }, { + unique: "ind3", + $t: "hello third indexed world" + }] } } - }, { - expected: { - testing: { - hello: { - $t: "world" - }, - "hello.to": { - $t: "another world" - }, - "with-params": { - foo: "bar" + } + }, { + expected: { + testing: { + hello: { + $t: "world" + }, + "hello.to": { + $t: "another world" + }, + "with-params": { + foo: "bar" + }, + "double": { + hello1: { + iam: "here" }, - "double": { - hello1: { - iam: "here" - }, - hello2: { - iam: "alsohere", - and: { - $t: "me as well" - } + hello2: { + iam: "alsohere", + and: { + $t: "me as well" } - }, - simpleArray: { - settings: { - ind1: { - $t: "hello indexed world" - }, - ind2: { - $t: "hello other indexed world" - }, - ind3: { - $t: "hello third indexed world" - } - } - }, - "Sociable.Launcher.Properties.Settings": { - setting: { - GameResources: { - serializeAs: "String", - value: { - $t: "C:\\Sociable\\Games\\Resources" - } - }, - Resources: { - serializeAs: "String", - value: { - $t: "C:\\Sociable\\Games\\Resources\\Launcher" - } - }, - AutoLogin: { - serializeAs: "String", - value: { - $t: "0" - } - } + } + }, + simpleArray: { + settings: { + ind1: { + $t: "hello indexed world" + }, + ind2: { + $t: "hello other indexed world" + }, + ind3: { + $t: "hello third indexed world" } } } - }, - options: { - rules: payload2Options.rules } - }] - }]; + }, + options: { + rules: payload2Options.rules + } + }] +}]; - function testXMLHandlerXMLtoJSON(spec) { - fluid.each(spec.tests, function (test) { - var actual = gpii.settingsHandlers.XMLHandler.parser.parse(spec.xml, test.options); - jqUnit.assertDeepEq("XMLHandler.parser.parse correctly converts xml to json", test.expected, actual); - }); - } - jqUnit.test("XMLHandler.parser.parse", function () { - fluid.each(xmlHandlerXMLtoJSONSpec, testXMLHandlerXMLtoJSON); +function testXMLHandlerXMLtoJSON(spec) { + fluid.each(spec.tests, function (test) { + var actual = gpii.settingsHandlers.XMLHandler.parser.parse(spec.xml, test.options); + jqUnit.assertDeepEq("XMLHandler.parser.parse correctly converts xml to json", test.expected, actual); }); +} - var xmlHandlerJSONtoXMLSpec = [{ - tests: [{ - name: "no rules test", - xml: '\r\n\r\n world\r\n another world\r\n \r\n \r\n \r\n \r\n me as well\r\n \r\n \r\n \r\n hello indexed world\r\n hello other indexed world\r\n hello third indexed world\r\n \r\n \r\n \r\n C:\\Sociable\\Games\\Resources\r\n \r\n \r\n C:\\Sociable\\Games\\Resources\\Launcher\r\n \r\n \r\n 0\r\n \r\n \r\n\r\n', - json: { - testing: { - hello: { - $t: "world" - }, - "hello.to": { - $t: "another world" - }, - "with-params": { - foo: "bar" +var xmlHandlerJSONtoXMLSpec = [{ + tests: [{ + name: "no rules test", + xml: "\r\n\r\n world\r\n another world\r\n \r\n \r\n \r\n \r\n me as well\r\n \r\n \r\n \r\n hello indexed world\r\n hello other indexed world\r\n hello third indexed world\r\n \r\n\r\n", + json: { + testing: { + hello: { + $t: "world" + }, + "hello.to": { + $t: "another world" + }, + "with-params": { + foo: "bar" + }, + "double": { + hello1: { + iam: "here" }, - "double": { - hello1: { - iam: "here" - }, - hello2: { - iam: "alsohere", - and: { - $t: "me as well" - } + hello2: { + iam: "alsohere", + and: { + $t: "me as well" } + } + }, + simpleArray: { + settings: [{ + unique: "ind1", + $t: "hello indexed world" + }, { + unique: "ind2", + $t: "hello other indexed world" + }, { + unique: "ind3", + $t: "hello third indexed world" + }] + } + } + }, + options: { + "xml-tag": "" + } + }, { + name: "with rules test", + xml: "\r\n\r\n world\r\n another world\r\n \r\n \r\n \r\n \r\n me as well\r\n \r\n \r\n \r\n hello indexed world\r\n hello other indexed world\r\n hello third indexed world\r\n \r\n\r\n", + json: { + testing: { + hello: { + $t: "world" + }, + "hello.to": { + $t: "another world" + }, + "with-params": { + foo: "bar" + }, + "double": { + hello1: { + iam: "here" }, - simpleArray: { - settings: [{ - unique: "ind1", + hello2: { + iam: "alsohere", + and: { + $t: "me as well" + } + } + }, + simpleArray: { + settings: { + ind1: { $t: "hello indexed world" - }, { - unique: "ind2", + }, + ind2: { $t: "hello other indexed world" - }, { - unique: "ind3", + }, + ind3: { $t: "hello third indexed world" - }] - }, - "Sociable.Launcher.Properties.Settings": { - setting: [{ - serializeAs: "String", - name: "GameResources", - value: { - $t: "C:\\Sociable\\Games\\Resources" - } - }, { - serializeAs: "String", - name: "Resources", - value: { - $t: "C:\\Sociable\\Games\\Resources\\Launcher" - } - }, { - serializeAs: "String", - name: "AutoLogin", - value: { - $t: "0" - } - }] + } } } - }, - options: { - "xml-tag": '' } - }, { - name: "with rules test", - xml: '\r\n\r\n world\r\n another world\r\n \r\n \r\n \r\n \r\n me as well\r\n \r\n \r\n \r\n hello indexed world\r\n hello other indexed world\r\n hello third indexed world\r\n \r\n \r\n \r\n C:\\Sociable\\Games\\Resources\r\n \r\n \r\n C:\\Sociable\\Games\\Resources\\Launcher\r\n \r\n \r\n 0\r\n \r\n \r\n\r\n', - json: { - testing: { - hello: { - $t: "world" - }, - "hello.to": { - $t: "another world" - }, - "with-params": { - foo: "bar" + }, + options: { + rules: payload2Options.rules, + "xml-tag": "" + } + }] +}]; + +function testXMLHandlerJSONtoXML(spec) { + fluid.each(spec.tests, function (test) { + var actual = gpii.settingsHandlers.XMLHandler.parser.stringify(test.json, test.options); + jqUnit.assertEquals("XMLHandler.parser.stringify correctly converts json to xml (" + test.name + ")", test.xml, actual); + }); +} + +var xmlHandlerOperationTestSpecs = [ + { + name: "Payload 1", // TODO: Give these tests meaningful names! + originalFilePath: __dirname + "/data/XMLSettingsHandler/test1.xml", + outputFilePath: testTempFilePaths[0], + expectedFilePath: __dirname + "/data/XMLSettingsHandler/expected_output1.xml", + set: { + payload: { + // TODO: These are not actually valid settings + "com.texthelp.readwritegold": [{ + settings: { + "ApplicationSettings.IsEnterprise.$t": true, + "ApplicationSettings.SpeechInput.LanguageId.$t": 1 }, - "double": { - hello1: { - iam: "here" - }, - hello2: { - iam: "alsohere", - and: { - $t: "me as well" - } + options: { + "filename": testTempFilePaths[0], + "encoding": "utf-8", + "xml-tag": "", + "rules": { + "ApplicationSettings": "ApplicationSettings" } + } + }] + }, + expectedResult: { + "com.texthelp.readwritegold": [{ + settings: { + "ApplicationSettings.IsEnterprise.$t": { oldValue: "false", newValue: true }, + "ApplicationSettings.SpeechInput.LanguageId.$t": { oldValue: "0", newValue: 1 } }, - simpleArray: { - settings: { - ind1: { - $t: "hello indexed world" - }, - ind2: { - $t: "hello other indexed world" - }, - ind3: { - $t: "hello third indexed world" - } + options: { + "filename": testTempFilePaths[0], + "encoding": "utf-8", + "xml-tag": "", + "rules": { + "ApplicationSettings": "ApplicationSettings" } + } + }] + } + }, + get: { + payload: { + "com.texthelp.readwritegold": [{ + settings: { + "ApplicationSettings.IsEnterprise.$t": null, + "ApplicationSettings.SpeechInput.LanguageId.$t": null }, - "Sociable.Launcher.Properties.Settings": { - setting: { - GameResources: { - serializeAs: "String", - value: { - $t: "C:\\Sociable\\Games\\Resources" - } - }, - Resources: { - serializeAs: "String", - value: { - $t: "C:\\Sociable\\Games\\Resources\\Launcher" - } - }, - AutoLogin: { - serializeAs: "String", - value: { - $t: "0" - } - } + options: { + "filename": testTempFilePaths[0], + "encoding": "utf-8", + "xml-tag": "", + "rules": { + "ApplicationSettings": "ApplicationSettings" } } - } + }] }, - options: { - rules: payload2Options.rules, - "xml-tag": '' + expectedResult: { + "com.texthelp.readwritegold": [{ + settings: { + "ApplicationSettings.IsEnterprise.$t": "false", + "ApplicationSettings.SpeechInput.LanguageId.$t": "0" + } + }] } - }] - }]; - - function testXMLHandlerJSONtoXML(spec) { - fluid.each(spec.tests, function (test) { - var actual = gpii.settingsHandlers.XMLHandler.parser.stringify(test.json, test.options); - jqUnit.assertEquals("XMLHandler.parser.stringify correctly converts json to xml (" + test.name + ")", test.xml, actual); - }); - } - jqUnit.test("XMLHandler.parser.stringify", function () { - fluid.each(xmlHandlerJSONtoXMLSpec, testXMLHandlerJSONtoXML); - }); - - jqUnit.test("XMLHandler.formatXml", function () { - var xml = 'worldanother worldme as wellhello indexed worldhello other indexed worldhello third indexed worldC:\\Sociable\\Games\\ResourcesC:\\Sociable\\Games\\Resources\\Launcher0'; - var expected = '\r\n\r\n world\r\n another world\r\n \r\n \r\n \r\n \r\n me as well\r\n \r\n \r\n \r\n hello indexed world\r\n hello other indexed world\r\n hello third indexed world\r\n \r\n \r\n \r\n C:\\Sociable\\Games\\Resources\r\n \r\n \r\n C:\\Sociable\\Games\\Resources\\Launcher\r\n \r\n \r\n 0\r\n \r\n \r\n\r\n' - var formatted = gpii.settingsHandlers.XMLHandler.formatXml(xml); - jqUnit.assertEquals("XMLHandler.formatXml correctly formats xml", expected, formatted); - }); - - var xmlHandlerOperationTestSpecs = [ - { - name: "Payload 1", // TODO: Give these tests meaningful names! - originalFilePath: __dirname+"/data/XMLSettingsHandler/test1.xml", - outputFilePath: testTempFilePaths[0], - expectedFilePath: __dirname+"/data/XMLSettingsHandler/expected_output1.xml", - set: { - payload: { - "com.texthelp.readwritegold": [{ - settings: { - "ApplicationSettings.IsEnterprise.$t": true, - "ApplicationSettings.SpeechInput.LanguageId.$t": 1 - }, - options: { - "filename": testTempFilePaths[0], - "encoding": "utf-8", - "xml-tag": "", - "rules": { - "ApplicationSettings": "ApplicationSettings" - } - } - }] - }, - expectedResult: { - 'com.texthelp.readwritegold': [{ + } + }, + { + name: "Payload 2", // TODO: Meaningful name! + originalFilePath: __dirname + "/data/XMLSettingsHandler/test2.xml", + outputFilePath: testTempFilePaths[1], + expectedFilePath: __dirname + "/data/XMLSettingsHandler/expected_output2.xml", + set: { + payload: { + "setreq": [ + { settings: { - 'ApplicationSettings.IsEnterprise.$t': { oldValue: 'false', newValue: true }, - 'ApplicationSettings.SpeechInput.LanguageId.$t': { oldValue: '0', newValue: 1 } + "testing.hello.$t": "space", + "testing.hello\\.to.$t": "another space", + "testing.new-variable.here": "KAPAW", + "testing.with-params.foo": "oof", + "testing.double.hello2.iam": "bond, james bond", + "testing.double.hello2.and.$t": "bond, james bond, jr", + "testing.simpleArray.settings.ind2.$t": "hello other indexed space" }, - options: { - "filename": testTempFilePaths[0], - "encoding": "utf-8", - "xml-tag": "", - "rules": { - "ApplicationSettings": "ApplicationSettings" - } - } - }] - } + options: payload2Options + } + ] }, - get: { - payload: { - "com.texthelp.readwritegold": [{ + + expectedResult: { + "setreq": [ + { settings: { - "ApplicationSettings.IsEnterprise.$t": null, - "ApplicationSettings.SpeechInput.LanguageId.$t": null + "testing.hello.$t": { oldValue: "world", newValue: "space" }, + "testing.hello\\.to.$t": { oldValue: "another world", newValue: "another space" }, + "testing.new-variable.here": { oldValue: undefined, newValue: "KAPAW" }, + "testing.with-params.foo": { oldValue: "bar", newValue: "oof" }, + "testing.double.hello2.iam": { oldValue: "alsohere", newValue: "bond, james bond" }, + "testing.double.hello2.and.$t": { oldValue: "me as well", newValue: "bond, james bond, jr" }, + "testing.simpleArray.settings.ind2.$t": { oldValue: "hello other indexed world", newValue: "hello other indexed space" } }, - options: { - "filename": testTempFilePaths[0], - "encoding": "utf-8", - "xml-tag": "", - "rules": { - "ApplicationSettings": "ApplicationSettings" - } - } - }] - }, - expectedResult: { - 'com.texthelp.readwritegold': [{ - settings: { - 'ApplicationSettings.IsEnterprise.$t': 'false', - 'ApplicationSettings.SpeechInput.LanguageId.$t': '0' - } - }] - } + options: payload2Options + } + ] } }, - { - name: "Payload 2", // TODO: Meaningful name! - originalFilePath: __dirname+"/data/XMLSettingsHandler/test2.xml", - outputFilePath: testTempFilePaths[1], - expectedFilePath: __dirname+"/data/XMLSettingsHandler/expected_output2.xml", - set: { - payload: { - "setreq": [ - { - settings: { - "testing.hello.$t": "space", - "testing.hello\\.to.$t": "another space", - "testing.new-variable.here": "KAPAW", - "testing.with-params.foo": "oof", - "testing.double.hello2.iam": "bond, james bond", - "testing.double.hello2.and.$t": "bond, james bond, jr", - "testing.simpleArray.settings.ind2.$t":"hello other indexed space", - "testing.Sociable\\.Launcher\\.Properties\\.Settings.setting.AutoLogin.serializeAs": "New Value", - "testing.Sociable\\.Launcher\\.Properties\\.Settings.setting.AutoLogin.value.$t": "10000" - }, - options: payload2Options - } - ] - }, - - expectedResult: { - "setreq": [ - { - settings: { - "testing.hello.$t": { oldValue: "world", newValue: "space" }, - "testing.hello\\.to.$t": { oldValue: "another world", newValue: "another space" }, - "testing.new-variable.here": { oldValue: undefined, newValue: "KAPAW" }, - "testing.with-params.foo": { oldValue: "bar", newValue: "oof" }, - "testing.double.hello2.iam": { oldValue: "alsohere", newValue: "bond, james bond" }, - "testing.double.hello2.and.$t": { oldValue: "me as well", newValue: "bond, james bond, jr" }, - "testing.simpleArray.settings.ind2.$t": { oldValue: "hello other indexed world", newValue: "hello other indexed space" }, - "testing.Sociable\\.Launcher\\.Properties\\.Settings.setting.AutoLogin.serializeAs": { oldValue: "String", newValue: "New Value" }, - "testing.Sociable\\.Launcher\\.Properties\\.Settings.setting.AutoLogin.value.$t": { oldValue: "0", newValue: "10000" } - }, - options: payload2Options - } - ] - } + get: { + payload: { + "setreq": [ + { + settings: { + "testing.hello.$t": null, + "testing.hello\\.to.$t": null, + "testing.new-variable.here": null, + "testing.with-params.foo": null, + "testing.double.hello2.iam": null, + "testing.double.hello2.and.$t": null, + "testing.simpleArray.settings.ind2.$t": null + }, + options: payload2Options + } + ] }, - get: { - payload: { - "setreq": [ - { - settings: { - "testing.hello.$t": null, - "testing.hello\\.to.$t": null, - "testing.new-variable.here": null, - "testing.with-params.foo": null, - "testing.double.hello2.iam": null, - "testing.double.hello2.and.$t": null, - "testing.simpleArray.settings.ind2.$t": null, - "testing.Sociable\\.Launcher\\.Properties\\.Settings.setting.AutoLogin.serializeAs": null, - "testing.Sociable\\.Launcher\\.Properties\\.Settings.setting.AutoLogin.value.$t": null - }, - options: payload2Options - } - ] - }, - expectedResult: { - "setreq": [ - { - settings: { - "testing.hello.$t": "world", - "testing.hello\\.to.$t": "another world", - "testing.new-variable.here": undefined, - "testing.with-params.foo": "bar", - "testing.double.hello2.iam": "alsohere", - "testing.double.hello2.and.$t": "me as well", - "testing.simpleArray.settings.ind2.$t": "hello other indexed world", - "testing.Sociable\\.Launcher\\.Properties\\.Settings.setting.AutoLogin.serializeAs": "String", - "testing.Sociable\\.Launcher\\.Properties\\.Settings.setting.AutoLogin.value.$t": "0", - } + expectedResult: { + "setreq": [ + { + settings: { + "testing.hello.$t": "world", + "testing.hello\\.to.$t": "another world", + "testing.new-variable.here": undefined, + "testing.with-params.foo": "bar", + "testing.double.hello2.iam": "alsohere", + "testing.double.hello2.and.$t": "me as well", + "testing.simpleArray.settings.ind2.$t": "hello other indexed world" } - ] - } + } + ] } } - ]; + } +]; - var testXMLHandlerGetOperation = function (testSpec) { - //read original XML and copy to file from options: - var origXML = fs.readFileSync(testSpec.originalFilePath, "utf-8"); - fs.writeFileSync(testSpec.outputFilePath, origXML, "utf-8"); - var returnPayload = gpii.settingsHandlers.XMLHandler.get(testSpec.get.payload); - jqUnit.assertDeepEq(testSpec.name + ": Checking return payload", testSpec.get.expectedResult, returnPayload); - }; +var testXMLHandlerGetOperation = function (testSpec) { + // read original XML and copy to file from options: + var origXML = fs.readFileSync(testSpec.originalFilePath, "utf-8"); + fs.writeFileSync(testSpec.outputFilePath, origXML, "utf-8"); + var returnPayload = gpii.settingsHandlers.XMLHandler.get(testSpec.get.payload); + jqUnit.assertDeepEq(testSpec.name + ": Checking return payload", testSpec.get.expectedResult, returnPayload); +}; - jqUnit.test("XMLHandler.get", function () { - fluid.each(xmlHandlerOperationTestSpecs, testXMLHandlerGetOperation); - }); +var testXMLHandlerSetOperation = function (testSpec) { + // read original XML and copy to a file we can mess up: + var origXML = fs.readFileSync(testSpec.originalFilePath, "utf-8"); + fs.writeFileSync(testSpec.outputFilePath, origXML, "utf-8"); + // test return payload + var returnPayload = gpii.settingsHandlers.XMLHandler.set(testSpec.set.payload); + jqUnit.assertDeepEq(testSpec.name + ": Checking return payload for simple XML file: ", testSpec.set.expectedResult, returnPayload); - var testXMLHandlerSetOperation = function (testSpec) { - //read original XML and copy to a file we can mess up: - var origXML = fs.readFileSync(testSpec.originalFilePath, "utf-8"); - fs.writeFileSync(testSpec.outputFilePath, origXML, "utf-8"); - //test return payload - var returnPayload = gpii.settingsHandlers.XMLHandler.set(testSpec.set.payload); - jqUnit.assertDeepEq(testSpec.name + ": Checking return payload for simple XML file: ", testSpec.set.expectedResult, returnPayload); + // next check that the written XML file is as expected: + var savedXML = fs.readFileSync(testSpec.outputFilePath, "utf-8"); + var expectedXML = fs.readFileSync(testSpec.expectedFilePath, "utf-8"); + jqUnit.assertEquals(testSpec.name + ": checking XML file: ", expectedXML, savedXML); +}; - //next check that the written XML file is as expected: - var savedXML = fs.readFileSync(testSpec.outputFilePath, "utf-8"); - var expectedXML = fs.readFileSync(testSpec.expectedFilePath, "utf-8"); - jqUnit.assertEquals(testSpec.name + ": checking XML file: ", expectedXML, savedXML); - }; +gpii.tests.XMLHandlerSet = function () { + fluid.each(xmlHandlerOperationTestSpecs, testXMLHandlerSetOperation); +}; - jqUnit.test("XMLHandler.set", function () { - fluid.each(xmlHandlerOperationTestSpecs, testXMLHandlerSetOperation); - }); -}()); +gpii.tests.XMLHandlerGet = function () { + fluid.each(xmlHandlerOperationTestSpecs, testXMLHandlerGetOperation); +}; + +gpii.tests.XMLHandlerParserParse = function () { + fluid.each(xmlHandlerXMLtoJSONSpec, testXMLHandlerXMLtoJSON); +}; + +gpii.tests.XMLHandlerParserStringify = function () { + fluid.each(xmlHandlerJSONtoXMLSpec, testXMLHandlerJSONtoXML); +}; + +gpii.tests.XMLHandlerFormatXml = function () { + var xml = "worldanother worldme as wellhello indexed worldhello other indexed worldhello third indexed world"; + var expected = "\r\n\r\n world\r\n another world\r\n \r\n \r\n \r\n \r\n me as well\r\n \r\n \r\n \r\n hello indexed world\r\n hello other indexed world\r\n hello third indexed world\r\n \r\n\r\n"; + var formatted = gpii.settingsHandlers.XMLHandler.formatXml(xml); + jqUnit.assertEquals("XMLHandler.formatXml correctly formats xml", expected, formatted); +}; + +fluid.defaults("gpii.tests.XMLSettingsHandler", { + gradeNames: ["fluid.test.testEnvironment"], + components: { + tester: { + type: "gpii.tests.XMLSettingsHandlerTester" + } + } +}); + +fluid.defaults("gpii.tests.XMLSettingsHandlerTester", { + gradeNames: ["fluid.test.testCaseHolder"], + listeners: { + onDestroy: deleteTestFiles + }, + modules: [{ + name: "XMLSettingsHandler", + tests: [{ + expect: 4, + name: "XMLHandler.set", + func: "gpii.tests.XMLHandlerSet" + }, { + expect: 2, + name: "XMLHandler.get", + func: "gpii.tests.XMLHandlerGet" + }, { + expect: 2, + name: "XMLHandler.parser.parse", + func: "gpii.tests.XMLHandlerParserParse" + }, { + expect: 2, + name: "XMLHandler.parser.stringify", + func: "gpii.tests.XMLHandlerParserStringify" + }, { + expect: 1, + name: "XMLHandler.FormatXml", + func: "gpii.tests.XMLHandlerFormatXml" + }] + }] +}); + +module.exports = kettle.test.bootstrap("gpii.tests.XMLSettingsHandler"); diff --git a/gpii/node_modules/settingsHandlers/test/data/INISettingsHandler/test1_default.ini b/gpii/node_modules/settingsHandlers/test/data/INISettingsHandler/test1_default.ini index 7e2d1806b..db3d4da9b 100644 --- a/gpii/node_modules/settingsHandlers/test/data/INISettingsHandler/test1_default.ini +++ b/gpii/node_modules/settingsHandlers/test/data/INISettingsHandler/test1_default.ini @@ -10,4 +10,4 @@ ; Speech settings [speech] synth = espeak - symbolLevel = 100 \ No newline at end of file + symbolLevel = 100 diff --git a/gpii/node_modules/settingsHandlers/test/data/INISettingsHandler/test1_expectedOutput.ini b/gpii/node_modules/settingsHandlers/test/data/INISettingsHandler/test1_expectedOutput.ini index a19f24ffe..11f16abd5 100644 --- a/gpii/node_modules/settingsHandlers/test/data/INISettingsHandler/test1_expectedOutput.ini +++ b/gpii/node_modules/settingsHandlers/test/data/INISettingsHandler/test1_expectedOutput.ini @@ -1,10 +1,13 @@ +; NVDA Configuration File + [general] -language = Windows -saveConfigurationOnExit = False -askToExit = false -loggingLevel = INFO -showWelcomeDialogAtStartup = false + language = Windows + saveConfigurationOnExit = False + askToExit = false + loggingLevel = INFO + showWelcomeDialogAtStartup = false +; Speech settings [speech] -synth = espeak -symbolLevel = 90 + synth = espeak + symbolLevel = 90 diff --git a/gpii/node_modules/settingsHandlers/test/data/INISettingsHandler/test2_default.ini b/gpii/node_modules/settingsHandlers/test/data/INISettingsHandler/test2_default.ini index 8de9f9cbf..606feacb2 100644 --- a/gpii/node_modules/settingsHandlers/test/data/INISettingsHandler/test2_default.ini +++ b/gpii/node_modules/settingsHandlers/test/data/INISettingsHandler/test2_default.ini @@ -30,5 +30,9 @@ [[espeak2]] capPitchChange = 32 +[braille] + [[noBraille]] + port = "" + [final] foo = bar diff --git a/gpii/node_modules/settingsHandlers/test/data/INISettingsHandler/test2_expectedOutput.ini b/gpii/node_modules/settingsHandlers/test/data/INISettingsHandler/test2_expectedOutput.ini index cb1afea0b..c5e34a830 100644 --- a/gpii/node_modules/settingsHandlers/test/data/INISettingsHandler/test2_expectedOutput.ini +++ b/gpii/node_modules/settingsHandlers/test/data/INISettingsHandler/test2_expectedOutput.ini @@ -1,34 +1,39 @@ +# NVDA Configuration File + [general] -language = Windows -saveConfigurationOnExit = False -askToExit = True -loggingLevel = INFO -showWelcomeDialogAtStartup = True + language = Windows + saveConfigurationOnExit = False + askToExit = True + loggingLevel = INFO + showWelcomeDialogAtStartup = True +# Speech settings [speech] -synth = espeak -symbolLevel = 90 -beepSpeechModePitch = 10000 -outputDevice = default -autoLanguageSwitching = True -autoDialectSwitching = False - -[[espeak]] -capPitchChange = 30 -sayCapForCapitals = False -beepForCapitals = False -useSpellingFunctionality = True -voice = en\en -variant = max -rate = 40 -rateBoost = False -pitch = 30 -inflection = 75 -volume = 100 + synth = espeak + symbolLevel = 90 + beepSpeechModePitch = 10000 + outputDevice = default + autoLanguageSwitching = True + autoDialectSwitching = False + [[espeak]] + capPitchChange = 30 + sayCapForCapitals = False + beepForCapitals = False + useSpellingFunctionality = True + voice = en\en + variant = max + rate = 40 + rateBoost = False + pitch = 30 + inflection = 75 + volume = 100 + [[espeak2]] + capPitchChange = 32 +newEntry=30 -[[espeak2]] -capPitchChange = 32 -newEntry = 30 +[braille] + [[noBraille]] + port = "" [final] -foo = bar + foo = bar diff --git a/gpii/node_modules/settingsHandlers/test/data/JSONSettingsHandler/test1_default.json b/gpii/node_modules/settingsHandlers/test/data/JSONSettingsHandler/test1_default.json index f4a10d4da..394c49dae 100644 --- a/gpii/node_modules/settingsHandlers/test/data/JSONSettingsHandler/test1_default.json +++ b/gpii/node_modules/settingsHandlers/test/data/JSONSettingsHandler/test1_default.json @@ -1,16 +1,7 @@ { - "userType":"touch", - "theme":"colour", - "speakTitles":"yes", - "showLabels":"yes", - "showImages":"yes", - "useSkype":"no", - "scanMode":"AUTO1SWITCHAUTOSTART", - "scanRate":"2500", - "speakLabels":"no", - "selectionsSetSize":"2x3", - "passwordItems":"images", - "passwordSetSize":"4x3", - "splashTime":2, - "name":"Maavis" + "fish": "raw", + "lemon": "whole", + "mayo": "creamy", + "peas": "frozen", + "potatoes": "raw" } diff --git a/gpii/node_modules/settingsHandlers/test/data/JSONSettingsHandler/test1_expectedOutput.json b/gpii/node_modules/settingsHandlers/test/data/JSONSettingsHandler/test1_expectedOutput.json index dd13e710a..b558bfe27 100644 --- a/gpii/node_modules/settingsHandlers/test/data/JSONSettingsHandler/test1_expectedOutput.json +++ b/gpii/node_modules/settingsHandlers/test/data/JSONSettingsHandler/test1_expectedOutput.json @@ -1,16 +1,7 @@ { - "userType":"scan", - "theme":"bw", - "speakTitles":"yes", - "showLabels":"yes", - "showImages":"yes", - "useSkype":"no", - "scanMode":"AUTO1SWITCHAUTOSTART", - "scanRate":"2500", - "speakLabels":"no", - "selectionsSetSize":"2x3", - "passwordItems":"images", - "passwordSetSize":"4x3", - "splashTime":2, - "name":"Test" + "fish": "battered and fried", + "lemon": "sliced", + "mayo": "creamy", + "peas": "mushy", + "potatoes": "fried" } diff --git a/gpii/node_modules/settingsHandlers/test/data/JSONSettingsHandler/test3_expectedOutput.json b/gpii/node_modules/settingsHandlers/test/data/JSONSettingsHandler/test3_expectedOutput.json new file mode 100644 index 000000000..7a1c30c9e --- /dev/null +++ b/gpii/node_modules/settingsHandlers/test/data/JSONSettingsHandler/test3_expectedOutput.json @@ -0,0 +1,40 @@ +{ + "keybindings": { + "a": "pitch-up" + }, + "profiles": { + "default": { + "speechServerFactory": "orca.speechdispatcherfactory", + "profile": [ + "undefault", + "default" + ], + "brailleContractionTable": "/usr/share/liblouis/tables/en-us-comp6.ctb", + "voices": { + "default": { + }, + "uppercase": { + "average-pitch": 5.6 + }, + "system": { + "established": false + }, + "hyperlink": { + "established": false + }, + "foo.bar": { + } + + }, + "speechServerInfo": [ + "Default Synthesizer", + "default" + ], + "quitOrcaNoConfirmation": true, + "keybindings": { + }, + "pronunciations": { + } + } + } +} diff --git a/gpii/node_modules/settingsHandlers/test/data/XMLSettingsHandler/expected_output2.xml b/gpii/node_modules/settingsHandlers/test/data/XMLSettingsHandler/expected_output2.xml index b9e140c6f..72f7daae4 100644 --- a/gpii/node_modules/settingsHandlers/test/data/XMLSettingsHandler/expected_output2.xml +++ b/gpii/node_modules/settingsHandlers/test/data/XMLSettingsHandler/expected_output2.xml @@ -14,16 +14,5 @@ hello other indexed space hello third indexed world - - - C:\Sociable\Games\Resources - - - C:\Sociable\Games\Resources\Launcher - - - 10000 - - diff --git a/gpii/node_modules/settingsHandlers/test/data/XMLSettingsHandler/test2.xml b/gpii/node_modules/settingsHandlers/test/data/XMLSettingsHandler/test2.xml index 72cac9530..bb12de98d 100644 --- a/gpii/node_modules/settingsHandlers/test/data/XMLSettingsHandler/test2.xml +++ b/gpii/node_modules/settingsHandlers/test/data/XMLSettingsHandler/test2.xml @@ -14,15 +14,4 @@ hello other indexed world hello third indexed world - - - C:\Sociable\Games\Resources - - - C:\Sociable\Games\Resources\Launcher - - - 0 - - - \ No newline at end of file + diff --git a/gpii/node_modules/settingsHandlers/test/data/basic.json b/gpii/node_modules/settingsHandlers/test/data/basic.json new file mode 100644 index 000000000..02bb47e1d --- /dev/null +++ b/gpii/node_modules/settingsHandlers/test/data/basic.json @@ -0,0 +1,3 @@ +{ + "Hello": "World" +} diff --git a/gpii/node_modules/settingsHandlers/test/settingsHandlerUtilitiesTests.js b/gpii/node_modules/settingsHandlers/test/settingsHandlerUtilitiesTests.js new file mode 100644 index 000000000..28ddc11cc --- /dev/null +++ b/gpii/node_modules/settingsHandlers/test/settingsHandlerUtilitiesTests.js @@ -0,0 +1,108 @@ +/* + * Settings Handler Utilities Tests + * + * Copyright 2016 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.registerNamespace("jqUnit"), + kettle = require("kettle"); + +fluid.registerNamespace("gpii.tests.settingsHandlers"); + +kettle.loadTestingSupport(); + +require("settingsHandlers"); + +gpii.tests.settingsHandlers.readFile = function () { + // check that we're able to read existing file: + var result = gpii.settingsHandlers.readFile({ + filename: __dirname + "/data/basic.json" + }); + jqUnit.assertDeepEq("Expecting to have read the file properly", { Hello: "World" }, JSON.parse(result)); + + // check reading non-existing file + var result2 = gpii.settingsHandlers.readFile({ + filename: __dirname + "/data/bogus.json" + }); + jqUnit.assertEquals("Expecting undefined on reading non-existing file", undefined, result2); +}; + +var handleFileSolutionEntryData = { + existingFileSolutionEntry: { + options: { + "filename": __dirname + "/data/basic.json" + }, + settings: { + "Hello": "Mars" + } + }, + missingFileSolutionEntry: { + options: { + "filename": __dirname + "/data/bogus.json" + }, + settings: { + "Hello": "Mars" + } + } +}; + +gpii.tests.settingsHandlers.handleFileSolutionEntry = function () { + // an existing file should be parsed and the current value returned: + var result = gpii.settingsHandlers.handleFileSolutionEntry(handleFileSolutionEntryData.existingFileSolutionEntry, + gpii.settingsHandlers.getSettings, gpii.settingsHandlers.JSONSettingsHandler.parser, false); + jqUnit.assertDeepEq("Expecting to have read the file properly", { + "settings": { + "Hello": "World" + } + }, result); + + // a non-existing file should return undefined for the requested settings: + result = gpii.settingsHandlers.handleFileSolutionEntry(handleFileSolutionEntryData.missingFileSolutionEntry, + gpii.settingsHandlers.getSettings, gpii.settingsHandlers.JSONSettingsHandler.parser, false); + jqUnit.assertDeepEq("Expecting to get 'undefined' values for settings requested", { + "settings": { + "Hello": undefined + } + }, result); +}; + +fluid.defaults("gpii.tests.settingsHandlers.env", { + gradeNames: ["fluid.test.testEnvironment"], + components: { + tester: { + type: "gpii.tests.settingsHandlers.caseHolder" + } + } +}); + +fluid.defaults("gpii.tests.settingsHandlers.caseHolder", { + gradeNames: ["fluid.test.testCaseHolder"], + modules: [{ + name: "File Dependency Methods tests", + tests: [{ + expect: 2, + name: "gpii.settingsHandlers.readFile tests", + func: "gpii.tests.settingsHandlers.readFile" + }, { + expect: 2, + name: "gpii.settingsHandlers.handleFileSolutionEntry tests", + func: "gpii.tests.settingsHandlers.handleFileSolutionEntry" + }] + }] +}); + +module.exports = kettle.test.bootstrap("gpii.tests.settingsHandlers.env"); diff --git a/gpii/node_modules/settingsHandlers/test/web/html/LaunchHandlerTest.html b/gpii/node_modules/settingsHandlers/test/web/html/LaunchHandlerTest.html new file mode 100644 index 000000000..4e66b8577 --- /dev/null +++ b/gpii/node_modules/settingsHandlers/test/web/html/LaunchHandlerTest.html @@ -0,0 +1,36 @@ + + + + + GPII Launch Handler Tests + + + + + + + + + + + + + + + + + + + + + +

                      GPII Launch Handlers Tests

                      +

                      +
                      +

                      +
                        + + + + diff --git a/gpii/node_modules/settingsHandlers/test/web/html/SettingsHandlerUtilitiesTest.html b/gpii/node_modules/settingsHandlers/test/web/html/SettingsHandlerUtilitiesTest.html new file mode 100644 index 000000000..ef071106d --- /dev/null +++ b/gpii/node_modules/settingsHandlers/test/web/html/SettingsHandlerUtilitiesTest.html @@ -0,0 +1,39 @@ + + + + + GPII Settings Handler Utilities Tests + + + + + + + + + + + + + + + + + + + + + + + + +

                        GPII Settings Handler Utilities Tests

                        +

                        +
                        +

                        +
                          + + + + diff --git a/gpii/node_modules/settingsHandlers/test/web/js/LaunchHandlerTests.js b/gpii/node_modules/settingsHandlers/test/web/js/LaunchHandlerTests.js new file mode 100644 index 000000000..9e9c9f6a1 --- /dev/null +++ b/gpii/node_modules/settingsHandlers/test/web/js/LaunchHandlerTests.js @@ -0,0 +1,347 @@ +/* + * Launch Handler Tests + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +/* global fluid, jqUnit, gpii */ + +"use strict"; + +(function () { + // TODO: Rewrite all these tests into browser-style jqUnit tests, leaving just + // a stub tester here to test filesystem integration + + fluid.registerNamespace("gpii.tests"); + fluid.registerNamespace("gpii.tests.flexibleHandler"); + + + fluid.defaults("gpii.tests.flexibleHandler.assertDirective", { + gradeNames: "fluid.function", + argumentMap: {} + }); + + gpii.tests.flexibleHandler.assertDirective = function () { + jqUnit.assertTrue("Assertion block", true); + }; + + fluid.defaults("gpii.tests.flexibleHandler.failBlock", { + gradeNames: "fluid.function", + argumentMap: {} + }); + + gpii.tests.flexibleHandler.failBlock = function () { + jqUnit.assertTrue("Asserted that this block should not be called", false); + }; + + fluid.defaults("gpii.tests.flexibleHandler.returnTrue", { + gradeNames: "fluid.function", + argumentMap: {} + }); + + gpii.tests.flexibleHandler.returnTrue = function () { + return true; + }; + + fluid.defaults("gpii.tests.flexibleHandler.returnFalse", { + gradeNames: "fluid.function", + argumentMap: {} + }); + + gpii.tests.flexibleHandler.returnFalse = function () { + return false; + }; + + gpii.tests.flexibleHandler.assertBlock = { + "type": "gpii.tests.flexibleHandler.assertDirective" + }; + + gpii.tests.flexibleHandler.reportRunning = { + "type": "gpii.tests.flexibleHandler.returnTrue" + }; + + gpii.tests.flexibleHandler.reportNotRunning = { + "type": "gpii.tests.flexibleHandler.returnFalse" + }; + + gpii.tests.flexibleHandler.basicTestDefs = [ + { + description: "Passing true to handler if already running should not execute start block", + expect: 1, + input: { + settings: { + "running": true + }, + options: { + "setTrue": gpii.tests.flexibleHandler.failBlock, + "setFalse": gpii.tests.flexibleHandler.failBlock, + "getState": gpii.tests.flexibleHandler.reportRunning + } + }, + expectedSettings: { + "running": { + "newValue": true, + "oldValue": true + } + } + }, { + description: "Passing false to handler if already not running should not execute stop block", + expect: 1, + input: { + settings: { + "running": false + }, + options: { + "setTrue": gpii.tests.flexibleHandler.failBlock, + "setFalse": gpii.tests.flexibleHandler.failBlock, + "getState": gpii.tests.flexibleHandler.reportNotRunning + } + }, + expectedSettings: { + "running": { + "newValue": false, + "oldValue": false + } + } + }, { + description: "Passing true to handler if solution is not running should execute start block", + expect: 2, + input: { + settings: { + "running": true + }, + options: { + "setTrue": gpii.tests.flexibleHandler.assertBlock, + "setFalse": gpii.tests.flexibleHandler.failBlock, + "getState": gpii.tests.flexibleHandler.reportNotRunning + } + }, + expectedSettings: { + "running": { + "newValue": true, + "oldValue": false + } + } + }, { + description: "Passing false to handler if solution is running should execute stop block", + expect: 2, + input: { + settings: { + "running": false + }, + options: { + "setTrue": gpii.tests.flexibleHandler.failBlock, + "setFalse": gpii.tests.flexibleHandler.assertBlock, + "getState": gpii.tests.flexibleHandler.reportRunning + } + }, + expectedSettings: { + "running": { + "newValue": false, + "oldValue": true + } + } + }, { + description: "Testing for multiple start blocks", + expect: 3, + input: { + settings: { + "running": true + }, + options: { + "setTrue": [ + gpii.tests.flexibleHandler.assertBlock, + gpii.tests.flexibleHandler.assertBlock + ], + "setFalse": gpii.tests.flexibleHandler.failBlock, + "getState": gpii.tests.flexibleHandler.reportNotRunning + } + }, + expectedSettings: { + "running": { + "newValue": true, + "oldValue": false + } + } + }, { + description: "Testing for multiple stop blocks", + expect: 3, + input: { + settings: { + "running": false + }, + options: { + "setTrue": gpii.tests.flexibleHandler.failBlock, + "setFalse": [ + gpii.tests.flexibleHandler.assertBlock, + gpii.tests.flexibleHandler.assertBlock + ], + "getState": gpii.tests.flexibleHandler.reportRunning + } + }, + expectedSettings: { + "running": { + "newValue": false, + "oldValue": true + } + } + } + ]; + + fluid.each(gpii.tests.flexibleHandler.basicTestDefs, function (spec) { + jqUnit.test(spec.description, function () { + jqUnit.expect(spec.expect); + var actual = gpii.launchHandlers.flexibleHandler.set({ + "my.solution1": [ + spec.input + ] + }); + // build expected return payload: + jqUnit.assertDeepEq("Return payload is correct", spec.expectedSettings, actual["my.solution1"][0].settings); + }); + }); + + gpii.tests.flexibleHandler.extraTestDefs = [ + { + description: "Multiple solution entries sent - both should attempt to start", + expect: 3, + input: { + "my.solution1": [ + { + settings: { + "running": true + }, + options: { + "setTrue": gpii.tests.flexibleHandler.assertBlock, + "setFalse": gpii.tests.flexibleHandler.failBlock, + "getState": gpii.tests.flexibleHandler.reportNotRunning + } + } + ], + "my.solution2": [ + { + settings: { + "running": true + }, + options: { + "setTrue": gpii.tests.flexibleHandler.assertBlock, + "setFalse": gpii.tests.flexibleHandler.failBlock, + "getState": gpii.tests.flexibleHandler.reportNotRunning + } + } + ] + }, + expected: { + "my.solution1": [ + { + settings: { + running: { + "newValue": true, + "oldValue": false + } + }, + options: { + "setTrue": gpii.tests.flexibleHandler.assertBlock, + "setFalse": gpii.tests.flexibleHandler.failBlock, + "getState": gpii.tests.flexibleHandler.reportNotRunning + } + } + ], + "my.solution2": [ + { + settings: { + running: { + "newValue": true, + "oldValue": false + } + }, + options: { + "setTrue": gpii.tests.flexibleHandler.assertBlock, + "setFalse": gpii.tests.flexibleHandler.failBlock, + "getState": gpii.tests.flexibleHandler.reportNotRunning + } + } + ] + } + }, + { + description: "Multiple solution entries sent - different actions", + expect: 2, + input: { + "my.solution1": [ + { + settings: { + "running": false + }, + options: { + "setTrue": gpii.tests.flexibleHandler.failBlock, + "setFalse": gpii.tests.flexibleHandler.assertBlock, + "getState": gpii.tests.flexibleHandler.reportRunning + } + } + ], + "my.solution2": [ + { + settings: { + "running": false + }, + options: { + "setTrue": gpii.tests.flexibleHandler.failBlock, + "setFalse": gpii.tests.flexibleHandler.failBlock, + "getState": gpii.tests.flexibleHandler.reportNotRunning + } + } + ] + }, + expected: { + "my.solution1": [ + { + settings: { + running: { + "newValue": false, + "oldValue": true + } + }, + options: { + "setTrue": gpii.tests.flexibleHandler.failBlock, + "setFalse": gpii.tests.flexibleHandler.assertBlock, + "getState": gpii.tests.flexibleHandler.reportRunning + } + } + ], + "my.solution2": [ + { + settings: { + running: { + "newValue": false, + "oldValue": false + } + }, + options: { + "setTrue": gpii.tests.flexibleHandler.failBlock, + "setFalse": gpii.tests.flexibleHandler.failBlock, + "getState": gpii.tests.flexibleHandler.reportNotRunning + } + } + ] + } + } + ]; + + fluid.each(gpii.tests.flexibleHandler.extraTestDefs, function (spec) { + jqUnit.test(spec.description, function () { + jqUnit.expect(spec.expect); + var actual = gpii.launchHandlers.flexibleHandler.set(spec.input); + // build expected return payload: + jqUnit.assertDeepEq("Return payload is correct", spec.expected, actual); + }); + }); + + +})(); diff --git a/gpii/node_modules/settingsHandlers/test/web/js/SettingsHandlerUtilitiesTests.js b/gpii/node_modules/settingsHandlers/test/web/js/SettingsHandlerUtilitiesTests.js new file mode 100644 index 000000000..8f089415e --- /dev/null +++ b/gpii/node_modules/settingsHandlers/test/web/js/SettingsHandlerUtilitiesTests.js @@ -0,0 +1,282 @@ +/* + * Settings Handler Utilities Tests + * + * Copyright 2012 Raising the Floor - International + * Copyright 2013 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +/* global fluid, jqUnit, gpii */ + +"use strict"; + +(function () { + // TODO: Rewrite all these tests into browser-style jqUnit tests, leaving just + // a stub tester here to test filesystem integration + + fluid.registerNamespace("gpii.tests"); + fluid.registerNamespace("gpii.tests.settingsHandlersUtilitiesTests"); + + gpii.tests.settingsHandlersUtilitiesTests.filterSupportedSettingsSpecs = { + "If supportedSettings is undefined, return settings": { + settings: { + "weirdSetting": "foo" + }, + supportedSettings: undefined, + expected: { + "weirdSetting": "foo" + } + }, + "If settings is undefined, return undefined": { + settings: undefined, + supportedSettings: { + "weirdSetting": {} + }, + expected: undefined + }, + "Filter by supportedSettings": { + settings: { + "weirdSetting": "foo", + "otherSetting": "bar" + }, + supportedSettings: { + "weirdSetting": {} + }, + expected: { + "weirdSetting": "foo" + } + }, + "If supportedSettings directive is present - test no matching": { + settings: { + "weirdSetting": "foo", + "otherSetting": "bar" + }, + supportedSettings: { + "thirdSetting": {} + }, + expected: {} + }, + "Works with `undefined` values for settings (required when restoring)": { + settings: { + "weirdSetting": undefined, + "otherSetting": undefined + }, + supportedSettings: { + "weirdSetting": {} + }, + expected: { + "weirdSetting": undefined + } + } + }; + + jqUnit.test("filterSupportedSettings tests", function () { + fluid.each(gpii.tests.settingsHandlersUtilitiesTests, function (test, name) { + var result = gpii.settingsHandlers.filterSupportedSettings(test.settings, test.supportedSettings); + jqUnit.assertDeepEq(name, test.expected, result); + }); + }); + + + jqUnit.test("settingsHandlers.setSettings", function () { + // check simple json structure + var result = gpii.settingsHandlers.setSettings( + {settings: { "key": "val1" }}, { "key": "val2"}); + jqUnit.assertDeepEq("Simple setting", + { key: { oldValue: "val2", newValue: "val1" } }, result.settings); + + // complex json structure + var json = { + firstentry: { + lvl2: { + key1: "vala", + key2: "valb" + } + }, + nextentry: { + key3: "valc" + } + }; + var settings = { + "firstentry.lvl2.key1": "newValue", + "nextentry.key3": "otherNewValue" + }; + var exp = { + "firstentry.lvl2.key1": { oldValue: "vala", newValue: "newValue" }, + "nextentry.key3": { oldValue: "valc", newValue: "otherNewValue" } + }; + result = gpii.settingsHandlers.setSettings({settings: settings}, json); + jqUnit.assertDeepEq("Multiple and nested settings", exp, + result.settings); + + // check setSettings for unset values + json = { + somekey: { + somekey2: { + key1: "vala" + } + } + }; + settings = { + "newkey1.newkey2": "someval" + }; + exp = { "newkey1.newkey2": { oldValue: undefined, newValue: "someval" }}; + result = gpii.settingsHandlers.setSettings({settings: settings}, json); + jqUnit.assertDeepEq("Non existing settings (multilevel)", exp, result.settings); + }); + + jqUnit.test("numberify tests", function () { + jqUnit.assertEquals("Single number", 3, gpii.settingsHandlers.numberify("3")); + jqUnit.assertDeepEq("Array of numbers", [1, "3a", 59.5], gpii.settingsHandlers.numberify(["1", "3a", "59.5"])); + jqUnit.assertDeepEq("Structure of numbers", {a: 3.9998, b: [false, 55]}, gpii.settingsHandlers.numberify({a: "3.9998", b: [false, "55"]})); + }); + + gpii.tests.settingsHandlersUtilitiesTests.copySettingsDefs = [{ + name: "Copy with extra setting in source", + source: { + "myconf": { + "settings": { + "cross-hairs-clip": true, + "mysetting": "some value" + }, + "type": "gpii.tests.lifecycleManager.mockSettingsHandler", + "options": {} + } + }, + target: { + "myconf": { + "settings": { + "cross-hairs-clip": false + }, + "type": "gpii.tests.lifecycleManager.mockSettingsHandler", + "options": {} + } + }, + expected: { + "myconf": { + "settings": { + "cross-hairs-clip": true, + "mysetting": "some value" + }, + "type": "gpii.tests.lifecycleManager.mockSettingsHandler", + "options": {} + } + } + }, { + name: "Copy with extra setting in target", + source: { + "myconf": { + "settings": { + "cross-hairs-clip": true + }, + "type": "gpii.tests.lifecycleManager.mockSettingsHandler", + "options": {} + } + }, + target: { + "myconf": { + "settings": { + "cross-hairs-clip": false, + "mysetting": "some value" + }, + "type": "gpii.tests.lifecycleManager.mockSettingsHandler", + "options": {} + } + }, + expected: { + "myconf": { + "settings": { + "cross-hairs-clip": true, + "mysetting": "some value" + }, + "type": "gpii.tests.lifecycleManager.mockSettingsHandler", + "options": {} + } + } + }, { + name: "Copy with 'undefined' value in source and target", + source: { + "myconf": { + "settings": { + "cross-hairs-clip": true, + "undefSetting": undefined + }, + "type": "gpii.tests.lifecycleManager.mockSettingsHandler", + "options": {} + } + }, + target: { + "myconf": { + "settings": { + "cross-hairs-clip": false, + "undefSetting2": undefined, + "undefSetting": "some value" + }, + "type": "gpii.tests.lifecycleManager.mockSettingsHandler", + "options": {} + } + }, + expected: { + "myconf": { + "settings": { + "cross-hairs-clip": true, + "undefSetting": undefined, + "undefSetting2": undefined + }, + "type": "gpii.tests.lifecycleManager.mockSettingsHandler", + "options": {} + } + } + }]; + + jqUnit.test("Testing gpii.settingsHandlers.copySettings function", function () { + fluid.each(gpii.tests.settingsHandlersUtilitiesTests.copySettingsDefs, function (testDef) { + gpii.settingsHandlers.copySettings(testDef.target, testDef.source); + jqUnit.assertDeepEq(testDef.name, testDef.expected, testDef.target); + }); + }); + + gpii.tests.settingsHandlersUtilitiesTests.changesToSettingsDefs = [{ + name: "Element with type \"DELETE\" returns undefined value", + source: { + type: "DELETE", + value: true + }, + expected: undefined + }, { + name: "\"path\" within the element is maintained", + source: { + "value": false, + "path": "pvParam.dwFlags.HCF_HIGHCONTRASTON", + "discard": true + }, + expected: { + "value": false, + "path": "pvParam.dwFlags.HCF_HIGHCONTRASTON" + } + }, { + name: "Only element value is returned in other cases", + source: { + "value": 1, + "discard": true + }, + expected: 1 + }]; + + jqUnit.test("Testing gpii.settingsHandlers.changesToSettings function", function () { + fluid.each(gpii.tests.settingsHandlersUtilitiesTests.changesToSettingsDefs, function (testDef) { + var assertFunc = fluid.isPlainObject(testDef.expected) ? "assertDeepEq" : testDef.expected === undefined ? "assertUndefined" : "assertEquals"; + var result = gpii.settingsHandlers.changesToSettings(testDef.source); + jqUnit[assertFunc](testDef.name, testDef.expected, result); + }); + }); +})(); diff --git a/gpii/node_modules/singleInstance/index.js b/gpii/node_modules/singleInstance/index.js new file mode 100644 index 000000000..ca47cd367 --- /dev/null +++ b/gpii/node_modules/singleInstance/index.js @@ -0,0 +1,25 @@ +/* + * Ensures there's only a single instance of GPII. + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"); + +fluid.module.register("singleInstance", __dirname, require); + +require("./src/singleInstance.js"); diff --git a/gpii/node_modules/singleInstance/package.json b/gpii/node_modules/singleInstance/package.json new file mode 100644 index 000000000..e12578e95 --- /dev/null +++ b/gpii/node_modules/singleInstance/package.json @@ -0,0 +1,13 @@ +{ + "name": "singleInstance", + "description": "Ensures the current GPII instance remains the only one running, if there isn't one already.", + "version": "0.3.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } +} diff --git a/gpii/node_modules/singleInstance/src/singleInstance.js b/gpii/node_modules/singleInstance/src/singleInstance.js new file mode 100644 index 000000000..d5cc83501 --- /dev/null +++ b/gpii/node_modules/singleInstance/src/singleInstance.js @@ -0,0 +1,142 @@ +/* + * Ensures there's only a single instance of GPII. + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"); +var fs = require("fs"); + +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.singleInstance"); + +/** + * Declares this instance of GPII to be the only instance, unless there's already one running. + * + * The "pid/lock file" method is used, where a file containing the pid of the current process is created. If it + * already exists, and the pid points to a running GPII instance, then the lock will fail. + * + * File IO is performed synchronously, ensuring the checks are made before GPII does anything else (eg, bind to a port). + * + * @todo More checks need to be made to ensure the pid is GPII, and not just another process. + * + * @param {String} [pidFile] - [optional] The path of the pid file. Default is "gpii.pid" in the settings directory. + * @return {Boolean} True if this instance is the only GPII instance. + */ +gpii.singleInstance.registerInstance = function (pidFile) { + if (!pidFile || typeof(pidFile) !== "string") { + pidFile = gpii.singleInstance.getDefaultPidFile(); + } + + var gpiiPid = gpii.singleInstance.checkInstance(pidFile); + if (gpiiPid === null) { + // Write the pid, but fail if the file exists. The stale one was deleted, but there might be another + // GPII instance just starting. + fs.writeFileSync(pidFile, process.pid, { flag: "wx" }); + } + + var success = !gpiiPid || gpiiPid === process.pid; + if (!success) { + fluid.log(fluid.logLevel.WARN, "There's already an instance of GPII detected."); + } + return success; +}; + +fluid.defaults("gpii.singleInstance.registerInstance", { + gradeNames: "fluid.function" +}); + + +/** + * Checks for another GPII instance. + * + * @param {String} [pidFile] - [optional] The path of the pid file. Default is "gpii.pid" in the settings directory. + * @return {Number} Non-zero PID of the other GPII process (including this process), or null if this is the only one. + */ +gpii.singleInstance.checkInstance = function (pidFile) { + if (!pidFile || typeof(pidFile) !== "string") { + pidFile = gpii.singleInstance.getDefaultPidFile(); + } + + var pid = null; + + try { + // Get the old pid from the lock file + var content = fs.readFileSync(pidFile, {encoding: "utf8"}); + pid = parseInt(content); + // A "0" PID will never be GPII (and process.kill will succeed on Linux). + if (pid && pid !== process.pid) { + try { + // Check if it's still running. + process.kill(pid, 0); + // No error means the process is running. + // TODO: Ensure the process really is GPII, and not just a re-use of the pid. + } catch (e) { + // Process isn't running (or permission denied, which suggests it's not GPII). + pid = null; + } + } + + if (!pid) { + // Remove the pid file, as it's stale/invalid. + fs.unlinkSync(pidFile); + } + } catch (e) { + // The file doesn't exist. + } + + return pid || null; +}; + +fluid.defaults("gpii.singleInstance.checkInstance", { + gradeNames: "fluid.function" +}); + +/** + * Prevents the current GPII instance from being the only one, called when the GPII process is being shutdown. + * + * @param {String} [pidFile] - [optional] The path of the pid file. Default is "gpii.pid" in the settings directory. + * @return {Boolean} True if this instance was de-registered, false if it wasn't registered. + */ +gpii.singleInstance.deregisterInstance = function (pidFile) { + if (!pidFile || typeof(pidFile) !== "string") { + pidFile = gpii.singleInstance.getDefaultPidFile(); + } + + var success = false; + var gpiiPid = gpii.singleInstance.checkInstance(pidFile); + if (gpiiPid === process.pid) { + success = true; + fs.unlinkSync(pidFile); + } + + return success; +}; + +fluid.defaults("gpii.singleInstance.deregisterInstance", { + gradeNames: "fluid.function" +}); + +/* + * Get the default pid file path, "/gpii.pid". + */ +gpii.singleInstance.getDefaultPidFile = function () { + var settingsDirComponent = gpii.settingsDir(); + var pidFile = settingsDirComponent.getGpiiSettingsDir() + "/gpii.pid"; + settingsDirComponent.destroy(); + return pidFile; +}; diff --git a/gpii/node_modules/singleInstance/test/SingleInstanceTests.js b/gpii/node_modules/singleInstance/test/SingleInstanceTests.js new file mode 100644 index 000000000..7ace9926f --- /dev/null +++ b/gpii/node_modules/singleInstance/test/SingleInstanceTests.js @@ -0,0 +1,209 @@ +/* + * singleInstance Tests + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + fs = require("fs"), + os = require("os"), + child_process = require("child_process"); + +var jqUnit = fluid.require("node-jqunit"); +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.tests.singleInstance"); + +fluid.require("%gpii-universal"); + +require("singleInstance"); + +jqUnit.module("gpii.tests.singleInstance"); + +gpii.tests.singleInstance.testDefs = fluid.freezeRecursive([ + { + // no pidfile + testData: null, + expected: { + checkInstance: null, + registerInstance: true, + file: "$processes.own.pid", + deregisterInstance: false + } + }, + { + // junk content + testData: "not a pid", + expected: { + checkInstance: null, + registerInstance: true, + file: "$processes.own.pid", + deregisterInstance: false + } + }, + { + // non-running pid + testData: "-100", + expected: { + checkInstance: null, + registerInstance: true, + file: "$processes.own.pid", + deregisterInstance: false + } + }, + { + // this process + testData: "$processes.own.pid", + expected: { + checkInstance: "$processes.own.pid", + registerInstance: true, + file: "$processes.own.pid", + deregisterInstance: true + } + }, + { + // another process + testData: "$processes.other.pid", + expected: { + checkInstance: "$processes.other.pid", + // If the implementation was complete, then registerInstance should succeed because the test process + // isn't another GPII instance. + registerInstance: false, + file: null, + deregisterInstance: false + } + } +]); + +fluid.defaults("gpii.tests.singleInstance.testData", { + gradeNames: "fluid.modelComponent", + model: { + processes: {} + } +}); + +/** + * Extracts the test items from model, performing look-ups from it ("$like.this") + * + * @param {Object} model - The object to resolve from. + * @param {Object} value - The value (or values) to resolve. + * @return {Array} The test items. + */ +gpii.tests.singleInstance.resolveReferences = function (model, value) { + if (typeof(value) === "string") { + return value.charAt(0) === "$" ? fluid.get(model, value.substring(1)) : value; + } else { + return fluid.transform(value, function (obj) { + return gpii.tests.singleInstance.resolveReferences(model, obj); + }); + } +}; + +/** + * Deletes the pid file, hiding errors if the file doesn't exist. + * + * @param {String} pidFile - The pid file. + */ +gpii.tests.singleInstance.deletePidFile = function (pidFile) { + try { + fs.unlinkSync(pidFile); + } catch (e) { + // Don't care if it failed, as long as it's gone. + if (fs.existsSync(pidFile)) { + throw e; + } + } +}; + +jqUnit.test("Testing checkInstance, registerGpiiInstance, and deregisterInstance", function () { + + var pidFile = os.tmpdir() + "/gpii-test-pid" + Date.now(); + + // Start another process (a cross-platform "sleep"), to test against a live pid. + var otherProcess = child_process.exec("node -e \"setTimeout(console.log, 5000)\""); + + var testData = gpii.tests.singleInstance.testData(); + testData.applier.change("processes", { + own: process, + other: otherProcess + }); + + var tests = gpii.tests.singleInstance.resolveReferences(testData.model, gpii.tests.singleInstance.testDefs); + var funcs = [ "checkInstance", "registerInstance", "deregisterInstance" ]; + + try { + while (funcs.length > 0) { + var func = funcs.shift(); + for (var n = 0; n < tests.length; n++) { + var test = tests[n]; + var testName = func + " test " + n + ": "; + + // Set the pid file. + if (test.testData === null) { + gpii.tests.singleInstance.deletePidFile(pidFile); + } else { + fs.writeFileSync(pidFile, test.testData); + } + + // Call the function + var returnValue = gpii.singleInstance[func](pidFile); + jqUnit.assertEquals(testName + " return", test.expected[func], returnValue); + + var exists = fs.existsSync(pidFile); + + if (func === "registerInstance") { + if (test.expected.file !== null) { + jqUnit.assertTrue(testName + "pid file should exist", exists); + // Check the new pid file + var content = fs.readFileSync(pidFile, {encoding: "utf8"}); + var value = parseInt(content); + jqUnit.assertEquals(testName + "pidfile content", test.expected.file, value); + } + } else if (func === "deregisterInstance") { + if (returnValue) { + jqUnit.assertTrue(testName + "pidFile should not exist after a successful deregisterInstance", !exists); + } + } + + gpii.tests.singleInstance.deletePidFile(pidFile); + } + } + } finally { + gpii.tests.singleInstance.deletePidFile(pidFile); + testData.destroy(); + } + +}); + +jqUnit.test("Testing singleInstance end-to-end", function () { + var pidFile = os.tmpdir() + "/gpii-test-pid" + Date.now(); + + // perform an end-to-end test + var pid = gpii.singleInstance.checkInstance(pidFile); + jqUnit.assertEquals("Another instance should not be detected before registering", null, pid); + + var registered = gpii.singleInstance.registerInstance(pidFile); + jqUnit.assertTrue("This instance should have been registered", registered); + + pid = gpii.singleInstance.checkInstance(pidFile); + jqUnit.assertEquals("The current instance should be this one", process.pid, pid); + + var deregistered = gpii.singleInstance.deregisterInstance(pidFile); + jqUnit.assertTrue("This instance should have been deregistered", deregistered); + + pid = gpii.singleInstance.checkInstance(pidFile); + jqUnit.assertEquals("Another instance should not be detected after deregistering", null, pid); +}); diff --git a/gpii/node_modules/solutionsRegistry/README.md b/gpii/node_modules/solutionsRegistry/README.md index c5c69520a..9924385bf 100644 --- a/gpii/node_modules/solutionsRegistry/README.md +++ b/gpii/node_modules/solutionsRegistry/README.md @@ -1,44 +1,16 @@ -Solutions registry -=== - -A server app built to be deployed with node.js server that saves and returns (and can be queried) solution records. - -### Solutions registry API - -The app currently supports the following urls: - - {urlToSolutionsRegistryServer}/solutions/{id} // GET AND POST - {urlToSolutionsRegistryServer}/solutions?os={os} // GET ONLY - urlToSolutionsRegistryServer}/solutions?os={os}&version={version} // GET ONLY - -For example: - - {urlToSolutionsRegistryServer}/solutions/fluid.uiOptions.linux - {urlToSolutionsRegistryServer}/solutions?os=linux - {urlToSolutionsRegistryServer}/solutions?os=linux&version=2.6.27 - -### Dependencies - -[infusion](https://github.com/fluid-project/infusion) framework. - -### Installation - -Run the following command in your newly checked out universal repository. This -will pull all dependencies that are required by universal. - - npm install - -### Run - -To run solutions registry app simply type: - - [NODE_ENV={environment}] node node_modules/kettle/lib/init.js path/to/solutionsRegistry/configs/folder - -- Default environment is development. -- Path to configs folder can be absolute or relative to the current user directory. - -For example: - - node node_modules/kettle/lib/init.js gpii/node_modules/solutionsRegistry/configs/ - node node_modules/kettle/lib/init.js /Users/{userName}/universal/gpii/node_modules/solutionsRegistry/configs/ - NODE_ENV=production node node_modules/kettle/lib/init.js gpii/node_modules/solutionsRegistry/configs/ \ No newline at end of file +# Solutions Registry + +This directory contains the raw data used to produce the "solutions registry" data used by other parts of the system. +This data describes: + +1. What solutions exist. +2. How to detect whether each solution is installed. +3. How to start and stop each solution. +4. What settings the solution supports. +5. What each setting does. +6. What values are allowed for each setting. +7. How each setting relates to any "generic preference terms", so that a general preference can be expressed in multiple + solutions. + +In most cases you will not want to use the data in this directory directly. Instead, you should use the data generated +by the scripts in this directory, which is saved to the builds subdirectory. diff --git a/gpii/node_modules/solutionsRegistry/configs/base.json b/gpii/node_modules/solutionsRegistry/configs/base.json deleted file mode 100644 index 5e3ca8d3d..000000000 --- a/gpii/node_modules/solutionsRegistry/configs/base.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "modules": [ - "solutionsRegistry" - ] -} diff --git a/gpii/node_modules/solutionsRegistry/configs/development.json b/gpii/node_modules/solutionsRegistry/configs/development.json deleted file mode 100644 index 7f3266156..000000000 --- a/gpii/node_modules/solutionsRegistry/configs/development.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "typeName": "solutionsRegistry.production", - "options": { - "gradeNames": ["autoInit", "fluid.littleComponent"], - "components": { - "server": { - "type": "kettle.server", - "options": { - "logging": true, - "components": { - "solutionsRegistry": { - "type": "gpii.solutionsRegistry", - "options": { - "gradeNames": ["gpii.solutionsRegistry.dev"], - "solutionRegistryUrl": "file://%root/../../../testData/solutions/%id.json", - "solutionQueryUrl": "file://%root/../../../testData/solutions/%os.json" - } - } - } - } - } - } - }, - "includes": [ - "./base.json" - ] -} diff --git a/gpii/node_modules/solutionsRegistry/configs/kettleModuleLoader.js b/gpii/node_modules/solutionsRegistry/configs/kettleModuleLoader.js deleted file mode 100644 index 20c0170a7..000000000 --- a/gpii/node_modules/solutionsRegistry/configs/kettleModuleLoader.js +++ /dev/null @@ -1,16 +0,0 @@ -/*! -Kettle Module Loader. - -Copyright 2012-2013 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/kettle/LICENSE.txt -*/ - -// The purpose of this file is to be copied to a location for which the -// require function is needed. It allows to find node modules relative -// to that location that are otherwise non-resolvable. -module.exports = require; \ No newline at end of file diff --git a/gpii/node_modules/solutionsRegistry/configs/production.json b/gpii/node_modules/solutionsRegistry/configs/production.json deleted file mode 100644 index 9424e2319..000000000 --- a/gpii/node_modules/solutionsRegistry/configs/production.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "typeName": "solutionsRegistry.production", - "options": { - "gradeNames": ["autoInit", "fluid.littleComponent"], - "components": { - "server": { - "type": "kettle.server", - "options": { - "logging": false, - "components": { - "solutionsRegistry": { - "type": "gpii.solutionsRegistry", - "options": { - "solutionRegistryUrl": "http://localhost:5984/gpii-sol-registry/_design/gpii-solutions/_rewrite/solutions/%id", - "solutionQueryUrl": "http://localhost:5984/gpii-sol-registry/_design/gpii-solutions/_rewrite/solutions?os=%os" - } - } - } - } - } - } - }, - "includes": [ - "./base.json" - ] -} diff --git a/gpii/node_modules/solutionsRegistry/index.js b/gpii/node_modules/solutionsRegistry/index.js index c69936c48..e947675c6 100644 --- a/gpii/node_modules/solutionsRegistry/index.js +++ b/gpii/node_modules/solutionsRegistry/index.js @@ -1,5 +1,17 @@ -var fluid = require("infusion"); +/* + * The data and scripts used to produce the "solutions registry" data used throughout the GPII. + * + * Copyright 2019 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; -var loader = fluid.getLoader(__dirname); +var fluid = require("infusion"); -loader.require("./src/solutionsRegistry.js"); \ No newline at end of file +fluid.module.register("solutionsRegistry", __dirname, require); diff --git a/gpii/node_modules/solutionsRegistry/package.json b/gpii/node_modules/solutionsRegistry/package.json deleted file mode 100644 index 8b82eebe8..000000000 --- a/gpii/node_modules/solutionsRegistry/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "solutionsRegistry", - "description": "Solutions Registry server provides an API for accessing solutions and settings.", - "version": "0.1", - "author": "GPII", - "bugs": "http://issues.fluidproject.org/browse/FLUID", - "homepage": "http://gpii.net/", - "dependencies": {}, - "licenses": [ - { - "type": "BSD-3-Clause", - "url": "http://www.opensource.org/licenses/BSD-3-Clause" - } - ], - "keywords": ["infusion", "framework", "application", "fluid", "IoC", "Inversion of Control", "MVC", "evented"], - "repository": "git://github.com/GPII/universal.git", - "main": "./index.js", - "engines": { "node" : ">=0.1.9" } -} \ No newline at end of file diff --git a/gpii/node_modules/solutionsRegistry/src/js/generate-schemas.js b/gpii/node_modules/solutionsRegistry/src/js/generate-schemas.js new file mode 100644 index 000000000..b9a7a91df --- /dev/null +++ b/gpii/node_modules/solutionsRegistry/src/js/generate-schemas.js @@ -0,0 +1,157 @@ +/* + + A script to generate a schema "codex" for all known solutions and generic preferences. The "codex" is a map, + keyed by URI. For application settings, the URI looks like: + + http://registry.gpii.net/applications/com.microsoft.windows.onscreenKeyboard + + For "generic preference terms", the URI looks like: + + http://registry.gpii.net/common/fontSize + + Each map entry is a schema that describes all possible settings for a given solution, as shown here: + + { + "http://registry.gpii.net/applications/my.sample.app": { + properties: { + setting1: { + // setting1 sub-schema + }, + setting2: { + // setting2 sub-schema + } + } + } + } + + This structure allows for more efficient settings/preferences validation than previous strategies. Payloads to be + validated can be broken down into a series of small "per solution" validation passes, which typically take less than + 5ms even for our largest sample settings payloads. + + */ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +var mkdirp = require("mkdirp"); +var fs = require("fs"); +var path = require("path"); + +// Add the ability to require JSON5 files. +require("json5/lib/register"); + +// Require universal so that we can resolve paths to it and any sub-modules. +fluid.require("%gpii-universal"); + +fluid.registerNamespace("gpii.solutionsRegistry.schemaGenerator"); + +gpii.solutionsRegistry.schemaGenerator.generateSolutionsSchemaCodex = function (that) { + // TODO: Add all solutions/settings and schemas from that.options.solutionsRegistryPath + + var solutionsSettings = gpii.solutionsRegistry.schemaGenerator.settingsFromSolutions(that); + var genericSettings = gpii.solutionsRegistry.schemaGenerator.genericSettings(that); + + var allSettings = fluid.extend({}, solutionsSettings, genericSettings); + + // Sort the keys so that the final generated file is more readable. + var orderedSettings = {}; + var allSettingsKeys = fluid.keys(allSettings).sort(); + fluid.each(allSettingsKeys, function (settingURI) { + orderedSettings[settingURI] = allSettings[settingURI]; + }); + + var resolvedCodexOutputPath = fluid.module.resolvePath(that.options.codexOutputPath); + var settingsOutputDir = path.dirname(resolvedCodexOutputPath); + if (!fs.existsSync(settingsOutputDir)) { + mkdirp.sync(settingsOutputDir); + } + fs.writeFileSync(resolvedCodexOutputPath, JSON.stringify(orderedSettings, null, 2)); +}; + +gpii.solutionsRegistry.schemaGenerator.settingsFromSolutions = function (that) { + var settingsBySolution = {}; + var resolvedSrPath = fluid.module.resolvePath(that.options.solutionsRegistryPath); + var srFiles = fs.readdirSync(resolvedSrPath).filter(function (filename) { return filename.match(/\.json5?$/i); }); + fluid.each(srFiles, function (filename) { + var pathToFile = path.resolve(resolvedSrPath, filename); + var srFileData = require(pathToFile); + fluid.each(srFileData, function (solutionDef, solutionKey) { + var solutionURI = "http://registry.gpii.net/applications/" + solutionKey; + var solutionSchema = { + "$schema": "gss-v7-full#", + // Add the name as a courtesy to make reading the generated file more palatable. + title: solutionDef.name, + type: "object", + properties: {}, + additionalProperties: false + }; + + fluid.each(fluid.get(solutionDef, "settingsHandlers"), function (settingsHandlerDef) { + fluid.each(fluid.get(settingsHandlerDef, "supportedSettings"), function (settingsDef, settingsKey) { + if (settingsDef.schema) { + solutionSchema.properties[settingsKey] = settingsDef.schema; + } + }); + }); + + // If there is not an explicit "enabled" setting for a solution, add it now. + if (fluid.get(solutionSchema, "properties.enabled") === undefined) { + var solutionName = fluid.get(solutionDef, "name"); + var title = solutionName ? "Enable " + solutionName : "Enable"; + fluid.set(solutionSchema, "properties.enabled", { + title: title, + description: title + ".", + type: "boolean", + default: false + }); + }; + + if (!fluid.jQueryStandalone.isEmptyObject(solutionSchema.properties)) { + settingsBySolution[solutionURI] = solutionSchema; + } + }); + }); + + return settingsBySolution; +}; + +gpii.solutionsRegistry.schemaGenerator.genericSettings = function (that) { + var allGenericSettings = {}; + var resolvedPath = fluid.module.resolvePath(that.options.genericPreferenceTermsPath); + var rawGenericSettings = require(resolvedPath); + fluid.each(rawGenericSettings, function (settingDef, settingURI) { + var schema = fluid.get(settingDef, "schema"); + if (schema) { + allGenericSettings[settingURI] = fluid.extend({ "$schema": "gss-v7-full#" }, schema); + } + }); + return allGenericSettings; +}; + +fluid.defaults("gpii.solutionsRegistry.schemaGenerator", { + gradeNames: ["fluid.component"], + baseSettingsSchema: "%solutionsRegistry/src/schemas/settings-base-schema.json5", + codexOutputPath: "%gpii-universal/build/schemas/solution-schema-codex.json", + // TODO: Move this out of test data and break down into individual solutions. + solutionsRegistryPath: "%gpii-universal/testData/solutions", + // TODO: Move this out of test data to a suitable location. + genericPreferenceTermsPath: "%gpii-universal/testData/ontologies/flat.json5", + events: { + createAllSchemas: null + }, + listeners: { + "onCreate.createAllSchemas": { + funcName: "fluid.promise.fireTransformEvent", + args: ["{that}.events.createAllSchemas"] + }, + "createAllSchemas.generateSettingsSchema": { + priority: "first", + funcName: "gpii.solutionsRegistry.schemaGenerator.generateSolutionsSchemaCodex", + args: ["{that}"] + } + // TODO: Generate prefs safe schema from the settings schema. + } +}); + +gpii.solutionsRegistry.schemaGenerator(); diff --git a/gpii/node_modules/solutionsRegistry/src/js/sr-validation-middleware.js b/gpii/node_modules/solutionsRegistry/src/js/sr-validation-middleware.js new file mode 100644 index 000000000..81529b361 --- /dev/null +++ b/gpii/node_modules/solutionsRegistry/src/js/sr-validation-middleware.js @@ -0,0 +1,228 @@ +/* + + kettle.middleware "validators" designed to validate preference and settings payloads. + + */ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +// Add the ability to require JSON5 files, the default usage of these grades will need to have this or they will fail to +// expand the base schemas. +require("json5/lib/register"); + +fluid.registerNamespace("gpii.universal.solutionsRegistry.validators"); + +fluid.require("%gpii-json-schema"); + +require("../../../transformer/src/js/Transformer"); +require("../../../settingsHandlers/"); +require("../../../flowManager/src/SystemUtils"); + +/** + * + * A replacement for the default kettle validation middleware handle invokers backing function, which is more suitable + * for working with a schema that is generated based on the payload. + * + * @param {gpii.universal.solutionsRegistry.validators.base} that - The validation middleware component. + * @param {gpii.schema.validator} globalValidator - The persistent global validator component. + * @param {kettle.request.http} requestHandler - The component fielding the individual request. + * @return {Promise} - A `fluid.promise` that will resolve if the payload is valid or reject with errors if the payload is invalid. + * + */ +gpii.universal.solutionsRegistry.validators.validateRequest = function (that, globalValidator, requestHandler) { + var transformationRules = that.options.rules.outerPayload; + var outerPayload = fluid.model.transformWithRules(requestHandler.req, transformationRules); + + var validationStages = []; + + // "gross" validation based on the overall structure + validationStages.push(gpii.universal.solutionsRegistry.validators.singleValidation(that, globalValidator, that.options.baseSchema, outerPayload, that.options.baseSchemaHash)); + + // "deep" validation of individual settings, per solution. + var innerPayload = fluid.model.transformWithRules(requestHandler.req, that.options.rules.innerPayload); + + fluid.each(innerPayload, function (solutionSettings, settingURI) { + var solutionID = gpii.universal.solutionsRegistry.validators.solutionIdFromURI(settingURI); + fluid.each(fluid.get(that.options.sr, [solutionID, "settingsHandlers"]), function (settingsHandler) { + var transformed = gpii.transformer.transformOneSettingsHandler(fluid.copy(settingsHandler), { settings: innerPayload }, solutionID); + if (Object.keys(transformed.settings).length) { + innerPayload[settingURI] = transformed.settings; + } + }); + }); + + fluid.each(innerPayload, function (solutionSettings, settingURI) { + var solutionSchema = that.options.codex[settingURI]; + if (solutionSchema) { + if (!that.schemaHashesByUri[settingURI]) { + that.schemaHashesByUri[settingURI] = gpii.schema.hashSchema(solutionSchema); + } + validationStages.push(gpii.universal.solutionsRegistry.validators.singleValidation(that, globalValidator, solutionSchema, solutionSettings, that.schemaHashesByUri[settingURI], [settingURI])); + } + }); + // Although our validation is synchronous, we use this pattern so that processing will be cleanly interrupted on the + // first validation error. + return fluid.promise.sequence(validationStages); +}; + +/** + * + * Extract the solution ID (such as `com.microsoft.windows.desktopBackground`) from a solution URI, such as + * `http://registry.gpii.net/applications/com.microsoft.windows.desktopBackground`. + * + * @param {String} solutionURI - The solution URI. + * @return {String} - The solution ID. + * + */ +gpii.universal.solutionsRegistry.validators.solutionIdFromURI = function (solutionURI) { + var segments = solutionURI.split("/"); + return segments[segments.length - 1]; +}; + +/** + * + * Return a promise-returning function that represents a single attempt at validating a given payload. Meant to be used + * in a multi-pass validation approach with `fluid.promise.sequence`. + * + * @param {gpii.universal.solutionsRegistry.validators.base} validationMiddleware - The validation middleware component. + * @param {gpii.schema.validator} globalValidator - The persistent global validator component. + * @param {GssSchema} schema - A GSS schema. + * @param {Any} toValidate - The payload to validate. + * @param {String} schemaHash - A hash of the schema to use in + * @param {Array} [dataPathPrefix] - A path to prepend to the `dataPath` reported in any validation errors. + * @return {Function} - A `fluid.promise` returning function. + * + */ +gpii.universal.solutionsRegistry.validators.singleValidation = function (validationMiddleware, globalValidator, schema, toValidate, schemaHash, dataPathPrefix) { + return function () { + var validationPromise = fluid.promise(); + + var validationResults = globalValidator.validate(schema, toValidate, schemaHash); + + if (validationResults.isValid) { + validationPromise.resolve(); + } + else { + // TODO: We have to reconstruct the original path somehow + if (dataPathPrefix) { + fluid.each(validationResults.errors, function (singleError) { + singleError.dataPath = fluid.copy(dataPathPrefix).concat(singleError.dataPath); + }); + } + var localisedErrors = gpii.schema.validator.localiseErrors(validationResults.errors, toValidate, validationMiddleware.model.messages, validationMiddleware.options.rules.localisation); + var localisedPayload = fluid.copy(validationResults); + localisedPayload.errors = localisedErrors; + + var failurePayload = fluid.extend({}, validationMiddleware.options.errorTemplate, localisedPayload); + validationPromise.reject(failurePayload); + } + + return validationPromise; + }; +}; + +fluid.defaults("gpii.universal.solutionsRegistry.validators.base", { + gradeNames: ["kettle.middleware", "fluid.modelComponent"], + members: { + schemaHashesByUri: {} + }, + model: { + messages: gpii.schema.messages.validationErrors + }, + errorTemplate: { + // "Bad Request": https://developer.mozilla.org/nl/docs/Web/HTTP/Status/400 + statusCode: 400, + message: "Your request was invalid. See the errors for details." + }, + mergePolicy: { + "rules.localisation": "nomerge", + "rules.innerPayload": "nomerge", + "rules.outerPayload": "nomerge", + "rules.transformedPayload": "nomerge" + }, + pathToSettings: "", + codexPath: "%gpii-universal/build/schemas/solution-schema-codex.json", + codex: "@expand:fluid.require({that}.options.codexPath)", + // TODO: Discuss combining the "codex" and "solutions registry" in the upcoming LSR work. + srPath: "%gpii-universal/testData/solutions/win32.json5", + sr: "@expand:fluid.require({that}.options.srPath)", + baseSchemaPath: "%gpii-universal/gpii/node_modules/solutionsRegistry/src/schemas/settings-base-schema.json5", + baseSchema: "@expand:fluid.require({that}.options.baseSchemaPath)", + baseSchemaHash: "@expand:gpii.schema.hashSchema({that}.options.requestSchema)", + rules: { + localisation: { + "": "" + }, + outerPayload: { + "": "body" + }, + innerPayload: { + "": "" + }, + transformedPayload: { + "": "toValidate", + flat: { + contexts: { + "gpii-default": { + "preferences": "transformedSettings" + } + } + } + } + }, + invokers: { + handle: { + funcName: "gpii.universal.solutionsRegistry.validators.validateRequest", + args: ["{that}", "{gpii.schema.validator}", "{arguments}.0"] // kettleValidator, globalValidator, request + } + } +}); + +// TODO: discuss how to handle contexts other than gpii-default in both of these stacks. + +fluid.defaults("gpii.universal.solutionsRegistry.validators.settings", { + gradeNames: ["gpii.universal.solutionsRegistry.validators.base"], + rules: { + innerPayload: { + "": "body.flat.contexts.gpii-default.preferences" + } + } +}); + +fluid.defaults("gpii.universal.solutionsRegistry.requestHandlers.settings", { + gradeNames: ["kettle.request.http"], + requestMiddleware: { + validate: { + middleware: "{gpii.universal.solutionsRegistry.validators.settings}" + } + } +}); + +fluid.defaults("gpii.universal.solutionsRegistry.validators.preferences", { + gradeNames: ["gpii.universal.solutionsRegistry.validators.base"], + baseSchemaPath: "%gpii-universal/gpii/node_modules/solutionsRegistry/src/schemas/prefs-base-schema.json5", + rules: { + innerPayload: { + "": { + transform: { + type: "fluid.transforms.firstValue", + values: [ + "body.contexts.gpii-default.preferences.application", + "body.contexts.gpii-default.preferences" + ] + } + } + } + } +}); + +fluid.defaults("gpii.universal.solutionsRegistry.requestHandlers.preferences", { + gradeNames: ["kettle.request.http"], + requestMiddleware: { + validate: { + middleware: "{gpii.universal.solutionsRegistry.validators.preferences}" + } + } +}); diff --git a/gpii/node_modules/solutionsRegistry/src/schemas/prefs-base-schema.json5 b/gpii/node_modules/solutionsRegistry/src/schemas/prefs-base-schema.json5 new file mode 100644 index 000000000..1a6e152b8 --- /dev/null +++ b/gpii/node_modules/solutionsRegistry/src/schemas/prefs-base-schema.json5 @@ -0,0 +1,41 @@ +// The "base" schema for preferences payloads, which only naively represents individual settings. This is written +// broadly enough to support multiple contexts. +{ + "$schema": "gss-v7-full#", + "properties": { + "contexts": { + "required": true, + "additionalProperties": { + "type": "object", + "properties": { + name: { + type: "string" + }, + preferences: { + "required": true, + "type": "object", + anyOf: [ + { + "properties": { + "application": { + "type": "object", + "propertyNames": { + "type": "string", + "format": "uri" + } + } + } + }, + { + "propertyNames": { + "type": "string", + "format": "uri" + } + } + ] + } + } + } + } + } +} diff --git a/gpii/node_modules/solutionsRegistry/src/schemas/settings-base-schema.json5 b/gpii/node_modules/solutionsRegistry/src/schemas/settings-base-schema.json5 new file mode 100644 index 000000000..6e1405d65 --- /dev/null +++ b/gpii/node_modules/solutionsRegistry/src/schemas/settings-base-schema.json5 @@ -0,0 +1,31 @@ +// The "base" schema for settings payloads, which only naively represents individual settings. This is written broadly +// enough to support multiple contexts. +{ + "$schema": "gss-v7-full#", + "properties": { + "flat": { + "type": "object", + "required": true, + "properties": { + "contexts": { + "type": "object", + "required": true, + "additionalProperties": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "preferences": { + "type": "object", + "propertyNames": { + "type": "string", + "format": "uri" + } + } + } + }, + "minProperties": 1 + } + } + } + } +} diff --git a/gpii/node_modules/solutionsRegistry/src/schemas/solution-schema.json5 b/gpii/node_modules/solutionsRegistry/src/schemas/solution-schema.json5 new file mode 100644 index 000000000..646fc6bf5 --- /dev/null +++ b/gpii/node_modules/solutionsRegistry/src/schemas/solution-schema.json5 @@ -0,0 +1,99 @@ +{ + "$schema": "gss-v7-full#", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "required": true + }, + "contexts": { + "type": "object", + "properties": { + "OS": { + "type": "array", + "items": { + "properties": { + "id": { + "type": "string" + } + } + } + } + } + }, + "capabilities": { + "type": "array", + "items": { + "type": "string", + "pattern": "^http://registry\\\\.gpii\\\\.net/common/.+$" // Entries are entered escaped, and we must escape them again. + } + }, + "settingsHandlers": { + "additionalProperties": { + "properties": { + "type": { + "type": "string", + "minLength": 1, + "required": true + }, + "liveness": { + "required": true, + "type": "string", + "minLength": 1 + }, + "options": { + "type": "object" + }, + "supportedSettings": { + "required": true, + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "properties": { + "label": { + "type": "string", + "required": false + } + } + }, + "properties": { + "label": { + "type": "string", + "required": false + }, + "schema": { "$ref": "gss-v7-full#", "required": true } + } + } + }, + "capabilitiesTransformations": { + "type": "object" + }, + "inverseCapabilitiesTransformations": { + "type": "object" + } + } + } + }, + "launchHandlers": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "type": { + "type": "string", + "minLength": 1 + } + } + } + }, + "isInstalled": { + "type": "array", + "items": { + "properties": { + "type": { "type": "string" } + } + } + } + } +} diff --git a/gpii/node_modules/solutionsRegistry/src/solutionGet.js b/gpii/node_modules/solutionsRegistry/src/solutionGet.js deleted file mode 100644 index acf1ae4ed..000000000 --- a/gpii/node_modules/solutionsRegistry/src/solutionGet.js +++ /dev/null @@ -1,38 +0,0 @@ -/*! -GPII Solutions Registry GET Handler - -Copyright 2012 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - gpii = fluid.registerNamespace("gpii"), - kettle = fluid.require("kettle", require); - - fluid.defaults("kettle.requests.request.handler.solutionGet", { - gradeNames: ["autoInit", "fluid.littleComponent"], - invokers: { - handle: { - funcName: "gpii.handleSolutionGet", - args: ["{request}.req.params.id", "{requestProxy}.", "{solutionsRegistry}.solutionSource"], - dynamic: true - } - } - }); - - gpii.handleSolutionGet = function (id, requestProxy, solutionSource) { - solutionSource.get({ - id: id - }, requestProxy.events.onSuccess.fire); - }; - -})(); \ No newline at end of file diff --git a/gpii/node_modules/solutionsRegistry/src/solutionPost.js b/gpii/node_modules/solutionsRegistry/src/solutionPost.js deleted file mode 100644 index 411207806..000000000 --- a/gpii/node_modules/solutionsRegistry/src/solutionPost.js +++ /dev/null @@ -1,38 +0,0 @@ -/*! -GPII Preferences Server POST Handler - -Copyright 2012 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - gpii = fluid.registerNamespace("gpii"), - kettle = fluid.require("kettle", require); - - fluid.defaults("kettle.requests.request.handler.solutionPost", { - gradeNames: ["autoInit", "fluid.littleComponent"], - invokers: { - handle: { - funcName: "gpii.handleSolutionPost", - args: ["{request}.req", "{requestProxy}", "{solutionsRegistry}.solutionSource"], - dynamic: true - } - } - }); - - gpii.handleSolutionPost = function (req, requestProxy, solutionSource) { - solutionSource.set({ - id: req.params.id - }, req.body, requestProxy.events.onSuccess.fire); - }; - -})(); \ No newline at end of file diff --git a/gpii/node_modules/solutionsRegistry/src/solutionQuery.js b/gpii/node_modules/solutionsRegistry/src/solutionQuery.js deleted file mode 100644 index 55bac7912..000000000 --- a/gpii/node_modules/solutionsRegistry/src/solutionQuery.js +++ /dev/null @@ -1,45 +0,0 @@ -/* -GPII Preferences Server GET Handler - -Copyright 2012 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - gpii = fluid.registerNamespace("gpii"), - kettle = fluid.require("kettle", require); - - fluid.defaults("kettle.requests.request.handler.solutionQuery", { - gradeNames: ["autoInit", "fluid.littleComponent"], - invokers: { - handle: { - funcName: "gpii.handleSolutionQuery", - args: ["{requestProxy}", "{request}.req.query.os", "{solutionsRegistry}.solutionQuerySource"], - dynamic: true - } - } - }); - - gpii.handleSolutionQuery = function (requestProxy, os, solutionQuerySource) { - if (!os) { - requestProxy.events.onError.fire({ - isError: true, - message: "No os specified." - }); - return; - } - solutionQuerySource.get({ - os: os - }, requestProxy.events.onSuccess.fire); - }; - -})(); \ No newline at end of file diff --git a/gpii/node_modules/solutionsRegistry/src/solutionsRegistry.js b/gpii/node_modules/solutionsRegistry/src/solutionsRegistry.js deleted file mode 100644 index a20357534..000000000 --- a/gpii/node_modules/solutionsRegistry/src/solutionsRegistry.js +++ /dev/null @@ -1,87 +0,0 @@ -/*! -GPII Preferences Server - -Copyright 2012 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -(function () { - - "use strict"; - - var fluid = require("infusion"), - path = require("path"), - gpii = fluid.registerNamespace("gpii"); - - fluid.require("kettle", require); - fluid.require("./solutionGet.js", require); - fluid.require("./solutionPost.js", require); - fluid.require("./solutionQuery.js", require); - - fluid.defaults("gpii.solutionsRegistry", { - gradeNames: ["kettle.app", "autoInit"], - handlers: { - solutionGet: { - route: "/solution/:id", - type: "get" - }, - solutionPost: { - route: "/solution/:id", - type: "post" - }, - solutionQuery: { - route: "/solution?:query", - type: "get" - } - }, - solutionRegistryUrl: "", - solutionQueryUrl: "", - root: path.join(__dirname, ".."), - components: { - solutionSource: { - type: "kettle.dataSource.CouchDB", - options: { - gradeNames: ["kettle.dataSource.simpleCallbackWrapper"], - url: "{gpii.solutionsRegistry}.options.solutionRegistryUrl", - writable: true, - termMap: { - id: "%id" - } - } - }, - solutionQuerySource: { - type: "kettle.dataSource.CouchDB", - options: { - gradeNames: ["kettle.dataSource.simpleCallbackWrapper"], - url: "{gpii.solutionsRegistry}.options.solutionQueryUrl", - termMap: { - os: "%os" - } - } - } - } - }); - - fluid.defaults("gpii.solutionsRegistry.dev", { - gradeNames: ["autoInit", "fluid.littleComponent"], - components: { - solutionSource: { - type: "kettle.dataSource.URL" - }, - solutionQuerySource: { - type: "kettle.dataSource.URL" - } - }, - distributeOptions: { - source: "{that}.options.urlExpanderGradeNames", - target: "{that urlExpander}.options.gradeNames" - }, - urlExpanderGradeNames: ["kettle.urlExpander.development"] - }); - -})(); diff --git a/gpii/node_modules/solutionsRegistry/test/GenericTermsTests.js b/gpii/node_modules/solutionsRegistry/test/GenericTermsTests.js new file mode 100644 index 000000000..0832dc74a --- /dev/null +++ b/gpii/node_modules/solutionsRegistry/test/GenericTermsTests.js @@ -0,0 +1,200 @@ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); +var jqUnit = require("node-jqunit"); + +require("json5/lib/register"); +require("gpii-json-schema"); +require("./lib/validation"); +require("./lib/utils"); +require("../../transformer/src/js/Transformer"); + +jqUnit.module("Sanity checks for Generic Preference Terms data."); + +fluid.registerNamespace("gpii.tests.universal.solutionsRegistry.genericTerms"); + +// Validate the generic preference settings to confirm that they're at least valid GSS schemas. +gpii.tests.universal.solutionsRegistry.genericTerms.validateCommonTerms = function (that, globalValidator) { + var resolvedGenericTermsPath = fluid.module.resolvePath(that.options.genericPreferenceTermsPath); + var genericTerms = require(resolvedGenericTermsPath); + + jqUnit.test("Validating generic preference term schemas.", function () { + fluid.each(genericTerms, function (termDef, termKey) { + if (termDef.schema) { + var schemaAsGss = fluid.merge({}, { "$schema": "gss-v7-full#" }, termDef.schema); + // Separate check here as we are only validating the schema itself. In theory, this is already hit for + // any settings in our test data set, but we do it explicitly for all schemas here. + var validationResults = gpii.schema.validator.validateSchema(schemaAsGss); + + // We perform these checks a bit flipped so that only a particular value trips it, and not a missing value. + jqUnit.assertFalse("There should be no validation errors in generic preference term '" + termKey + "'.", validationResults.isValid === false); + jqUnit.assertFalse("There should be no low-level errors in generic preference term '" + termKey + "'.", validationResults.isError === true); + + if (!validationResults.isError && !validationResults.isValid) { + var localisedErrors = gpii.schema.validator.localiseErrors(validationResults.errors, schemaAsGss); + + fluid.each(localisedErrors, function (singleError) { + fluid.log("Validation error is as folows:"); + fluid.log(" - " + singleError.dataPath.join(".") + ": " + singleError.message); + }); + } + } + else { + jqUnit.fail("Generic Preference Term '" + termKey + "' has no schema."); + } + }); + }); + + jqUnit.test("Validating raw default values for all generic preference terms.", function () { + // We do this so we can report all validation errors rather than failing on the first. + var rawDefaultErrorCount = 0; + fluid.each(genericTerms, function (termDef, termKey) { + var schema = fluid.get(termDef, "schema"); + var defaultValue = fluid.get(termDef, "schema.default"); + if (defaultValue !== undefined) { + // Check the raw value against the schema + var isValid = gpii.tests.universal.solutionsRegistry.validateSinglePayload( + globalValidator, + defaultValue, + schema, + "Default raw value for generic preference term '" + termKey + "'" + ); + if (!isValid) { + rawDefaultErrorCount++; + } + } + }); + jqUnit.assertEquals("There should be no validation errors for the raw defaults.", 0, rawDefaultErrorCount); + }); + + jqUnit.test("Ensure that generic preference term defaults can be transformed to valid application-specific settings.", function () { + var combinedDefaults = {}; + var combinedMaximums = {}; + var combinedMinimums = {}; + + fluid.each(genericTerms, function (termDef, termKey) { + var defaultValue = fluid.get(termDef, "schema.default"); + if (defaultValue !== undefined) { + combinedDefaults[termKey] = defaultValue; + // If there is no min/max, we add the default so there's at least some value. Otherwise any + // transform that refers to a generic preference term will end up with a value of `undefined` + // and likely fail. + combinedMaximums[termKey] = defaultValue; + combinedMinimums[termKey] = defaultValue; + } + + var maxValue = fluid.get(termDef, "schema.maximum"); + if (maxValue !== undefined) { + combinedMaximums[termKey] = maxValue; + } + + var minValue = fluid.get(termDef, "schema.minimum"); + if (minValue !== undefined) { + combinedMinimums[termKey] = minValue; + } + }); + + // Build a crude rollup of all possible transformations for all solutions. + var solutionsDefs = fluid.require(that.options.srPath); + var combinedCapabilitiesTransform = {}; + fluid.each(solutionsDefs, function (solutionDef, solutionKey) { + var escapedSolutionKey = "http://registry\\.gpii\\.net/applications/" + solutionKey.replace(/\./g, "\\."); + fluid.each(solutionDef.settingsHandlers, function (settingsHandlerDef) { + if (settingsHandlerDef.capabilitiesTransformations) { + // Allow for at least a shallow mix of intra-application and common terms transforms. + var filteredTransforms = {}; + fluid.each(settingsHandlerDef.capabilitiesTransformations, function (singleTransform, transformKey) { + var hasIntraApplicationTransform = gpii.tests.universal.solutionsRegistry.hasIntraApplicationTransform(singleTransform); + if (!hasIntraApplicationTransform) { + filteredTransforms[transformKey] = singleTransform; + } + }); + if (Object.keys(filteredTransforms).length) { + combinedCapabilitiesTransform[escapedSolutionKey] = fluid.extend({}, combinedCapabilitiesTransform[escapedSolutionKey], filteredTransforms); + } + } + }); + }); + + // Transform the defaults into application-specific terms. + var transformedDefaults = fluid.model.transformWithRules(combinedDefaults, combinedCapabilitiesTransform); + var transformedMaximums = fluid.model.transformWithRules(combinedMaximums, combinedCapabilitiesTransform); + var transformedMinimums = fluid.model.transformWithRules(combinedMinimums, combinedCapabilitiesTransform); + + // Now validate against each solution individually. + var validationErrorCount = 0; + var codex = fluid.require(that.options.codexPath); + + fluid.each(codex, function (solutionSchema, solutionKey) { + var defaultToValidate = transformedDefaults[solutionKey]; + if (defaultToValidate) { + var defaultValidationResults = globalValidator.validate(solutionSchema, defaultToValidate); + + if (defaultValidationResults.isError || !defaultValidationResults.isValid) { + validationErrorCount += defaultValidationResults.errors.length; + var localisedDefaultErrors = gpii.schema.validator.localiseErrors(defaultValidationResults.errors, defaultToValidate); + + // Log the failure details. + fluid.log("Error validating generic preference term default(s) for " + solutionKey + ":"); + gpii.tests.universal.solutionsRegistry.genericTerms.logValidationErrors(localisedDefaultErrors, defaultToValidate); + } + } + + var maximumToValidate = transformedMaximums[solutionKey]; + if (maximumToValidate) { + var maxValidationResults = globalValidator.validate(solutionSchema, maximumToValidate); + + if (maxValidationResults.isError || !maxValidationResults.isValid) { + validationErrorCount += maxValidationResults.errors.length; + var localisedMaxErrors = gpii.schema.validator.localiseErrors(maxValidationResults.errors, maximumToValidate); + + // Log the failure details. + fluid.log("Error validating generic preference term maximum value(s) for " + solutionKey + ":"); + gpii.tests.universal.solutionsRegistry.genericTerms.logValidationErrors(localisedMaxErrors, maximumToValidate); + } + } + + var minimumToValidate = transformedMinimums[solutionKey]; + if (minimumToValidate) { + var minValidationResults = globalValidator.validate(solutionSchema, minimumToValidate); + + if (minValidationResults.isError || !minValidationResults.isValid) { + validationErrorCount += minValidationResults.errors.length; + var localisedMinErrors = gpii.schema.validator.localiseErrors(minValidationResults.errors, minimumToValidate); + + // Log the failure details. + fluid.log("Error validating generic preference term minimum value(s) for " + solutionKey + ":"); + gpii.tests.universal.solutionsRegistry.genericTerms.logValidationErrors(localisedMinErrors, minimumToValidate); + } + } + }); + + // Ensure that the tests also fail if there are errors. + jqUnit.assertEquals("There should be no validation errors that result from transforming generic preference terms defaults.", 0, validationErrorCount); + }); +}; + +gpii.tests.universal.solutionsRegistry.genericTerms.logValidationErrors = function (localisedErrors, toValidate) { + fluid.each(localisedErrors, function (singleError) { + // TODO: Track down bogus data paths like `.properties['BounceKeysInterval']` + var dataPath = Array.isArray(singleError.dataPath) ? singleError.dataPath : []; + var problematicValue = fluid.get(toValidate, dataPath); + fluid.log(" - " + dataPath.join(".") + " -> " + JSON.stringify(problematicValue) + " : " + singleError.message); + }); +}; + +fluid.defaults("gpii.tests.universal.solutionsRegistry.genericTerms", { + gradeNames: ["fluid.component"], + genericPreferenceTermsPath: "%gpii-universal/testData/ontologies/flat.json5", + codexPath: "%gpii-universal/build/schemas/solution-schema-codex.json", + // TODO: Replace this with all solutions as part of the LSR refactor. + srPath: "%gpii-universal/testData/solutions/win32.json5", + listeners: { + "onCreate.validateCommonTerms": { + funcName: "gpii.tests.universal.solutionsRegistry.genericTerms.validateCommonTerms", + args: ["{that}", "{gpii.schema.validator}"] + } + } +}); + +gpii.tests.universal.solutionsRegistry.genericTerms(); diff --git a/gpii/node_modules/solutionsRegistry/test/SettingsPayloadTests.js b/gpii/node_modules/solutionsRegistry/test/SettingsPayloadTests.js new file mode 100644 index 000000000..1f5de32fe --- /dev/null +++ b/gpii/node_modules/solutionsRegistry/test/SettingsPayloadTests.js @@ -0,0 +1,169 @@ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); +var jqUnit = require("node-jqunit"); + +require("json5/lib/register"); + +var fs = require("fs"); +var path = require("path"); + +require("../src/js/sr-validation-middleware"); + +jqUnit.module("Sanity checks for test settings payloads."); + +fluid.registerNamespace("gpii.tests.universal.solutionsRegistry.settingsPayloads"); + +gpii.tests.universal.solutionsRegistry.settingsPayloads.validateSettingsDir = function (that, validationMiddleware, globalValidator) { + jqUnit.asyncTest("Validating preferences test data.", function () { + var validationTimesByFile = {}; + var promises = []; + + fluid.each(that.options.settingsDataPaths, function (settingsDataPath) { + var resolvedPath = fluid.module.resolvePath(settingsDataPath); + var pathFiles = fs.readdirSync(resolvedPath); + var jsonFiles = pathFiles.filter(function (singlePath) { return singlePath.match(/.json5?$/i); }); + + fluid.each(jsonFiles, function (jsonFile) { + promises.push(function () { + // Clear the cache before each run to ensure that previous runs don't skew the "uncached" results. + globalValidator.clearCache(); + fluid.set(validationTimesByFile, [jsonFile, "startUncached"], Date.now()); + }); + + // Uncached pass + var settingsFilePath = path.resolve(resolvedPath, jsonFile); + var promiseFn = gpii.tests.universal.solutionsRegistry.settingsPayloads.validateSettingsFile(that, settingsFilePath, validationMiddleware); + promises.push(promiseFn); + + promises.push(function () { + fluid.set(validationTimesByFile, [jsonFile, "endUncached"], Date.now()); + }); + + // Cached pass + var cachedPromiseFn = gpii.tests.universal.solutionsRegistry.settingsPayloads.validateSettingsFile(that, settingsFilePath, validationMiddleware); + promises.push(cachedPromiseFn); + + promises.push(function () { + fluid.set(validationTimesByFile, [jsonFile, "endCached"], Date.now()); + }); + + // Check for settings not found in the "codex", i.e. that aren't in any solution registry or a common term. + var rogueSettingsPromiseFn = gpii.tests.universal.solutionsRegistry.settingsPayloads.findRogueSettings(that.options.codex, settingsFilePath); + promises.push(rogueSettingsPromiseFn); + }); + }); + + fluid.promise.sequence(promises).then( + function () { + jqUnit.start(); + jqUnit.assert("All test preference files are valid."); + + // console.log("Filename, Uncached, Cached"); + fluid.each(validationTimesByFile, function (validationTimes, filename) { + var uncachedTime = validationTimes.endUncached - validationTimes.startUncached; + jqUnit.assertTrue("The uncached validation time for file '" + filename + "' (" + uncachedTime + "ms) should be reasonable (< 2000ms).", uncachedTime < 2000); + + var cachedTime = validationTimes.endCached - validationTimes.endUncached; + jqUnit.assertTrue("The cached validation time for file '" + filename + "' (" + cachedTime + "ms) should be reasonable (< 100ms).", cachedTime < 100); + // console.log([path.basename(filename, ".json5"), uncachedTime, cachedTime].join(", ")); + }); + }, + function () { + jqUnit.start(); + jqUnit.fail("All test preference files could not be validated."); + } + ); + }); +}; + +// Catch invalid test data that can't be detected by our "per solution" validation middleware. +gpii.tests.universal.solutionsRegistry.settingsPayloads.findRogueSettings = function (codex, settingsFilePath) { + var settingsData = require(settingsFilePath); + var rogueSettingsCount = 0; + + // There are files that have purely test data mixed in with others, so we exclude them invidually. + var isTestData = false; + fluid.each(["testUser1.json5", "giant.json5"], function (toExclude) { + if (settingsFilePath.endsWith(toExclude)) { + isTestData = true; + } + }); + + if (isTestData) { return; } + + var prefsToCheck = fluid.get(settingsData, ["flat", "contexts", "gpii-default", "preferences"]); + fluid.each(prefsToCheck, function (prefsPayload, prefKey) { + var keyToCheck = prefKey; + if (prefKey.startsWith("http://registry.gpii.net/applications/") && prefKey.endsWith("enabled")) { + keyToCheck = prefKey.replace(/\/enabled$/, ""); + } + + // We have to use the array notation for fluid.get to avoid interpreting the dots in the URI as path segments. + if (fluid.get(codex, [keyToCheck]) === undefined) { + fluid.log("Setting '" + prefKey + "' in file '" + settingsFilePath + " is not associated with a known solution or common term."); + rogueSettingsCount++; + } + }); + + jqUnit.assertEquals("There should be no 'rogue' settings in file '" + settingsFilePath + "'.", 0, rogueSettingsCount); +}; + +gpii.tests.universal.solutionsRegistry.settingsPayloads.validateSettingsFile = function (that, settingsFilePath, validationMiddleware) { + return function () { + var settingsData = {}; + try { + settingsData = require(settingsFilePath); + } + catch (error) { + jqUnit.fail("There should not be any invalid settings files (failed checking '" + settingsFilePath + "'."); + fluid.log(error); + } + + // Validate each file using the new "per solution" settings validation middleware. + var requestData = { body: settingsData }; + var validationPromise = validationMiddleware.handle({ req: requestData}); + validationPromise.then( + function () { + jqUnit.assert("Settings file '" + settingsFilePath + "' is valid."); + }, + function (validationError) { + jqUnit.fail("Settings file '" + settingsFilePath + "' is invalid."); + fluid.log("The error returned was: " + validationError.message); + fluid.each(validationError.errors, function (singleError) { + fluid.log("Validation error in file '" + settingsFilePath + "':"); + fluid.log(" - " + singleError.dataPath.join(".") + ": " + singleError.message); + + var validatedMaterial = fluid.model.transformWithRules(requestData, validationMiddleware.options.rules.innerPayload); + var failingMaterial = fluid.get(validatedMaterial, singleError.dataPath); + fluid.log("The failing value was '" + failingMaterial + "'."); + }); + } + ); + + return validationPromise; + }; +}; + +fluid.defaults("gpii.tests.universal.solutionsRegistry.settingsPayloads", { + gradeNames: ["fluid.component"], + settingsDataPaths: [ + "%gpii-universal/testData/preferences", + "%gpii-universal/tests/data/preferences" + ], + codexPath: "%gpii-universal/build/schemas/solution-schema-codex.json", + codex: "@expand:fluid.require({that}.options.codexPath)", + components: { + settingsValidator: { + type: "gpii.universal.solutionsRegistry.validators.settings" + } + }, + listeners: { + "onCreate.validateSettingsDir": { + funcName: "gpii.tests.universal.solutionsRegistry.settingsPayloads.validateSettingsDir", + args: ["{that}", "{gpii.universal.solutionsRegistry.validators.settings}", "{gpii.schema.validator}"] + } + } +}); + +gpii.tests.universal.solutionsRegistry.settingsPayloads(); diff --git a/gpii/node_modules/solutionsRegistry/test/SolutionsRegistryFileTests.js b/gpii/node_modules/solutionsRegistry/test/SolutionsRegistryFileTests.js new file mode 100644 index 000000000..8d027e766 --- /dev/null +++ b/gpii/node_modules/solutionsRegistry/test/SolutionsRegistryFileTests.js @@ -0,0 +1,282 @@ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); +var jqUnit = require("node-jqunit"); + +require("json5/lib/register"); + +// TODO: reenable once we're ready to fill in missing schema data in other platforms. +//var fs = require("fs"); +var path = require("path"); + +require("gpii-json-schema"); + +require("./lib/validation"); +require("../../transformer/src/js/Transformer"); + +var solutionSchema = require("../src/schemas/solution-schema.json5"); + +jqUnit.module("Sanity checks for Solutions Registry files."); + +fluid.registerNamespace("gpii.tests.universal.solutionsRegistry.srFiles"); + +gpii.tests.universal.solutionsRegistry.srFiles.checkSolutionsFiles = function (that, globalValidator) { + var resolvedGenericTermsPath = fluid.module.resolvePath(that.options.genericPreferenceTermsPath); + var genericTerms = require(resolvedGenericTermsPath); + + var resolvedSrPath = fluid.module.resolvePath(that.options.solutionsRegistryPath); + + // TODO: reenable once we're ready to fill in missing schema data in other platforms. + //var files = fs.readdirSync(resolvedSrPath); + var files = ["win32.json5"]; + + fluid.each(files, function (filename) { + if (filename.match(/^.+\.json5?$/i)) { + var filePath = path.resolve(resolvedSrPath, filename); + var singleFileSolutions = require(filePath); + + fluid.each(singleFileSolutions, function (solutionDef, solutionKey) { + gpii.tests.universal.solutionsRegistry.srFiles.checkSingleSolution(globalValidator, filename, solutionKey, solutionDef, genericTerms); + }); + } + }); +}; + +gpii.tests.universal.solutionsRegistry.srFiles.checkSingleSolution = function (globalValidator, filename, solutionKey, solutionDef, genericTerms) { + jqUnit.test("Sanity-checking solution '" + solutionKey + "' in file '" + filename + "'.", function () { + var isSolutionValid = gpii.tests.universal.solutionsRegistry.validateSinglePayload( + globalValidator, + solutionDef, + solutionSchema, + "solution file '" + filename + "' -> solution '" + solutionKey + "'" + ); + jqUnit.assertTrue("Solution '" + solutionKey + "' in file '" + filename + "' should be valid according to the schema.", isSolutionValid); + + gpii.tests.universal.solutionsRegistry.srFiles.checkDefaults(globalValidator, filename, solutionKey, solutionDef); + gpii.tests.universal.solutionsRegistry.srFiles.checkCapabilities(filename, solutionKey, solutionDef, genericTerms); + gpii.tests.universal.solutionsRegistry.srFiles.checkTransforms(filename, solutionKey, solutionDef, genericTerms); + gpii.tests.universal.solutionsRegistry.srFiles.checkWindowsSettingsHandlers(filename, solutionKey, solutionDef); + + // TODO: Ensure that all forward and inverse transforms are sane, i.e. that they can at least be used with an empty object. + // TODO: Ensure that all references to generic preference settings in transforms actually refer to generic preference settings that exist. + // Discuss with Antranig how best to accomplish these checks, i.e. how much in the way of resolvers, etc. do we + // actually need to meaningfully sanity check transforms? + }); + +}; + +gpii.tests.universal.solutionsRegistry.srFiles.checkWindowsSettingsHandlers = function (filename, solutionKey, solutionDef) { + // These settings handlers require a deeper structure, i.e an object with properties. + var settingsHandlersDemandingObjects = [ + "gpii.windows.nativeSettingsHandler", + "gpii.windows.spiSettingsHandler", + "gpii.windows.systemSettingsHandler", + "gpii.windows.wmiSettingsHandler" + ]; + + var settingsHandlersDemandingDataTypes = [ + "gpii.windows.registrySettingsHandler" + ]; + + var objectivelyAwfulSettingsCount = 0; + var brokenRegistrySettingsCount = 0; + + fluid.each(fluid.get(solutionDef, "settingsHandlers"), function (settingsHandlerDef, settingsHandlerKey) { + var settingsHandlerType = fluid.get(settingsHandlerDef, "type"); + fluid.each(settingsHandlerDef.supportedSettings, function (settingDef, settingKey) { + var settingType = fluid.get(settingDef, "schema.type"); + var isEnum = fluid.get(settingDef, "schema.enum") !== undefined; + + if (isEnum && settingType !== undefined) { + fluid.log("WARNING: enum " + filename + " -> " + solutionKey + " -> " + settingsHandlerKey + " -> " + settingKey + " should not have a type specified."); + } + + if (settingsHandlersDemandingObjects.indexOf(settingsHandlerType) !== -1) { + var isObject = settingType === "object"; + var hasProperties = fluid.get(settingDef, "schema.properties") !== undefined; + + if (!isObject || !hasProperties) { + objectivelyAwfulSettingsCount++; + } + + if (!isObject && !hasProperties) { + fluid.log("WARNING: " + filename + " -> " + solutionKey + " -> " + settingsHandlerKey + " -> " + settingKey + " should be an object."); + } + else if (!hasProperties && isObject) { + fluid.log("WARNING: " + filename + " -> " + solutionKey + " -> " + settingsHandlerKey + " -> " + settingKey + " should have properties defined."); + } + else if (!isObject && hasProperties) { + fluid.log("WARNING: " + filename + " -> " + solutionKey + " -> " + settingsHandlerKey + " -> " + settingKey + " does not have the correct 'object' type value."); + } + } + + if (settingType === "number" || settingType === "integer") { + if (fluid.get(settingDef, "schema.minimum") === undefined) { + fluid.log("WARNING: Numeric setting " + filename + " -> " + solutionKey + " -> " + settingsHandlerKey + " -> " + settingKey + " does not specify a minimum value."); + } + + if (fluid.get(settingDef, "schema.maximum") === undefined) { + fluid.log("WARNING: Numeric setting " + filename + " -> " + solutionKey + " -> " + settingsHandlerKey + " -> " + settingKey + " does not specify a maximum value."); + } + } + }); + + if (settingsHandlersDemandingDataTypes.indexOf(settingsHandlerType) !== -1) { + var dataTypeKeys = Object.keys(fluid.get(settingsHandlerDef, "options.dataTypes")); + var supportedSettingsKeys = Object.keys(fluid.get(settingsHandlerDef, "supportedSettings")); + var keyCounts = {}; + fluid.each([dataTypeKeys, supportedSettingsKeys], function (keys) { + fluid.each(keys, function (key) { + var existingValue = fluid.get(keyCounts, key) || 0; + fluid.set(keyCounts, key, existingValue + 1); + }); + }); + + fluid.each(keyCounts, function (count, key) { + if (count !== 2) { + fluid.log("WARNING: " + filename + " -> " + solutionKey + " -> " + settingsHandlerKey + " -> " + key + " should have both a data type and/or supported settings entry."); + brokenRegistrySettingsCount++; + } + }); + } + }); + + jqUnit.assertEquals("There should be no settings that lack a proper object structure.", 0, objectivelyAwfulSettingsCount); + jqUnit.assertEquals("There should be no mismatch between registry settings and data types.", 0, brokenRegistrySettingsCount); +}; + +// Ensure that all "capabilities" refer to a generic preference setting. +gpii.tests.universal.solutionsRegistry.srFiles.checkCapabilities = function (filename, solutionKey, solutionDef, genericTerms) { + var missingTermsCount = 0; + if (solutionDef.capabilities && solutionDef.capabilities.length) { + fluid.each(solutionDef.capabilities, function (capabilityKey) { + var unescapedKey = capabilityKey.replace(/\\/g, ""); + var termDef = genericTerms[unescapedKey]; + if (termDef === undefined) { + missingTermsCount++; + fluid.log("ERROR: The capability '" + capabilityKey + "' specified in solution '" + solutionKey + "' does not exist."); + } + }); + + jqUnit.assertEquals("There should be no missing capabilities in solution '" + solutionKey + "'.", 0, missingTermsCount); + } + else { + jqUnit.assert("Entry '" + solutionKey + "' has no capabilities, which is fine."); + } +}; + +// Ensure that all transforms involving common terms refer to common terms that actually exist. +gpii.tests.universal.solutionsRegistry.srFiles.checkTransforms = function (filename, solutionKey, solutionDef, genericTerms) { + fluid.each(solutionDef.settingsHandlers, function (settingsHandlerDef) { + var allTransformTerms = []; + + if (settingsHandlerDef.capabilitiesTransformations) { + var forwardTransformTerms = gpii.tests.universal.solutionsRegistry.srFiles.extractCommonTerms(settingsHandlerDef.capabilitiesTransformations); + if (forwardTransformTerms.length) { + allTransformTerms = allTransformTerms.concat(forwardTransformTerms); + } + } + + if (settingsHandlerDef.inverseCapabilitiesTransformations) { + var inverseTransformTerms = gpii.tests.universal.solutionsRegistry.srFiles.extractCommonTerms(settingsHandlerDef.inverseCapabilitiesTransformations); + if (inverseTransformTerms.length) { + allTransformTerms = allTransformTerms.concat(inverseTransformTerms); + } + } + var missingTermsCount = 0; + + fluid.each(allTransformTerms, function (transformTerm) { + var unescapedTermKey = transformTerm.replace(/\\\./g, "."); + var termExists = (fluid.get(genericTerms, [unescapedTermKey]) !== undefined); + + if (!termExists) { + missingTermsCount++; + } + }); + + jqUnit.assertEquals("There should be no transforms that use missing common terms.", 0, missingTermsCount); + }); +}; + +gpii.tests.universal.solutionsRegistry.srFiles.extractCommonTerms = function (toInspect) { + var foundTerms = []; + + if (typeof toInspect === "object") { + fluid.each(toInspect, function (deepValue) { + var subSegments = gpii.tests.universal.solutionsRegistry.srFiles.extractCommonTerms(deepValue); + if (subSegments.length) { + foundTerms = foundTerms.concat(subSegments); + } + }); + } + else if (typeof toInspect === "string" && toInspect.startsWith("http://registry.gpii.net/common/")) { + foundTerms.push(toInspect); + } + + return foundTerms; +}; + + +// If there are default values, test them against the schema. +gpii.tests.universal.solutionsRegistry.srFiles.checkDefaults = function (globalValidator, filename, solutionKey, solutionDef) { + var fileErrors = 0; + fluid.each(solutionDef.settingsHandlers, function (settingsHandler) { + fluid.each(settingsHandler.supportedSettings, function (supportedSetting, settingKey) { + // Check the specified default against its schema. This only works for simple settings, and not for + // deeply nested objects. + var defaultValue = fluid.get(supportedSetting, "schema.default"); + if (defaultValue === undefined) { + fluid.log("WARNING: " + filename + " -> " + solutionKey + " -> " + settingKey + " does not have a default value."); + } + else { + // Check the raw value + var isValid = gpii.tests.universal.solutionsRegistry.validateSinglePayload( + globalValidator, + defaultValue, + supportedSetting.schema, + "Default value for " + filename + " -> " + solutionKey + " -> " + settingKey + ); + if (!isValid) { + fileErrors++; + } + + // Transform the default value to any "intra-application" variant we might expect to see and ensure that it's still valid. + if (fluid.get(settingsHandler, ["capabilitiesTransformations", settingKey]) && settingsHandler.inverseCapabilitiesTransformations) { + var settingWrapper = {}; + fluid.set(settingWrapper, [settingKey], defaultValue); + var invertedPayload = fluid.model.transformWithRules(settingWrapper, settingsHandler.inverseCapabilitiesTransformations); + if (Object.keys(invertedPayload).length > 0) { + var reinvertedPayload = fluid.model.transformWithRules(invertedPayload, settingsHandler.capabilitiesTransformations); + var reinvertedDefaultValue = fluid.get(reinvertedPayload, [settingKey]); + + if (reinvertedDefaultValue !== defaultValue) { + // Warn on "softly equal" things like "truthy" and "falsy" values. + if (reinvertedDefaultValue != defaultValue) { // eslint-disable-line eqeqeq + fluid.log("ERROR: The capabilities transforms for " + filename + " -> " + solutionKey + " -> " + settingKey + " may result in data corruption."); + fileErrors++; + } + else { + fluid.log("WARNING: The capabilities transforms for " + filename + " -> " + solutionKey + " -> " + settingKey + " are only safe for truthy and falsy values."); + } + } + } + } + } + }); + }); + jqUnit.assertEquals("There should be no validation errors in solutions registry file '" + filename + "'.", 0, fileErrors); +}; + +fluid.defaults("gpii.tests.universal.solutionsRegistry.srFiles", { + gradeNames: ["fluid.component"], + solutionsRegistryPath: "%gpii-universal/testData/solutions", + genericPreferenceTermsPath: "%gpii-universal/testData/ontologies/flat.json5", + listeners: { + "onCreate.checkAll": { + funcName: "gpii.tests.universal.solutionsRegistry.srFiles.checkSolutionsFiles", + args: ["{that}", "{gpii.schema.validator}"] + } + } +}); + +gpii.tests.universal.solutionsRegistry.srFiles(); diff --git a/gpii/node_modules/solutionsRegistry/test/UtilsTests.js b/gpii/node_modules/solutionsRegistry/test/UtilsTests.js new file mode 100644 index 000000000..98545f2c9 --- /dev/null +++ b/gpii/node_modules/solutionsRegistry/test/UtilsTests.js @@ -0,0 +1,68 @@ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +require("./lib/utils"); + +var jqUnit = require("node-jqunit"); + +jqUnit.module("Unit tests for test utilities."); + +jqUnit.test("Test `hasIntraApplicationTransform` utility function.", function () { + var testDefs = { + shallowMatch: { + shouldMatch: true, + toCheck: "http://registry.gpii.net/applications/com.microsoft.windows.desktopBackground.Scaling" + }, + escapedMatch: { + shouldMatch: true, + toCheck: "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.desktopBackground.Scaling" + }, + shallowMiss: { + toCheck: "http://registry.gpii.net/common/cursorSize" + }, + shallowEscapedMiss: { + toCheck: "http://registry\\.gpii\\.net/common/cursorSize" + }, + deepHit: { + shouldMatch: true, + toCheck: { + deep: { + deeper: { + deepest: "http://registry.gpii.net/applications/com.microsoft.windows.desktopBackground.Scaling" + } + } + } + }, + deepEscapedHit: { + shouldMatch: true, + toCheck: { + deep: { + deeper: { + deepest: "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.desktopBackground.Scaling" + } + } + } + }, + deepMiss: { + toCheck: { + one: { + two: "http://registry.gpii.net/common/cursorSize" + } + } + }, + deepEscapedMiss: { + toCheck: { + outer: { + inner: "http://registry\\.gpii\\.net/common/cursorSize" + } + } + } + }; + + fluid.each(testDefs, function (testDef, testDefKey) { + var hasIntraApplicationTransform = gpii.tests.universal.solutionsRegistry.hasIntraApplicationTransform(testDef.toCheck); + var testFn = testDef.shouldMatch ? "assertTrue" : "assertFalse"; + jqUnit[testFn]("The output for " + testDefKey + " should be as expected.", hasIntraApplicationTransform); + }); +}); diff --git a/gpii/node_modules/solutionsRegistry/test/ValidationMiddlewareTests.js b/gpii/node_modules/solutionsRegistry/test/ValidationMiddlewareTests.js new file mode 100644 index 000000000..a68a1aeee --- /dev/null +++ b/gpii/node_modules/solutionsRegistry/test/ValidationMiddlewareTests.js @@ -0,0 +1,348 @@ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +var jqUnit = require("node-jqunit"); + +// Add the ability to require JSON5 files. +require("json5/lib/register"); + +// Require universal so that we can resolve paths to it and any sub-modules. +fluid.require("%gpii-universal"); + +// We reuse the fairly lightweight caseholder from gpii-json-schema to avoid having to perform our own mixing of +// sequenceGrades into each test sequence. +fluid.require("%gpii-json-schema/tests/js/node/lib/fixtures.js"); + +fluid.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.universal.solutionsRegistry.kettle.caseHolder"); + +gpii.tests.universal.solutionsRegistry.kettle.caseHolder.examineResponse = function (body, shouldBeValid) { + if (shouldBeValid) { + jqUnit.assertEquals("The response body should be correct.", "Payload accepted.", body.message); + } + else { + jqUnit.assertEquals("The data should be flagged as invalid.", body.isValid, false); + jqUnit.assertTrue("There should be at least one validation error", body.errors && body.errors.length > 0); + } +}; + +fluid.defaults("gpii.tests.universal.solutionsRegistry.kettle.caseHolder", { + gradeNames: ["gpii.test.schema.caseHolder"], + validPayloadPath: "%gpii-universal/testData/preferences/jaws.json5", + inputs: { + validSettings: "@expand:fluid.require({that}.options.validPayloadPath)", + shallowlyInvalidSettings: {}, + deeplyInvalidSettings: { + "flat": { + "name": "JAWS", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.freedomscientific.jaws": { + "Braille.AttributeRotationDelay": "nonsense" + } + } + } + } + } + }, + validPrefs: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.freedomscientific.jaws": { + "Braille.AttributeRotationDelay": 500 + } + } + } + } + }, + shallowlyInvalidPrefs: {}, + deeplyInvalidPrefs: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.freedomscientific.jaws": { + "Braille.AttributeRotationDelay": "nonsense" + } + } + } + } + }, + // Test "intra-application" transforms, see https://issues.gpii.net/browse/GPII-4323 + intraApplicationPrefs: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.desktopBackground": { + "Image": "%SystemRoot%\\Web\\Wallpaper\\Windows\\img0.jpg" + } + } + } + } + } + }, + rawModules: [{ + name: "Filtered settings middleware tests.", + tests: [ + { + name: "Testing a valid settings payload.", + type: "test", + sequence: [ + { + func: "{validSettingsRequest}.send", + args: ["{that}.options.inputs.validSettings"] + }, + { + listener: "gpii.tests.universal.solutionsRegistry.kettle.caseHolder.examineResponse", + event: "{validSettingsRequest}.events.onComplete", + args: ["@expand:JSON.parse({arguments}.0)", true] + } + ] + }, + { + name: "Testing a shallowly invalid settings payload.", + type: "test", + sequence: [ + { + func: "{shallowlyInvalidSettingsRequest}.send", + args: ["{that}.options.inputs.shallowlyInvalidSettings "] + }, + { + listener: "gpii.tests.universal.solutionsRegistry.kettle.caseHolder.examineResponse", + event: "{shallowlyInvalidSettingsRequest}.events.onComplete", + args: ["@expand:JSON.parse({arguments}.0)", false] + } + ] + }, + { + name: "Testing a deeply invalid settings payload.", + type: "test", + sequence: [ + { + func: "{deeplyInvalidSettingsRequest}.send", + args: ["{that}.options.inputs.deeplyInvalidSettings "] + }, + { + listener: "gpii.tests.universal.solutionsRegistry.kettle.caseHolder.examineResponse", + event: "{deeplyInvalidSettingsRequest}.events.onComplete", + args: ["@expand:JSON.parse({arguments}.0)", false] + } + ] + }, + { + name: "Testing a valid preferences payload.", + type: "test", + sequence: [ + { + func: "{validPrefsRequest}.send", + args: ["{that}.options.inputs.validPrefs"] + }, + { + listener: "gpii.tests.universal.solutionsRegistry.kettle.caseHolder.examineResponse", + event: "{validPrefsRequest}.events.onComplete", + args: ["@expand:JSON.parse({arguments}.0)", true] + } + ] + }, + { + name: "Testing a shallowly invalid preferences payload.", + type: "test", + sequence: [ + { + func: "{shallowlyInvalidPrefsRequest}.send", + args: ["{that}.options.inputs.shallowlyInvalidPrefs"] + }, + { + listener: "gpii.tests.universal.solutionsRegistry.kettle.caseHolder.examineResponse", + event: "{shallowlyInvalidPrefsRequest}.events.onComplete", + args: ["@expand:JSON.parse({arguments}.0)", false] + } + ] + }, + { + name: "Testing a deeply invalid preferences payload.", + type: "test", + sequence: [ + { + func: "{deeplyInvalidPrefsRequest}.send", + args: ["{that}.options.inputs.deeplyInvalidPrefs"] + }, + { + listener: "gpii.tests.universal.solutionsRegistry.kettle.caseHolder.examineResponse", + event: "{deeplyInvalidPrefsRequest}.events.onComplete", + args: ["@expand:JSON.parse({arguments}.0)", false] + } + ] + }, + { + name: "Testing support for transforming preferences.", + type: "test", + sequence: [ + { + func: "{intraApplicationPrefsRequest}.send", + args: ["{that}.options.inputs.intraApplicationPrefs"] + }, + { + listener: "gpii.tests.universal.solutionsRegistry.kettle.caseHolder.examineResponse", + event: "{intraApplicationPrefsRequest}.events.onComplete", + args: ["@expand:JSON.parse({arguments}.0)", true] + } + ] + } + ] + }], + components: { + validPrefsRequest: { + type: "gpii.tests.universal.solutionsRegistry.request", + options: { + method: "POST", + path: "/gatedPreferences" + } + }, + shallowlyInvalidPrefsRequest: { + type: "gpii.tests.universal.solutionsRegistry.request", + options: { + method: "POST", + path: "/gatedPreferences" + } + }, + deeplyInvalidPrefsRequest: { + type: "gpii.tests.universal.solutionsRegistry.request", + options: { + method: "POST", + path: "/gatedPreferences" + } + }, + validSettingsRequest: { + type: "gpii.tests.universal.solutionsRegistry.request", + options: { + method: "POST", + path: "/gatedSettings" + } + }, + deeplyInvalidSettingsRequest: { + type: "gpii.tests.universal.solutionsRegistry.request", + options: { + method: "POST", + path: "/gatedSettings" + } + }, + shallowlyInvalidSettingsRequest: { + type: "gpii.tests.universal.solutionsRegistry.request", + options: { + method: "POST", + path: "/gatedSettings" + } + }, + intraApplicationPrefsRequest: { + type: "gpii.tests.universal.solutionsRegistry.request", + options: { + method: "POST", + path: "/gatedPreferences" + } + } + } +}); + +// A wrapper for `kettle.request.http` designed for use with the `testEnvironment` below. +fluid.defaults("gpii.tests.universal.solutionsRegistry.request", { + gradeNames: ["kettle.test.request.http"], + port: "{testEnvironment}.options.port", + path: { + expander: { + funcName: "fluid.stringTemplate", + args: ["http://localhost:%port/%endpoint", { port: "{testEnvironment}.options.port", endpoint: "{that}.options.endpoint"}] + } + } +}); + +// A mix-in grade to ensure a "success" response is sent if the payload is valid. +fluid.defaults("gpii.tests.universal.solutionsRegistry.handlers.base", { + gradeNames: ["fluid.component"], + invokers: { + handleRequest: { + func: "{that}.events.onSuccess.fire", + args: [{ message: "Payload accepted." }] + } + } +}); + +// handler with settings validation. +fluid.defaults("gpii.tests.universal.solutionsRegistry.handlers.settings", { + gradeNames: ["gpii.universal.solutionsRegistry.requestHandlers.settings", "gpii.tests.universal.solutionsRegistry.handlers.base"] +}); + +// handler with settings validation. +fluid.defaults("gpii.tests.universal.solutionsRegistry.handlers.preferences", { + gradeNames: ["gpii.universal.solutionsRegistry.requestHandlers.preferences", "gpii.tests.universal.solutionsRegistry.handlers.base"] +}); + +fluid.defaults("gpii.tests.universal.solutionsRegistry.kettle.app", { + gradeNames: ["kettle.app"], + components: { + preferencesValidator: { + type: "gpii.universal.solutionsRegistry.validators.preferences" + }, + settingsValidator: { + type: "gpii.universal.solutionsRegistry.validators.settings" + } + }, + requestHandlers: { + gatedPreferences: { + type: "gpii.tests.universal.solutionsRegistry.handlers.preferences", + route: "/gatedPreferences", + method: "post" + }, + gatedSettings: { + type: "gpii.tests.universal.solutionsRegistry.handlers.settings", + route: "/gatedSettings", + method: "post" + } + } +}); + +fluid.defaults("gpii.tests.universal.solutionsRegistry.testEnvironment", { + gradeNames: ["fluid.test.testEnvironment"], + port: 7734, + events: { + constructFixtures: null, + onKettleReady: null, + onFixturesConstructed: { + events: { + onKettleReady: "onKettleReady" + } + } + }, + components: { + kettle: { + createOnEvent: "constructFixtures", + type: "kettle.server", + options: { + port: "{testEnvironment}.options.port", + listeners: { + "onListen.notifyEnvironment": { + func: "{testEnvironment}.events.onKettleReady.fire" + } + }, + components: { + app: { + type: "gpii.tests.universal.solutionsRegistry.kettle.app" + } + } + } + }, + caseHolder: { + type: "gpii.tests.universal.solutionsRegistry.kettle.caseHolder" + } + } +}); + +fluid.test.runTests("gpii.tests.universal.solutionsRegistry.testEnvironment"); diff --git a/gpii/node_modules/solutionsRegistry/test/all-tests.js b/gpii/node_modules/solutionsRegistry/test/all-tests.js new file mode 100644 index 000000000..63c6239a7 --- /dev/null +++ b/gpii/node_modules/solutionsRegistry/test/all-tests.js @@ -0,0 +1,8 @@ +/* eslint-env node */ +"use strict"; + +require("./ValidationMiddlewareTests.js"); +require("./GenericTermsTests.js"); +require("./SettingsPayloadTests.js"); +require("./SolutionsRegistryFileTests.js"); +require("./UtilsTests"); diff --git a/gpii/node_modules/solutionsRegistry/test/lib/utils.js b/gpii/node_modules/solutionsRegistry/test/lib/utils.js new file mode 100644 index 000000000..f6f8f86a5 --- /dev/null +++ b/gpii/node_modules/solutionsRegistry/test/lib/utils.js @@ -0,0 +1,27 @@ +"use strict"; +// Convenience functions used in this repo's tests. +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.tests.universal.solutionsRegistry"); + +// Filter out "intra-application" transforms so that we can cleanly test just the common terms +// (the intra-application transforms are tested in SolutionsRegistryFileTests.js) +gpii.tests.universal.solutionsRegistry.hasIntraApplicationTransform = function (toInspect) { + var hasIntraApplicationTransform = false; + + if (fluid.isPlainObject(toInspect)) { + var elementWithIntraApplicationTransform = fluid.find(toInspect, function (value) { + return gpii.tests.universal.solutionsRegistry.hasIntraApplicationTransform(value) ? toInspect : undefined; + }); + + if (elementWithIntraApplicationTransform) { + hasIntraApplicationTransform = true; + } + } + else if (typeof toInspect === "string" && toInspect.match("/applications/")) { + hasIntraApplicationTransform = true; + } + + return hasIntraApplicationTransform; +}; diff --git a/gpii/node_modules/solutionsRegistry/test/lib/validation.js b/gpii/node_modules/solutionsRegistry/test/lib/validation.js new file mode 100644 index 000000000..cc8a1178f --- /dev/null +++ b/gpii/node_modules/solutionsRegistry/test/lib/validation.js @@ -0,0 +1,20 @@ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.tests.universal.solutionsRegistry"); + +// A helper function to standardise the display of validation errors in this module's tests. +gpii.tests.universal.solutionsRegistry.validateSinglePayload = function (globalValidator, payload, schema, payloadLabel) { + var validationResults = globalValidator.validate(schema, payload); + if (!validationResults.isError && !validationResults.isValid) { + var localisedErrors = gpii.schema.validator.localiseErrors(validationResults.errors, payload); + + fluid.each(localisedErrors, function (singleError) { + fluid.log("Validation error in " + payloadLabel + ":"); + fluid.log(" - " + singleError.dataPath.join(".") + ": " + singleError.message); + }); + } + return validationResults.isValid && !validationResults.isError; +}; diff --git a/gpii/node_modules/testing/README.md b/gpii/node_modules/testing/README.md new file mode 100644 index 000000000..4f281efd7 --- /dev/null +++ b/gpii/node_modules/testing/README.md @@ -0,0 +1,7 @@ +# GPII Personalization Framework: Testing + +Main entry points: + +gpii.acceptanceTesting.buildTests + +gpii.tests.cloudBased.buildTests diff --git a/gpii/node_modules/testing/index.js b/gpii/node_modules/testing/index.js new file mode 100644 index 000000000..5dcee5d71 --- /dev/null +++ b/gpii/node_modules/testing/index.js @@ -0,0 +1,31 @@ +"use strict"; + +var fluid = require("infusion"), + kettle = fluid.registerNamespace("kettle"); + +kettle.loadTestingSupport(); + +// Some definitions to make sure that errors during request get logged to the console. This should really be +// a default +fluid.defaults("gpii.test.errorLoggingRequest", { + listeners: { + "onRequestError.log": "fluid.logUncaughtException", + "onRequestError.logActivity": { + funcName: "fluid.logActivity", + args: [null], + priority: "after:log" + } + } +}); + +fluid.makeGradeLinkage("gpii.test.requestErrorLinkage", ["kettle.request"], "gpii.test.errorLoggingRequest"); + +require("./src/Fixtures.js"); +require("./src/Mocks.js"); +require("./src/TestUtils.js"); +require("./src/CloudBased.js"); +require("./src/CloudBasedOAuth2TestsUtils.js"); +require("./src/Acceptance.js"); +require("./src/Integration.js"); +require("./src/NockUtils.js"); +require("./src/RunTestDefs"); diff --git a/gpii/node_modules/testing/package.json b/gpii/node_modules/testing/package.json new file mode 100644 index 000000000..561d032fc --- /dev/null +++ b/gpii/node_modules/testing/package.json @@ -0,0 +1,13 @@ +{ + "name": "gpii-testing", + "description": "This module contains common definitions which assist in writing acceptance and integration tests for the GPII framework", + "version": "0.3.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } +} diff --git a/gpii/node_modules/testing/src/Acceptance.js b/gpii/node_modules/testing/src/Acceptance.js new file mode 100644 index 000000000..2a70a2f0c --- /dev/null +++ b/gpii/node_modules/testing/src/Acceptance.js @@ -0,0 +1,74 @@ +/** +GPII Acceptance Testing + +Copyright 2013 Raising the Floor International +Copyright 2013 OCAD University +Copyright 2014 Lucendo Development Ltd. + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + child_process = require("child_process"); + +// Component which manages execution and observation of an arbitrary process +// By default, will make 12 attempts spaced by 500ms to execute it with the expected output +fluid.defaults("gpii.test.acceptance.exec", { + gradeNames: ["gpii.test.common.exec"], + invokers: { + exec: { + funcName: "gpii.test.acceptance.exec.exec", + args: ["{that}", "{arguments}.0", "{arguments}.1"] + } + }, + timeout: 500, + maxTimeouts: 12 +}); + +gpii.test.acceptance.exec.exec = function (that, processSpec, expected) { + var command = processSpec.command, + timeout = processSpec.timeout || that.options.timeout, + maxTimeouts = processSpec.maxTimeouts || that.options.maxTimeouts; + + fluid.log("Executing: ", command); + + child_process.exec(command, function (err, stdout, stderr) { + if (stderr) { + fluid.log("stderr from command \"", command, "\": ", stderr); + } + if (stdout.trim() === expected) { + that.events.onExecExit.fire(true, processSpec); + } else { + processSpec.count = processSpec.count || 0; + + if (processSpec.count >= maxTimeouts) { + that.events.onExecExit.fire(false, processSpec); + return; + } + + processSpec.count++; + setTimeout(function () { + gpii.test.acceptance.exec.exec(that, processSpec, expected); + }, timeout); + } + }); +}; + +fluid.defaults("gpii.test.acceptance.testCaseHolder", { + gradeNames: ["gpii.test.testCaseHolder"], + components: { + exec: { + type: "gpii.test.acceptance.exec" + } + } +}); diff --git a/gpii/node_modules/testing/src/BrowserIncludes.js b/gpii/node_modules/testing/src/BrowserIncludes.js new file mode 100644 index 000000000..c39af3928 --- /dev/null +++ b/gpii/node_modules/testing/src/BrowserIncludes.js @@ -0,0 +1,26 @@ +/** +GPII Testing Browser Includes + +Copyright 2014 Lucendo Development Ltd. + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +/* global fluid */ + +"use strict"; + +// TODO: Get rid of this if we can. + +// Supply this grade a dummy so that browsers may safely include Testing.js + +fluid.defaults("kettle.test.testCaseHolder", { + gradeNames: ["fluid.test.testCaseHolder"] +}); diff --git a/gpii/node_modules/testing/src/CloudBased.js b/gpii/node_modules/testing/src/CloudBased.js new file mode 100644 index 000000000..384375ce1 --- /dev/null +++ b/gpii/node_modules/testing/src/CloudBased.js @@ -0,0 +1,45 @@ +/*! +GPII Cloud-based Flow Manager Test Infrastructure + +Copyright 2012 OCAD University +Copyright 2012 Antranig Basman +Copyright 2013 Raising the Floor +Copyright 2014 Lucendo Development Ltd. +Copyright 2018 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.test.cloudBased"); + +gpii.test.cloudBased.computeDevice = function (solutionIds, OSid) { + var device = { + OS: { + id: OSid || "web" + }, + solutions: [] + }; + fluid.each(solutionIds, function (solutionId) { + device.solutions.push({ id: solutionId }); + }); + return JSON.stringify(device); +}; + +gpii.test.cloudBased.gpiiConfig = function () { + return { + configName: "gpii.config.cloudBased.development", + configPath: "%gpii-universal/gpii/configs/shared" + }; +}; diff --git a/gpii/node_modules/testing/src/CloudBasedOAuth2TestsUtils.js b/gpii/node_modules/testing/src/CloudBasedOAuth2TestsUtils.js new file mode 100644 index 000000000..b315dfc6c --- /dev/null +++ b/gpii/node_modules/testing/src/CloudBasedOAuth2TestsUtils.js @@ -0,0 +1,184 @@ +/*! +GPII Cloud-based Flow Manager with OAuth2 Test Infrastructure + +Copyright 2012 OCAD University +Copyright 2012 Antranig Basman +Copyright 2013 Raising the Floor +Copyright 2014 Lucendo Development Ltd. +Copyright 2015-2018 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/gpii/universal/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + querystring = require("querystring"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.registerNamespace("jqUnit"); + +fluid.registerNamespace("gpii.test.cloudBased.oauth2"); + +// Cache for storing access tokens +gpii.test.cloudBased.oauth2.accessTokenCache = []; +gpii.test.cloudBased.oauth2.addAccessTokenToCache = function (aToken) { + gpii.test.cloudBased.oauth2.accessTokenCache.push(aToken); +}; +gpii.test.cloudBased.oauth2.clearAccessTokenCache = function () { + gpii.test.cloudBased.oauth2.accessTokenCache.length = 0; +}; + +gpii.test.stringifyFormBody = function (formBody) { + var encoded = fluid.transform(formBody, function (element) { + return typeof(element) === "string" ? element : JSON.stringify(element); + }); + return querystring.stringify(encoded); +}; + +gpii.test.verifyJSONResponse = function (body, request) { + var response = request.nativeResponse; + jqUnit.assertEquals("Should have received a 200 response", 200, response.statusCode); + jqUnit.assertTrue("Should have received an JSON response type", response.headers["content-type"].indexOf("application/json") === 0); + return JSON.parse(body); +}; + +gpii.test.verifyStatusCodeResponse = function (body, request, statusCode) { + var response = request.nativeResponse; + jqUnit.assertEquals("Should have received a " + statusCode + " response", statusCode, response.statusCode); +}; + +gpii.test.cloudBased.oauth2.sendRequest = function (request, options, formBody, filterName) { + formBody = gpii.test.cloudBased.oauth2.filter(formBody, options, filterName); + request.send(gpii.test.stringifyFormBody(formBody)); +}; + +gpii.test.cloudBased.oauth2.verifyFieldInResponse = function (response, request, funcName, fieldName) { + var fieldValue = fluid.get(response, fieldName); + fluid.log(funcName + " Got " + fieldName + ": ", fieldValue); + jqUnit.assertValue("Should have received " + fieldName, fieldValue); + fluid.set(request, fieldName, fieldValue); +}; + +gpii.test.cloudBased.oauth2.sendResourceOwnerGpiiKeyAccessTokenRequest = function (accessTokenRequest, options) { + var formBody = { + grant_type: "password", + client_id: options.client_id, + client_secret: options.client_secret, + username: options.username, + password: options.password + }; + gpii.test.cloudBased.oauth2.sendRequest(accessTokenRequest, options, formBody, "accessTokenForm"); +}; + +gpii.test.cloudBased.oauth2.verifyResourceOwnerGpiiKeyAccessTokenInResponse = function (body, accessTokenRequest) { + var response = gpii.test.verifyJSONResponse(body, accessTokenRequest); + gpii.test.cloudBased.oauth2.verifyFieldInResponse(response, accessTokenRequest, "oauth2.verifyResourceOwnerGpiiKeyAccessTokenInResponse", "access_token"); + gpii.test.cloudBased.oauth2.verifyFieldInResponse(response, accessTokenRequest, "oauth2.verifyResourceOwnerGpiiKeyAccessTokenInResponse", "expiresIn"); + if (accessTokenRequest.access_token) { + gpii.test.cloudBased.oauth2.addAccessTokenToCache(accessTokenRequest.access_token); + fluid.log("Cached access token ", accessTokenRequest.access_token); + } + return response; +}; + +gpii.test.cloudBased.oauth2.sendRequestWithAccessToken = function (request, accessToken, data) { + var securedHeader = { + headers: { + Authorization: "Bearer " + accessToken + } + }; + + request.send(data ? data : null, securedHeader); +}; + +fluid.defaults("gpii.test.cloudBased.oauth2.testCaseHolder", { + gradeNames: ["kettle.test.testCaseHolder"], + selectedPreferences: { + "": true + }, + components: { + accessTokenRequest: { + type: "kettle.test.request.http", + options: { + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + path: "/access_token", + port: 8081, + method: "POST" + } + } + } +}); + +gpii.test.cloudBased.oauth2.buildTestFixtureCommon = function (testDef, commonRec, config, testCaseGrade) { + testDef = fluid.extend(true, {}, testDef, commonRec); + + testDef.sequence = fluid.makeArray(testDef.sequence); + testDef.gradeNames = fluid.makeArray(testDef.gradeNames); + testDef.gradeNames.push("gpii.test.cloudBased.oauth2.testCaseHolder"); + if (testCaseGrade) { + testDef.gradeNames.push(testCaseGrade); + } + testDef.config = config; + return testDef; +}; + +gpii.test.cloudBased.oauth2.buildDisruptedFixture = function (testDef, commonRec, disruption, config, testCaseGrade) { + var options = fluid.defaults(disruption.sequenceGrade); + testDef = gpii.test.cloudBased.oauth2.buildTestFixtureCommon(testDef, commonRec, config, testCaseGrade); + if (options.expect) { + testDef.expect = options.expect; + } + if (disruption.expected) { + testDef.expected = disruption.expected; + } + if (disruption.name) { + testDef.name += " - " + disruption.name; + } + testDef.gradeNames = testDef.gradeNames.concat(fluid.makeArray(options.testCaseGradeNames)); + + testDef.expectedStatusCode = disruption.expectedStatusCode || options.expectedStatusCode; + + var changes = fluid.makeArray(options.changes).concat(fluid.makeArray(disruption.changes)); + fluid.set(testDef, [options.recordName, "changes"], changes); + return testDef; +}; + +// Marker grade for "disruptions", whose unique variations are superimposed on a "common" test definition. +fluid.defaults("gpii.test.disruption", { + gradeNames: ["fluid.component"] +}); + +gpii.test.cloudBased.oauth2.filter = function (model, options, filterName) { + var changes = fluid.get(options, [filterName, "changes"]); + fluid.log("cloudBased.oauth2.filter fetched changes " + JSON.stringify(changes, null, 2) + " for filter name " + filterName); + return changes ? gpii.test.cloudBased.oauth2.filterModel(model, changes) : model; +}; + +gpii.test.cloudBased.oauth2.filterModel = function (model, changes) { + var holder = { + model: fluid.copy(model) + }; + var applier = fluid.makeHolderChangeApplier(holder); + fluid.fireChanges(applier, changes); + fluid.log("cloudBased.oauth2.filterModel returning filtered model ", JSON.stringify(holder.model, null, 2)); + return holder.model; +}; + +gpii.test.cloudBased.oauth2.runDisruptedTests = function (testDefsToDisrupt, config, testEnvironment, testCaseGrade) { + fluid.each(testDefsToDisrupt, function (testDefToDisrupt) { + fluid.each(testDefToDisrupt.disruptions, function (disruption) { + var disruptedTestDef = gpii.test.cloudBased.oauth2.buildDisruptedFixture(testDefToDisrupt.testDef, {}, disruption, config, testCaseGrade); + + gpii.test.runTestDefs([disruptedTestDef], function (testDef) { + var gradedTestDef = gpii.test.testDefToEnvironment(testDef, testEnvironment, disruption.sequenceGrade); + return gradedTestDef; + }); + }); + }); +}; diff --git a/gpii/node_modules/testing/src/Fixtures.js b/gpii/node_modules/testing/src/Fixtures.js new file mode 100644 index 000000000..864c71718 --- /dev/null +++ b/gpii/node_modules/testing/src/Fixtures.js @@ -0,0 +1,363 @@ +/* + + Common test fixtures (sequence elements, sequence grades, case holders, and test environments). + + Copyright 2019 Raising the Floor International + + Licensed under the New BSD license. You may not use this file except in + compliance with this License. + + You may obtain a copy of the License at + https://github.com/gpii/universal/LICENSE.txt + +*/ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +require("gpii-couchdb-test-harness"); +gpii.test.couchdb.loadTestingSupport(); + +fluid.defaults("gpii.test.startServerSequence", { + gradeNames: "fluid.test.sequenceElement", + sequence: [ + { // This sequence point is required because of a QUnit bug - it defers the start of sequence by 13ms "to avoid any current callbacks" in its words + func: "{testEnvironment}.events.constructServer.fire" + }, + { + event: "{testEnvironment}.events.onServerReady", + listener: "fluid.identity" + } + ] +}); + +fluid.defaults("gpii.test.stopServerSequence", { + gradeNames: "fluid.test.sequenceElement", + sequence: [ + { + func: "{testCaseHolder}.configuration.server.stop" + }, + { + event: "{testCaseHolder}.configuration.server.events.onStopped", + listener: "fluid.identity" + } + ] +}); + +fluid.defaults("gpii.test.standardServerSequenceGrade", { + gradeNames: ["fluid.test.sequence"], + sequenceElements: { + startServer: { + gradeNames: "gpii.test.startServerSequence", + priority: "before:sequence" + }, + stopServer: { + gradeNames: "gpii.test.stopServerSequence", + priority: "after:sequence" + } + } +}); + +fluid.defaults("gpii.test.testCaseHolder", { + gradeNames: ["fluid.test.testCaseHolder"], + secret: "GPII tests secret", + members: { + // The "orig" member of this area will be written by gpii.test.snapshotSettings, and read back by gpii.test.checkRestoredConfiguration + settingsStore: {} + }, + mergePolicy: { + "settingsHandlers": "noexpand", + "initialState": "noexpand" + }, + events: { + onSnapshotComplete: null, + onInitialStateSet: null, + onInitialStateConfirmed: null, + onCheckConfigurationComplete: null, + onCheckRestoredConfigurationComplete: null, + onCheckRestoredInitialStateComplete: null + }, + distributeOptions: { + "testCaseHolder.variableResolver": { + record: { + funcName: "gpii.test.common.receiveVariableResolver", + args: ["{testCaseHolder}", "{arguments}.0"] + }, + target: "{that lifecycleManager variableResolver}.options.listeners.onCreate" + }, + "testCaseHolder.port": { + record: "{configuration}.options.mainServerPort", + target: "{that kettle.test.request}.options.port" + }, + "cookieJar.secret": { + source: "{that}.options.secret", + target: "{that > cookieJar}.options.secret" + }, + "server.secret": { + source: "{that}.options.secret", + target: "{that server}.options.secret" + } + }, + components: { + configuration: { + type: "gpii.test.configuration" + }, + cookieJar: { + type: "kettle.test.cookieJar" + }, + // The following configuration should be valid: + // variableResolver: "{tests}.configuration.server.flowManager.lifecycleManager.variableResolver" but clearly stresses + // ginger resolution in the framework too much. + // In fact, a proper solution would allow us to directly write: + // variableResolver: "{that lifecycleManager variableResolver}" without the listener hack above - this requires FLUID-5556 + loginRequest: { + type: "gpii.test.common.loginRequestComponent" + }, + logoutRequest: { + type: "gpii.test.common.logoutRequestComponent" + } + } +}); + +fluid.defaults("gpii.test.serverEnvironment", { + gradeNames: ["fluid.test.testEnvironment"], + events: { + onServerReady: null, + constructServer: null + }, + components: { + tests: { + type: "gpii.test.testCaseHolder" + } + } +}); + +// TODO: If we don't end up needing to modify this, consider collapsing it and simply using kettle.test.configuration directly. +fluid.defaults("gpii.test.configuration", { + gradeNames: ["fluid.component", "{testEnvironment}.options.configurationName"], + components: { + server: { + createOnEvent: "{testEnvironment}.events.constructServer", + options: { + gradeNames: "kettle.test.server", + listeners: { + onListen: "{testEnvironment}.events.onServerReady" + } + } + } + } +}); + +fluid.defaults("gpii.test.startCouchSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [{ + task: "{harness}.startup", + resolve: "fluid.log", + resolveArgs: ["Harness startup successful."] + }] +}); + +fluid.defaults("gpii.test.stopCouchSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [{ + task: "{harness}.shutdown", + resolve: "fluid.log", + resolveArgs: ["Harness shutdown successful."] + }] +}); + +fluid.defaults("gpii.test.couchSequenceGrade", { + gradeNames: ["gpii.test.standardServerSequenceGrade"], + sequenceElements: { + startCouch: { + gradeNames: "gpii.test.startCouchSequence", + priority: "after:startServer" + }, + stopCouch: { + gradeNames: "gpii.test.stopCouchSequence", + priority: "before:stopServer" + } + } +}); + +// The CouchDB grade used for running tests. It cleans up and reloads data in between each test run. +// A more persistent CouchDB data loader for manually testing GPII is at +// gpii/configs/shared/gpii.config.couch.development.base. +fluid.defaults("gpii.test.couchEnvironment", { + gradeNames: ["gpii.test.couchdb.environment.base", "gpii.test.serverEnvironment"], + databases: { + gpii: { + data: [ + "%gpii-universal/tests/data/dbData/clientCredentials.json", + "%gpii-universal/tests/data/dbData/gpiiAppInstallationClients.json", + "%gpii-universal/tests/data/dbData/gpiiKeys.json", + "%gpii-universal/build/tests/dbData/gpiiKeys.json", + "%gpii-universal/build/tests/dbData/prefsSafes.json", + "%gpii-universal/build/dbData/snapset/gpiiKeys.json", + "%gpii-universal/build/dbData/snapset/prefsSafes.json", + "%gpii-universal/testData/dbData/views.json" + ] + } + } +}); + +fluid.defaults("gpii.test.assessComponentLeaksSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { func: "gpii.test.assessComponentLeaks", + args: "{testEnvironment}" + } + ] +}); + +fluid.defaults("gpii.test.initialStateSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { func: "fluid.log", args: ["Executing initial state sequence."]}, + { + func: "gpii.test.expandSettings", + args: [ "{tests}", "initialState" ] + }, + { + func: "gpii.test.setInitialSettingsState", + args: [ "{tests}.initialState", "{nameResolver}", "{testCaseHolder}.events.onInitialStateSet.fire"] + }, + { + event: "{testCaseHolder}.events.onInitialStateSet", + listener: "fluid.identity" + }, + { + func: "gpii.test.checkConfiguration", + args: ["{tests}.initialState", "{nameResolver}", "{testCaseHolder}.events.onInitialStateConfirmed.fire", "Confirming initial state"] + }, + { + event: "{testCaseHolder}.events.onInitialStateConfirmed", + listener: "fluid.identity" + } + ] +}); + +fluid.defaults("gpii.test.snapshotSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { func: "fluid.log", args: ["Executing snapshot sequence."]}, + { + func: "gpii.test.expandSettings", + args: [ "{tests}", "settingsHandlers" ] + }, + { + func: "gpii.test.snapshotSettings", + args: ["{tests}.settingsHandlers", "{tests}.settingsStore", "{nameResolver}", "{testCaseHolder}.events.onSnapshotComplete.fire"] + }, + { + event: "{testCaseHolder}.events.onSnapshotComplete", + listener: "fluid.identity" + } + ] +}); + +fluid.defaults("gpii.test.loginSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { func: "fluid.log", args: ["Executing login sequence."]}, + { + func: "{loginRequest}.send" + }, + { + event: "{loginRequest}.events.onComplete", + listener: "gpii.test.loginRequestListen" + }, + { + func: "gpii.test.checkConfiguration", + args: ["{tests}.settingsHandlers", "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, + { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + } + ] +}); + +fluid.defaults("gpii.test.logoutSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { func: "fluid.log", args: ["Executing logout sequence."]}, + { + func: "{logoutRequest}.send" + }, + { + event: "{logoutRequest}.events.onComplete", + listener: "gpii.test.logoutRequestListen" + } + ] +}); + +fluid.defaults("gpii.test.checkSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { func: "fluid.log", args: ["Executing check sequence."]}, + { + func: "gpii.test.checkRestoredConfiguration", + args: ["{tests}.settingsHandlers", "{tests}.settingsStore", "{nameResolver}", "{testCaseHolder}.events.onCheckRestoredConfigurationComplete.fire"] + }, + { + event: "{testCaseHolder}.events.onCheckRestoredConfigurationComplete", + listener: "fluid.identity" + }, + { // anything in the initial state should also be restored upon logout + func: "gpii.test.checkRestoredInitialState", + args: [ "{tests}.initialState", "{nameResolver}", "{testCaseHolder}.events.onCheckRestoredInitialStateComplete.fire"] + }, + { + event: "{testCaseHolder}.events.onCheckRestoredInitialStateComplete", + listener: "fluid.identity" + } + ] +}); + +/* + + A sequence grade that executes a test login once the server and database are available, and that logs out once the + tests are complete. + + */ +fluid.defaults("gpii.test.snapshotSequenceGrade", { + gradeNames: ["gpii.test.couchSequenceGrade"], + sequenceElements: { + assessComponentLeaks: { + gradeNames: "gpii.test.assessComponentLeaksSequence", + priority: "first" + }, + snapshot: { + gradeNames: "gpii.test.snapshotSequence", + priority: "after:startServer" + }, + login: { + gradeNames: "gpii.test.loginSequence", + priority: "after:snapshot" + }, + logout: { + gradeNames: "gpii.test.logoutSequence", + priority: "before:stopServer" + }, + check: { + gradeNames: "gpii.test.checkSequence", + priority: "after:logout" + } + } +}); + +/* + + A sequence grade that logs in, as above, but also checks the initial state before running the tests. + + */ +fluid.defaults("gpii.test.initialStateSequenceGrade", { + gradeNames: ["gpii.test.snapshotSequenceGrade"], + sequenceElements: { + initialStateSequence: { + gradeNames: "gpii.test.initialStateSequence", + priority: "before:snapshot" + } + } +}); diff --git a/gpii/node_modules/testing/src/Integration.js b/gpii/node_modules/testing/src/Integration.js new file mode 100644 index 000000000..ff39b1686 --- /dev/null +++ b/gpii/node_modules/testing/src/Integration.js @@ -0,0 +1,36 @@ +/** +GPII Integration Testing + +Copyright 2013 Raising the Floor International +Copyright 2013 OCAD University +Copyright 2014 Lucendo Development Ltd. + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = fluid || require("infusion"); + +fluid.defaults("gpii.test.integration.testCaseHolder", { + gradeNames: ["gpii.test.testCaseHolder"] +}); + +fluid.defaults("gpii.test.integration.testCaseHolder.windows", { + gradeNames: ["gpii.test.integration.testCaseHolder", "gpii.test.integration.mockHolder.windows"] +}); + +fluid.defaults("gpii.test.integration.testCaseHolder.linux", { + gradeNames: ["gpii.test.integration.testCaseHolder", "gpii.test.integration.mockHolder.linux"] +}); + +fluid.defaults("gpii.test.integration.testCaseHolder.android", { + gradeNames: ["gpii.test.integration.testCaseHolder", "gpii.test.integration.mockHolder.android"] +}); diff --git a/gpii/node_modules/testing/src/Mocks.js b/gpii/node_modules/testing/src/Mocks.js new file mode 100644 index 000000000..b8d38f750 --- /dev/null +++ b/gpii/node_modules/testing/src/Mocks.js @@ -0,0 +1,674 @@ +/** +GPII Mock Definitions for all platform-specific components + +Copyright 2017 Raising the Floor - International +Copyright 2013 OCAD University +Copyright 2014 Lucendo Development Ltd. + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = fluid || require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.defaults("gpii.test.common.exec", { + gradeNames: ["fluid.component"], + events: { + onExecExit: null + }, + invokers: { +// exec: + } +}); + +// Maintains a mock model of currently executing processes +fluid.defaults("gpii.test.integration.exec", { + gradeNames: ["gpii.test.common.exec"], + members: { + processModel: [] // list of process objects + }, + invokers: { + exec: { + funcName: "gpii.test.integration.exec.exec", + args: ["{that}", "{arguments}.0", "{arguments}.1"] + } + } +}); + +gpii.test.integration.exec.exec = function (that, processSpec /*, expected */) { + // TODO: actually determine that the required process was started using the mock model + that.events.onExecExit.fire(true, processSpec); +}; + +fluid.defaults("gpii.test.integration.mockHolder", { + gradeNames: "fluid.component", + // TODO: Namespace these distributions so they can be overridden + distributeOptions: [{ + record: { + funcName: null, + func: "{gpii.test.integration.mockHolder}.nameResolver.resolveName" + }, + target: "{that lifecycleManager > nameResolver}.options.invokers.resolveName" + }, { + source: "{that}.options.deviceReporters", + target: "{that gpii.test.integration.mockDeviceReporters}.options.deviceReporters" + }, { + record: { // Note, contrary to the previous comment at this site, this distribution is essential + funcName: null, + func: "{gpii.test.integration.mockDeviceReporters}.resolveName" + }, + target: "{that deviceReporter > nameResolver}.options.invokers.resolveName" + }, { + record: { + funcName: null, + func: "{gpii.test.integration.mockHolder}.mockPlatformReporter.reportPlatform" + }, + target: "{that deviceReporter platformReporter}.options.invokers.reportPlatform" + }, { + target: "{that lifecycleManager > variableResolver}.options.components.resolverConfig.type", + record: "gpii.test.integration.standardResolverConfig" + }], + components: { + exec: { + type: "gpii.test.integration.exec" + }, + mockSettingsHandlers: { + type: "gpii.test.integration.mockSettingsHandlerRegistry.universal" + }, + mockActionHandlers: { + type: "gpii.test.integration.mockActionHandlers" + }, + mockDeviceReporters: { + type: "gpii.test.integration.mockDeviceReporters" + }, + nameResolver: { + type: "fluid.component", + options: { + invokers: { + resolveName: { + funcName: "gpii.test.integration.resolveName", + args: ["{mockSettingsHandlers}", "{mockDeviceReporters}", "{mockActionHandlers}", "{arguments}.0", "{arguments}.1"] + } + } + } + } + } +}); + +fluid.defaults("gpii.test.integration.standardResolverConfig", { + gradeNames: "fluid.component", + resolvers: { + environment: "gpii.test.integration.environmentResolver" + } +}); + + +// Return the environment variable name unmodified +gpii.test.integration.environmentResolver = fluid.identity; + +fluid.defaults("gpii.test.integration.mockHolder.windows", { + gradeNames: ["gpii.test.integration.mockHolder"], + components: { + mockSettingsHandlers: { + type: "gpii.test.integration.mockSettingsHandlerRegistry.windows" + }, + mockPlatformReporter: { + type: "gpii.test.integration.mockPlatformReporter", + options: { + platform: { + id: "win32", + version: "6.1.7601" + } + } + }, + mockActionsHandlers: { + type: "gpii.test.integration.mockActionHandlers.windows" + }, + mockDeviceReporters: { + type: "gpii.test.integration.mockDeviceReporters.windows" + } + } +}); + + +fluid.defaults("gpii.test.integration.mockHolder.linux", { + gradeNames: ["gpii.test.integration.mockHolder"], + components: { + mockSettingsHandlers: { + type: "gpii.test.integration.mockSettingsHandlerRegistry.linux" + }, + mockPlatformReporter: { + type: "gpii.test.integration.mockPlatformReporter", + options: { + platform: { + id: "linux", + version: "2.6.26" + } + } + }, + mockActionsHandlers: { + type: "gpii.test.integration.mockActionHandlers" + }, + mockDeviceReporters: { + type: "gpii.test.integration.mockDeviceReporters.linux" + } + } +}); + +fluid.defaults("gpii.test.integration.mockHolder.android", { + gradeNames: ["gpii.test.integration.mockHolder"], + components: { + mockSettingsHandlers: { + type: "gpii.test.integration.mockSettingsHandlerRegistry.android" + }, + mockPlatformReporter: { + type: "gpii.test.integration.mockPlatformReporter", + options: { + platform: { + id: "android", + version: "3.0.31" + } + } + } + } +}); + +fluid.defaults("gpii.test.integration.mockPlatformReporter", { + gradeNames: ["fluid.component"], + invokers: { + reportPlatform: { + funcName: "fluid.identity", + args: "{that}.options.platform" + } + } +}); + + +gpii.test.integration.mockExecHandler = function (command, args) { + // TODO: we should record the executing process in a process model - ideally we should have a more reliable model of extracting what it is + fluid.log("Mock exec handler for command ", command, " args ", args); +}; + +fluid.defaults("gpii.test.integration.mockExecHandler", { + gradeNames: "fluid.function", + argumentMap: { + command: 0, + args: 1 + } +}); + +gpii.test.integration.resolveName = function (settingsHandlerRegistry, mockDeviceReporters, mockActionHandlers, name, category) { + if (category === "settingsHandler") { + return settingsHandlerRegistry.resolveName(name); + } else if (category === "deviceReporter") { + return mockDeviceReporters.resolveName(name); + } else if (category === "action") { + return mockActionHandlers.resolveName(name); + } else { + return "gpii.test.integration.mockExecHandler"; + } +}; + +// cf. much less sophisticated mock in LifecycleManagerTest.js +fluid.defaults("gpii.test.integration.mockSettingsHandler", { + gradeNames: ["fluid.component"], + members: { + settingsStore: {} + }, + async: false, + // must be configured for each settingsHandler instance to resolve to a path within + // the options structure which determines a unique key for the particular payload + // can also resolve to a function which returns such a key given the options + optionsPathKey: null, + invokers: { + set: { + funcName: "gpii.test.integration.mockSettingsHandler.set", + args: ["{that}", "{arguments}.0"] + }, + setImpl: { + funcName: "gpii.test.integration.mockSettingsHandler.setImpl", + args: ["{that}", "{arguments}.0", "{arguments}.1"] + // set payload, solutionId + }, + get: { + funcName: "gpii.test.integration.mockSettingsHandler.get", + args: ["{that}", "{arguments}.0"] + }, + getImpl: { + funcName: "gpii.test.integration.mockSettingsHandler.getImpl", + args: ["{that}", "{arguments}.0", "{arguments}.1"] + // get payload, solutionId + } + } +}); + +gpii.test.integration.resolveHandlerKey = function (that, payload, solutionId) { + var pathKey = that.options.optionsPathKey; + if (typeof(pathKey) === "string") { + return fluid.get(payload.options, pathKey); + } else if (typeof(pathKey) === "function") { + return pathKey(payload.options, solutionId); + } +}; + +gpii.test.integration.mockSettingsHandler.applySettings = function (store, settings) { + fluid.each(settings, function (value, key) { + if (value === undefined) { + delete store[key]; + } else { + store[key] = value; + } + }); +}; + +gpii.test.integration.mockSettingsHandler.makeSetReturn = function (oldStore, settings) { + return fluid.transform(settings, function (value, key) { + return { + oldValue: oldStore[key], + value: value + }; + }); +}; + +gpii.test.integration.mockSettingsHandler.returnAsyncResponse = function (value, isAsync) { + if (isAsync) { + var togo = fluid.promise(); + gpii.invokeLater(function () { + togo.resolve(value); + }, 100); + return togo; + } else { + return value; + } +}; + +gpii.test.integration.mockSettingsHandler.setImpl = function (that, payload, solutionId) { + var setSettings = function () { + var key = gpii.test.integration.resolveHandlerKey(that, payload, solutionId); + var oldStore = fluid.copy(that.settingsStore[key]) || {}; + var store = (that.settingsStore[key] = that.settingsStore[key] || {}); + gpii.test.integration.mockSettingsHandler.applySettings(store, payload.settings); + + return gpii.test.integration.mockSettingsHandler.makeSetReturn(oldStore, payload.settings); + }; + var togo = gpii.test.integration.mockSettingsHandler.returnAsyncResponse(setSettings(), that.options.async); + return togo; +}; + +gpii.test.integration.mockSettingsHandler.set = function (that, payload) { + return gpii.settingsHandlers.invokeSettingsHandler(that.setImpl, payload); +}; + +gpii.test.integration.mockSettingsHandler.getImpl = function (that, payload, solutionId) { + var key = gpii.test.integration.resolveHandlerKey(that, payload, solutionId); + var store = that.settingsStore[key] || {}; + var response = fluid.transform(payload.settings, function (value, key) { + return store[key]; + }); + var togo = gpii.test.integration.mockSettingsHandler.returnAsyncResponse(response, that.options.asyncGet); + return togo; +}; + +gpii.test.integration.mockSettingsHandler.get = function (that, payload) { + return gpii.settingsHandlers.invokeSettingsHandler(that.getImpl, payload); +}; + +// The platform-specific registry of mock settings handlers + +gpii.test.integration.registrySettingsHandlerKey = function (options) { + return options.hKey + "|" + options.path; +}; + +gpii.test.integration.JAWSSettingsHandlerKey = function (options) { + return options.filename + "|" + options.foldername; +}; + +gpii.test.integration.flexibleHandlerKey = function (options, solutionId) { + return solutionId + ".flexibleHandler"; +}; + +gpii.test.integration.gsettingsLaunchKey = function (options) { + return options.schema + "|" + options.key; +}; + +fluid.defaults("gpii.test.integration.mockSettingsHandlerRegistry", { + gradeNames: ["fluid.component"], + rootPath: "gpii.test.integration.mockSettingsHandlers", // the global names for mocks will be deposited in here + members: { + settingsHandlers: {} + }, + invokers: { + resolveName: { + funcName: "gpii.test.integration.mockSettingsHandlerRegistry.resolveName", + args: ["{that}", "{arguments}.0"] + } + }, + listeners: { + "onCreate.populate": "gpii.test.integration.mockSettingsHandlerRegistry.populate", + "onDestroy.depopulate": "gpii.test.integration.mockSettingsHandlerRegistry.depopulate" + } +}); + +gpii.test.integration.mockSettingsHandlers = {}; + +gpii.test.integration.mockSettingsHandlerRegistry.populateOne = function (that, mock, key) { + that.settingsHandlers[key] = mock; + // Mount the mock handler at a global path + var setPath = that.options.rootPath + "." + key + ".set"; + fluid.setGlobalValue(setPath, mock.set); + fluid.setGlobalValue(that.options.rootPath + "." + key + ".get", mock.get); +}; + +gpii.test.integration.mockSettingsHandlerRegistry.depopulate = function (that) { + fluid.each(that.settingsHandlers, function (settingsHandler) { + if (fluid.isComponent(settingsHandler)) { + settingsHandler.destroy(); + } + }); + fluid.setGlobalValue(that.options.rootPath, {}); +}; + +gpii.test.integration.mockSettingsHandlerRegistry.populate = function (that) { + fluid.each(that.options.settingsHandlers, function (options, key) { + var mock = options.noMock ? + fluid.getGlobalValue(key) : gpii.test.integration.mockSettingsHandler(options); + gpii.test.integration.mockSettingsHandlerRegistry.populateOne(that, mock, key); + }); +}; + +gpii.test.integration.mockSettingsHandlerRegistry.resolveName = function (that, name) { + var mock = that.settingsHandlers[name]; + if (!mock) { + var registered = fluid.keys(that.settingsHandlers).join(", "); + fluid.fail("Error in settings handler mock configuration - handler " + name + + " is not registered - registered handlers are " + registered); + } else { + return that.options.rootPath + "." + name; + } +}; + +fluid.defaults("gpii.test.integration.mockSettingsHandlerRegistry.universal", { + gradeNames: ["gpii.test.integration.mockSettingsHandlerRegistry"], + settingsHandlers: { + "gpii.settingsHandlers.INISettingsHandler": { + optionsPathKey: "filename" + }, + "gpii.settingsHandlers.JAWSSettingsHandler": { + optionsPathKey: gpii.test.integration.JAWSSettingsHandlerKey + }, + "gpii.settingsHandlers.XMLHandler": { + optionsPathKey: "filename" + }, + "gpii.settingsHandlers.JSONSettingsHandler": { + optionsPathKey: "filename" + }, + "gpii.launchHandlers.flexibleHandler": { + optionsPathKey: gpii.test.integration.flexibleHandlerKey + }, + "gpii.settingsHandlers.webSockets" : {}, + "gpii.settingsHandlers.noSettings": {}, + "gpii.settingsHandlers.exploding": { + noMock: true + } + } +}); + +fluid.defaults("gpii.test.integration.mockSettingsHandlerRegistry.windows", { + gradeNames: ["gpii.test.integration.mockSettingsHandlerRegistry.universal"], + settingsHandlers: { + "gpii.windows.registrySettingsHandler": { + optionsPathKey: gpii.test.integration.registrySettingsHandlerKey + }, + "gpii.windows.spiSettingsHandler": { + // delaying: true, + optionsPathKey: "setAction" + }, + "gpii.windows.displaySettingsHandler": {}, + "gpii.windows.enableRegisteredAT": { + optionsPathKey: "registryName" + }, + "gpii.windows.systemSettingsHandler": {}, + "gpii.windows.wmiSettingsHandler": {}, + "gpii.windows.nativeSettingsHandler": { + optionsPathKey: "functionName" + } + } +}); + +fluid.defaults("gpii.test.integration.mockSettingsHandlerRegistry.linux", { + gradeNames: ["gpii.test.integration.mockSettingsHandlerRegistry.universal"], + settingsHandlers: { + "gpii.gsettings": { + optionsPathKey: "schema" + }, + "gpii.orca": { + async: true, + asyncGet: true, + optionsPathKey: "user" + }, + "gpii.alsa": { + optionsPathKey: "NONE" + }, + "gpii.xrandr": { + optionsPathKey: "NONE" + }, + "gpii.gsettings.launch": { + optionsPathKey: gpii.test.integration.gsettingsLaunchKey + } + } +}); + +fluid.defaults("gpii.test.integration.mockSettingsHandlerRegistry.android", { + gradeNames: ["gpii.test.integration.mockSettingsHandlerRegistry.universal"], + settingsHandlers: { + "gpii.androidSettings": { + optionsPathKey: "settingType" + }, + "gpii.androidAudioManager.volume": { + optionsPathKey: "NONE" + }, + "gpii.androidPersistentConfiguration": { + optionsPathKey: "NONE" + } + } +}); + +fluid.defaults("gpii.test.integration.mockDeviceReporters", { + gradeNames: ["fluid.component"], + rootPath: "gpii.test.integration.mockDeviceReporterRegistry", + invokers: { + resolveName: { + funcName: "gpii.test.integration.mockDeviceReporters.resolveName", + args: ["{that}", "{arguments}.0"] + } + }, + listeners: { + "onCreate.populate": "gpii.test.integration.mockDeviceReporters.populate", + "onDestroy.depopulate": "gpii.test.integration.mockDeviceReporters.depopulate" + } +}); + +fluid.registerNamespace("gpii.test.integration.mockDeviceReporterImpls"); + +gpii.test.integration.mockDeviceReporterImpls.findNameInList = function (expectInstalled, name) { + return expectInstalled.some(function (el) { return el === name; }); +}; + +gpii.test.integration.mockDeviceReporterImpls.registryKeyExists = function (expectInstalled, hKey, path, subPath) { + return expectInstalled.some(function (el) { + return el.hKey === hKey && el.path === path && el.subPath === subPath; + }); +}; + +gpii.test.integration.mockDeviceReporterImpls.wmiSettingSupported = function (expectInstalled, namespace, query) { + return expectInstalled.some(function (el) { + return el.namespace === namespace && el.query === query; + }); +}; + +gpii.test.integration.mockDeviceReporters.populate = function (that) { + var backMocks = fluid.transform(that.options.mockDeviceReporters, function () { + return { + expectInstalled: [] + }; + }); + var devReporters = fluid.extend(backMocks, that.options.deviceReporters); + fluid.each(devReporters, function (options, key) { + var mock = that.options.mockDeviceReporters[key]; + if (!mock) { + var registered = fluid.keys(that.options.mockDeviceReporters).join(", "); + fluid.fail("Error in dynamic device reporter mock configuration - handler " + key + + " is not registered - registered handlers are " + registered); + } + + fluid.defaults(that.options.rootPath + "." + key, mock.defaults); + var mockFunc = fluid.getGlobalValue(mock.mockFunc); + + var mockedFunction = mockFunc.bind(undefined, options.expectInstalled); + fluid.setGlobalValue(that.options.rootPath + "." + key, mockedFunction); + }); +}; + +gpii.test.integration.mockDeviceReporters.depopulate = function (that) { + // mock deviceReporters are not components and so do not need destruction + fluid.setGlobalValue(that.options.rootPath, {}); +}; + +gpii.test.integration.mockDeviceReporters.resolveName = function (that, name) { + var resolvedName; + + if (name === "gpii.deviceReporter.alwaysInstalled") { + resolvedName = name; + } else { + resolvedName = that.options.rootPath + "." + name; + } + + return resolvedName; +}; + +fluid.defaults("gpii.test.integration.mockDeviceReporters.linux", { + gradeNames: ["gpii.test.integration.mockDeviceReporters"], + mockDeviceReporters: { + "gpii.packageKit.find": { + mockFunc: "gpii.test.integration.mockDeviceReporterImpls.findNameInList", + defaults: { + gradeNames: "fluid.function", + argumentMap: { + name: 0 + } + } + } + } +}); + +fluid.defaults("gpii.test.integration.mockDeviceReporters.windows", { + gradeNames: "gpii.test.integration.mockDeviceReporters", + mockDeviceReporters: { + "gpii.deviceReporter.registryKeyExists": { + mockFunc: "gpii.test.integration.mockDeviceReporterImpls.registryKeyExists", + defaults: { + gradeNames: "fluid.function", + argumentMap: { + hKey: 0, + path: 1, + subPath: 2 + } + } + }, + "gpii.deviceReporter.wmiSettingSupported": { + mockFunc: "gpii.test.integration.mockDeviceReporterImpls.wmiSettingSupported", + defaults: { + gradeNames: "fluid.function", + argumentMap: { + namespace: 0, + query: 1 + } + } + } + } +}); + +fluid.defaults("gpii.test.integration.mockActionHandlers", { + gradeNames: ["fluid.component"], + rootPath: "gpii.test.integration.mockActionsHandlerRegistry", + invokers: { + resolveName: { + funcName: "gpii.test.integration.mockActionHandlers.resolveName", + args: ["{that}", "{arguments}.0"] + } + }, + listeners: { + "onCreate.populate": "gpii.test.integration.mockActionHandlers.populate", + "onDestroy.depopulate": "gpii.test.integration.mockActionHandlers.depopulate" + } +}); + + +gpii.test.integration.mockActionHandlers.populate = function (that) { + fluid.each(that.options.mockActionHandlers, function (actionHandler, key) { + var mountName = that.options.rootPath + "." + key; + fluid.defaults(mountName, actionHandler.defaults); + + var mockedFunction = fluid.getGlobalValue(actionHandler.mockFunc); + if (!mockedFunction) { + fluid.fail("Error populating mock action handler for ", actionHandler, ": mock function " + actionHandler.mockFunc + + " could not be located"); + } + fluid.setGlobalValue(mountName, mockedFunction); + }); +}; + +gpii.test.integration.mockActionHandlers.depopulate = function (that) { + // mock actionHandlers are not components and so do not need destruction + fluid.setGlobalValue(that.options.rootPath, {}); +}; + +gpii.test.integration.mockActionHandlers.resolveName = function (that, name) { + var resolvedName = that.options.rootPath + "." + name; + return resolvedName; +}; + +fluid.defaults("gpii.test.integration.mockActionHandlers.windows", { + gradeNames: "gpii.test.integration.mockActionHandlers", + mockActionHandlers: { + "gpii.windows.spiSettingsHandler.setHighContrastTheme": { + mockFunc: "fluid.identity", + defaults: { + gradeNames: "fluid.function", + argumentMap: { + newTheme: 0, + currentTheme: 1, + saveAs: 2 + } + } + }, + "gpii.windows.spiSettingsHandler.applyCustomTheme": { + mockFunc: "fluid.identity", + defaults: { + gradeNames: "fluid.function", + argumentMap: { + themeFile: 0, + allThemes: 1 + } + } + }, + "gpii.windows.updateLanguage": { + mockFunc: "fluid.identity", + defaults: { + gradeNames: "fluid.function", + argumentMap: { + currentLanguage: 0 + } + } + } + } +}); diff --git a/gpii/node_modules/testing/src/NockUtils.js b/gpii/node_modules/testing/src/NockUtils.js new file mode 100644 index 000000000..4ab1e1b7f --- /dev/null +++ b/gpii/node_modules/testing/src/NockUtils.js @@ -0,0 +1,36 @@ +/*! +GPII add-on grade with Nock set up + +Copyright 2017 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/gpii/universal/LICENSE.txt +*/ + +// Nock (https://github.com/node-nock/nock) is a mocking library on node.js that can +// mock http server responses. This file creates a add-on component that integrates +// the setup and clean up of Nock into the component lifecycles. + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + nock = require("nock"); + +fluid.defaults("gpii.test.testWithNock", { + gradeNames: ["fluid.component"], + listeners: { + "onCreate.setUpNock": "{that}.setUpNock", + "onDestroy.cleanNock": "gpii.test.testWithNock.cleanNock" + }, + invokers: { + setUpNock: "fluid.notImplemented" // must be supplied by integrators + } +}); + +gpii.test.testWithNock.cleanNock = function () { + nock.cleanAll(); +}; diff --git a/gpii/node_modules/testing/src/RunTestDefs.js b/gpii/node_modules/testing/src/RunTestDefs.js new file mode 100644 index 000000000..d17e5f693 --- /dev/null +++ b/gpii/node_modules/testing/src/RunTestDefs.js @@ -0,0 +1,213 @@ +/* + + A collection of functions to expand "test defs" and run them as tests. Test definitions are used throughout this + package to reuse test steps between different environments, for example, running the same tests against a local + development environment or the cloud. See the documentation for `gpii.test.runTestDefs` for more details on + supported options. + + Copyright 2019 Raising the Floor International + + Licensed under the New BSD license. You may not use this file except in + compliance with this License. + + You may obtain a copy of the License at + https://github.com/gpii/universal/LICENSE.txt + +*/ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); +var kettle = require("kettle"); + +var path = require("path"); + +/** + * + * A function that wraps test definitions in a test environment, case holder, and runs them. Uses + * sequence elements instead of manipulating arrays. + * + * The test definitions handled by this function support the following elements: + * + * 1. {String} `name` : The name of this block of tests, which will be reported in test output. + * 2. {Integer} `expect`: The number of tests to expect to be run. + * 3. {Array} `sequence`: The Fluid IoC test sequences to be run. + * 4. {Object} `config`: The configuration options, generally for the server used in these tests. + * 5. {String} `configType`: A namespaced "type" for the configuration, typically based on parsing of configuration + * files using Kettle. + * + * @param {Object} testDefs - A map of test definitions (see above). + * @param {Function} testDefExpander - A function that will be used to expand the test definition. + * + */ +gpii.test.runTestDefs = function (testDefs, testDefExpander) { + var testEnvironments = fluid.transform(testDefs, testDefExpander); + fluid.test.runTests(testEnvironments); +}; + +gpii.test.runServerTestDefs = function (testDefs) { + gpii.test.runTestDefs(testDefs, gpii.test.testDefToServerEnvironment); +}; + +gpii.test.runCouchTestDefs = function (testDefs) { + gpii.test.runTestDefs(testDefs, gpii.test.testDefToCouchEnvironment); +}; + +gpii.test.runSnapshotTestDefs = function (testDefs) { + gpii.test.runTestDefs(testDefs, gpii.test.testDefToSnapshotEnvironment); +}; + +gpii.test.runSnapshotWithInitialStateTestDefs = function (testDefs) { + gpii.test.runTestDefs(testDefs, gpii.test.testDefToSnapshotWithInitialStateEnvironment); +}; + +gpii.test.testDefToEnvironment = function (testDef, environmentType, sequenceGrade) { + var configurationName = testDef.configType || kettle.config.createDefaults(testDef.config); + //var caseHolderOptions = fluid.filterKeys(testDef, ["modules", "sequence", "config"], true); + var caseHolderOptions = fluid.filterKeys(testDef, ["modules", "sequence"], true); + caseHolderOptions.modules = [{ + name: configurationName + " tests", + tests: [{ + name: testDef.name, + expect: testDef.expect, + sequence: testDef.sequence, + sequenceGrade: testDef.sequenceGrade || sequenceGrade + }] + }]; + + return { + type: environmentType, + options: { + configurationName: configurationName, + components: { + tests: { + options: caseHolderOptions + } + } + } + }; +}; + +gpii.test.testDefToServerEnvironment = function (testDef) { + return gpii.test.testDefToEnvironment(testDef, "gpii.test.serverEnvironment", "gpii.test.standardServerSequenceGrade"); +}; + +gpii.test.testDefToCouchEnvironment = function (testDef) { + return gpii.test.testDefToEnvironment(testDef, "gpii.test.couchEnvironment", "gpii.test.couchSequenceGrade"); +}; + +gpii.test.testDefToSnapshotEnvironment = function (testDef) { + return gpii.test.testDefToEnvironment(testDef, "gpii.test.couchEnvironment", "gpii.test.snapshotSequenceGrade"); +}; + +gpii.test.testDefToSnapshotWithInitialStateEnvironment = function (testDef) { + return gpii.test.testDefToEnvironment(testDef, "gpii.test.couchEnvironment", "gpii.test.initialStateSequenceGrade"); +}; + +/* Build a test fixture for integration/acceptance tests operating the stereotypical workflow - + * snapshot, login, expectConfigured, logout and expectRestored + */ +gpii.test.buildSingleTestFixture = function (testDef, rootGrades) { + testDef.gradeNames = fluid.makeArray(testDef.gradeNames).concat(fluid.makeArray(rootGrades)); + + testDef.sequenceGrade = testDef.initialState ? "gpii.test.initialStateSequenceGrade" : "gpii.test.snapshotSequenceGrade"; + + return testDef; +}; + +/** Build a fixture set suitable for sending to kettle.test.bootstrapServer that includes a non-flat + * array of sequences within the member `sequenceSegments` as well as a base record to be merged into each testDef + * @param {Array} fixtures - An array of proto-testDefs, each members `name` and `expect` with the same semantic + * as a standard testDef, and a member `sequenceSegments` whose elements may themselves be arrays of sequences. + * @param {Object} baseTestDef - A base test def record, holding members `configName`, `configPath` and other + * members appropriate for this options structure. + * @return {Object} - The "segmented" fixtures generated from the base fixtures and test def. + */ +gpii.test.buildSegmentedFixtures = function (fixtures, baseTestDef) { + return fluid.transform(fixtures, function (fixture) { + var overlay = { + name: fixture.name, + expect: fixture.expect, + sequence: fluid.flatten(fixture.sequenceSegments) + }; + return fluid.extend(true, {}, baseTestDef, overlay); + }); +}; + + +// Convert a record as returned by a "portable test" into a full testDefs structure as accepted by gpii.test.buildTests +// TODO better docs and explanation +gpii.test.recordToTestDefs = function (record) { + var testDefs = fluid.copy(fluid.getGlobalValue(record.testDefs)); + fluid.each(testDefs, function (testDef) { + testDef.config = { + configName: testDef.configName || record.configName, + configPath: testDef.configPath || record.configPath + }; + }); + return testDefs; +}; + +gpii.test.runTests = function (record, rootGrades, runFn) { + runFn = runFn || gpii.test.runCouchTestDefs; + var testDefs = gpii.test.recordToTestDefs(record); + var testDefs2 = gpii.test.buildTests(testDefs, rootGrades); + return runFn(testDefs2); +}; + +// Run from the base of every platform-specific acceptance/integration test fixture file as its per-file bootstrap. +// The long argument list is required in order to detect whether the file is being run as a top-level +// node executable - if it is, we default to running it as an integration test - otherwise, we just +// return its record. + +gpii.test.bootstrap = function (record, rootGrades, foreignModule, foreignRequire, runFn) { + if (foreignRequire.main === foreignModule) { // the file was executed directly from the command line - run integration tests immediately + return gpii.test.runTests(record, rootGrades, runFn); + } else { // otherwise just return its record for processing + return record; + } +}; + +gpii.test.buildTests = function (testDefs, rootGrades) { + return fluid.transform(fluid.copy(testDefs), function (testDef) { + return gpii.test.buildSingleTestFixture(testDef, rootGrades); + }); +}; + +/** Runs a suite of integration or acceptance tests as indexed by a platform-specific root + * such as "index-windows.js". + * @param {Array} files - The array of test fixture files, as returned by the index module. + * @param {String} baseDir - The base directory holding the index module. + * @param {Array} rootGrades - An array of grade names to be contributed to the component root + * (currently testCaseHolder) of the fixtures. This controls the variety of test which is executed - + * e.g. a mocked Windows environment integration test, or a full acceptance test (the default). + */ + +gpii.test.runSuite = function (files, baseDir, rootGrades) { + fluid.log("Running test suites:\n", JSON.stringify(files, null, 2)); + fluid.each(files, function (oneFile) { + var filePath = path.resolve(baseDir, "platform", oneFile); + var record = require(filePath); + gpii.test.runTests(record, rootGrades); + }); +}; + +// simple utility to select a set of test suites to run based on presence of substrings in their names +// these are ANDed together - i.e. they must each appear in a selected suite's name +gpii.test.filterSuites = function (files, specs) { + fluid.each(specs, function (spec) { + files = fluid.remove_if(fluid.copy(files), function (file) { + return file.indexOf(spec) === -1; + }); + }); + + return files; +}; + +gpii.test.filterSuitesByArgs = function (files, args) { + return args.length === 2 ? files : gpii.test.filterSuites(files, args.slice(2)); +}; + +gpii.test.runSuitesWithFiltering = function (files, baseDir, rootGrades) { + var filtered = gpii.test.filterSuitesByArgs(files, process.argv); + gpii.test.runSuite(filtered, baseDir, rootGrades); +}; diff --git a/gpii/node_modules/testing/src/TestUtils.js b/gpii/node_modules/testing/src/TestUtils.js new file mode 100644 index 000000000..70a940ecd --- /dev/null +++ b/gpii/node_modules/testing/src/TestUtils.js @@ -0,0 +1,311 @@ +/** +GPII Testing (full lifecycle of FlowManager) + +Copyright 2013-2016 Raising the Floor International +Copyright 2013 OCAD University +Copyright 2014 Lucendo Development Ltd. + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = fluid || require("infusion"), + jqUnit = jqUnit || fluid.require("node-jqunit", require, "jqUnit"), + kettle = fluid.registerNamespace("kettle"), + gpii = fluid.registerNamespace("gpii"); + +// TODO: Move all of this file into a more specific namespace - "test" is too generic + +// Given a promise-containing structure, return a promise yielding the structure with all promises replaced by their +// resolution, or else a rejection for the first to reject. The supplied structure will be modified. + +// TODO: This algorithm will go into FluidPromises.js in the end +gpii.test.settleStructure = function (structure) { + if (fluid.isPromise(structure)) { + return structure; + } + var settleRec = { + // This flag is an awkward consequence of our choice to allow synchronous promise resolution + inSync: true, // Are we still in synchronous scanning - necessary to avoid double resolution on hitting unresolved === 0 on initial scan + unresolved: 0, + depth: 0, + togo: fluid.promise(), + structure: structure, + resolve: function () { + settleRec.togo.resolve(structure); + } + }; + gpii.test.settleStructureRecurse(structure, settleRec); + settleRec.inSync = false; + if (settleRec.unresolved === 0) { // Case of 0 asynchronous promises found + settleRec.resolve(); + } + return settleRec.togo; +}; + +gpii.test.settleStructureRecurse = function (structure, settleRec) { + ++settleRec.depth; + if (settleRec.depth > fluid.strategyRecursionBailout) { + fluid.fail("Recursion exceeded for value " + JSON.stringify(structure) + " overall structure " + JSON.stringify(settleRec.structure, null, 2)); + } + if (fluid.isPlainObject(structure)) { + fluid.each(structure, function (value, key) { + if (fluid.isPromise(value)) { + gpii.test.settleStructurePush(settleRec, structure, value, key); + } else { + gpii.test.settleStructureRecurse(value, settleRec); + } + }); + } + --settleRec.depth; +}; + +gpii.test.settleStructurePush = function (settleRec, holder, promise, key) { + ++settleRec.unresolved; + promise.then(function (value) { + fluid.log("settleStructure received response " + fluid.prettyPrintJSON(value)); + holder[key] = value; + --settleRec.unresolved; + if (settleRec.unresolved === 0 && !settleRec.inSync) { + settleRec.resolve(); + } + }, function (err) { + var errObj = { message: err }; + settleRec.togo.reject(kettle.upgradeError(errObj, " while resolving promise with key " + key)); + }); +}; + +// END ALGORITHM gpii.test.settleStructure + +// TODO: Move this utility into KettleTestUtils.http.js + +/** Asserts that a successful response with a JSON payload body has been received, which will be tested using `jqUnit.assertLeftHand` + * In addition to the core fields listed KettleTestUtils.http.js HTTP response assertion functions, options contains: + * expected {Object} The expected response body as JSON, supplied as the `expected` argument to `jqUnit.assertLeftHand` + */ + +gpii.test.assertJSONResponseSubset = function (options) { + var data; + try { + data = kettle.JSON.parse(options.string); + } catch (e) { + throw kettle.upgradeError(e, "\nwhile parsing HTTP response as JSON"); + } + jqUnit.assertLeftHand(options.message, options.expected, data); + kettle.test.assertResponseStatusCode(options, 200); +}; + +/** Given a settingsHandler payload, return a promise which resolves to the current state of each + * settingsHandler block within it. Called from `gpii.test.snapshotSettings`, `gpii.test.checkConfiguration` and `gpii.test.checkRestoredConfiguration` + * @param {Object} settingsHandlers - A map of settings handler names to `settingsHandler` blocks as seen in the `settingsHandlers` + * option of a `gpii.test.testCaseHolder`. + * @param {NameResolver|Null} nameResolver - A `nameResolver` mapping global names onto alternatives for use in a testing context. + * @return {Promise} A promise resolving to the fetched settings (a fatal error handler will be registered on reject). + */ + +gpii.test.getSettings = function (settingsHandlers, nameResolver) { + return gpii.test.operateSettings(settingsHandlers, nameResolver, "get"); +}; + +/** As for `gpii.test.getSettings` but performing a `set` action for the settingsHandler payloads, and accepting an + * `onSuccess` callback + */ + +gpii.test.setSettings = function (settingsHandlers, nameResolver, onSuccess) { + var promise = gpii.test.operateSettings(settingsHandlers, nameResolver, "set"); + promise.then(onSuccess); + return promise; +}; + +/** Common utility forwarded to by `gpii.test.getSettings` and `gpii.test.setSettings`. + * See documentation for all parameters other than: + * @param {Array} settingsHandlers - An array of settings handlers. + * @param {Object} nameResolver - The name resolver. + * @param {String} method - Either `"get"` or `"set"` depending on whether the settingsHandler `get` or `set` action is to be invoked. + * @return {Promise} A promise that will be resolved with the result or rejected on error. + */ +gpii.test.operateSettings = function (settingsHandlers, nameResolver, method) { + var ret = {}; + fluid.each(settingsHandlers, function (handlerBlock, handlerID) { + var resolvedName = nameResolver ? nameResolver.resolveName(handlerID, "settingsHandler") : handlerID; + var response = gpii.settingsHandlers.dispatchSettingsHandler(resolvedName, handlerBlock, method); + ret[handlerID] = response; + }); + var togo = gpii.test.settleStructure(ret); + togo.then(null, function (err) { + fluid.fail("Error when operating settings handler: ", err); + }); + return togo; +}; + +/* + * Used for settings the initial settings of an application. This is among other used for tests + * where an application needs to be running on user login + */ +gpii.test.setInitialSettingsState = function (settingsHandlers, nameResolver, onComplete) { + var promise = gpii.test.setSettings(settingsHandlers, nameResolver); + promise.then(onComplete); +}; + +/** Snapshot the state of all settingsHandlers by stashing them in a member named `orig` on the supplied settingsStore + * @param {Object} settingsHandlers - A map of settings handler names to `settingsHandler` blocks as seen in the `settingsHandlers` + * option of a `gpii.test.testCaseHolder`. + * @param {Object} settingsStore - The `settingsStore` member of a `gpii.test.testCaseHolder`. This will have a snapshot of + * the state of the supplied settingsHandlers stored in a member named `orig`. + * @param {NameResolver|Null} nameResolver - A `nameResolver` mapping global names onto alternatives for use in a testing context. + * @param {Function} onComplete - A callback to be notified when the state of all the supplied settingsHandlers have been read. + */ +gpii.test.snapshotSettings = function (settingsHandlers, settingsStore, nameResolver, onComplete) { + var origPromise = gpii.test.getSettings(settingsHandlers, nameResolver); + origPromise.then(function (origSettings) { + // any settings already defined in settingsStore.org overwrites the ones read by snapshotter + settingsStore.orig = fluid.extend(true, origSettings, settingsStore.orig); + onComplete(); + }); +}; + +gpii.test.loginRequestListen = function (data) { + jqUnit.assertNotEquals("Successful login message returned " + data, -1, + data.indexOf("was successfully logged in.")); +}; + +gpii.test.extractSettingsBlocks = function (settingsHandlers) { + return fluid.transform(settingsHandlers, gpii.settingsHandlers.extractSettingsBlocks); +}; + +gpii.test.checkConfiguration = function (settingsHandlers, nameResolver, onComplete, customMsg) { + var configPromise = gpii.test.getSettings(settingsHandlers, nameResolver); + configPromise.then(function (config) { + var noOptions = gpii.test.extractSettingsBlocks(settingsHandlers); + jqUnit.assertDeepEq(customMsg || "Checking that settings are set", noOptions, config); + onComplete(); + }); +}; + +gpii.test.logoutRequestListen = function (data) { + jqUnit.assertNotEquals("Successful logout message returned " + data, -1, + data.indexOf("was successfully logged out.")); +}; + +gpii.test.checkRestoredConfiguration = function (settingsHandlers, settingsStore, nameResolver, onComplete) { + var currentSettingsPromise = gpii.test.getSettings(settingsHandlers, nameResolver); + currentSettingsPromise.then(function (currentSettings) { + jqUnit.assertDeepEq("Checking that settings are properly reset", settingsStore.orig, currentSettings); + onComplete(); + }); +}; + +gpii.test.checkRestoredInitialState = function (initialSettings, nameResolver, onComplete) { + if (initialSettings === undefined) { + jqUnit.assertTrue("No initial settings defined, so not checking", true); + onComplete(); + return; + } + var currentSettingsPromise = gpii.test.getSettings(initialSettings, nameResolver); + currentSettingsPromise.then(function (currentSettings) { + jqUnit.assertDeepEq("Checking that initial settings are also properly reset", gpii.test.extractSettingsBlocks(initialSettings), currentSettings); + onComplete(); + }); +}; + +gpii.test.common.receiveVariableResolver = function (testCaseHolder, variableResolver) { + testCaseHolder.variableResolver = variableResolver; +}; + +fluid.defaults("gpii.test.common.loginRequestComponent", { // named oddly to avoid name conflicts with component whose member name is `loginRequest` + gradeNames: "kettle.test.request.http", + path: "/user/%gpiiKey/login", + termMap: { + gpiiKey: "{testCaseHolder}.options.gpiiKey" + } +}); + +fluid.defaults("gpii.test.common.logoutRequestComponent", { + gradeNames: "kettle.test.request.http", + path: "/user/%gpiiKey/logout", + termMap: { + gpiiKey: "{testCaseHolder}.options.gpiiKey" + } +}); + +/** mixin grades to hoist out the lifecycleManager, to a location where it can be + * easily resolved by various test fixtures. This will become unnecessary after the FLUID-4892 rewrite of Infusion + */ +fluid.defaults("gpii.test.common.lifecycleManagerReceiver", { + distributeOptions: { + record: { + funcName: "gpii.test.common.receiveComponent", + args: ["{testCaseHolder}", "{arguments}.0", "lifecycleManager"] + }, + target: "{that lifecycleManager}.options.listeners.onCreate" + } +}); + +fluid.defaults("gpii.test.common.untrusted.lifecycleManagerReceiver", { + distributeOptions: { + record: { + funcName: "gpii.test.common.receiveComponent", + args: ["{testCaseHolder}", "{arguments}.0", "lifecycleManager"] + }, + target: "{that localConfig lifecycleManager}.options.listeners.onCreate" + } +}); + + +gpii.test.common.receiveComponent = function (testCaseHolder, component, name) { + if (!testCaseHolder[name]) { + fluid.globalInstantiator.recordKnownComponent(testCaseHolder, component, name, false); + } +}; + +gpii.test.leakageCheckGrades = ["gpii.test.integration.mockSettingsHandler"]; + +gpii.test.sequenceNumberForComponent = function (component) { + return Number(component.id.split("-").pop()); +}; + +// Check for any leaks of components of particular grades that might have escaped cleanup from a previous test cycle. +// This uses a strategy of inspecting the component ids of all components matching a particular grade, and asserting +// that the id sequence numbers must not be earlier than those of the current testEnvironment. This has been +// introduced to assure that, for example, cleanup is done as part of GPII-4039 +gpii.test.assessComponentLeaks = function (testEnv) { + var rootSequence = gpii.test.sequenceNumberForComponent(testEnv); + gpii.test.leakageCheckGrades.forEach(function (checkGrade) { + var checkComponents = fluid.queryIoCSelector(fluid.rootComponent, checkGrade); + checkComponents.forEach(function (checkComponent) { + var checkSequence = gpii.test.sequenceNumberForComponent(checkComponent); + jqUnit.assertTrue("Component of grade " + checkGrade + " should not predate test environment", + checkSequence > rootSequence); + }); + }); +}; + +/** Expand material in a `gpii.test.testCaseHolder` fixture's options using the variableResolver fished out of the real implementation, + * and place it at a public member for the assertions to use + * @param {Component} testCaseHolder - An instance of gpii.test.testCaseHolder component with some expandable material in its options. + * @param {String|Array} members - One or more paths in the options to be expanded. + * @return {Component} *side-effect* - expanded material is placed at top-level in the supplied `testCaseHolder` at the paths given in the `members` argument. + */ + +gpii.test.expandSettings = function (testCaseHolder, members) { + members = fluid.makeArray(members); + fluid.each(members, function (val) { + testCaseHolder[val] = testCaseHolder.variableResolver.resolve(testCaseHolder.options[val]); + }); +}; + +gpii.test.unshift = function (array, elements) { + array.unshift.apply(array, elements); +}; + +gpii.test.push = function (array, elements) { + array.push.apply(array, elements); +}; diff --git a/gpii/node_modules/transformer/README.md b/gpii/node_modules/transformer/README.md index ec2993707..dadfb8b6f 100644 --- a/gpii/node_modules/transformer/README.md +++ b/gpii/node_modules/transformer/README.md @@ -1,7 +1,6 @@ -GPII Personalization Framework: Settings Transformer -==================================================== +# GPII Personalization Framework: Settings Transformer The Settings Transformer takes: -1. a user preferences profile -2. a Solution-specific transformation map -and produces the appropriate settings for the Solution. + +1. a user preferences profile +2. a solution-specific transformation map and produces the appropriate settings for the solution diff --git a/gpii/node_modules/transformer/index.js b/gpii/node_modules/transformer/index.js index 749013e1e..7874d89aa 100644 --- a/gpii/node_modules/transformer/index.js +++ b/gpii/node_modules/transformer/index.js @@ -1,5 +1,5 @@ -var fluid = require("infusion"); +"use strict"; -var loader = fluid.getLoader(__dirname); - -loader.require("./src/js/Transformer.js"); \ No newline at end of file +fluid.require("%flowManager/src/SystemUtils.js"); +require("./src/js/Transformer.js"); +require("../matchMakerFramework/src/MatchMakerUtilities.js"); diff --git a/gpii/node_modules/transformer/package.json b/gpii/node_modules/transformer/package.json index b01f3dd92..efe7821ee 100644 --- a/gpii/node_modules/transformer/package.json +++ b/gpii/node_modules/transformer/package.json @@ -1,19 +1,13 @@ { - "name": "transformer", - "description": "The Transformer module in the GPII personalization framework that is responsible for transforming generic user preferences into application-specific settings.", - "version": "0.1", - "author": "GPII", - "bugs": "http://wiki.gpii.net/index.php/Main_Page", - "homepage": "http://gpii.net/", - "dependencies": {}, - "licenses": [ - { - "type": "BSD-3-Clause", - "url": "http://www.opensource.org/licenses/BSD-3-Clause" - } - ], - "keywords": ["gpii", "accessibility", "settings", "fluid", "IoC", "Inversion of Control", "configuration", "evented"], - "repository": "git://github.com/GPII/universal.git", - "main": "./index.js", - "engines": { "node" : ">=0.1.9" } -} \ No newline at end of file + "name": "transformer", + "description": "The Transformer module in the GPII personalization framework that is responsible for transforming generic user preferences into application-specific settings.", + "version": "0.3.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } +} diff --git a/gpii/node_modules/transformer/src/js/Transformer.js b/gpii/node_modules/transformer/src/js/Transformer.js index 893887353..ec4e0a80a 100644 --- a/gpii/node_modules/transformer/src/js/Transformer.js +++ b/gpii/node_modules/transformer/src/js/Transformer.js @@ -3,108 +3,291 @@ GPII Settings Transformer Copyright 2012 OCAD University Copyright 2012 Antranig Basman -Copyright 2013 Raising the Floor +Copyright 2013-2014 Raising the Floor Licensed under the New BSD license. You may not use this file except in compliance with this License. +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt +https://github.com/GPII/universal/blob/master/LICENSE.txt */ -/*global require*/ +"use strict"; var fluid = fluid || require("infusion"), $ = fluid.registerNamespace("jQuery"), gpii = fluid.registerNamespace("gpii"); -(function () { +fluid.registerNamespace("gpii.transformer"); - fluid.defaults("gpii.transformer", { - gradeNames: ["fluid.littleComponent", "autoInit"] - }); +(function () { + /** + * Helper function to compute settings paths of a settings object. + * + * Setting paths extracted from input settings argument include: + * 1. All individual keys in the settings object; + * 2. All concatenated settings paths of the top level setting with the 2nd level settings provided + * the 2nd level path doesn't start with "http://registry.gpii.net/"; + * 3. All returned settings paths must start with "http://registry.gpii.net/" to extract both common terms + * and application specific terms. + * + * For example, if the input settings is: + * { + * "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + * "invertColours": true + * }, + * "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + * "http://registry.gpii.net/common/invertColours": true + * } + * } + * The returned array will look like: + * [ + * "http://registry.gpii.net/applications/com.microsoft.windows.magnifier", + * "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.magnifier/invertColours", + * "http://registry.gpii.net/applications/com.microsoft.windows.magnifier", + * "http://registry.gpii.net/common/invertColours" + * ] + * + * @param {Object} settings - The demanded settings. Usually a value of + * matchMakerOutput.inferredConfiguration.(context).applications.(solutionId).settings + * @return {Boolean} - An array of settings paths. + */ + gpii.transformer.computeDemandedSettingsPaths = function (settings) { + var settingsPaths = []; - gpii.transformer.indexApplicationSettings = function (applications, appSpecific) { - return $.extend(true, appSpecific, applications); + fluid.each(settings, function (value, key) { + if (key.startsWith("http://registry.gpii.net/")) { + settingsPaths.push(fluid.pathUtil.composeSegments(key)); + } + if (fluid.isPlainObject(value, true)) { + fluid.each(value, function (subValue, subKey) { + if (subKey.startsWith("http://registry.gpii.net/")) { + settingsPaths.push(fluid.pathUtil.composeSegments(subKey)); + } else { + settingsPaths.push(fluid.pathUtil.composeSegments(key, subKey)); + } + }); + } + }); + return settingsPaths; }; - gpii.transformer.findAndIndexApplications = function (preferences, appSpecific) { - if (fluid.isPrimitive(preferences)) { - return; + /** + * Helper function to find out if settings paths extracted from a given settings has a match in: + * 1. The capabilities; + * 2. The capabilitiesTransformations for a settingsHandler block has inputPaths + * @param {Array} capabilities - The capabilities block for a settingsHandler. + * @param {Object} capabilitiesTransformations - The capabilitiesTransformations block for a settingsHandler. + * @param {Object} settings - The settings. + * Usually from matchMakerOutput.inferredConfiguration.(context).applications.(solutionId).settings. + * @return {Boolean} - True if an inputPath is found, otherwise, return false. + */ + gpii.transformer.hasSupportedSettings = function (capabilities, capabilitiesTransformations, settings) { + if (!capabilitiesTransformations || !settings) { + return false; } - if (preferences.applications) { - gpii.transformer.indexApplicationSettings(preferences.applications, appSpecific); - delete preferences.applications; - } + // 1. Extract settings paths from the demanded settings + var demandedSettingPaths = gpii.transformer.computeDemandedSettingsPaths(settings); - fluid.each(preferences, function (preference) { - gpii.transformer.findAndIndexApplications(preference, appSpecific); - }); + // 2. Find settings paths supported by the settings handler + var capabilitiesArray = fluid.copy(fluid.makeArray(capabilities)); + var transformationSupportedSettingPaths = fluid.model.transform.collectInputPaths(capabilitiesTransformations); + var supportedSettingPaths = capabilitiesArray.concat(transformationSupportedSettingPaths); - return appSpecific; - }; + // 3. Find out if one or more actual settings are supported by the settings handler + var originalLength = supportedSettingPaths.length; + gpii.arrayDifference(supportedSettingPaths, demandedSettingPaths); - gpii.transformer.findApplicationSettings = function (preferences, appSpecific) { - appSpecific = appSpecific || {}; - preferences = fluid.copy(preferences); - return gpii.transformer.findAndIndexApplications(preferences, appSpecific); + return supportedSettingPaths.length < originalLength; }; - gpii.transformer.preferencesToSettings = function (preferences, handler, specificPrefs) { - var settings = {}, - rules = handler.capabilitiesTransformations; + /** + * Helper function for gpii.transformer.configurationToSettings. Used to modify the settings handler + * blocks to be in the format required (i.e. including the application specific settings to be + * set) by the actual settings handlers. This modifies the block in place - it has already been copied by the + * the caller + * + * @param {Object} settingsHandler - The settingsHandler information from the solutions registry to be transformed - MODIFIED IN PLACE. + * @param {Object} oneUserSolution - The user preferences related to this solution and settings handler. + * @param {Object} solutionId - The ID of the solution for which the settingsHandler is related. + * @return {Object} - The modified settingsHandler object - ready to be passed to the lifecycle manager + */ + gpii.transformer.transformOneSettingsHandler = function (settingsHandler, oneUserSolution, solutionId) { + var settings = fluid.copy(oneUserSolution.settings["http://registry.gpii.net/applications/" + solutionId]); - if (rules) { - settings = fluid.model.transformWithRules(preferences, handler.capabilitiesTransformations); - } + // extract any common terms that were defined in the application block and delete them from the settings block + var scopedCommon = {}; + fluid.each(settings, function (val, key) { + if (key.startsWith("http://registry.gpii.net/common/")) { + scopedCommon[key] = fluid.copy(val); // add to list of scoped common terms + delete settings[key]; // remove from the settings + } + }); + + // filter to only keep relevant application specific settings + settings = gpii.settingsHandlers.filterSupportedSettings(settings, settingsHandler.supportedSettings); - if (!specificPrefs || !specificPrefs.parameters) { - return settings; + var hasSupportedSettings = gpii.transformer.hasSupportedSettings(settingsHandler.capabilities, settingsHandler.capabilitiesTransformations, oneUserSolution.settings); + + if (hasSupportedSettings) { + var inferred = fluid.model.transformWithRules(oneUserSolution.settings, settingsHandler.capabilitiesTransformations); + var inferredScoped = (scopedCommon === undefined || $.isEmptyObject(scopedCommon)) ? {} : fluid.model.transformWithRules(scopedCommon, settingsHandler.capabilitiesTransformations); + settings = fluid.extend(true, {}, inferred, inferredScoped, settings); } - var mergedSettings = $.extend(true, settings, specificPrefs.parameters); - return mergedSettings; + settingsHandler.settings = settings || {}; + return settingsHandler; }; - gpii.transformer.transformSettingsHandlers = function (preferences, specificPrefs, solution, settingsHandlers) { - fluid.each(settingsHandlers, function (handler) { - delete handler.capabilities; - var transformedSettings = gpii.transformer.preferencesToSettings(preferences, handler, specificPrefs); - handler.settings = transformedSettings; + /** + * Modify a solution registry entry to clean up settingsHandlers that have empty settings block. + * 1. Remove settingsHandlers that have empty settings block. + * 2. Remove corresponding settingsHandler definitions from these solution registry paths: + * "update", "configure", "restore" + * + * @param {Object} oneSolution - A solution registry entry that is in transform - MODIFIED IN PLACE. + */ + gpii.transformer.adjustSettingsHandlerRelatedData = function (oneSolution) { + fluid.each(oneSolution.settingsHandlers, function (settingsHandler, key) { + if ($.isEmptyObject(settingsHandler.settings)) { + delete oneSolution.settingsHandlers[key]; + var elementToRemove = "settings." + key; + gpii.arrayDifference(oneSolution.update, elementToRemove); + gpii.arrayDifference(oneSolution.configure, elementToRemove); + gpii.arrayDifference(oneSolution.restore, elementToRemove); + } }); }; - gpii.transformer.finalInit = function (that) { - - that.transformSettings = function (settings) { - var specificPrefsForAllSolutions = gpii.transformer.findApplicationSettings(settings.preferences); - var transformedSolutions = fluid.copy(settings.solutions); + /** + * Helper function for `gpii.transformer.configurationToSettings`. Used to modify the launch handler + * blocks to be in the format required (i.e. setting the "settings.running" to true/false based on the + * corresponding /enabled preference value). This modifies the block in place - it has already been copied by the + * the caller. + * + * @param {Object} launchHandler - The launchHandler information from the solutions registry to be transformed - MODIFIED IN PLACE. + * @param {Object} oneUserSolution - The user preferences related to this solution and launch handler. + * @return {Object} - The modified launchHandler object - ready to be passed to the lifecycle manager + */ + gpii.transformer.transformOneLaunchHandler = function (launchHandler, oneUserSolution) { + var settings = fluid.copy(oneUserSolution.settings); + // launchHandler's "settings.running" value is set based on these rules: + // 1. If /enabled preference was provided, set the value to /enabled preference value; + // 2. If /enabled preference was not provided, set the value to `true` to launch the app for handling "settingsHandler". + // launchHandler's "settings.running" is set in all cases to ensure at user key-in when the key-in action needs + // to be merged with the default snapshot for "reset all" (see function gpii.lifecycleManager.userLogonHandling.startLifecycle() + // and GPII-3434), the structure of lifecycleInstructions for both key-in action and default snapshot match each other + // so that the launchHandler's "settings.running" value from the key-in will override the one from the default snapshot. + fluid.each(settings, function (value, key) { + fluid.set(launchHandler, ["settings", "running"], key.endsWith("/enabled") ? value : true); + }); + return launchHandler; + }; - fluid.each(transformedSolutions, function (solution) { - var settingsHandlers = solution.settingsHandlers; - var specificPrefs = specificPrefsForAllSolutions[solution.id]; - gpii.transformer.transformSettingsHandlers(settings.preferences, specificPrefs, solution, settingsHandlers); + /** + * Converts a system configuration (output from matchmaker output) to a format readable by the + * lifecycle manager. This includes translation of common terms to application specific settings. + * The following rules apply when doing this translation: + * - the translation is based on the transformations in the solutionsRegistry entry + * - if a common term transforms into a setting which is already specific as application specific + * in the configuration argument, the application specific takes priority. In other words: + * if the configuration contains both an application specific and (transformable) common + * term for some application setting, the application specific one will stay and the + * transformed will be discarded. + * + * @param {Object} configuration - An element of the matchmaker output payload for a specific preferences set + * @param {Object} solutionsRegistryEntries - A section of the solutions registry database, filtered for configured solutions (currently only done by OS and device context). + * @return {Object} An object isomorphic to the "configuration" with settings blocks transformed and evaluated - this is ready to be + * dispatched to settings handlers. + */ + gpii.transformer.configurationToSettings = function (configuration, solutionsRegistryEntries) { + var togo = fluid.transform(configuration.applications, function (oneUserSolution, solutionId) { + var oneSolution = fluid.copy(solutionsRegistryEntries[solutionId]); + if (oneSolution === undefined) { + fluid.log("The configuration to be applied contains an entry for " + solutionId + + " which was not found in the solutionsRegistry. Aborting configuration"); + return undefined; + } + delete oneSolution.contexts; // solutions registry entries should include this as "old device reporter context info" + oneSolution.settingsHandlers = fluid.transform(oneSolution.settingsHandlers, function (settingsHandler) { + return gpii.transformer.transformOneSettingsHandler(settingsHandler, oneUserSolution, solutionId); }); - return transformedSolutions; - }; + // Remove settingsHandlers that have empty settings. + gpii.transformer.adjustSettingsHandlerRelatedData(oneSolution); + // Remove settingsHandlers path itself if it doesn't contain any settingsHandler. + if ($.isEmptyObject(oneSolution.settingsHandlers)) { + delete oneSolution.settingsHandlers; + } + + // Handle /enabled preferences to launch or stop solutions + if (oneSolution.launchHandlers) { + oneSolution.launchHandlers = fluid.transform(oneSolution.launchHandlers, function (launchHandler) { + return gpii.transformer.transformOneLaunchHandler(launchHandler, oneUserSolution, solutionId); + }); + } + oneSolution.active = oneUserSolution.active; // copy over active directive for later use + return oneSolution; + }); + + // ensure no empty entries exist in the settings payload + for (var i in togo) { + if (togo[i] === undefined) { + delete togo[i]; + } + } + return togo; }; - fluid.defaults("gpii.transformer.quantize", { + // TODO: This transform has no tests + fluid.defaults("gpii.transformer.booleanToNumber", { gradeNames: "fluid.standardTransformFunction" }); - gpii.transformer.quantize = function (value, transformSpec, transform) { - if (!transformSpec.ranges || !transformSpec.ranges.length) { - fluid.fail("gpii.transformer.quantize should have a key called ranges containing an array defining ranges to quantize"); - } - for (var i = 0; i < transformSpec.ranges.length; i++) { - var rangeSpec = transformSpec.ranges[i]; - if (rangeSpec.upperBound === undefined || value <= rangeSpec.upperBound) { - //note that we might return undefined if output evaluates to that on an expand - return fluid.isPrimitive(rangeSpec.output) ? rangeSpec.output : transform.expand(rangeSpec.output); - } + gpii.transformer.booleanToNumber = function (value) { + return value ? 1 : 0; + }; + + /** + * timeInRange is implemented specifically for checking whether an input time falls in between + * two given times. The valid and required inputs, besides the standard input/inputPath are `to` + * and `from`. All inputs should be strings of the type "hh:mm" (eg. "17:30"). The range wraps + * midnight, so given a `from` of "20:00" and a `to` of `"08:00"` all the following inputs would + * result in true: "20:00", 23:00", "02:45". + */ + fluid.defaults("gpii.transformer.timeInRange", { + gradeNames: "fluid.standardTransformFunction" + }); + + + gpii.transformer.timeInRange = function (value, transformSpec) { + var timeInRange = gpii.transformer.timeInRange; + var currentTime = timeInRange.timeParser(value); + var fromTime = timeInRange.timeParser(transformSpec.from); + var toTime = timeInRange.timeParser(transformSpec.to); + + if (timeInRange.isEarlier(toTime, fromTime)) { // if time wraps + return (timeInRange.isEarlier(fromTime, currentTime) || timeInRange.isEarlier(currentTime, toTime)); + } else { + return (timeInRange.isEarlier(fromTime, currentTime) && timeInRange.isEarlier(currentTime, toTime)); } }; -}()); + + gpii.transformer.timeInRange.timeParser = function (time) { + time = time.split(":"); + return { + hours: parseInt(time[0], 10), + minutes: parseInt(time[1], 10) + }; + }; + + // checks whether 'first' is earlier than 'second' - returns true if so + gpii.transformer.timeInRange.isEarlier = function (first, second) { + return (60 * (first.hours - second.hours) + first.minutes - second.minutes) < 0; + }; + +})(); diff --git a/gpii/node_modules/transformer/test/TransformerTests.js b/gpii/node_modules/transformer/test/TransformerTests.js new file mode 100644 index 000000000..2f0326295 --- /dev/null +++ b/gpii/node_modules/transformer/test/TransformerTests.js @@ -0,0 +1,1546 @@ +/*! +GPII Transformer Tests + +Copyright 2012 OCAD University +Copyright 2013-2014 Raising the Floor +Copyright 2019 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.require("node-jqunit", require, "jqUnit"); + +fluid.require("%flowManager/src/SystemUtils.js"); +fluid.require("%settingsHandlers/src/settingsHandlerUtilities.js"); +require("../src/js/Transformer.js"); + +fluid.registerNamespace("gpii.tests.transformer"); + +// TODO: Consolidate this kind of "canned" SR data so that we don't have to change schemas more than once. +var testSolutionsEntry = { + "com.microsoft.windows.magnifier": { + "name": "Windows Built-in Screen Magnifier", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "conf": { + "type": "gpii.windows.registrySettingsHandler.set", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\ScreenMagnifier", + "dataTypes": { + "Magnification": "REG_DWORD", + "Invert": "REG_DWORD", + "FollowFocus": "REG_DWORD", + "FollowCaret": "REG_DWORD", + "FollowMouse": "REG_DWORD", + "MagnificationMode": "REG_DWORD", + "ZoomIncrement": "REG_DWORD" + } + }, + "supportedSettings": { + "Invert": {}, + "Magnification": {}, + "FollowMouse": {}, + "FollowCaret": {}, + "FollowFocus": {}, + "MagnificationMode": {} + }, + "capabilitiesTransformations": { + "Invert": "http://registry\\.gpii\\.net/common/invertColours", + "Magnification": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/magnification", + "factor": 100 + } + } + } + }, + "transform": [ + { + "type": "fluid.transforms.arrayToSetMembership", + "inputPath": "http://registry\\.gpii\\.net/common/tracking", + "outputPath": "", + "presentValue": 1, + "missingValue": 0, + "options": { + "focus": "FollowFocus", + "caret": "FollowCaret", + "mouse": "FollowMouse" + } + } + ], + "MagnificationMode": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/magnifierPosition", + "match": { + "FullScreen": 2, + "Lens": 3, + "LeftHalf": 1, + "RightHalf": 1, + "TopHalf": 1, + "BottomHalf": 1, + "Custom": 2 + } + } + } + } + } + }, + "configure": [ + "settings.conf" + ], + "restore": [ + "settings.conf" + ], + "start": [ + { + "type": "gpii.launch.exec", + "command": "${{environment}.SystemRoot}\\System32\\Magnify.exe" + } + ], + "stop": [ + { + "type": "gpii.launch.exec", + "command": "${{environment}.SystemRoot}\\System32\\taskkill.exe /f /im Magnify.exe" + } + ] + }, + + "org.gnome.desktop.a11y.magnifier": { + "name": "GNOME Shell Magnifier", + "contexts": { + "OS": [ + { + "id": "linux", + "version": ">=2.6.26" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + "settingsHandlers": { + "configuration": { + "type": "gpii.gsettings", + "liveness": "live", + "options": { + "schema": "org.gnome.desktop.a11y.magnifier" + }, + "supportedSettings": { + "mag-factor": {}, + "show-cross-hairs": {}, + "focus-tracking": {}, + "caret-tracking": {}, + "mouse-tracking": {}, + "screen-position": {} + }, + "capabilitiesTransformations": { + "mag-factor": "http://registry\\.gpii\\.net/common/magnification", + "show-cross-hairs": "http://registry\\.gpii\\.net/common/showCrosshairs", + "transform": { + "type": "fluid.transforms.arrayToSetMembership", + "inputPath": "http://registry\\.gpii\\.net/common/tracking", + "outputPath": "", + "presentValue": "proportional", + "missingValue": "none", + "options": { + "focus": "focus-tracking", + "caret": "caret-tracking", + "mouse": "mouse-tracking" + } + }, + "screen-position": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/magnifierPosition", + "match": { + "FullScreen": "full-screen", + "Lens": "full-screen", + "LeftHalf": "left-half", + "RightHalf": "right-half", + "TopHalf": "top-half", + "BottomHalf": "bottom-half" + }, + "noMatch": { + "outputValue": "full-screen" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/magnification": "mag-factor", + "http://registry\\.gpii\\.net/common/showCrosshairs": "show-cross-hairs", + "transform": [ + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "screen-position", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/magnifierPosition", + "match": { + "full-screen": "FullScreen", + "left-half": "LeftHalf", + "right-half": "RightHalf", + "top-half": "TopHalf", + "bottom-half": "BottomHalf" + } + }, + { + "type": "fluid.transforms.valueMapper", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/tracking", + "defaultInputPath": "mouse-tracking", + "match": { + "centered": "mouse" + } + } + ] + } + } + }, + "launchHandlers": { + "launcher": { + "type": "gpii.gsettings.launch", + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-magnifier-enabled" + } + } + }, + "update": [ + "settings.configuration" + ], + "isInstalled": [ + { + "type": "gpii.packageKit.find", + "name": "gnome-shell" + } + ] + }, + // TODO: Consolidate this kind of "canned" SR data so that we don't have to change schemas more than once. + "com.microsoft.windows.mouseSettings": { + "name": "Windows typing autocorrection", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configureMousePrimaryButton": { + // TODO: Consolidate this kind of "canned" SR data so that we don't have to change schemas more than once. + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETMOUSEBUTTONSWAP", + "setAction": "SPI_SETMOUSEBUTTONSWAP", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + }, + "fWinIni": "SPIF_UPDATEINIFILE|SPIF_SENDCHANGE" + }, + "supportedSettings": { + "SwapMouseButtonsConfig": { + "schema": { + "title": "Swap mouse primary button", + "description": "Swap mouse left/right buttons", + "type": "boolean", + "default": false + } + } + }, + "capabilitiesTransformations": { + "SwapMouseButtonsConfig": { + "transform": { + "type": "fluid.transforms.value", + "inputPath": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.SwapMouseButtons", + "outputPath": "value" + }, + "path": { + "literalValue": { + "get": "pvParam", + "set": "uiParam" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.SwapMouseButtons": "SwapMouseButtonsConfig.value" + } + }, + "configureScrollWheelMode": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETWHEELSCROLLLINES", + "setAction": "SPI_SETWHEELSCROLLLINES", + "uiParam": 0, + "pvParam": { + "type": "UINT" + }, + "fWinIni": "SPIF_UPDATEINIFILE|SPIF_SENDCHANGE" + }, + "supportedSettings": { + "ScrollWheelModeConfig": { + "schema": { + "title": "Mouse scroll wheel mode", + "description": "Changes the number of lines to scroll when moving the scrollwheel.", + "default": 3, + "oneOf": [ + { + "title": "Range of accepted regular values", + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + { + "title": "Values with special meaning", + "enum": [4294967295], + "enumLabels": ["full-page"] + } + ] + } + } + }, + "capabilitiesTransformations": { + "ScrollWheelModeConfig": { + "transform": { + "type": "fluid.transforms.value", + "inputPath": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.ScrollLines", + "outputPath": "value" + }, + "path": { + "literalValue": { + "get": "pvParam", + "set": "uiParam" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.ScrollLines": "ScrollWheelModeConfig.value" + } + } + }, + "configure": [ + "settings.configureMousePrimaryButton", + "settings.configureScrollWheelMode" + ], + "restore": [ + "settings.configureMousePrimaryButton", + "settings.configureScrollWheelMode" + ], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + // TODO: Consolidate this kind of "canned" SR data so that we don't have to change schemas more than once. + "com.microsoft.windows.highContrast": { + "name": "Windows High Contrast", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure-spi": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETHIGHCONTRAST", + "setAction": "SPI_SETHIGHCONTRAST", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "HIGHCONTRAST" + } + }, + "supportedSettings": { + "HighContrastOn": { + "schema": { + "title": "High Contrast", + "description": "Whether to enable/disable High Contrast", + "type": "boolean", + "default": false + } + } + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/highContrast/enabled", + "http://registry\\.gpii\\.net/common/highContrastTheme" + ], + "capabilitiesTransformations": { + "HighContrastOn": { + // (highContrast.enabled || highContrastTheme) && (highContrastTheme != "regular-contrast") + "transform": { + "type": "fluid.transforms.binaryOp", + "left": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/highContrast/enabled", + "left": false, + "operator": "||", + "rightPath": "http://registry\\.gpii\\.net/common/highContrastTheme", + "right": false + } + }, + "operator": "&&", + "right": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": "", + "leftPath": "http://registry\\.gpii\\.net/common/highContrastTheme", + "operator": "!==", + "right": "regular-contrast" + } + }, + "outputPath": "value" + }, + "path": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": "pvParam.dwFlags.HCF_HIGHCONTRASTON" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/highContrast/enabled": "HighContrastOn.value" + } + }, + "configure-registry": { + // This sets the high-contrast theme. + "type": "gpii.windows.registrySettingsHandler", + "liveness": "live", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes", + "dataTypes": { + "LastHighContrastTheme": "REG_SZ" + } + }, + "supportedSettings": { + "LastHighContrastTheme": { + "schema": { + "title": "High Contrast theme", + "description": "High Contrast Theme", + "type": "string", + "default": "%SystemRoot%\\resources\\Ease of Access Themes\\hcwhite.theme", + "enum": [ + "%SystemRoot%\\resources\\Ease of Access Themes\\hcwhite.theme", + "%SystemRoot%\\resources\\Ease of Access Themes\\hcblack.theme", + "%SystemRoot%\\resources\\Ease of Access Themes\\hc1.theme", + "%SystemRoot%\\resources\\Ease of Access Themes\\yellowOnBlack.theme", + "%SystemRoot%\\resources\\Ease of Access Themes\\hc2.theme", + "%SystemRoot%\\resources\\Ease of Access Themes\\blackOnBrown.theme", + "%SystemRoot%\\resources\\Ease of Access Themes\\grayOnDark.theme", + "%SystemRoot%\\resources\\Ease of Access Themes\\grayOnWhite.theme" + ], + "enumLabels": [ + "Black on White", + "White on Black", + "Yellow on Black", + "Black on Yellow", + "Lime on Black", + "Black on Brown", + "Grey on Dark", + "Grey on White" + ] + } + } + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/highContrast/enabled", + "http://registry\\.gpii\\.net/common/highContrastTheme" + ], + "capabilitiesTransformations": { + "LastHighContrastTheme": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/highContrastTheme", + "match": { + "black-white": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\hcwhite.theme", + "white-black": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\hcblack.theme", + "yellow-black": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\hc1.theme", + "black-yellow": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\yellowOnBlack.theme", + "lime-black": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\hc2.theme", + "black-brown": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\blackOnBrown.theme", + "grey-black": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\grayOnDark.theme", + "grey-white": "${{environment}.SystemRoot}\\resources\\Ease of Access Themes\\grayOnWhite.theme" + } + } + } + } + }, + "configure-theme": { + // This is used to specify the theme file that Windows will use when restoring high-contrast. The + // file this setting points to is created by gpii, containing the currently unsaved theme settings. + "type": "gpii.windows.registrySettingsHandler", + "liveness": "live", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\HighContrast", + "dataTypes": { + "Pre-High Contrast Scheme": "REG_SZ" + } + }, + "supportedSettings": { + "Pre-High Contrast Scheme": {} + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/highContrast/enabled", + "http://registry\\.gpii\\.net/common/highContrastTheme" + ], + "capabilitiesTransformations": { + "Pre-High Contrast Scheme": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": "${{environment}.LOCALAPPDATA}\\Microsoft\\Windows\\Themes\\Morphic.theme" + } + } + } + } + }, + "configure": [ + // Set the "restore" theme. + "settings.configure-theme", + // Set the high-contrast theme. + "settings.configure-registry", + { + // Save the current theme, and set the new high-contrast one. + "type": "gpii.windows.spiSettingsHandler.setHighContrastTheme", + "newTheme": "${{registry}HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\LastHighContrastTheme}", + "currentTheme": "${{registry}HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\CurrentTheme}", + "saveAs": "${{registry}HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\HighContrast\\Pre-High Contrast Scheme}" + }, + // Apply high-contrast. + "settings.configure-spi" + ], + "restore": [ + // Restore the high-contrast theme. + "settings.configure-registry", + // Disable high-contrast. + "settings.configure-spi", + // Restore the current theme. + "settings.configure-theme" + ], + "isInstalled": [ + { + "type": "gpii.deviceReporter.registryKeyExists", + "hKey": "HKEY_LOCAL_MACHINE", + "path": "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe", + "subPath": "", + "dataType": "REG_SZ" + } + ] + } +}; + +gpii.tests.transformer.configurationToSettingsTestSpecs = [ + { + name: "Application specific settings for the same application", + input: { + "applications": { + "com.microsoft.windows.magnifier": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "FollowFocus": 0, + "FollowCaret": 1, + "FollowMouse": 1, + "Invert": false + } + } + } + } + }, + expected: { + "com.microsoft.windows.magnifier": { + "active": true, + "name": testSolutionsEntry["com.microsoft.windows.magnifier"].name, + "start": testSolutionsEntry["com.microsoft.windows.magnifier"].start, + "stop": testSolutionsEntry["com.microsoft.windows.magnifier"].stop, + "configure": testSolutionsEntry["com.microsoft.windows.magnifier"].configure, + "restore": testSolutionsEntry["com.microsoft.windows.magnifier"].restore, + "settingsHandlers": { + "conf": { + "type": "gpii.windows.registrySettingsHandler.set", + "options": { + "dataTypes": { + "FollowCaret": "REG_DWORD", + "FollowFocus": "REG_DWORD", + "FollowMouse": "REG_DWORD", + "Invert": "REG_DWORD", + "Magnification": "REG_DWORD", + "MagnificationMode": "REG_DWORD", + "ZoomIncrement": "REG_DWORD" + }, + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\ScreenMagnifier" + }, + "settings": { + "FollowCaret": 1, + "FollowFocus": 0, + "FollowMouse": 1, + "Invert": false + }, + "capabilitiesTransformations": testSolutionsEntry["com.microsoft.windows.magnifier"].settingsHandlers.conf.capabilitiesTransformations, + "supportedSettings": testSolutionsEntry["com.microsoft.windows.magnifier"].settingsHandlers.conf.supportedSettings + } + } + } + } + }, { + name: "Application specific settings filtered by supported settings", + input: { + "applications": { + "com.microsoft.windows.magnifier": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "FollowFocus": 0, + "FollowCaret": 1, + "FollowMouse": 1, + "Invert": false, + "wrongSetting": 200 + } + } + } + } + }, + expected: { + "com.microsoft.windows.magnifier": { + "active": true, + "name": testSolutionsEntry["com.microsoft.windows.magnifier"].name, + "start": testSolutionsEntry["com.microsoft.windows.magnifier"].start, + "stop": testSolutionsEntry["com.microsoft.windows.magnifier"].stop, + "configure": testSolutionsEntry["com.microsoft.windows.magnifier"].configure, + "restore": testSolutionsEntry["com.microsoft.windows.magnifier"].restore, + "settingsHandlers": { + "conf": { + "type": "gpii.windows.registrySettingsHandler.set", + "options": { + "dataTypes": { + "FollowCaret": "REG_DWORD", + "FollowFocus": "REG_DWORD", + "FollowMouse": "REG_DWORD", + "Invert": "REG_DWORD", + "Magnification": "REG_DWORD", + "MagnificationMode": "REG_DWORD", + "ZoomIncrement": "REG_DWORD" + }, + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\ScreenMagnifier" + }, + "settings": { + "FollowCaret": 1, + "FollowFocus": 0, + "FollowMouse": 1, + "Invert": false + }, + "capabilitiesTransformations": testSolutionsEntry["com.microsoft.windows.magnifier"].settingsHandlers.conf.capabilitiesTransformations, + "supportedSettings": testSolutionsEntry["com.microsoft.windows.magnifier"].settingsHandlers.conf.supportedSettings + } + } + } + } + }, { + name: "Common terms only", + input: { + "applications": { + "com.microsoft.windows.magnifier": { + "active": true, + "settings": { + "http://registry.gpii.net/common/tracking": [ "mouse", "caret" ] + } + } + } + }, + expected: { + "com.microsoft.windows.magnifier": { + "active": true, + "name": testSolutionsEntry["com.microsoft.windows.magnifier"].name, + "start": testSolutionsEntry["com.microsoft.windows.magnifier"].start, + "stop": testSolutionsEntry["com.microsoft.windows.magnifier"].stop, + "configure": testSolutionsEntry["com.microsoft.windows.magnifier"].configure, + "restore": testSolutionsEntry["com.microsoft.windows.magnifier"].restore, + "settingsHandlers": { + "conf": { + "type": "gpii.windows.registrySettingsHandler.set", + "options": { + "dataTypes": { + "FollowCaret": "REG_DWORD", + "FollowFocus": "REG_DWORD", + "FollowMouse": "REG_DWORD", + "Invert": "REG_DWORD", + "Magnification": "REG_DWORD", + "MagnificationMode": "REG_DWORD", + "ZoomIncrement": "REG_DWORD" + }, + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\ScreenMagnifier" + }, + "settings": { + "FollowCaret": 1, + "FollowFocus": 0, + "FollowMouse": 1 + }, + "capabilitiesTransformations": testSolutionsEntry["com.microsoft.windows.magnifier"].settingsHandlers.conf.capabilitiesTransformations, + "supportedSettings": testSolutionsEntry["com.microsoft.windows.magnifier"].settingsHandlers.conf.supportedSettings + } + } + } + } + }, { + name: "Application scoped common terms are converted to application specific settings", + input: { + "applications": { + "com.microsoft.windows.magnifier": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "http://registry.gpii.net/common/invertColours": true + } + } + } + } + }, + expected: { + "com.microsoft.windows.magnifier": { + "active": true, + "name": testSolutionsEntry["com.microsoft.windows.magnifier"].name, + "start": testSolutionsEntry["com.microsoft.windows.magnifier"].start, + "stop": testSolutionsEntry["com.microsoft.windows.magnifier"].stop, + "configure": testSolutionsEntry["com.microsoft.windows.magnifier"].configure, + "restore": testSolutionsEntry["com.microsoft.windows.magnifier"].restore, + "settingsHandlers": { + "conf": { + "type": "gpii.windows.registrySettingsHandler.set", + "options": { + "dataTypes": { + "FollowCaret": "REG_DWORD", + "FollowFocus": "REG_DWORD", + "FollowMouse": "REG_DWORD", + "Invert": "REG_DWORD", + "Magnification": "REG_DWORD", + "MagnificationMode": "REG_DWORD", + "ZoomIncrement": "REG_DWORD" + }, + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\ScreenMagnifier" + }, + "capabilitiesTransformations": testSolutionsEntry["com.microsoft.windows.magnifier"].settingsHandlers.conf.capabilitiesTransformations, + "supportedSettings": testSolutionsEntry["com.microsoft.windows.magnifier"].settingsHandlers.conf.supportedSettings, + "settings": { + "Invert": true + } + } + } + } + } + }, { + name: "App scoped common terms overwrites top level common terms", + input: { + "applications": { + "com.microsoft.windows.magnifier": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "http://registry.gpii.net/common/invertColours": true + }, + "http://registry.gpii.net/common/invertColours": false + } + } + } + }, + expected: { + "com.microsoft.windows.magnifier": { + "active": true, + "name": testSolutionsEntry["com.microsoft.windows.magnifier"].name, + "start": testSolutionsEntry["com.microsoft.windows.magnifier"].start, + "stop": testSolutionsEntry["com.microsoft.windows.magnifier"].stop, + "configure": testSolutionsEntry["com.microsoft.windows.magnifier"].configure, + "restore": testSolutionsEntry["com.microsoft.windows.magnifier"].restore, + "settingsHandlers": { + "conf": { + "type": "gpii.windows.registrySettingsHandler.set", + "options": { + "dataTypes": { + "FollowCaret": "REG_DWORD", + "FollowFocus": "REG_DWORD", + "FollowMouse": "REG_DWORD", + "Invert": "REG_DWORD", + "Magnification": "REG_DWORD", + "MagnificationMode": "REG_DWORD", + "ZoomIncrement": "REG_DWORD" + }, + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\ScreenMagnifier" + }, + "capabilitiesTransformations": testSolutionsEntry["com.microsoft.windows.magnifier"].settingsHandlers.conf.capabilitiesTransformations, + "supportedSettings": testSolutionsEntry["com.microsoft.windows.magnifier"].settingsHandlers.conf.supportedSettings, + "settings": { + "Invert": true + } + } + } + } + } + }, { + name: "App specific overwrites common terms", + input: { + "applications": { + "com.microsoft.windows.magnifier": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "FollowFocus": 0, + "Invert": false + }, + "http://registry.gpii.net/common/invertColours": false + } + } + } + }, + expected: { + "com.microsoft.windows.magnifier": { + "active": true, + "name": testSolutionsEntry["com.microsoft.windows.magnifier"].name, + "start": testSolutionsEntry["com.microsoft.windows.magnifier"].start, + "stop": testSolutionsEntry["com.microsoft.windows.magnifier"].stop, + "configure": testSolutionsEntry["com.microsoft.windows.magnifier"].configure, + "restore": testSolutionsEntry["com.microsoft.windows.magnifier"].restore, + "settingsHandlers": { + "conf": { + "type": "gpii.windows.registrySettingsHandler.set", + "options": { + "dataTypes": { + "FollowCaret": "REG_DWORD", + "FollowFocus": "REG_DWORD", + "FollowMouse": "REG_DWORD", + "Invert": "REG_DWORD", + "Magnification": "REG_DWORD", + "MagnificationMode": "REG_DWORD", + "ZoomIncrement": "REG_DWORD" + }, + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\ScreenMagnifier" + }, + "settings": { + "Invert": false, + "FollowFocus": 0 + }, + "capabilitiesTransformations": testSolutionsEntry["com.microsoft.windows.magnifier"].settingsHandlers.conf.capabilitiesTransformations, + "supportedSettings": testSolutionsEntry["com.microsoft.windows.magnifier"].settingsHandlers.conf.supportedSettings + } + } + } + } + }, { + name: "App specific overwrites app scoped common terms", + input: { + "applications": { + "com.microsoft.windows.magnifier": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "FollowFocus": 0, + "Invert": false, + "http://registry.gpii.net/common/invertColours": true + } + } + } + } + }, + expected: { + "com.microsoft.windows.magnifier": { + "active": true, + "name": testSolutionsEntry["com.microsoft.windows.magnifier"].name, + "start": testSolutionsEntry["com.microsoft.windows.magnifier"].start, + "stop": testSolutionsEntry["com.microsoft.windows.magnifier"].stop, + "configure": testSolutionsEntry["com.microsoft.windows.magnifier"].configure, + "restore": testSolutionsEntry["com.microsoft.windows.magnifier"].restore, + "settingsHandlers": { + "conf": { + "type": "gpii.windows.registrySettingsHandler.set", + "options": { + "dataTypes": { + "FollowCaret": "REG_DWORD", + "FollowFocus": "REG_DWORD", + "FollowMouse": "REG_DWORD", + "Invert": "REG_DWORD", + "Magnification": "REG_DWORD", + "MagnificationMode": "REG_DWORD", + "ZoomIncrement": "REG_DWORD" + }, + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\ScreenMagnifier" + }, + "settings": { + "Invert": false, + "FollowFocus": 0 + }, + "capabilitiesTransformations": testSolutionsEntry["com.microsoft.windows.magnifier"].settingsHandlers.conf.capabilitiesTransformations, + "supportedSettings": testSolutionsEntry["com.microsoft.windows.magnifier"].settingsHandlers.conf.supportedSettings + } + } + } + } + }, { + name: "Irelevant App specific settings are ignored", + input: { + "applications": { + "com.microsoft.windows.magnifier": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/some.app": { + "settingA": 0 + }, + "http://registry.gpii.net/common/invertColours": false + } + } + } + }, + expected: { + "com.microsoft.windows.magnifier": { + "active": true, + "name": testSolutionsEntry["com.microsoft.windows.magnifier"].name, + "start": testSolutionsEntry["com.microsoft.windows.magnifier"].start, + "stop": testSolutionsEntry["com.microsoft.windows.magnifier"].stop, + "configure": testSolutionsEntry["com.microsoft.windows.magnifier"].configure, + "restore": testSolutionsEntry["com.microsoft.windows.magnifier"].restore, + "settingsHandlers": { + "conf": { + "type": "gpii.windows.registrySettingsHandler.set", + "options": { + "dataTypes": { + "FollowCaret": "REG_DWORD", + "FollowFocus": "REG_DWORD", + "FollowMouse": "REG_DWORD", + "Invert": "REG_DWORD", + "Magnification": "REG_DWORD", + "MagnificationMode": "REG_DWORD", + "ZoomIncrement": "REG_DWORD" + }, + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\ScreenMagnifier" + }, + "settings": { + "Invert": false + }, + "capabilitiesTransformations": testSolutionsEntry["com.microsoft.windows.magnifier"].settingsHandlers.conf.capabilitiesTransformations, + "supportedSettings": testSolutionsEntry["com.microsoft.windows.magnifier"].settingsHandlers.conf.supportedSettings + } + } + } + } + }, { + name: "multi applications blocks", + input: { + "applications": { + "com.microsoft.windows.magnifier": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "Invert": false + } + } + } + } + }, + expected: { + "com.microsoft.windows.magnifier": { + "active": true, + "name": testSolutionsEntry["com.microsoft.windows.magnifier"].name, + "start": testSolutionsEntry["com.microsoft.windows.magnifier"].start, + "stop": testSolutionsEntry["com.microsoft.windows.magnifier"].stop, + "configure": testSolutionsEntry["com.microsoft.windows.magnifier"].configure, + "restore": testSolutionsEntry["com.microsoft.windows.magnifier"].restore, + "settingsHandlers": { + "conf": { + "type": "gpii.windows.registrySettingsHandler.set", + "options": { + "dataTypes": { + "FollowCaret": "REG_DWORD", + "FollowFocus": "REG_DWORD", + "FollowMouse": "REG_DWORD", + "Invert": "REG_DWORD", + "Magnification": "REG_DWORD", + "MagnificationMode": "REG_DWORD", + "ZoomIncrement": "REG_DWORD" + }, + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\ScreenMagnifier" + }, + "settings": { + "Invert": false + }, + "capabilitiesTransformations": testSolutionsEntry["com.microsoft.windows.magnifier"].settingsHandlers.conf.capabilitiesTransformations, + "supportedSettings": testSolutionsEntry["com.microsoft.windows.magnifier"].settingsHandlers.conf.supportedSettings + } + } + } + } + }, { + name: "Application specific settings for non-existing solution", + input: { + "applications": { + "com.microsoft.windows.bogus-app": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/com.microsoft.windows.bogus-app": { + "FollowFocus": 0, + "FollowCaret": 1, + "FollowMouse": 1, + "Invert": false + } + } + } + } + }, + expected: {} + }, { + name: "GPII-3435: Common terms ending with /enabled control launchHandler blocks", + input: { + "applications": { + "org.gnome.desktop.a11y.magnifier": { + "active": true, + "settings": { + "http://registry.gpii.net/common/magnification/enabled": true + } + } + } + }, + expected: { + "org.gnome.desktop.a11y.magnifier": { + "name": "GNOME Shell Magnifier", + "capabilities": [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + "launchHandlers": { + "launcher": { + "type": "gpii.gsettings.launch", + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-magnifier-enabled" + }, + "settings": { + "running": true + } + } + }, + "update": [], + "isInstalled": [ + { + "type": "gpii.packageKit.find", + "name": "gnome-shell" + } + ], + "active": true + } + } + }, { + name: "GPII-3653: Filter out settings handlers that have no capability to handle the given setting", + input: { + "applications": { + "com.microsoft.windows.mouseSettings": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/com.microsoft.windows.mouseSettings":{ + "SwapMouseButtonsConfig": 1 + } + } + } + } + }, + expected: { + "com.microsoft.windows.mouseSettings": { + "name": testSolutionsEntry["com.microsoft.windows.mouseSettings"].name, + "isInstalled": testSolutionsEntry["com.microsoft.windows.mouseSettings"].isInstalled, + "settingsHandlers": { + "configureMousePrimaryButton": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETMOUSEBUTTONSWAP", + "setAction": "SPI_SETMOUSEBUTTONSWAP", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + }, + "fWinIni": "SPIF_UPDATEINIFILE|SPIF_SENDCHANGE" + }, + "supportedSettings": testSolutionsEntry["com.microsoft.windows.mouseSettings"].settingsHandlers.configureMousePrimaryButton.supportedSettings, + "capabilitiesTransformations": testSolutionsEntry["com.microsoft.windows.mouseSettings"].settingsHandlers.configureMousePrimaryButton.capabilitiesTransformations, + "inverseCapabilitiesTransformations": testSolutionsEntry["com.microsoft.windows.mouseSettings"].settingsHandlers.configureMousePrimaryButton.inverseCapabilitiesTransformations, + "settings": { + "SwapMouseButtonsConfig": 1 + } + } + }, + "configure": [ + "settings.configureMousePrimaryButton" + ], + "restore": [ + "settings.configureMousePrimaryButton" + ], + "active": true + } + } + }, { + name: "GPII-3653: Include settings handlers that have one of the given settings in the capabilities block", + input: { + "applications": { + "com.microsoft.windows.highContrast": { + "active": true, + "settings": { + "http://registry.gpii.net/common/highContrast/enabled": true, + "http://registry.gpii.net/applications/com.microsoft.windows.highContrast": { + "HighContrastOn": { + "path": "pvParam.dwFlags.HCF_HIGHCONTRASTON", + "value": true + } + } + } + } + } + }, + expected: { + "com.microsoft.windows.highContrast": { + "active": true, + "configure": [ + "settings.configure-theme", + { + "currentTheme": "${{registry}HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\CurrentTheme}", + "newTheme": "${{registry}HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\LastHighContrastTheme}", + "saveAs": "${{registry}HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\HighContrast\\Pre-High Contrast Scheme}", + "type": "gpii.windows.spiSettingsHandler.setHighContrastTheme" + }, + "settings.configure-spi" + ], + "isInstalled": [{ + "dataType": "REG_SZ", + "hKey": "HKEY_LOCAL_MACHINE", + "path": "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe", + "subPath": "", + "type": "gpii.deviceReporter.registryKeyExists" + }], + "name": "Windows High Contrast", + "restore": [ + "settings.configure-spi", + "settings.configure-theme" + ], + "settingsHandlers": { + "configure-spi": { + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/highContrast/enabled": "HighContrastOn.value" + }, + "liveness": "live", + "options": { + "getAction": "SPI_GETHIGHCONTRAST", + "pvParam": { + "name": "HIGHCONTRAST", + "type": "struct" + }, + "setAction": "SPI_SETHIGHCONTRAST", + "uiParam": "struct_size" + }, + "settings": { + "HighContrastOn": { + "path": "pvParam.dwFlags.HCF_HIGHCONTRASTON", + "value": true + } + }, + "capabilities": testSolutionsEntry["com.microsoft.windows.highContrast"].settingsHandlers["configure-spi"].capabilities, + "supportedSettings": testSolutionsEntry["com.microsoft.windows.highContrast"].settingsHandlers["configure-spi"].supportedSettings, + "capabilitiesTransformations": testSolutionsEntry["com.microsoft.windows.highContrast"].settingsHandlers["configure-spi"].capabilitiesTransformations, + "type": "gpii.windows.spiSettingsHandler" + }, + "configure-theme": { + "liveness": "live", + "options": { + "dataTypes": { + "Pre-High Contrast Scheme": "REG_SZ" + }, + "hKey": "HKEY_CURRENT_USER", + "path": "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\HighContrast" + }, + "settings": { + "Pre-High Contrast Scheme": "${{environment}.LOCALAPPDATA}\\Microsoft\\Windows\\Themes\\Morphic.theme" + }, + "capabilities": testSolutionsEntry["com.microsoft.windows.highContrast"].settingsHandlers["configure-theme"].capabilities, + "supportedSettings": testSolutionsEntry["com.microsoft.windows.highContrast"].settingsHandlers["configure-theme"].supportedSettings, + "capabilitiesTransformations": testSolutionsEntry["com.microsoft.windows.highContrast"].settingsHandlers["configure-theme"].capabilitiesTransformations, + "type": "gpii.windows.registrySettingsHandler" + } + } + } + } + } +]; + +jqUnit.module("Tester"); +jqUnit.test("Transformation tests", function () { + fluid.each(gpii.tests.transformer.configurationToSettingsTestSpecs, function (test) { + var result = gpii.transformer.configurationToSettings(test.input, testSolutionsEntry); + jqUnit.assertDeepEq(test.name, test.expected, result); + }); +}); + +/* --------------- gpii.transformer.timeInRange tests -------------------- */ +gpii.tests.transformer.timeInRangeTests = { + rules: { + normalOrder: { + "foo": { + "transform": { + "type": "gpii.transformer.timeInRange", + "inputPath": "bar", + "from": "10:30", + "to": "19:45" + } + } + }, + normalOrderMinutes: { + "foo": { + "transform": { + "type": "gpii.transformer.timeInRange", + "inputPath": "bar", + "from": "10:30", + "to": "10:59" + } + } + }, + wrappedOrder: { + "foo": { + "transform": { + "type": "gpii.transformer.timeInRange", + "inputPath": "bar", + "from": "22:15", + "to": "00:18" + } + } + } + }, + expects: { + "Basic hours - matches": { + rule: "normalOrder", + input: { + bar: "11:00" + }, + expected: { + foo: true + } + }, + "Basic hours - above threshold": { + rule: "normalOrder", + input: { + bar: "19:47" + }, + expected: { + foo: false + } + }, + "Basic hours - below threshold": { + rule: "normalOrder", + input: { + bar: "10:28" + }, + expected: { + foo: false + } + }, + "Minutes windows - matches": { + rule: "normalOrderMinutes", + input: { + bar: "10:37" + }, + expected: { + foo: true + } + }, + "Wrapped order - matches morning": { + rule: "wrappedOrder", + input: { + bar: "00:12" + }, + expected: { + foo: true + } + }, + "Wrapped order - matches evening": { + rule: "wrappedOrder", + input: { + bar: "23:00" + }, + expected: { + foo: true + } + }, + "Wrapped hours - above threshold": { + rule: "wrappedOrder", + input: { + bar: "10:28" + }, + expected: { + foo: false + } + } + } +}; + +jqUnit.test("gpii.transformer.timeInRange tests", function () { + fluid.each(gpii.tests.transformer.timeInRangeTests.expects, function (test, tname) { + var transformed = fluid.model.transformWithRules(test.input, gpii.tests.transformer.timeInRangeTests.rules[test.rule]); + jqUnit.assertDeepEq("timeInRange transformation tests - " + tname, test.expected, transformed); + }); +}); + +/* --------------- gpii.transformer.computeDemandedSettingsPaths tests -------------------- */ +gpii.tests.transformer.computeDemandedSettingsPathsTests = [ + { + name: "Flat settings object", + settings: { + "http://registry.gpii.net/common/invertColours": ["mouse", "caret"] + }, + expected: ["http://registry\\.gpii\\.net/common/invertColours"] + }, { + name: "Nested settings object", + settings: { + "com.microsoft.windows.magnifier": { + "http://registry.gpii.net/common/invertColours": ["mouse", "caret"], + "http://registry.gpii.net/common/magnification": 2 + } + }, + expected: [ + "http://registry\\.gpii\\.net/common/invertColours", + "http://registry\\.gpii\\.net/common/magnification" + ] + }, { + name: "Concatenated settings path", + settings: { + "http://registry.gpii.net/applications/com.microsoft.windows.mouseSettings": { + "SwapMouseButtons": true, + "ScrollLines": 2 + } + }, + expected: [ + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings", + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.SwapMouseButtons", + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.ScrollLines" + ] + }, { + name: "All registry terms at both levels", + settings: { + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "http://registry.gpii.net/common/invertColours": true, + "FollowFocus": true + } + }, + expected: [ + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.magnifier", + "http://registry\\.gpii\\.net/common/invertColours", + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.magnifier.FollowFocus" + ] + } +]; + +jqUnit.test("gpii.transformer.computeDemandedSettingsPaths() tests", function () { + fluid.each(gpii.tests.transformer.computeDemandedSettingsPathsTests, function (oneTest) { + var result = gpii.transformer.computeDemandedSettingsPaths(oneTest.settings); + jqUnit.assertDeepEq("gpii.transformer.computeDemandedSettingsPaths() test - " + oneTest.name, oneTest.expected, result); + }); +}); + +/* --------------- gpii.transformer.hasSupportedSettings tests -------------------- */ +gpii.tests.transformer.hasSupportedSettingsTests = [ + { + name: "Direct match", + capabilitiesTransformations: { + "Invert": { + "transform": { + "type": "gpii.transformer.booleanToNumber", + "inputPath": "http://registry\\.gpii\\.net/common/invertColours" + } + } + }, + settings: { + "http://registry.gpii.net/common/invertColours": ["mouse", "caret"] + }, + expected: true + }, { + name: "Match a nested path", + capabilitiesTransformations: { + "Invert": { + "transform": { + "type": "gpii.transformer.booleanToNumber", + "inputPath": "http://registry\\.gpii\\.net/common/invertColours" + } + } + }, + settings: { + "com.microsoft.windows.magnifier": { + "http://registry.gpii.net/common/invertColours": ["mouse", "caret"], + "http://registry.gpii.net/common/magnification": 2 + } + }, + expected: true + }, { + name: "Match a concatenated path", + capabilitiesTransformations: { + "SwapMouseButtonsConfig": { + "transform": { + "type": "fluid.transforms.value", + "inputPath": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.SwapMouseButtons", + "outputPath": "value" + }, + "path": { + "literalValue": { + "get": "pvParam", + "set": "uiParam" + } + } + } + }, + settings: { + "http://registry.gpii.net/applications/com.microsoft.windows.mouseSettings": { + "SwapMouseButtons": true, + "ScrollLines": true + } + }, + expected: true + }, { + name: "Match a concatenated path with an array value", + capabilitiesTransformations: { + "SwapMouseButtonsConfig": { + "transform": { + "type": "fluid.transforms.value", + "inputPath": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.SwapMouseButtons", + "outputPath": "value" + }, + "path": { + "literalValue": { + "get": "pvParam", + "set": "uiParam" + } + } + } + }, + settings: { + "http://registry.gpii.net/applications/com.microsoft.windows.mouseSettings": { + "SwapMouseButtons": ["button", "menu"], + "ScrollLines": true + } + }, + expected: true + }, { + name: "Match an element in capabilities but not capabilitiesTransformations", + capabilities: [ + "http://registry\\.gpii\\.net/common/highContrast/enabled", + "http://registry\\.gpii\\.net/common/highContrastTheme" + ], + capabilitiesTransformations: { + "SwapMouseButtonsConfig": { + "transform": { + "type": "fluid.transforms.value", + "inputPath": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.SwapMouseButtons", + "outputPath": "value" + }, + "path": { + "literalValue": { + "get": "pvParam", + "set": "uiParam" + } + } + } + }, + settings: { + "http://registry.gpii.net/common/highContrast/enabled": true + }, + expected: true + }, { + name: "GPII-3784: Match an interim setting path", + capabilitiesTransformations: { + "ImageConfig": { + // This transform is necessary due to issue GPII-3784. + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.desktopBackgroundColor.SolidColor", + "true": "", + "false": "", + "outputPath": "value" + }, + "path": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": "pvParam" + } + } + } + }, + settings: { + "http://registry.gpii.net/applications/com.microsoft.windows.desktopBackgroundColor": { + "SolidColor": { "r": 67, "g": 187, "b": 19 } + } + }, + expected: true + }, { + name: "Cannot find a match", + capabilitiesTransformations: { + "SwapMouseButtonsConfig": { + "transform": { + "type": "fluid.transforms.value", + "inputPath": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.SwapMouseButtons", + "outputPath": "value" + }, + "path": { + "literalValue": { + "get": "pvParam", + "set": "uiParam" + } + } + } + }, + settings: { + "http://registry.gpii.net/applications/com.microsoft.windows.mouseSettings": { + "ScrollLines": true, + "ScrollRouting": false + } + }, + expected: false + } +]; + +jqUnit.test("gpii.transformer.hasSupportedSettings() tests", function () { + fluid.each(gpii.tests.transformer.hasSupportedSettingsTests, function (oneTest) { + var result = gpii.transformer.hasSupportedSettings(oneTest.capabilities, oneTest.capabilitiesTransformations, oneTest.settings); + jqUnit[oneTest.expected ? "assertTrue" : "assertFalse"]("gpii.transformer.hasSupportedSettings() test - " + oneTest.name, result); + }); +}); diff --git a/gpii/node_modules/transformer/test/html/TransformerTests.html b/gpii/node_modules/transformer/test/html/TransformerTests.html deleted file mode 100644 index 78a818a77..000000000 --- a/gpii/node_modules/transformer/test/html/TransformerTests.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - GPII Settings Transformer Tests - - - - - - - - - - - - - - - - -

                          GPII Settings Transformer Tests

                          -

                          -
                          -

                          -
                            - - - - diff --git a/gpii/node_modules/transformer/test/js/TransformerTests.js b/gpii/node_modules/transformer/test/js/TransformerTests.js deleted file mode 100644 index 1fe05b527..000000000 --- a/gpii/node_modules/transformer/test/js/TransformerTests.js +++ /dev/null @@ -1,398 +0,0 @@ -/*! -GPII Settings Transformer Tests - -Copyright 2012 OCAD University -Copyright 2013 Raising the Floor - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -/*global require,jqUnit*/ - -var fluid = fluid || require("infusion"), - $ = fluid.registerNamespace("jQuery"), - gpii = fluid.registerNamespace("gpii"); - -(function () { - fluid.registerNamespace("gpii.tests.transformer"); - - var makeExpectedTransformerOutput = function (solutionsSpec, settingsBlocks) { - var settings = fluid.copy(solutionsSpec); - settingsBlocks = $.makeArray(settingsBlocks); - - fluid.each(settingsBlocks, function (settingsBlock, idx) { - settings[idx].settingsHandlers[0].settings = settingsBlock; - delete settings[idx].settingsHandlers[0].capabilities; - }); - - return settings; - }; - - var gnomeMagnifierSolutionEntry = { - "id": "org.gnome.desktop.a11y.magnifier", - "settingsHandlers": [ - { - "type": "gpii.gsettings.set", - "options": { - "schema": "org.gnome.desktop.a11y.magnifier" - }, - "capabilities": [ - "display.screenEnhancement.screenMagnification.applications.org\\.gnome\\.desktop\\.a11y\\.magnifier.name" - ], - "capabilitiesTransformations": { - "mag-factor": { - "transform": { - "type": "fluid.transforms.linearScale", - "valuePath": "display.screenEnhancement.magnification", - "factor": 100 - } - }, - "show-cross-hairs": "display.screenEnhancement.-provisional-showCrosshairs", - "mouse-tracking": { - "transform": { - "type": "fluid.transforms.valueMapper", - "inputPath": "display.screenEnhancement.tracking", - "options": { - "mouse": { - "outputValue": "centered" - } - } - } - } - } - } - ] - }; - - var onlyGnomeMagnifierSolutionSpec = [ - gnomeMagnifierSolutionEntry - ]; - - var gnomeKeyboardSolutionEntry = { - "name": "GNOME Shell Keyboard Settings", - "id": "org.gnome.desktop.a11y.keyboard", - "settingsHandlers": [ { - "type": "gpii.gsettings.set", - "capabilities": [ - "control.onscreenKeyboard.applications.org\\.gnome\\.desktop\\.a11y\\.keyboard.name", - "display.applications.org\\.gnome\\.desktop\\.a11y\\.keyboard.name" - ], - "capabilitiesTransformations": { - "stickykeys-enable": "control.keyboardEnhancement.stickyKeys", - }, - "options": { - "schema": "org.gnome.desktop.a11y.keyboard" - } - } - ] - }; - - var multipleSolutionSpec = [ - gnomeMagnifierSolutionEntry, - gnomeKeyboardSolutionEntry - ]; - - var appSpecificPreferences = { - "display": { - "screenEnhancement": { - "applications": { - "org.gnome.desktop.a11y.magnifier": { - "name": "GNOME Shell Magnifier", - "priority": 100, - "parameters": { - "show-cross-hairs": true - } - } - } - } - } - }; - - var expectedAppSpecificSettings = makeExpectedTransformerOutput(onlyGnomeMagnifierSolutionSpec, { - "show-cross-hairs": true - }); - - var genericPreferences = { - "display": { - "screenEnhancement": { - "magnification": 2.0, - "tracking": ["mouse"] - } - } - }; - - var expectedGenericSettings = makeExpectedTransformerOutput(onlyGnomeMagnifierSolutionSpec, { - "mag-factor": 200, - "mouse-tracking": "centered" - }); - - var genericAndAppSpecificTransformerPayload = { - "preferences": { - "display": { - "screenEnhancement": { - "magnification": 2.0, - "tracking": ["mouse"], - "applications": { - "org.gnome.desktop.a11y.magnifier": { - "name": "GNOME Shell Magnifier", - "priority": 100, - "parameters": { - "show-cross-hairs": true - } - } - } - } - } - }, - - "solutions": onlyGnomeMagnifierSolutionSpec - }; - - var expectedGenericAndAppSpecificSettings = makeExpectedTransformerOutput(onlyGnomeMagnifierSolutionSpec, { - "mag-factor": 200, - "mouse-tracking": "centered", - "show-cross-hairs": true - }); - - var noMagnificationPreferences = { - "display": { - "screenEnhancement": { - "tracking": ["mouse"] - } - } - }; - - var expectedNoMagnificationSettings = makeExpectedTransformerOutput(onlyGnomeMagnifierSolutionSpec, { - "mouse-tracking": "centered" - }); - - var multipleAppSpecificPreferencesTopLevelAndNested = { - "applications": { - "org.gnome.desktop.a11y.keyboard": { - "name": "GNOME Shell Keyboard", - "priority": 100, - "parameters": { - "stickykeys-enable": true - } - } - }, - - "display": { - "screenEnhancement": { - "tracking": ["mouse"], - "applications": { - "org.gnome.desktop.a11y.magnifier": { - "name": "GNOME Shell Magnifier", - "priority": 100, - "parameters": { - "show-cross-hairs": true - } - } - } - } - } - }; - - var multipleNestedAppSpecificPreferences = { - "display": { - "applications": { - "org.gnome.desktop.a11y.keyboard": { - "name": "GNOME Shell Keyboard", - "priority": 100, - "parameters": { - "stickykeys-enable": true - } - } - }, - "screenEnhancement": { - "tracking": ["mouse"], - "applications": { - "org.gnome.desktop.a11y.magnifier": { - "name": "GNOME Shell Magnifier", - "priority": 100, - "parameters": { - "show-cross-hairs": true - } - } - } - } - } - }; - var expectedMultipleAppSpecificSettings = makeExpectedTransformerOutput(multipleSolutionSpec, [ - { - "mouse-tracking": "centered", - "show-cross-hairs": true - }, - { - "stickykeys-enable": true - } - ]); - - var transformationConfigs = [ - { - name: "only application specific preferences.", - input: { - "preferences": appSpecificPreferences, - "solutions": onlyGnomeMagnifierSolutionSpec - }, - - expectedOutput: expectedAppSpecificSettings - }, - { - name: "only generic preferences.", - input: { - "preferences": genericPreferences, - "solutions": onlyGnomeMagnifierSolutionSpec - }, - - expectedOutput: expectedGenericSettings - }, - { - name: "mix of generic and application-specific preferences.", - input: genericAndAppSpecificTransformerPayload, - expectedOutput: expectedGenericAndAppSpecificSettings - }, - { - name: "transform nonexistent path.", - input: { - preferences: noMagnificationPreferences, - solutions: onlyGnomeMagnifierSolutionSpec - }, - expectedOutput: expectedNoMagnificationSettings - }, - { - name: "multiple app-specific preferences; one at top-level, the other nested.", - input: { - preferences: multipleAppSpecificPreferencesTopLevelAndNested, - solutions: multipleSolutionSpec - }, - expectedOutput: expectedMultipleAppSpecificSettings - }, - { - name: "multiple app-specific preferences, both nested within the hierarchy.", - input: { - preferences: multipleNestedAppSpecificPreferences, - solutions: multipleSolutionSpec - }, - expectedOutput: expectedMultipleAppSpecificSettings - } - ]; - - - var testTransformation = function (input, expectedOutput) { - var transformer = gpii.transformer(); - var actual = transformer.transformSettings(input); - jqUnit.expect(1); - jqUnit.assertDeepEq("The input preferences should be transformed into a valid application-specific settingsHandler block.", - expectedOutput, actual); - }; - - var testTransformationConfig = function (config) { - jqUnit.test("gpii.transformer.transformSettings(), " + config.name, function () { - testTransformation(config.input, config.expectedOutput); - }); - }; - - gpii.tests.transformer.runTests = function () { - jqUnit.module("Settings Transformer"); - - jqUnit.test("gpii.transformer.findApplicationSettings()", function () { - var expected = fluid.get(genericAndAppSpecificTransformerPayload, "preferences.display.screenEnhancement.applications"); - var actual = gpii.transformer.findApplicationSettings(genericAndAppSpecificTransformerPayload.preferences); - jqUnit.assertDeepEq("", expected, actual); - }); - - for (var i = 0; i < transformationConfigs.length; i++) { - var config = transformationConfigs[i]; - testTransformationConfig(config); - } - }; - - /* --------------- gpii.transformer.quantize tests -------------------- */ - var quantizeTests = { - name: "Basic quantize transformations", - rules: { - "foo": { - "transform": { - "type": "gpii.transformer.quantize", - "inputPath": "someval", - "ranges": [ - { - "upperBound": 380, - "output": 1 - }, { - "upperBound": 450, - "output": 2 - }, { - "upperBound": 600, - "output": { - "transform": { - "type": "fluid.transforms.literalValue", - "value": "DOGG", - "outputPath": "bar" - } - } - }, { - "output": 3 - } - ] - } - } - }, - expects: { - "low value check": { - input: { - someval: 200 - }, - expected: { - foo: 1 - } - }, - "on upper bound limit": { - input: { - someval: 380 - }, - expected: { - foo: 1 - } - }, - "interval check": { - input: { - someval: 429 - }, - expected: { - foo: 2 - } - }, - "upper default value": { - input: { - someval: 1000 - }, - expected: { - foo: 3 - } - }, - "expander in output": { - input: { - someval: 500 - }, - expected: { - foo: { - bar: "DOGG" - } - } - } - } - }; - - jqUnit.test("Quantize tests", function () { - fluid.each(quantizeTests.expects, function (test, tname) { - var transformed = fluid.model.transformWithRules(test.input, quantizeTests.rules); - jqUnit.assertDeepEq(quantizeTests.name + " - " + tname, test.expected, transformed); - }); - }); -}()); diff --git a/gpii/node_modules/userListeners/README.md b/gpii/node_modules/userListeners/README.md new file mode 100644 index 000000000..3c85395d2 --- /dev/null +++ b/gpii/node_modules/userListeners/README.md @@ -0,0 +1,34 @@ +# User listeners + +The user listeners start when GPII does. + +## RFID Listener + +`gpii.userlisteners.pcsc` + +* [pcsc.js](src/pcsc.js) +* Uses [@pokusew/pcsclite](https://www.npmjs.com/package/@pokusew/pcsclite) to access the card reader. + This has build dependencies on Linux, which prevents it being tested easily in CI, so the actual reference to this + package is in gpii-windows. +* Cards supported (tested): + * Mifare Classic 1k and 4k + * NTAG203/213/216 + * Mifare Ultralight. + +### Testing + +* Dry testing (with an emulated card) in [pcscTests.js](test/pcscTests.js). +* There are tests that use a real card in [test/cardTests/](test/cardTests). These are not tested in CI (they require a + reader), and only useful for developing/debugging (see [test/cardTests/README.md](test/cardTests/README.md)) + +## USB Listener + +`gpii.userlisteners.usb` + +* [usb.js](src/usb.js) +* Only the OS generic code is here. That is, after the device has been detected, and the mount point is known. + +## Proximity Listener + +* The listener for the proximity device (built into the VAIO) is only available to Windows. Code for that is in the + windows repository. diff --git a/gpii/node_modules/userListeners/index.js b/gpii/node_modules/userListeners/index.js new file mode 100644 index 000000000..b43dee974 --- /dev/null +++ b/gpii/node_modules/userListeners/index.js @@ -0,0 +1,27 @@ +/* + * User listeners. + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"); + +fluid.module.register("userListeners", __dirname, require); + +require("./src/listeners.js"); +require("./src/pcsc.js"); +require("./src/usb.js"); diff --git a/gpii/node_modules/userListeners/package.json b/gpii/node_modules/userListeners/package.json new file mode 100644 index 000000000..ae1848ecd --- /dev/null +++ b/gpii/node_modules/userListeners/package.json @@ -0,0 +1,13 @@ +{ + "name": "userListeners", + "description": "User listeners.", + "version": "0.3.0", + "author": "GPII", + "bugs": "http://issues.gpii.net/browse/GPII", + "homepage": "http://gpii.net/", + "dependencies": {}, + "license" : "BSD-3-Clause", + "repository": "git://github.com/GPII/universal.git", + "main": "./index.js", + "engines": { "node" : ">=4.2.1" } +} diff --git a/gpii/node_modules/userListeners/src/listeners.js b/gpii/node_modules/userListeners/src/listeners.js new file mode 100644 index 000000000..304a96b84 --- /dev/null +++ b/gpii/node_modules/userListeners/src/listeners.js @@ -0,0 +1,210 @@ +/* User listeners + * + * Copyright 2017 Raising the Floor - International + * Copyright 2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"); + +var gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.userListeners"); + +// The user listeners. +fluid.defaults("gpii.userListeners", { + gradeNames: ["fluid.component", "fluid.contextAware"], + contextAwareness: { + platform: { + checks: { + windows: { + contextValue: "{gpii.contexts.windows}", + gradeNames: ["gpii.userListeners.windows"] + } + } + } + }, + components: { + pcsc: { + type: "gpii.userListeners.pcsc", + options: { + events: { + onListenersStart: "{userListeners}.events.onListenersStart", + onListenersStop: "{userListeners}.events.onListenersStop" + } + } + }, + usb: { + type: "gpii.userListeners.usb", + options: { + events: { + onListenersStart: "{userListeners}.events.onListenersStart", + onListenersStop: "{userListeners}.events.onListenersStop" + } + } + } + }, + events: { + // The listeners are starting. + "onListenersStart": null, + // The listeners are stopping. + "onListenersStop": null + }, + listeners: { + "onCreate.startListeners": "{that}.events.onListenersStart" + } +}); + +// A user listener. +fluid.defaults("gpii.userListener", { + gradeNames: ["fluid.component"], + components: { + lifecycleManager: { + type: "gpii.lifecycleManager" + } + }, + events: { + onListenersStart: null, + onListenersStop: null, + + // A token has arrived. + "onTokenArrive": null, + // A token has removed. + "onTokenRemove": null, + // The listener has failed. + "onFail": null + }, + invokers: { + // Start the listener, returning a Promise + startListener: "fluid.notImplemented", + // Stop the listener, returning a Promise + stopListener: "fluid.notImplemented", + // Parse a Buffer containing an NDEF message, returning a string. + decodeNDEF: { + funcName: "gpii.userListeners.decodeNDEF", + args: [ + "{arguments}.0" // NDEF buffer + ] + }, + // The listener has failed, and should be restarted. + failed: { + funcName: "gpii.userListeners.failed", + args: [ + "{that}", + "{arguments}.0" // The error. + ] + } + }, + members: { + // Set to true to call proximityTriggered, otherwise login/logout. + proximity: false, + // Indicates that the logins by this listener are environmental logons (such as auto-logons) [GPII-3988] + environmental: false, + // Override to provide the name of the listener. + listenerName: "no-name", + // Number of failures + failCount: 0 + }, + listeners: { + // Start/stop this listener + "onListenersStart.startListener": "{that}.startListener", + "onListenersStop.stopListener": "{that}.stopListener", + + "onTokenArrive.callFlowManager": { + funcName: "gpii.userListeners.tokenArrived", + args: [ + "{that}", + "{arguments}.1" // token + ] + }, + "onTokenRemove.callFlowManager": { + funcName: "gpii.userListeners.tokenRemoved", + args: [ + "{that}", + "{arguments}.1" // token + ] + } + }, + + // Seconds (multiplied by failureCount) to wait before restarting. + failDelay: 10 +}); + +/** + * Handles the onTokenArrive event. + * + * It calls the "login" action for non-proximity devices, otherwise "proximityTriggered". + * + * @param {Component} that - An instance of gpii.userListener. + * @param {String} token - The token from the user listener. + * @param {Object} loginInfo - [optional] Extra login information + */ +gpii.userListeners.tokenArrived = function (that, token) { + fluid.log(that.listenerName + " token arrived: " + token); + var loginInfo = { + isEnvironmentalLogin: that.environmental + }; + if (that.environmental && that.proximity) { + // proximity listeners are for momentary triggers, which isn't compatible with an environmental state. + fluid.fail("A listener can not be both 'environmental' and 'proximity'"); + } else { + that.lifecycleManager[that.proximity ? "performProximityTriggered" : "performLogin"](token, loginInfo); + } +}; + +/** + * Handles the onTokenRemove event. + * + * It calls the "logout" action for non-proximity devices, otherwise it does nothing. + * + * @param {Component} that - An instance of gpii.userListener. + * @param {String} token - The token from the user listener. + */ +gpii.userListeners.tokenRemoved = function (that, token) { + fluid.log(that.listenerName + " token removed: " + token); + + if (!that.proximity) { + // Temporary workaround for GPII-3769 + setTimeout(function () { + that.lifecycleManager.performLogout(token); + }, 0); + } +}; + +/** + * The listener has failed, and needs to be restarted. To avoid fast repeating failures, the restart will have a delay + * which increases each time. + * + * @param {Component} that - An instance of gpii.userListener. + * @param {Any} err - The error. + */ +gpii.userListeners.failed = function (that, err) { + var delay = Math.min(that.failCount++, 10) * that.options.failDelay; + + fluid.log(fluid.logLevel.WARN, + "Listener " + that.listenerName + " has failed. Restarting in " + delay + " seconds"); + + that.events.onFail.fire(that, err); + + process.nextTick(function () { + that.stopListener().then(function () { + setTimeout(function () { + fluid.log("Restarting " + that.listenerName + " listener"); + that.startListener(); + }, delay * 1000); + }); + }); +}; diff --git a/gpii/node_modules/userListeners/src/pcsc.js b/gpii/node_modules/userListeners/src/pcsc.js new file mode 100644 index 000000000..fe1a808af --- /dev/null +++ b/gpii/node_modules/userListeners/src/pcsc.js @@ -0,0 +1,678 @@ +/* PC/SC user listener. + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +/** + * The cards supported are NTAG-203/213/216, Mifare Ultralight, and Mifare classic 1k/4k + * + * References: + * [ACR-API] "ACR1251U-A1 Application Programming Interface V1.08", + * The spec used for the card commands (the official NXP specs are NDA classified). + * https://www.acs.com.hk/download-manual/4414/API-ACR1251U-1.08.pdf + * + * Mifare classic card memory layout: + * + * [ card ...] + * [ sector 0 ][ sector 1 ][ sector 2 ] ... + * [ reserved ][block 4][block 5][block 6][block 7][block 8][block 9][block 10][block 11] ... + * [ NDEF TLV Tag ...]reserved [... NDEF tag ] [reserved] + * [ NDEF Records ...] + * [ NDEF Record 1 ][Rec 2 ...] + * [header][ GPII Token ][other ...] + * + * Blocks are 16 bytes, the last block of each sector is reserved. + * + * NTAG memory is roughly the same, but no trailer block. Blocks (called pages) are only 4 bytes. + * + */ + +var fluid = require("infusion"), + ndef = require("ndef"); + +var gpii = fluid.registerNamespace("gpii"); + +// The PCSC user listener. +fluid.defaults("gpii.userListeners.pcsc", { + gradeNames: ["fluid.component", "fluid.contextAware", "gpii.userListener"], + contextAwareness: { + platform: { + checks: { + // The PC/SC is an OS specific library, so these mixins are used to perform the actual loading. + windows: { + contextValue: "{gpii.contexts.windows}", + gradeNames: "gpii.userListeners.pcsc.windows" + }, + linux: { + contextValue: "{gpii.contexts.linux}", + gradeNames: "gpii.userListeners.pcsc.linux" + } + } + } + }, + events: { + // A card is presented that doesn't have a tag (or some other problem). + onCardRejected: null + }, + members: { + currentToken: null, + active: false + }, + invokers: { + startListener: { + funcName: "gpii.userListeners.pcsc.startListener", + args: ["{that}"] + }, + stopListener: { + funcName: "gpii.userListeners.pcsc.stopListener", + args: ["{that}"] + }, + getPCSCLite: "fluid.identity", + monitorReader: { + funcName: "gpii.userListeners.pcsc.monitorReader", + args: ["{that}", "{arguments}.0"] + } + } +}); + +// https://pcsclite.alioth.debian.org/api/pcsclite_8h.html +gpii.userListeners.pcsc.SCARD_STATE_EMPTY = 0x0010; +gpii.userListeners.pcsc.SCARD_STATE_PRESENT = 0x0020; +gpii.userListeners.pcsc.SCARD_SHARE_SHARED = 0x0002; + +/** + * Recognised cards and their characteristics. + * @typedef {Object} Card + */ +gpii.userListeners.pcsc.knownCards = [ + { + name: "Mifare", + atr: [ + // Mifare 1k + "3B 8F 80 01 80 4F 0C A0 00 00 03 06 03 00 01 00 00 00 00 6A", + // Mifare 4k + "3B 8F 80 01 80 4F 0C A0 00 00 03 06 03 00 02 00 00 00 00 69" + ], + startBlock: 4, // first sector (4 blocks) is reserved. + blockLength: 16, + skipBlocks: 3, // bit mask. Every 4th block is ignored (The trailer block of a sector). + authenticate: true + }, + { + // NTAG 203/213/216, Mifare ultralight + name: "NTAG", + atr: "3B 8F 80 01 80 4F 0C A0 00 00 03 06 03 00 03 00 00 00 00 68", + startBlock: 4, + blockLength: 4, + skipBlocks: 0, + authenticate: false + } +]; + +// Convert the ATR strings into buffers. +fluid.each(gpii.userListeners.pcsc.knownCards, function (card) { + card.atrs = []; + fluid.each(fluid.makeArray(card.atr), function (atr) { + card.atrs.push(Buffer.from(atr.replace(/\s+/g, ""), "hex")); + }); +}); + +/** + * Start the user listener. + * + * @param {Component} that - The gpii.userListeners.pcsc instance. + * @return {Promise} A resolved promise if it succeeds. + */ +gpii.userListeners.pcsc.startListener = function (that) { + + var promise = fluid.promise(); + + that.active = true; + if (!that.pcsclite) { + that.pcsclite = that.getPCSCLite(); + + if (that.pcsclite) { + that.pcsclite.on("reader", function (reader) { + that.monitorReader(reader); + }); + that.pcsclite.on("error", function (err) { + fluid.log(fluid.logLevel.FAIL, "PCSC error", err); + that.failed(err); + }); + promise.resolve(); + } else { + fluid.log(fluid.logLevel.WARN, "PC/SC user listener is not available."); + promise.reject({ + isError: true, + message: "PC/SC user listener is not available." + }); + } + } + return promise; +}; + +/** + * Stop the user listener + * + * @param {Component} that - The gpii.userListeners.pcsc instance. + * @return {Promise} Resolved with a value of true if the listener was stopped, false if it wasn't running. + */ +gpii.userListeners.pcsc.stopListener = function (that) { + var promise = fluid.promise(); + var value = false; + if (that.pcsclite) { + that.pcsclite.close(); + that.pcsclite = null; + value = true; + } + that.active = false; + promise.resolve(value); + return promise; +}; + +/** + * Monitors a card reader. When a card is presented to the reader, read it and raise the onTokenArrive event when + * a token is found. + * + * @param {Component} that The gpii.userListeners.pcsc instance. + * @param {CardReader} reader - The reader. + */ +gpii.userListeners.pcsc.monitorReader = function (that, reader) { + fluid.log("Got card reader: ", reader.name); + + reader.on("status", function (status) { + var action = gpii.userListeners.pcsc.getCardAction(reader.state, status.state); + if (action === "presented") { + // Get the data from it. + gpii.userListeners.pcsc.readCard(that, reader, status.atr).then(function (tag) { + that.currentToken = tag; + that.events.onTokenArrive.fire(that, that.currentToken); + }, function (err) { + fluid.log(err); + that.events.onCardRejected.fire(that); + }); + } else if (action === "removed") { + if (that.currentToken) { + that.events.onTokenRemove.fire(that, that.currentToken); + that.currentToken = null; + } + } + }); + + reader.on("end", function () { + fluid.log("Lost card reader: ", reader.name); + }); + reader.on("error", function (e) { + // The device has gone, or is in an unknown state - restart the user listener. + fluid.log("Card reader error (" + reader.name + "): ", e.message); + that.failed(e); + }); +}; + +/** + * Get the card action, based on the state change. + * + * @param {Number} oldState - Bit-mask of the old state. + * @param {Number} newState - Bit-mask of the new state. + * @return {String} Action of the card: "presented", "removed", or "unchanged". + */ +gpii.userListeners.pcsc.getCardAction = function (oldState, newState) { + + // Get the states that have changed to on. + var newStates = (oldState ^ newState) & newState; + var action; + if (newStates & gpii.userListeners.pcsc.SCARD_STATE_EMPTY) { + // The "empty" bit has been set. + action = "removed"; + } else if (newStates & gpii.userListeners.pcsc.SCARD_STATE_PRESENT) { + // The "present" bit has been set. + action = "presented"; + } else { + action = "unchanged"; + } + return action; +}; + +/** + * Detect a card based on the ATR (Answer to reset), returning the appropriate member of knownCards. + * + * (contact-less cards don't actually have an ATR; it's emulated by the reader) + * + * @param {Buffer} atr - The card's ATR. + * @return {Object} Item from gpii.userListeners.pcsc.knownCards, or null if unrecognised. + */ +gpii.userListeners.pcsc.detectCard = function (atr) { + if (!atr instanceof Buffer) { + fluid.fail("atr needs to be a Buffer."); + } + + var knownCards = gpii.userListeners.pcsc.knownCards; + + var card = fluid.find(knownCards, function (card) { + return fluid.find(card.atrs, function (atrTest) { + return atrTest.equals(atr) ? card : undefined; + }); + }); + + return card || null; +}; + +/** + * Reads a card for the NDEF message. Resolves with the first text record from the first NDEF text message. + * + * @param {Component} that - The gpii.userListeners.pcsc instance. + * @param {CardReader} reader - The card reader. + * @param {Buffer} atr - The card's ATR. + * @return {Promise} Resolves with the NDEF message text. + */ +gpii.userListeners.pcsc.readCard = function (that, reader, atr) { + var promise = fluid.promise(); + + var cardType = gpii.userListeners.pcsc.detectCard(atr); + if (cardType) { + gpii.userListeners.pcsc.connectToCard(reader, cardType).then(function (card) { + gpii.userListeners.pcsc.readNDEF(card).then(function (tagBuffer) { + var text = that.decodeNDEF(tagBuffer); + if (text) { + promise.resolve(text); + } else { + promise.reject({ + isError: true, + message: "No tag found on card" + }); + } + }, promise.reject); + }, promise.reject); + + promise.then(function () { + reader.disconnect(fluid.identity); + }, function () { + reader.disconnect(fluid.identity); + }); + } else { + promise.reject({ + isError: true, + message: "Unrecognised card" + }); + } + + return promise; +}; + +/** + * Connect to a card. + * + * @param {CardReader} reader - The card reader. + * @param {Object} cardType - Card information, from detectCard. + * @return {Promise} Resolves when a connection has been established. + */ +gpii.userListeners.pcsc.connectToCard = function (reader, cardType) { + var promise = fluid.promise(); + reader.connect({share_mode: reader.SCARD_SHARE_SHARED}, function (err, proto) { + if (err) { + promise.reject({ + isError: true, + message: "Couldn't connect to card", + error: err + }); + } else { + var card = Object.assign({}, cardType); + card.transmit = function (data, expectedLength, callback) { + try { + reader.transmit(data, expectedLength, proto, callback); + } catch (e) { + callback(e, null); + } + }; + promise.resolve(card); + } + }); + + return promise; +}; + +/** + * Sends an APDU to the card, resolving with the response buffer. + * + * The status code (last two bytes) is removed from the response, and a status code of not 9000 (success) will reject. + * + * See https://en.wikipedia.org/wiki/Smart_card_application_protocol_data_unit + * + * @param {Card} card - The card. + * @param {Object} apdu - The APDU. + * @param {byte} apdu.cla - Instruction class (CLA). + * @param {byte} apdu.ins - Instruction code (INS). + * @param {byte} apdu.p1 - Parameter 1 (P1). + * @param {byte} apdu.p2 - Parameter 2 (P2). + * @param {byte} [apdu.lc] - [optional] Command data length (Lc). + * @param {byte|byte[]} [apdu.data] - [optional] Command data. + * @param {byte} [apdu.le] - [optional] Response length expected (Le). + * @param {String} [apdu.text] - [optional] Ignored - used for logging. + * @param {Boolean} ignoreStatusCode - True to ignore the status code (don't reject fail, and keep it in the response). + * @return {Promise} Resolves with the response. + * @typedef {Number} byte + */ +gpii.userListeners.pcsc.sendAPDU = function (card, apdu, ignoreStatusCode) { + // The order in which the bytes go. + var order = ["cla", "ins", "p1", "p2", "lc", "data", "le"]; + // The required bytes. + var required = ["cla", "ins", "p1", "p2"]; + var bytes = []; + + var promise = fluid.promise(); + + // Assemble the bytes. + order.forEach(function (field) { + var gotValue = apdu.hasOwnProperty(field); + var value = gotValue && apdu[field]; + if (!gotValue && required.indexOf(field) > -1) { + // If a required byte is omitted, use zero. + value = 0x00; + gotValue = true; + } + + if (gotValue) { + // Add the byte to the buffer. + if (Array.isArray(value)) { + bytes.push.apply(bytes, value); + } else { + bytes.push(value); + } + + } + }); + + var maxResponseLength = apdu.le || 0; + + var data = Buffer.from(bytes); + var statusCodeLength = 2; + var successResponse = 0x9000; + + fluid.log("APDU: ", data.toString("hex").replace(/(..)/g, "$1 "), apdu.text || " "); + + // Send it to the card. + card.transmit(data, maxResponseLength + statusCodeLength, function (err, buf) { + if (err) { + promise.reject({ + isError: true, + message: "Unable to send command to card", + error: err + }); + } else { + fluid.log("Response: ", buf.toString("hex")); + if (ignoreStatusCode) { + promise.resolve(buf); + } else { + // Status code is the last 2 bytes (and cards are big endian). + var statusCode; + if (buf.length >= statusCodeLength) { + statusCode = buf.readUInt16BE(buf.length - statusCodeLength); + buf = buf.slice(0, -2); + } + + if (statusCode === successResponse) { + promise.resolve(buf); + } else { + promise.reject({ + isError: true, + message: "Non-successful status code from card: " + (statusCode && statusCode.toString(16)), + statusCode: statusCode, + response: buf.toString("hex") + }); + } + } + } + }); + + return promise; +}; + +/** + * Load the authentication keys for a particular block. + * + * @param {Card} card - The card. + * @param {Number} block - The block to authenticate. + * @param {Number} keyType - Key type: 0x60 (type A) or 0x61 (type B). + * @param {Number} keyNumber - Key number: 0 or 1. + * @return {Promise} Resolves when successfully authenticated, rejects on failure. + */ +gpii.userListeners.pcsc.loadAuthenticationKeys = function (card, block, keyType, keyNumber) { + // [ACR-API] 5.4.2 Authentication for MIFARE Classic (1K/4K) + var apdu = { + text: "Load Authentication Keys (block " + block + ", key " + keyType.toString(16) + ", keyNumber " + keyNumber + ")", + cla: 0xff, + ins: 0x86, + p1: 0x00, + p2: 0x00, + lc: 0x05, + data: [ + 0x01, // version: 1 + 0x00, // always 0 + block, // block number + keyType, // key type + keyNumber // key number + ] + }; + + return gpii.userListeners.pcsc.sendAPDU(card, apdu); +}; + +/** + * Authenticate a block, if required, so it can be read from. + * + * @param {Card} card - The card. + * @param {Number} block - The block to authenticate. + * @return {Promise} Resolves when it's done. + */ +gpii.userListeners.pcsc.authenticateBlock = function (card, block) { + + var authRequired = false; + if (card.authenticate) { + // Mifare: Only need to authenticate once per sector - When a block is authenticated, the other blocks in the + // sector are also accessible. + var sectorSize = 4; + var sector = Math.floor(block / sectorSize); + card.authed = card.authed || {}; + authRequired = !card.authed[sector]; + card.authed[sector] = true; + } + + var promise = fluid.promise(); + if (authRequired) { + // There are two keys (0 and 1), and there are two types of key (TYPE A or B). Try each one until it successful. + var typeA = 0x60, + typeB = 0x61; + var keys = [ + { type: typeA, number: 0 }, + { type: typeB, number: 0 }, + { type: typeA, number: 1 }, + { type: typeB, number: 1 } + ]; + + // Try each key until successful. + var tryKey = function (keyIndex, attempt) { + keyIndex = keyIndex % keys.length; + gpii.userListeners.pcsc.loadAuthenticationKeys(card, block, keys[keyIndex].type, keys[keyIndex].number) + .then(function () { + // Use the same one next time. + gpii.userListeners.pcsc.keyIndex = keyIndex; + promise.resolve(); + }, function (err) { + if (attempt > keys.length) { + promise.reject({ + isError: true, + message: "Unable to authenticate block.", + error: err + }); + } else { + tryKey(keyIndex + 1, attempt + 1); + } + }); + }; + + tryKey(gpii.userListeners.pcsc.lastKeyIndex || 0, 0); + } else { + promise.resolve(); + } + + return promise; +}; + +/** + * Reads the next block from the card. + * + * The block is authenticated, and the data is cached. + * + * @param {Card} card - The card. + * @return {Promise} Resolves with the data. + */ +gpii.userListeners.pcsc.readNextBlock = function (card) { + var promise = fluid.promise(); + + var block = card.nextBlock || card.startBlock; + if (card.skipBlocks && (block & card.skipBlocks) === card.skipBlocks) { + // Ignore the trailer block (Mifare) + block++; + } + + card.nextBlock = block + 1; + + if (!card.blockCache) { + card.blockCache = {}; + } + + if (card.blockCache[block]) { + promise.resolve(card.blockCache[block]); + } else { + // [ACR-API] 5.4.3. Read Binary Blocks + gpii.userListeners.pcsc.authenticateBlock(card, block).then(function () { + gpii.userListeners.pcsc.sendAPDU(card, { + text: "Read block " + block, + cla: 0xFF, + ins: 0xB0, + p1: 0x00, + p2: block, + le: card.blockLength + }).then(function (data) { + card.blockCache[block] = data; + promise.resolve(data); + }, promise.reject); + }, promise.reject); + } + return promise; +}; + +/** + * Read some bytes from the card, starting at the start block. + * + * @param {Card} card - The card. + * @param {Number} length - Number of bytes to read. + * @return {Promise} Resolves with the data. + */ +gpii.userListeners.pcsc.readBytes = function (card, length) { + var promise = fluid.promise(); + var sequence = []; + card.nextBlock = card.startBlock; + + for (var bytes = 0, block = 0; bytes < length; bytes += card.blockLength, block++) { + sequence.push(gpii.userListeners.pcsc.readNextBlock); + } + + fluid.promise.sequence(sequence, card).then(function (result) { + var allData = Buffer.concat(result); + promise.resolve(allData); + }, promise.reject); + + return promise; +}; + +/** + * Get the NDEF tag from a card. + * + * @param {Card} card - The card. + * @return {Promise} - A promise that will be resolved with the results of a read or rejected on error. + */ +gpii.userListeners.pcsc.readNDEF = function (card) { + var promise = fluid.promise(); + + gpii.userListeners.pcsc.readBytes(card, 16).then(function (buf) { + var offset = 0; + var tagType; + + // Get the first TLV that's an NDEF + // Tag: 1 byte (0x03 for NDEF) + // Length: 1 byte, or 2 bytes if at least 0xff + // Value: bytes (the NDEF message) + + var tagLength; + while (offset <= buf.length - 3) { + // Skip leading 0's + do { + tagType = buf[offset++]; + } while (!tagType && offset < buf.length - 2); + + tagLength = buf[offset++]; + if (tagLength === 0xff) { + // Length is 2 bytes, 0xff to 0xffff + tagLength |= (buf[offset++] << 8); + } + + if (tagType === 0x03) { + break; + } else { + // ignore this tag + offset += tagLength; + } + } + + if (tagType === 0x03) { + gpii.userListeners.pcsc.readBytes(card, offset + tagLength).then(function (buf) { + promise.resolve(buf.slice(offset, offset + tagLength)); + }, promise.reject); + } else { + promise.resolve(null); + } + + }, promise.reject); + + return promise; +}; + +/** + * Decodes an NDEF message (the tag from a card), returning the value of the first text record. + * + * @param {Array} tagBuffer - The tag buffer. + * @return {String} The text value, or null if none. + */ +gpii.userListeners.decodeNDEF = function (tagBuffer) { + var text = null; + if (tagBuffer) { + // Parse the NDEF buffer. + var bytes = Array.prototype.slice.call(tagBuffer); + var records = ndef.decodeMessage(bytes); + // Get the first text record. + var textRecord = records.find(function (record) { + return record.type === ndef.RTD_TEXT; + }); + + if (textRecord) { + text = textRecord.value; + } + } + + return text; +}; diff --git a/gpii/node_modules/userListeners/src/usb.js b/gpii/node_modules/userListeners/src/usb.js new file mode 100644 index 000000000..8d0698d78 --- /dev/null +++ b/gpii/node_modules/userListeners/src/usb.js @@ -0,0 +1,127 @@ +/* USB user listener. + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + fs = require("fs"), + path = require("path"); + +var gpii = fluid.registerNamespace("gpii"); + +// The USB user listener. +fluid.defaults("gpii.userListeners.usb", { + gradeNames: ["fluid.component", "fluid.contextAware", "gpii.userListener"], + contextAwareness: { + platform: { + checks: { + windows: { + contextValue: "{gpii.contexts.windows}", + gradeNames: "gpii.userListeners.usb.windows" + }, + linux: { + contextValue: "{gpii.contexts.linux}", + gradeNames: "gpii.userListeners.usb.linux" + } + } + } + }, + events: { + // A volume has been mounted + onMount: null, // (that, mountPoint) + // A volume has been unmounted + onUnmount: null // (that, mountPoint) + }, + members: { + tokenFile: ".gpii-user-token.txt", + currentDevice: { + token: null, + path: null + } + }, + listeners: { + "onMount.readToken": "{that}.readUSBToken({that}, {arguments}.1)", + "onUnmount.deviceRemoved": "{that}.deviceRemoved({that}, {arguments}.1)" + }, + invokers: { + startListener: "fluid.identity", + stopListener: "fluid.identity", + readUSBToken: { + funcName: "gpii.userListeners.usb.readUSBToken", + args: ["{that}", "{arguments}.1"] + }, + deviceRemoved: { + funcName: "gpii.userListeners.usb.deviceRemoved", + args: ["{that}", "{arguments}.1"] + } + } +}); + +/** + * Reads a token from the USB device. If one is found, the onTokenArrive event is raised. + * + * If there's already a device with a token, then the new one takes precedence. See GPII-2682. + * + * @param {Component} that - The gpii.userListeners.usb instance. + * @param {String} usbPath - The root of the USB device. + * @return {Promise} resolves when the token is read, rejects on error (or there's already a device) + */ +gpii.userListeners.usb.readUSBToken = function (that, usbPath) { + var promise = fluid.promise(); + + // Try to read the token file. + var tokenFile = path.join(usbPath, that.tokenFile); + fs.readFile(tokenFile, "utf8", function (err, data) { + if (err) { + fluid.log("Error reading token from device (" + usbPath + "):"); + promise.reject({ + isError: true, + message: "Error reading token from device (" + usbPath + ")", + error: err + }); + } else { + that.currentDevice.path = usbPath; + fluid.log("Got token from USB device (" + that.currentDevice.path + "):", that.currentDevice.token); + if (that.currentDevice.token !== data.trim()) { + that.currentDevice.token = data.trim(); + that.events.onTokenArrive.fire(that, that.currentDevice.token); + } else { + fluid.log("Ignoring key-in request. Got the same token from a different USB device (" + that.currentDevice.path + "):", + that.currentDevice.token); + } + promise.resolve(that.currentDevice.token); + } + }); + + return promise; +}; + +/** + * Called when a device has been removed. If the device had a token, then the onTokenRemove event is raised. + * + * @param {Component} that - The gpii.userListeners.usb instance. + * @param {String} usbPath - The root of the USB device. + * @return {Boolean} True if the device had a token that was used to login. + */ +gpii.userListeners.usb.deviceRemoved = function (that, usbPath) { + var wasActive = that.currentDevice.path === usbPath; + if (wasActive) { + that.events.onTokenRemove.fire(that, that.currentDevice.token); + that.currentDevice.token = that.currentDevice.path = null; + } + return wasActive; +}; diff --git a/gpii/node_modules/userListeners/test/all-tests.js b/gpii/node_modules/userListeners/test/all-tests.js new file mode 100644 index 000000000..1b2f7faba --- /dev/null +++ b/gpii/node_modules/userListeners/test/all-tests.js @@ -0,0 +1,22 @@ +/* + * GPII User listener tests + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +require("./pcscTests.js"); +require("./usbTests.js"); diff --git a/gpii/node_modules/userListeners/test/cardTests/README.md b/gpii/node_modules/userListeners/test/cardTests/README.md new file mode 100644 index 000000000..1832fb991 --- /dev/null +++ b/gpii/node_modules/userListeners/test/cardTests/README.md @@ -0,0 +1,50 @@ +# Card Tests + +These tests are not performed by CI - they operate on a real card. + +## The tests + +`real-card-tests.js` performs the following tests: + +* Detect card type. +* Get the token from the card. + +### Usage + +The universal repo doesn't provide the pcsc library. In order to get this, either run inside gpii-windows, or +run `npm install @pokusew/pcsclite`. + +1. Attach a reader. +2. Run `node real-card-tests.js`. +3. Put a card on the reader. The card can contain any tag. +4. The tests will run, showing PASS or FAIL at the end. + +## Transaction logger + +`record.js` records the transaction of retrieving a token from the card, outputting json that can be placed in +[cards.json5](../testData/cards.json5). These transactions are used to mock cards, in the unit tests. + +### Usage + +1. Attach a reader. +2. Run `node record.js` +3. Put a card on the reader. The card can contain any tag. +4. The tag will be read from the card, and some JSON will be displayed. Add a `name` field, and the JSON can be used in + [cards.json5](../testData/cards.json5) to mock the card. + +### Example log + +```json +{ + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046000": "6300", + "ff860000050100046100": "9000", + "ffb0000410": "030bd101075402656e656c6f64fe00009000" + }, + "expect": { + "text": "elod" + } +} +``` diff --git a/gpii/node_modules/userListeners/test/cardTests/real-card-tests.js b/gpii/node_modules/userListeners/test/cardTests/real-card-tests.js new file mode 100644 index 000000000..3c3307c4d --- /dev/null +++ b/gpii/node_modules/userListeners/test/cardTests/real-card-tests.js @@ -0,0 +1,166 @@ +/* + * Tests that use a real card - not invoked by CI. + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; +var fluid = require("infusion"), + kettle = fluid.require("kettle"); + +kettle.loadTestingSupport(); + +var jqUnit = fluid.require("node-jqunit"); +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.tests.card"); + +var teardowns = []; +jqUnit.module("gpii.tests.cards", { + teardown: function () { + while (teardowns.length) { + teardowns.pop()(); + } + } +}); + +require("../../index.js"); + +var Pcsclite = require("@pokusew/pcsclite"); + +fluid.defaults("gpii.userListeners.pcsc.test", { + invokers: { + getPCSCLite: "gpii.tests.card.getPCSCLite", + startListener: "fluid.identity" + } +}); + +/* + * Get an instance of the gpii.userListeners.pcsc component. + */ +gpii.tests.card.getPcscListener = function () { + var userListeners = gpii.userListeners({ + listeners: { + "onCreate.startListeners": "fluid.identity" + }, + distributeOptions: { + record: "gpii.userListeners.pcsc.test", + target: "{that pcsc}.options.gradeNames" + } + }); + + return userListeners.pcsc; +}; + +gpii.tests.card.getPCSCLite = function () { + return new Pcsclite(); +}; + +gpii.tests.card.connectToCard = function (pcsc) { + var promise = fluid.promise(); + + pcsc.on("reader", function (reader) { + fluid.log("Got card reader", reader.name); + + reader.on("status", function (status) { + var action = gpii.userListeners.pcsc.getCardAction(reader.state, status.state); + if (action === "presented" && !promise.disposition) { + reader.connect({share_mode: reader.SCARD_SHARE_SHARED}, function (err, proto) { + if (err) { + promise.reject(err); + } else { + process.nextTick(promise.resolve, { reader: reader, atr: status.atr, protocol: proto }); + } + }); + } + }); + }); + + pcsc.on("error", function (err) { + fluid.log("PCSC error", err); + }); + + teardowns.push(function () { + pcsc.close(); + }); + + return promise; +}; + +// Tests connectToCard and readNDEF (and the lower level functions) using a real card. +jqUnit.asyncTest("Card connect and read", function () { + + jqUnit.expect(2); + var pcsc = gpii.tests.card.getPCSCLite(); + + var timeout = setTimeout(jqUnit.fail, 5000, "timed out waiting for card"); + teardowns.push(function () { + clearTimeout(timeout); + }); + + // Connect to it. + gpii.tests.card.connectToCard(pcsc).then(function (result) { + var atr = result.atr, + reader = result.reader; + + fluid.log("ATR", atr.toString("hex")); + + jqUnit.assert("connected to card"); + + // See if the card is one of the known ones. + var cardType = gpii.userListeners.pcsc.detectCard(atr); + + jqUnit.assertNotNull("card should be a known type", card); + + if (!cardType) { + // Best not to perform any more tests if it's an unknown card. + fluid.log(fluid.logLevel.FATAL, "Not continuing with an unknown card."); + process.exit(1); + return; + } + + fluid.log("Card type:", cardType.name); + var card = Object.assign({ + transmit: function (data, expectedLength, callback) { + return reader.transmit(data, expectedLength, result.protocol, callback); + } + }, cardType); + + gpii.userListeners.pcsc.readNDEF(card).then(function (ndef) { + fluid.log("NDEF: ", ndef.toString("hex")); + fluid.log("TAG: ", gpii.userListeners.decodeNDEF(ndef)); + jqUnit.start(); + }, function (err) { + jqUnit.fail(err); + }).then(function () { + reader.disconnect(fluid.identity); + }); + + }); +}); + +// Tests the user listener with a real card. +jqUnit.asyncTest("Card user listener", function () { + jqUnit.expect(3); + var listener = gpii.tests.card.getPcscListener(); + + listener.events.onTokenArrive.addListener(function (that, token) { + jqUnit.assert("got token"); + jqUnit.assertEquals("that", listener, that); + jqUnit.assertNotNull("token", token); + jqUnit.start(); + }); + + gpii.userListeners.pcsc.startListener(listener); +}); diff --git a/gpii/node_modules/userListeners/test/cardTests/record.js b/gpii/node_modules/userListeners/test/cardTests/record.js new file mode 100644 index 000000000..0e2cec98f --- /dev/null +++ b/gpii/node_modules/userListeners/test/cardTests/record.js @@ -0,0 +1,111 @@ +/* + * Generates transaction logs from a real card. + * This uses a real card, and isn't invoked by CI. + * + * Copyright 2017 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.tests.card"); + +require("../../index.js"); +var Pcsclite = require("@pokusew/pcsclite"); + +fluid.setLogLevel(fluid.logLevel.INFO); + +var pcsc = new Pcsclite(); + +gpii.tests.card.connectToCard = function () { + var promise = fluid.promise(); + + pcsc.on("reader", function (reader) { + fluid.log("Got card reader", reader.name); + + reader.on("status", function (status) { + var action = gpii.userListeners.pcsc.getCardAction(reader.state, status.state); + if (action === "presented" && !promise.disposition) { + reader.connect({share_mode: reader.SCARD_SHARE_SHARED}, function (err, proto) { + if (err) { + promise.reject(err); + } else { + process.nextTick(promise.resolve, { reader: reader, atr: status.atr, protocol: proto }); + } + }); + } + }); + }); + + pcsc.on("error", function (err) { + fluid.log("PCSC error", err); + }); + + return promise; +}; + +// Connect to the card. +gpii.tests.card.connectToCard().then(function (result) { + var atr = result.atr, + reader = result.reader; + + fluid.log("connected to card"); + + // See if the card is one of the known ones. + var cardType = gpii.userListeners.pcsc.detectCard(atr); + + var log = { + atr: atr, + cardType: cardType.name, + commands: {} + }; + + var card = Object.assign({ + transmit: function (data, expectedLength, callback) { + return reader.transmit(data, expectedLength, result.protocol, function (err, response) { + log.commands[data.toString("hex")] = response; + callback(err, response); + }); + } + }, cardType); + + gpii.userListeners.pcsc.readNDEF(card).then(function (tagBuffer) { + var text = gpii.userListeners.decodeNDEF(tagBuffer); + + log.expect = { + //ndef: tagBuffer, + text: text + }; + var json = JSON.stringify(log, function (key, value) { + if (value && value.type === "Buffer") { + return fluid.transform(value.data, function (value) { + return (value <= 0xf ? "0" : "") + value.toString(16); + }).join(""); + } else { + return value; + } + }, " "); + + fluid.log("Transaction log:\n", json); + + }, function (err) { + fluid.fail(err); + + }).then(function () { + reader.disconnect(fluid.identity); + pcsc.close(); + process.exit(); + }); +}); diff --git a/gpii/node_modules/userListeners/test/pcscTests.js b/gpii/node_modules/userListeners/test/pcscTests.js new file mode 100644 index 000000000..49c7fa5fa --- /dev/null +++ b/gpii/node_modules/userListeners/test/pcscTests.js @@ -0,0 +1,287 @@ +/* + * PCSC User listener tests + * + * Copyright 2017 Raising the Floor - International + * Copyright 2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; +var fluid = require("infusion"); + +var jqUnit = fluid.require("node-jqunit"); +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.tests.userListener"); + +require("../index.js"); +require("./shared/lifecycleManagerTestGrade.js"); + +jqUnit.module("gpii.tests.userListener.pcsc"); + +require("json5/lib/register"); + +gpii.tests.userListener.detectCardTest = require("./testData/detectCard.json5"); +gpii.tests.userListener.cardStateTests = require("./testData/cardStates.json5"); +gpii.tests.userListener.APDUtests = require("./testData/apdu.json5"); +gpii.tests.userListener.cardTests = require("./testData/cards.json5"); + +jqUnit.test("PC/SC listener - Detect card", function () { + + var index = 0; + fluid.each(gpii.tests.userListener.detectCardTest, function (test) { + var messageSuffix = " - index=" + index++; + + var atrBuffer = Buffer.from(test.atr.replace(/\s+/g, ""), "hex"); + + var result = gpii.userListeners.pcsc.detectCard(atrBuffer); + + if (test.expect === null) { + jqUnit.assertNull("card should not be detected" + messageSuffix, result); + } else { + jqUnit.assertNotNull("card should be detected" + messageSuffix, result); + jqUnit.assertEquals("card should be correctly detected" + messageSuffix, test.expect, result.name); + } + }); +}); + +jqUnit.test("PC/SC listener - getCardAction", function () { + + var states = { + SCARD_STATE_EMPTY: gpii.userListeners.pcsc.SCARD_STATE_EMPTY, + SCARD_STATE_PRESENT: gpii.userListeners.pcsc.SCARD_STATE_PRESENT + }; + + var resolveState = function (value) { + return states.hasOwnProperty(value) ? states[value] : value; + }; + + var index = 0; + fluid.each(gpii.tests.userListener.cardStateTests, function (test) { + var old = resolveState(test.old); + var current = resolveState(test.current); + var result = gpii.userListeners.pcsc.getCardAction(old, current); + jqUnit.assertEquals("getCardAction should return the expected action - index " + index++, test.expect, result); + }); +}); + +jqUnit.test("PC/SC listener - sendAPDU", function () { + + var messageSuffix; + var index = 0; + + fluid.each(gpii.tests.userListener.APDUtests, function (test) { + messageSuffix = " - index " + index++; + if (test.apdu && test.apdu.text) { + messageSuffix += ": " + test.apdu.text; + } + + // Check the correct data is sent to the card. + var card = { + transmit: function (data, responseLength, callback) { + jqUnit.assertTrue("data is a Buffer" + messageSuffix, data instanceof Buffer); + var dataString = data.toString("hex"); + jqUnit.assertEquals("data must match expected" + messageSuffix, test.expect.data, dataString); + + jqUnit.assertEquals("responseLength matches expected (+ 2 byte status)" + messageSuffix, + test.expect.responseLength + 2, responseLength); + + jqUnit.assertTrue("callback must be a function" + messageSuffix, callback instanceof Function); + + callback(test.error, Buffer.from(test.responseData, "hex")); + } + }; + + var promise = gpii.userListeners.pcsc.sendAPDU(card, test.apdu); + + jqUnit.assertTrue("sendAPDU must return a promise" + messageSuffix, fluid.isPromise(promise)); + + // Check how it resolves + promise.then(function (responseData) { + jqUnit.assertFalse("must only resolve on non-error" + messageSuffix, test.expect.error); + + var responseString = responseData.toString("hex"); + jqUnit.assertEquals("responseData must match expected" + messageSuffix, + test.expect.responseData, responseString); + + }, function () { + jqUnit.assertTrue("must only reject on error" + messageSuffix, test.expect.error); + }); + }); + +}); + +fluid.defaults("gpii.test.userListeners.pcsc", { + gradeNames: ["gpii.userListeners.pcsc"], + invokers: { + getPCSCLite: "fluid.identity", + startListener: "fluid.identity" + }, + components: { + lifecycleManager: { + type: "gpii.tests.userListener.lifecycleManager" + } + } +}); + +/** + * Creates a card reader that emulates a real reader, by returning a canned response to commands. + * + * @param {Object} options - Options. + * @param {Function} options.onDisconnect - The callback. + * @return {CardReader} The card reader. + */ +gpii.tests.userListener.mockReader = function (options) { + options = Object.assign({}, options); + + var EventEmitter = require("events"); + var reader = new EventEmitter(); + + reader.name = "Mock reader"; + reader.state = gpii.userListeners.pcsc.SCARD_STATE_EMPTY; + + // Send a command. + reader.transmit = function (data, responseLength, protocol, callback) { + if (reader.commands.fail) { + reader.testFail(); + return; + } + var dataString = data.toString("hex"); + // Return 6300 (a general failure response) if the command isn't recognised. + var response = reader.commands[dataString] || Buffer.from([0x63, 0x00]); + process.nextTick(callback, null, response); + }; + + // Connect to the card. + reader.connect = function (options, callback) { + process.nextTick(callback, null, "T1"); + }; + + reader.disconnect = function (callback) { + options.onDisconnect && options.onDisconnect(); + callback && callback(); + }; + + // Simulate an error. + reader.testFail = function () { + reader.emit("error", {message: "test failure"}); + }; + + // Pretend a card has been presented. + reader.setCard = function (atr, commands) { + if (commands.fail) { + if (commands.fail === "presented") { + reader.testFail(); + return; + } + reader.commands = commands; + } else { + reader.commands = {}; + fluid.each(commands, function (response, command) { + reader.commands[command] = Buffer.from(response, "hex"); + }); + } + reader.state = gpii.userListeners.pcsc.SCARD_STATE_EMPTY; + + reader.emit("status", { + state: gpii.userListeners.pcsc.SCARD_STATE_PRESENT, + atr: Buffer.from(atr, "hex") + }); + reader.state = gpii.userListeners.pcsc.SCARD_STATE_PRESENT; + }; + + // Remove the card. + reader.removeCard = function () { + reader.emit("status", { + state: gpii.userListeners.pcsc.SCARD_STATE_EMPTY, + atr: null + }); + }; + + return reader; +}; + +// Test the user listener, with a mock card. +jqUnit.asyncTest("PC/SC listener - mock card", function () { + + jqUnit.expect(gpii.tests.userListener.cardTests.length * 3 + 20); + var index = 0; + + var listener = gpii.test.userListeners.pcsc(); + var reader; + + var messageSuffix; + var currentTest; + var testIndex = -1; + var disconnected = false; + + var nextTest = function () { + if (currentTest) { + if (!disconnected) { + fluid.fail("onDisconnect wasn't fired"); + } + } + disconnected = false; + + testIndex++; + if (testIndex >= gpii.tests.userListener.cardTests.length) { + jqUnit.start(); + } else { + currentTest = gpii.tests.userListener.cardTests[testIndex]; + messageSuffix = " - test: " + currentTest.test + ", index " + index++; + reader.setCard(currentTest.atr, currentTest.commands); + } + }; + + listener.events.onCardRejected.addListener(function () { + jqUnit.assertNotEquals("listener trigger should only fire onCardRejected when a card is presented" + + messageSuffix, -1, testIndex); + + jqUnit.assertNull("cards with a token should succeed" + messageSuffix, currentTest.expect.text); + + jqUnit.assert("balance the numbers for jqUnit.expect"); + nextTest(); + }); + listener.events.onTokenArrive.addListener(function (that, token) { + jqUnit.assertNotEquals("listener trigger should only fire onTokenArrive when a card is presented" + + messageSuffix, -1, testIndex); + + jqUnit.assertNotNull("cards with no token should not succeed" + messageSuffix, currentTest.expect.text); + jqUnit.assertEquals("token should match expected" + messageSuffix, currentTest.expect.text, token); + nextTest(); + }); + listener.events.onFail.addListener(function (that, err) { + jqUnit.assertTrue("listener should only fail when expected", !!currentTest.expect.fail); + jqUnit.assertEquals("listener should only fail with the expected error", "test failure", err.message); + + jqUnit.assert("balance the numbers for jqUnit.expect"); + + // onDisconnect will not have been called. + disconnected = true; + + setTimeout(nextTest, 500); + }); + + reader = gpii.tests.userListener.mockReader({ + onDisconnect: function () { + disconnected = true; + } + }); + + listener.monitorReader(reader); + listener.options.failDelay = 0; + + reader.removeCard(); + + nextTest(); +}); diff --git a/gpii/node_modules/userListeners/test/shared/lifecycleManagerTestGrade.js b/gpii/node_modules/userListeners/test/shared/lifecycleManagerTestGrade.js new file mode 100644 index 000000000..3b4a3a1a8 --- /dev/null +++ b/gpii/node_modules/userListeners/test/shared/lifecycleManagerTestGrade.js @@ -0,0 +1,31 @@ +/* + * Lifecycle Manager Test Grade for User listener tests + * + * Copyright 2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +fluid.defaults("gpii.tests.userListener.lifecycleManager", { + gradeNames: ["fluid.component"], + invokers: { + performLogin: { + funcName: "jqUnit.assert", + args: ["performLogin has been called"] + }, + performLogout: { + funcName: "jqUnit.assert", + args: ["performLogout has been called"] + }, + performProximityTriggered: { + funcName: "jqUnit.assert", + args: ["performProximityTriggered has been called"] + } + } +}); diff --git a/gpii/node_modules/userListeners/test/testData/apdu.json5 b/gpii/node_modules/userListeners/test/testData/apdu.json5 new file mode 100644 index 000000000..367698b2d --- /dev/null +++ b/gpii/node_modules/userListeners/test/testData/apdu.json5 @@ -0,0 +1,153 @@ +// Used by pcscTests.js to test various APDUs +[ + { + "apdu": { + "text": "all fields", + "cla": 0x01, + "ins": 0x02, + "p1": 0x03, + "p2": 0x04, + "lc": 0x05, + "le": 0x06, + "data": [ + 0x07, + 0x08, + 0x09 + ] + }, + "responseData": "9000", + "error": null, + + "expect": { + "data": "010203040507080906", + "responseLength": 0x06, + "error": false, + "responseData": "" + } + }, + { + "apdu": { + "text": "single byte data", + "cla": 0x01, + "ins": 0x02, + "p1": 0x03, + "p2": 0x04, + "lc": 0x05, + "le": 0x06, + "data": 0x07 + }, + "responseData": "9000", + "error": null, + + "expect": { + "data": "01020304050706", + "responseLength": 0x06, + "error": false, + "responseData": "" + } + }, + { + "apdu": { + "text": "no Le", + "cla": 0x01, + "ins": 0x02, + "lc": 0x05, + "data": 0x07 + }, + "responseData": "9000", + "error": null, + + "expect": { + "data": "010200000507", + "responseLength": 0x00, + "error": false, + "responseData": "" + } + }, + { + "apdu": { + "text": "empty APDU" + }, + "responseData": "9000", + "error": null, + + "expect": { + "data": "00000000", + "responseLength": 0x00, + "error": false, + "responseData": "" + } + }, + { + "apdu": { + "text": "error response" + }, + "responseData": "6400", + "error": null, + + "expect": { + "data": "00000000", + "responseLength": 0x00, + "error": true, + "responseData": "" + } + }, + { + "apdu": { + "text": "single-byte response" + }, + "responseData": "aa9000", + "error": null, + + "expect": { + "data": "00000000", + "responseLength": 0x00, + "error": false, + "responseData": "aa" + } + }, + { + "apdu": { + "text": "multi-byte response" + }, + "responseData": "aabbccddeeff9000", + "error": null, + + "expect": { + "data": "00000000", + "responseLength": 0x00, + "error": false, + "responseData": "aabbccddeeff" + } + }, + { + "apdu": { + "text": "error response" + }, + "responseData": "6400", + "error": null, + + "expect": { + "data": "00000000", + "responseLength": 0x00, + "error": true, + "responseData": "" + } + }, + { + "apdu": { + "text": "reader error" + }, + "responseData": "", + "error": { + "err": "some error" + }, + + "expect": { + "data": "00000000", + "responseLength": 0x00, + "error": true, + "responseData": "" + } + } +] diff --git a/gpii/node_modules/userListeners/test/testData/cardStates.json5 b/gpii/node_modules/userListeners/test/testData/cardStates.json5 new file mode 100644 index 000000000..388d1fa7e --- /dev/null +++ b/gpii/node_modules/userListeners/test/testData/cardStates.json5 @@ -0,0 +1,48 @@ +// Used by pcscTests.js to test the response to different card states +[ + { + "old": 0, + "current": 0, + "expect": "unchanged" + }, + { + "old": 0, + "current": "SCARD_STATE_EMPTY", + "expect": "removed" + }, + { + "old": 0, + "current": "SCARD_STATE_PRESENT", + "expect": "presented" + }, + { + "old": "SCARD_STATE_EMPTY", + "current": 0, + "expect": "unchanged" + }, + { + "old": "SCARD_STATE_EMPTY", + "current": "SCARD_STATE_EMPTY", + "expect": "unchanged" + }, + { + "old": "SCARD_STATE_EMPTY", + "current": "SCARD_STATE_PRESENT", + "expect": "presented" + }, + { + "old": "SCARD_STATE_PRESENT", + "current": 0, + "expect": "unchanged" // not "removed", because the empty bit isn't set + }, + { + "old": "SCARD_STATE_PRESENT", + "current": "SCARD_STATE_EMPTY", + "expect": "removed" + }, + { + "old": "SCARD_STATE_PRESENT", + "current": "SCARD_STATE_PRESENT", + "expect": "unchanged" + } +] diff --git a/gpii/node_modules/userListeners/test/testData/cards.json5 b/gpii/node_modules/userListeners/test/testData/cards.json5 new file mode 100644 index 000000000..663a13f0b --- /dev/null +++ b/gpii/node_modules/userListeners/test/testData/cards.json5 @@ -0,0 +1,452 @@ +// Used by pcscTests.js to mock cards. +// ../cardTests/record.js is used to create a transaction log from a real card. +[ + // Message sizes + { + "test": "Short message (NTAG)", + "atr": "3b8f8001804f0ca0000003060300030000000068", + "cardType": "NTAG", + "commands": { + "ffb0000404": "0103a00c9000", + "ffb0000504": "340308d19000", + "ffb0000604": "010454029000", + "ffb0000704": "656e41fe9000" + }, + "expect": { + "text": "A" + } + }, + { + "test": "Short message (Mifare)", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046000": "9000", + "ffb0000410": "0308d101045402656e41fe00000000009000" + }, + "expect": { + "text": "A" + } + }, + { + "test": "Data exactly 1 block length", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046000": "9000", + "ffb0000410": "030dd101095402656e616263646566fe9000" + }, + "expect": { + "text": "abcdef" + } + }, + { + "test": "Data exactly 2 blocks", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046000": "9000", + "ffb0000410": "031dd101195402656e616263646566679000", + "ffb0000510": "68696a6b6c6d6e6f70717273747576fe9000" + }, + "expect": { + "text": "abcdefghijklmnopqrstuv" + } + }, + { + "test": "Longer message (Mifare)", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046101": "9000", + "ffb0000410": "031ad101165402656e746869732069739000", + "ffb0000510": "20746865206d657373616765fe0000009000" + }, + "expect": { + "text": "this is the message" + } + }, + { + "test": "Longer message (Mifare 4k)", + "atr": "3b8f8001804f0ca0000003060300020000000069", + "cardType": "Mifare", + "commands": { + "ff860000050100046000": "9000", + "ffb0000410": "031ad101165402656e746869732069739000", + "ffb0000510": "20746865206d657373616765fe0000009000" + }, + "expect": { + "text": "this is the message" + } + }, + { + "test": "Longer message (NTAG)", + "atr": "3b8f8001804f0ca0000003060300030000000068", + "cardType": "NTAG", + "commands": { + "ffb0000404": "0103a00c9000", + "ffb0000504": "34031ad19000", + "ffb0000604": "011654029000", + "ffb0000704": "656e74689000", + "ffb0000804": "697320699000", + "ffb0000904": "732074689000", + "ffb0000a04": "65206d659000", + "ffb0000b04": "737361679000", + "ffb0000c04": "65fe00009000" + }, + "expect": { + "text": "this is the message" + } + }, + { + "test": "Message spanning 2 sectors", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046000": "9000", + "ffb0000410": "0333d1012f5402656e546865207175699000", + "ffb0000510": "636b2062726f776e20666f78206a756d9000", + "ffb0000610": "7073206f76657220746865206c617a799000", + "ff860000050100086000": "9000", + "ffb0000810": "20646f672efe000000000000000000009000" + }, + "expect": { + "text": "The quick brown fox jumps over the lazy dog." + } + }, + { + "test": "NDEF tag not at the start of card (Mifare)", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046000": "9000", + "ffb0000410": "0000000000000333d1012f5402656e549000", + "ffb0000510": "686520717569636b2062726f776e20669000", + "ffb0000610": "6f78206a756d7073206f7665722074689000", + "ff860000050100086000": "9000", + "ffb0000810": "65206c617a7920646f672efe000000009000" + }, + "expect": { + "text": "The quick brown fox jumps over the lazy dog." + } + }, + { + "test": "NDEF tag not at the start of card (NTAG)", + "atr": "3b8f8001804f0ca0000003060300030000000068", + "cardType": "NTAG", + "commands": { + "ffb0000404": "000000009000", + "ffb0000504": "0103a00c9000", + "ffb0000604": "34031ad19000", + "ffb0000704": "011654029000", + "ffb0000804": "656e74689000", + "ffb0000904": "697320699000", + "ffb0000a04": "732074689000", + "ffb0000b04": "65206d659000", + "ffb0000c04": "737361679000", + "ffb0000d04": "65fe00009000" + }, + "expect": { + "text": "this is the message" + } + }, + { + "test": "Blank card, empty tag (NTAG)", + "atr": "3b8f8001804f0ca0000003060300030000000068", + "cardType": "NTAG", + "commands": { + "ffb0000404": "0300fe009000", + "ffb0000504": "000000009000", + "ffb0000604": "000000009000", + "ffb0000704": "000000009000" + }, + "expect": { + "text": null + } + }, + { + "test": "Blank card, empty tag (Mifare)", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046000": "9000", + "ffb0000410": "0300fe000000000000000000000000009000" + }, + "expect": { + "text": null + } + }, + { + "test": "Blank card (NTAG)", + "atr": "3b8f8001804f0ca0000003060300030000000068", + "cardType": "NTAG", + "commands": { + "ffb0000404": "000000009000", + "ffb0000504": "000000009000", + "ffb0000604": "000000009000", + "ffb0000704": "000000009000" + }, + "expect": { + "text": null + } + }, + { + "test": "Blank card (Mifare)", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046000": "9000", + "ffb0000410": "000000000000000000000000000000009000" + }, + "expect": { + "text": null + } + }, + // Keys + { + "test": "Key type A, number 0", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046000": "9000", + "ffb0000410": "0308d101045402656e41fe00000000009000" + }, + "expect": { + "text": "A" + } + }, + { + "test": "Key type A, number 1", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046001": "9000", + "ffb0000410": "0308d101045402656e41fe00000000009000" + }, + "expect": { + "text": "A" + } + }, + { + "test": "Key type B, number 0", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046100": "9000", + "ffb0000410": "0308d101045402656e41fe00000000009000" + }, + "expect": { + "text": "A" + } + }, + { + "test": "Key type B, number 1", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046101": "9000", + "ffb0000410": "0308d101045402656e41fe00000000009000" + }, + "expect": { + "text": "A" + } + }, + + // non-working cards + { + "test": "No commands recognised (Mifare)", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + }, + "expect": { + "text": null + } + }, + { + "test": "No commands recognised (NTAG)", + "atr": "3b8f8001804f0ca0000003060300030000000068", + "cardType": "NTAG", + "commands": { + }, + "expect": { + "text": null + } + }, + { + "test": "No response (load key)", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046101": "" + }, + "expect": { + "text": null + } + }, + { + "test": "No response (read block)", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046101": "9000", + "ffb0000410": "" + }, + "expect": { + "text": null + } + }, + { + "test": "Error response (read block)", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046101": "9000", + "ffb0000410": "6300" + }, + "expect": { + "text": null + } + }, + { + "test": "Error response (non-first read block)", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ffb0000410": "0333d1012f5402656e546865207175699000", + "ffb0000510": "636b2062726f776e20666f78206a756d9000", + "ffb0000610": "6300" + }, + "expect": { + "text": null + } + }, + // NDEF Parsing + { + "test": "UTF-8 (NTAG)", + "atr": "3b8f8001804f0ca0000003060300030000000068", + "cardType": "NTAG", + "commands": { + "ffb0000404": "0103a00c9000", + "ffb0000504": "340321d19000", + "ffb0000604": "011d54029000", + "ffb0000704": "656e34319000", + "ffb0000804": "3a4120429000", + "ffb0000904": "363ac2b69000", + "ffb0000a04": "203031449000", + "ffb0000b04": "443ac79d9000", + "ffb0000c04": "203033449000", + "ffb0000d04": "363acf969000" + }, + "expect": { + "text": "41:A B6:\u00B6 01DD:\u01DD 03D6:\u03D6" + } + }, + { + "test": "UTF-8 (Mifare)", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046000": "9000", + "ffb0000410": "0321d1011d5402656e34313a412042369000", + "ffb0000510": "3ac2b620303144443ac79d20303344369000", + "ffb0000610": "3acf96fe0000000000000000000000009000" + }, + "expect": { + "text": "41:A B6:\u00B6 01DD:\u01DD 03D6:\u03D6" + } + }, + { + "test": "Not English (lang=it)", + "atr": "3b8f8001804f0ca0000003060300030000000068", + "cardType": "NTAG", + "commands": { + "ffb0000404": "0103a00c9000", + "ffb0000504": "34030bd19000", + "ffb0000604": "010754029000", + "ffb0000704": "697463699000", + "ffb0000804": "616ffe009000" + }, + "expect": { + "text": "ciao" + } + }, + { + "test": "Multiple text records", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046000": "9000", + "ffb0000410": "032d91010b5402656e7265636f7264209000", + "ffb0000510": "3111010b5402656e7265636f726420329000", + "ffb0000610": "51010b5402656e7265636f72642033fe9000" + }, + "expect": { + "text": "record 1" + } + }, + { + "test": "Not a text record", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046000": "9000", + "ffb0000410": "0326d10221537091010a5503677069699000", + "ffb0000510": "2e6e65742f51010f5402656e475049499000", + "ffb0000610": "2077656273697465fe000000000000009000" + }, + "expect": { + "text": null + } + }, + { + "test": "Not a text record, and a text record", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046000": "9000", + "ffb0000410": "0338910221537091010a5503677069699000", + "ffb0000510": "2e6e65742f51010f5402656e475049499000", + "ffb0000610": "207765627369746551010e5402656e749000", + "ff860000050100086000": "9000", + "ffb0000810": "6865206d657373616765fe00000000009000" + }, + "expect": { + "text": "the message" + } + }, + { + "test": "Failure when presented", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "fail": "presented" + }, + "expect": { + "fail": true + } + }, + { + "test": "Failure during commands", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "fail": "commands" + }, + "expect": { + "fail": true + } + }, + { + "test": "Ensure recovery after failure", + "atr": "3b8f8001804f0ca000000306030001000000006a", + "cardType": "Mifare", + "commands": { + "ff860000050100046000": "9000", + "ffb0000410": "0308d101045402656e41fe00000000009000" + }, + "expect": { + "text": "A" + } + } +] diff --git a/gpii/node_modules/userListeners/test/testData/detectCard.json5 b/gpii/node_modules/userListeners/test/testData/detectCard.json5 new file mode 100644 index 000000000..7a5231793 --- /dev/null +++ b/gpii/node_modules/userListeners/test/testData/detectCard.json5 @@ -0,0 +1,36 @@ +// Used by pcscTests.js to test card detection. +[ + { + // Mifare 1k + "atr": "3B 8F 80 01 80 4F 0C A0 00 00 03 06 03 00 01 00 00 00 00 6A", + "expect": "Mifare" + }, + { + // Mifare 4k + "atr": "3B 8F 80 01 80 4F 0C A0 00 00 03 06 03 00 02 00 00 00 00 69", + "expect": "Mifare" + }, + { + "atr": "3B 8F 80 01 80 4F 0C A0 00 00 03 06 03 00 03 00 00 00 00 68", + "expect": "NTAG" + }, + // Bad ones + { + "atr": "", + "expect": null + }, + { + "atr": "00 00", + "expect": null + }, + { + // partial match + "atr": "3B 8F 80 01 80 4F 0C", + "expect": null + }, + { + // visa + "atr": "3B 8D 80 01 00 73 C8 00 13 64 54 37 42 37 00 90 00 46", + "expect": null + } +] diff --git a/gpii/node_modules/userListeners/test/usbTests.js b/gpii/node_modules/userListeners/test/usbTests.js new file mode 100644 index 000000000..a3d2b15a4 --- /dev/null +++ b/gpii/node_modules/userListeners/test/usbTests.js @@ -0,0 +1,200 @@ +/* + * USB User listener tests + * + * Copyright 2017 Raising the Floor - International + * Copyright 2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; +var fluid = require("infusion"), + path = require("path"), + fs = require("fs"), + rimraf = require("rimraf"); + +var jqUnit = fluid.require("node-jqunit"); +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.tests.userListener"); + +require("../index.js"); +require("./shared/lifecycleManagerTestGrade.js"); + +fluid.defaults("gpii.tests.userListener.usbListener", { + gradeNames: ["gpii.userListeners.usb"], + components: { + lifecycleManager: { + type: "gpii.tests.userListener.lifecycleManager" + } + }, + invokers: { + startListener: "fluid.identity", + stopListener: "fluid.identity" + } +}); + +var teardowns = []; +jqUnit.module("gpii.tests.userListener.usb", { + teardown: function () { + while (teardowns.length) { + teardowns.pop()(); + } + } +}); + +gpii.tests.userListener.insertTests = [ + // Device added + { + current: null, + device: "token1", + expect: "token1" + }, + // Device added + { + current: null, + device: "token2", + expect: "token2" + }, + // Empty device added + { + current: null, + device: "empty", + expect: null + }, + // Already a device, another added + { + current: "token1", + device: "token2", + expect: "token2" + }, + // Already a device, empty added + { + current: "token1", + device: "empty", + expect: null + }, + // Already a device, added one with same token (rare) + { + current: "token1", + device: "token1", + expect: "token1" + } +]; + +gpii.tests.userListener.removeTests = [ + // Device removed + { + current: "token1", + device: "token1", + expect: true + }, + // Empty device removed + { + current: "token1", + device: "empty", + expect: false + }, + // Different device removed + { + current: "token1", + device: "token2", + expect: false + }, + // Device removed - no current + { + current: null, + device: "token1", + expect: false + } +]; + + +/** + * Perform the device add/remove tests - tests reading the token file from the mount point, with the correct reaction. + * + * @param {Array} tests - An array of test data objects. + * @param {Function} func - The function to test - gpii.userListeners.usb.readUSBToken or deviceRemoved. + * @param {Boolean} createFiles - True to create the directories for the devices. + * @param {Number} expectedNumOfAsserts - The expected number of assertions. + */ +gpii.tests.userListener.deviceMountTests = function (tests, func, createFiles, expectedNumOfAsserts) { + jqUnit.expect(expectedNumOfAsserts); + var tokenFile = ".gpii-user-token.txt"; + var usbListener = gpii.tests.userListener.usbListener(); + + var testDir = fs.mkdtempSync(path.join(process.env.temp || "/tmp", "gpii-usb-test")); + teardowns.push(function () { + rimraf.sync(testDir); + }); + + // Create some fake device mount points + var devices = {}; + fluid.each([ "token1", "token2", "empty" ], function (name) { + var dir = path.join(testDir, name); + devices[name] = { + dir: dir + }; + + if (createFiles) { + fs.mkdirSync(dir); + + if (name !== "empty") { + fs.writeFileSync(path.join(dir, tokenFile), name); + } + } + }); + + var testWork = fluid.transform(tests, function (test) { + return function () { + var promise = fluid.promise(); + fluid.log(test); + + // Set up the current state. + usbListener.currentDevice = { + path: test.current && devices[test.current].dir, + token: test.current + }; + + // Add/remove the device. + var p = func(usbListener, devices[test.device].dir); + fluid.toPromise(p).then( + function (value) { + jqUnit.assertEquals("token should match the expected", test.expect, value); + }, + function (value) { + if (test.expect) { + fluid.log(value); + } + jqUnit.assertNull("readUSBToken should reject", test.expect); + } + ).then(promise.resolve, promise.resolve); + + return promise; + }; + }); + + fluid.promise.sequence(testWork).then(jqUnit.start, jqUnit.fail); +}; + +// Tests USB device insertion. +jqUnit.asyncTest("USB listener - device inserted", function () { + gpii.tests.userListener.deviceMountTests( + gpii.tests.userListener.insertTests, gpii.userListeners.usb.readUSBToken, true, 9); +}); + +// Tests USB device removal. +jqUnit.asyncTest("USB listener - device removed", function () { + // The directories are not created with these tests, as the mount point shouldn't exist when the device has gone. + gpii.tests.userListener.deviceMountTests( + gpii.tests.userListener.removeTests, gpii.userListeners.usb.deviceRemoved, false, 5); +}); diff --git a/index.js b/index.js new file mode 100644 index 000000000..02334caa0 --- /dev/null +++ b/index.js @@ -0,0 +1,103 @@ +/*! +GPII Universal Personalization Framework GPII universal index + +Copyright 2012 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ +"use strict"; + +var fluid = require("infusion"), + kettle = fluid.registerNamespace("kettle"), + gpii = fluid.registerNamespace("gpii"); + +fluid.module.register("gpii-universal", __dirname, require); + +// TODO: proper module loader will eliminate these requires (FLUID-5521) +require("./gpii/node_modules/solutionsRegistry"); +require("./gpii/node_modules/transformer"); +require("./gpii/node_modules/deviceReporter"); +require("./gpii/node_modules/lifecycleManager"); +require("./gpii/node_modules/lifecycleActions"); +require("./gpii/node_modules/flowManager"); +require("./gpii/node_modules/settingsHandlers"); +require("./gpii/node_modules/preferencesServer"); +require("./gpii/node_modules/ontologyHandler"); +require("./gpii/node_modules/matchMakerFramework"); +require("./gpii/node_modules/flatMatchMaker"); +require("./gpii/node_modules/journal"); +require("./gpii/node_modules/eventLog"); +require("./gpii/node_modules/processReporter"); +require("./gpii/node_modules/gpii-db-operation"); +require("./gpii/node_modules/userListeners"); +require("./gpii/node_modules/gpii-ini-file"); + +gpii.loadTestingSupport = function () { + fluid.contextAware.makeChecks({ + "gpii.contexts.test": { + value: true + } + }); + require("./gpii/node_modules/testing"); +}; + +/** + * Query and fetch the array of configs for this GPII Kettle Server. + * These are the configuration that allow to see if the current running + * GPII is in local, cloud, development, production, or any of our + * other useful configurations. + * + * Our underlying implementation of this may be changed or streamlined + * in future Kettle releases. + * + * @return {Array} The array of Kettle config instances. In most situations + * there is only one. + */ +gpii.queryConfigs = function () { + return fluid.queryIoCSelector(fluid.rootComponent, "kettle.config"); +}; + +/** + * Starts the GPII using the default development configuration + * or if provided a custom config. Accepts an options block + * that allows specifying the configuration name and directory + * of configurations. + * + * @param {Object} options - Accepts the following options: + * - configName {String} Name of a configuration to use, specified by the name + * of the file without the .json extension. + * - configPath {String} Directory of the configuration json files. + */ +gpii.start = function (options) { + options = options || {}; + var configName = options.configName || "gpii.config.development.manualTesting"; + var configPath = options.configPath || __dirname + "/gpii/configs"; + kettle.config.loadConfig({ + configName: kettle.config.getConfigName(configName), + configPath: kettle.config.getConfigPath(configPath) + }); +}; + +/** + * Stops the GPII instance that was started with gpii.start() + */ +gpii.stop = function () { + // Destroy the configs in reverse order, so the first loaded one is destroyed last. + // There should properly never be more than one - see https://github.com/GPII/universal/pull/766 + var configs = gpii.queryConfigs().reverse(); + if (configs.length > 1) { + fluid.log(fluid.logLevel.WARN, "Error during gpii.stop - found more than one active config: ", configs); + } + fluid.each(configs, function (config) { + config.destroy(); + }); +}; + +module.exports = fluid; diff --git a/package.json b/package.json index 19cbebb6a..ee2f67c66 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,58 @@ { - "name": "universal", + "name": "gpii-universal", "description": "Cross platform, core components of the GPII personalization infrastructure.", - "version": "0.1.0", + "version": "0.3.0", "author": "GPII", - "bugs": "http://wiki.gpii.net/index.php/Main_Page", + "bugs": "http://issues.gpii.net/browse/GPII", "homepage": "http://gpii.net/", "dependencies": { - "infusion": "git://github.com/fluid-project/infusion.git#04a85a490a05d6ddb5de6e2dbf9a9b1e83e476dd", - "ini": "git://github.com/GPII/ini.git#be8a04aa22f5ad9321ebcbba7740314a53bc8ab2", - "node-uuid": "~1.4.0", - "semver": "~1.1.4", - "when": "~1.8.1", - "xml-mapping": "~1.2.0", - "kettle": "git://github.com/fluid-project/kettle.git#42ea9eb7d6d390f644f76476cfd85f4ad4707bd1" + "body-parser": "1.18.3", + "connect-ensure-login": "0.1.1", + "express": "4.16.4", + "express-handlebars": "3.0.0", + "express-session": "1.15.6", + "fluid-resolve": "1.3.0", + "glob": "7.1.3", + "@google-cloud/trace-agent": "3.4.0", + "gpii-couchdb-test-harness": "1.0.1", + "gpii-json-schema": "2.1.2-dev.20190704T103720Z.3c177e5.GPII-4009", + "infusion": "3.0.0-dev.20190328T144119Z.ec44dbfab", + "ipaddr.js": "1.9.0", + "json5": "2.1.0", + "kettle": "1.10.1", + "mkdirp": "0.5.1", + "moment": "2.24.0", + "ndef": "0.2.0", + "nock": "10.0.4", + "node-jqunit": "1.1.8", + "node-uuid": "1.4.8", + "oauth2orize": "1.11.0", + "passport": "0.4.0", + "passport-local": "1.0.0", + "passport-oauth2-client-password": "0.1.2", + "request": "2.88.0", + "rimraf": "2.6.3", + "semver": "5.6.0", + "write-file-atomic": "2.4.2", + "xml-mapping": "1.7.1", + "xregexp": "4.2.4" }, "devDependencies": { - "jqUnit": "git://github.com/fluid-project/node-jqUnit.git#e9bf72445bd343a4f3aabe3ba96a1087d3498612" + "browserify": "16.2.3", + "gpii-express": "1.0.15", + "gpii-grunt-lint-all": "1.0.5", + "gpii-testem": "2.1.7", + "grunt": "1.0.3", + "grunt-markdownlint": "2.1.0", + "http-browserify": "1.7.0", + "https-browserify": "1.0.0", + "nyc": "13.1.0", + "recursive-copy": "2.0.9", + "supports-color": "5.5.0", + "testem": "2.10.0", + "url": "0.11.0" }, - "licenses": [ - { - "type": "BSD-3-Clause", - "url": "http://www.opensource.org/licenses/BSD-3-Clause" - } - ], + "license": "BSD-3-Clause", "keywords": [ "gpii", "accessibility", @@ -34,8 +64,27 @@ "evented" ], "repository": "git://github.com/GPII/universal.git", - "main": "./gpii/index.js", + "main": "./index.js", "engines": { - "node": ">=0.1.9" + "node": ">=4.2.1" + }, + "scripts": { + "pretest": "node node_modules/rimraf/bin.js reports/* coverage/*", + "test": "npm run test:node && npm run test:browser", + "test:browser": "node node_modules/testem/testem.js ci --file tests/testem.js", + "test:node": "node node_modules/nyc/bin/nyc.js node tests/all-tests.js", + "test:vagrantBrowser": "vagrant ssh -c 'cd /home/vagrant/sync/universal; DISPLAY=:0; npm run test:browser'", + "test:vagrantNode": "vagrant ssh -c 'cd /home/vagrant/sync/universal; npm run test:node'", + "test:vagrantProduction": "vagrant ssh -c 'cd /home/vagrant/sync/universal; DISPLAY=:0; ./scripts/vagrantCloudBasedContainers.sh'", + "test:productionConfig": "node tests/production/AddUserSettingsToCouchTests.js && node tests/production/all-tests.js && node tests/production/DeleteUserSettingsFromCouchTests.js", + "test:vagrant": "vagrant ssh -c 'cd /home/vagrant/sync/universal; DISPLAY=:0; npm test'", + "posttest": "node node_modules/nyc/bin/nyc.js report -r text-summary -r html --report-dir reports --temp-directory coverage", + "start": "node gpii.js", + "postinstall": "npm run postinstall:browserify && npm run postinstall:testDataPrefsToSnapsets && npm run postinstall:testDataPrefsToDbData && npm run postinstall:testPrefsToSnapsets && npm run postinstall:generateSchemas", + "postinstall:browserify": "node scripts/browserifyTestDependency.js", + "postinstall:testDataPrefsToSnapsets": "node scripts/convertPrefs.js testData/preferences/ build/dbData/snapset/ snapset", + "postinstall:testDataPrefsToDbData": "node scripts/convertPrefs.js testData/preferences/ build/dbData/user/ user", + "postinstall:testPrefsToSnapsets": "node scripts/convertPrefs.js tests/data/preferences/ build/tests/dbData/ user", + "postinstall:generateSchemas": "node gpii/node_modules/solutionsRegistry/src/js/generate-schemas.js" } } diff --git a/provisioning/playbook.yml b/provisioning/playbook.yml new file mode 100644 index 000000000..5a31dd660 --- /dev/null +++ b/provisioning/playbook.yml @@ -0,0 +1,14 @@ +--- +- hosts: localhost + user: root + + vars_files: + # The Dockerfile, Vagrantfile or other means of running the playbook + # must set the UNIVERSAL_VARS_FILE variable appropriately for its + # environment + - "{{ lookup('env', 'UNIVERSAL_VARS_FILE') }}" + - vars.yml + + roles: + - facts + - nodejs diff --git a/provisioning/requirements.yml b/provisioning/requirements.yml new file mode 100644 index 000000000..b23bedc6b --- /dev/null +++ b/provisioning/requirements.yml @@ -0,0 +1,5 @@ +- src: https://github.com/idi-ops/ansible-facts + name: facts + +- src: https://github.com/idi-ops/ansible-nodejs + name: nodejs diff --git a/provisioning/vagrant-vars.yml b/provisioning/vagrant-vars.yml new file mode 100644 index 000000000..7cc79138c --- /dev/null +++ b/provisioning/vagrant-vars.yml @@ -0,0 +1,9 @@ +--- +# Vagrant-specific variables +nodejs_app_npm_packages: + - testem + +nodejs_app_commands: + - npm install + +nodejs_app_install_dir: /home/vagrant/sync/universal diff --git a/provisioning/vars.yml b/provisioning/vars.yml new file mode 100644 index 000000000..a87fadc24 --- /dev/null +++ b/provisioning/vars.yml @@ -0,0 +1,24 @@ +--- +# Common variables for universal shared between both +# Docker and Vagrant builds +# Please refer to https://github.com/idi-ops/ansible-nodejs/blob/master/defaults/main.yml +# for documentation related to these variables + +nodejs_app_name: universal + +nodejs_preferences_server_port: 9081 +nodejs_flow_manager_port: 9082 +nodejs_couchdb_port: 5984 + +# Choose Node.js branch (lts or current) +nodejs_branch: lts + +# If a specific Node.js version is needed, specify it here. If not defined, defaults to the latest within the branch. +#nodejs_version: 8.x.y + +# If a specific npm version is needed, specify it here +#nodejs_npm_version: 5.6.0 + +nodejs_app_start_script: gpii.js + +nodejs_app_git_clone: false diff --git a/scripts/browserifyTestDependency.js b/scripts/browserifyTestDependency.js new file mode 100644 index 000000000..6546be717 --- /dev/null +++ b/scripts/browserifyTestDependency.js @@ -0,0 +1,58 @@ +/*! +Copyright 2016 OCAD University +Copyright 2019 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +// The creation of this script is to work around the issue of having to run a npm postinstall step +// only within the context of a GPII development environment. +// This script detects the existence of a resolvable browserify module which is ordinarily only pulled in as a devDependency. +// If the directory is present, this script browserifies some node js scripts that are used by some +// in-browser tests, e.g. all tests located at %gpii-universal/gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/html/. +// Otherwise, skip the browserifying and exit quietly. + +"use strict"; + +var fluid = require("infusion"), + resolve = require("fluid-resolve"), + rimraf = require("rimraf"), + mkdirp = require("mkdirp"), + exec = require("child_process").exec; + +var universalPath = fluid.module.resolvePath("%gpii-universal"); + +console.log("Browserifying dependent modules for web tests ..."); +// Detect whether the devDependency module "browserify" exists +resolve("browserify", {basedir: universalPath}, function (err) { + if (err) { + console.log("GPII is not running in a development mode, skipped the step to browserify test dependent node js scripts."); + } else { + var browserifyDir = universalPath + "/build/tests/browserify"; + // Remove the browserify directory to start a fresh browserifying + rimraf(browserifyDir, function () { + // Create the browserify directory for holding browserfied files in the next steps + mkdirp(browserifyDir, function () { + var browserifyCommandTemplate = "node " + universalPath + "/node_modules/browserify/bin/cmd.js -s %module " + universalPath + "/node_modules/%moduleScript -o " + browserifyDir + "/%outputFile"; + + var browserifyUtilCommand = fluid.stringTemplate(browserifyCommandTemplate, { + module: "util", + moduleScript: "util/util.js", + outputFile: "util.js" + }); + + // execute browserify commands + exec(browserifyUtilCommand); + }); + }); + } +}); + +console.log("Finished browserifying!"); diff --git a/scripts/convertPrefs.js b/scripts/convertPrefs.js new file mode 100644 index 000000000..e3a569a3a --- /dev/null +++ b/scripts/convertPrefs.js @@ -0,0 +1,73 @@ +/*! +Copyright 2018 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +// This script reads files from an input directory that contains preferences JSON5 files and convert them to JSON files of GPII keys and +// preferences safes suitable for direct loading into CouchDB, which comply with the new GPII data model: +// https://wiki.gpii.net/w/Keys,_KeyTokens,_and_Preferences in the target directory +// Usage: node scripts/convertPrefs.js {input_path} {target_path} {prefsSafeType}, where {prefsSafeType} is one of "snapset" or "user" and defaults to "user" +// +// A sample command that runs this script in the universal root directory: +// node scripts/convertPrefs.js testData/preferences/ build/dbData/ snapset + +"use strict"; + +var fs = require("fs"), + rimraf = require("rimraf"), + mkdirp = require("mkdirp"), + JSON5 = require("json5"), + fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +require("./shared/prefsSetsDbUtils.js"); +var inputDir = process.argv[2]; +var targetDir = process.argv[3]; +var prefsSafeType = process.argv[4] || "user"; + +if (process.argv.length < 4 || (prefsSafeType !== "snapset" && prefsSafeType !== "user")) { + console.log("Usage: node scripts/convertPrefs.js InputFolder OutputFolder PrefsSafeType"); + console.log(" where PrefsSafeType, is one of 'snapset' or 'user' (defaults to 'user')"); + process.exit(1); +} +var prefsSafes = []; +var gpiiKeys = []; + +var filenames = fs.readdirSync(inputDir); + +console.log("Converting preferences data in the source directory " + inputDir + " to the target directory " + targetDir + " as " + prefsSafeType + " Prefs Safes ..."); + +// Read and loop thru json5 files in the input directory +rimraf(targetDir, function () { + mkdirp(targetDir, function () { + filenames.forEach(function (filename) { + if (filename.endsWith(".json5")) { + try { + var gpiiKeyId = filename.substr(0, filename.length - 6); + var preferences = fs.readFileSync(inputDir + "/" + filename, "utf-8"); + var keyData = gpii.prefsSetsDbUtils.generateKeyData(gpiiKeyId, JSON5.parse(preferences), prefsSafeType); + gpiiKeys.push(keyData.gpiiKey); + prefsSafes.push(keyData.prefsSafe); + } catch (e) { + e.message += (" while converting preferences file " + filename); + throw e; + } + } + }); + + // Write the target files + var prefsSafesFile = targetDir + "/prefsSafes.json"; + console.log("prefsSafesFile: " + prefsSafesFile); + fs.writeFileSync(prefsSafesFile, JSON.stringify(prefsSafes, null, 4)); + + var gpiiKeysFile = targetDir + "/gpiiKeys.json"; + fs.writeFileSync(gpiiKeysFile, JSON.stringify(gpiiKeys, null, 4)); + + console.log("Finished converting preferences data in the source directory " + inputDir + " to the target directory " + targetDir); + }); +}); diff --git a/scripts/copyUninstrumentedCode.js b/scripts/copyUninstrumentedCode.js new file mode 100644 index 000000000..cfa4c818b --- /dev/null +++ b/scripts/copyUninstrumentedCode.js @@ -0,0 +1,64 @@ +/* + + When running our tests with an eye towards preparing code coverage reports, we instrument our unique code, + but do not instrument libraries (like jQuery) or non-code content. + + This script copies the remaining content in uninstrumented form to the target directory, so that all required + content is available when running the tests. + + */ +/* eslint-env node */ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +var copy = require("recursive-copy"); +var path = require("path"); + +fluid.registerNamespace("gpii.test.testem.copier"); +gpii.test.testem.copier.copyDirs = function (srcRoot, targetRoot, dirsToCopy) { + var promises = []; + + var resolvedSrcRoot = fluid.module.resolvePath(srcRoot); + var resolvedTargetRoot = fluid.module.resolvePath(targetRoot); + fluid.each(dirsToCopy, function (dirToCopy) { + promises.push(function () { + var wrappedPromise = fluid.promise(); + var src = path.resolve(resolvedSrcRoot, dirToCopy); + var dest = path.resolve(resolvedTargetRoot, dirToCopy); + var rawPromise = copy(src, dest); + /* eslint dot-notation: "off" */ + rawPromise.then(wrappedPromise.resolve).catch(wrappedPromise.reject); + }); + }); + + var sequence = fluid.promise.sequence(promises); + + sequence.then( + function () { + fluid.log("Copied non-instrumented code succesfully..."); + }, + function (error) { + fluid.log("Error copying non-instrumented code:\n", error); + } + ); + return sequence; +}; + +fluid.defaults("gpii.test.testem.copier", { + gradeNames: ["fluid.component"], + srcRoot: "%gpii-universal/gpii", + targetRoot: "%gpii-universal/instrumented/universal/gpii", + dirsToCopy: [ + "node_modules/gpii-oauth2/gpii-oauth2-authz-server/public/lib", + "node_modules/gpii-oauth2/gpii-oauth2-authz-server/webTests/lib", + "node_modules/testing" + ], + listeners: { + "onCreate.copyDirs": { + funcName: "gpii.test.testem.copier.copyDirs", + args: ["{that}.options.srcRoot", "{that}.options.targetRoot", "{that}.options.dirsToCopy"] + } + } +}); +gpii.test.testem.copier(); diff --git a/scripts/deleteAndLoadSnapsets.js b/scripts/deleteAndLoadSnapsets.js new file mode 100644 index 000000000..5c0a02af9 --- /dev/null +++ b/scripts/deleteAndLoadSnapsets.js @@ -0,0 +1,417 @@ +/*! +Copyright 2018 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +// This script modifies the preferences database: +// 1. Update the views records for accessing Prefs Safes and GPII Keys, +// 2. Retrieves all the Prefs Safes of type "snapset" (prefsSafesType = "snapset") from the databsse. +// 3. Retrieves all the GPII Keys associated with each snapset Prefs Safe so found, +// 4. Deletes these Prefs Safes and their associated GPII Keys from the database, +// 5. Uploads the new Prefs Safes and their GPII Keys to the database, +// A sample command that runs this script: +// node deleteAndLoadSnapsets.js $COUCHDBURL $STATIC_DATA_DIR $BUILD_DATA_DIR +// +// There is also an optional final [--justDelete] argument for testing/debugging. +// If present, the script exits with a zero exit status after deleting all the +// snapset PrefsSafes and their GPII keys. That is, the script does only the +// first four steps listed above. + +"use strict"; + +var url = require("url"), + fs = require("fs"), + fluid = require("infusion"); + +fluid.require("%gpii-universal/scripts/shared/dbRequestUtils.js"); + +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.dataLoader"); + +fluid.setLogging(fluid.logLevel.INFO); + +// Handle command line +if (process.argv.length < 5) { + fluid.log("Usage: node deleteAndLoadSnapsets.js $COUCHDB_URL $STATIC_DATA_DIR $BUILD_DATA_DIR [--justDelete]"); + process.exit(1); +} + +/** + * Create a set of options for data loader and a function to retreive them. + * The options are based on the command line parameters and a set of database + * constants. + * @param {Array} processArgv - The command line arguments. + * @return {Object} - The options. + */ +gpii.dataLoader.initOptions = function (processArgv) { + var dbOptions = {}; + dbOptions.couchDbUrl = processArgv[2]; + dbOptions.staticDataDir = processArgv[3]; + dbOptions.buildDataDir = processArgv[4]; + // for debugging + dbOptions.justDelete = fluid.contains(processArgv, "--justDelete"); + + // Set up database specific options + dbOptions.viewsUrl = dbOptions.couchDbUrl + "/_design/views"; + dbOptions.prefsSafesViewUrl = dbOptions.couchDbUrl + "/_design/views/_view/findSnapsetPrefsSafes"; + dbOptions.gpiiKeysViewUrl = dbOptions.couchDbUrl + "/_design/views/_view/findAllGpiiKeys"; + dbOptions.parsedCouchDbUrl = url.parse(dbOptions.couchDbUrl); + dbOptions.staticData = []; + /* dbOptions.newViews; */ + /* dbOptions.oldViews; */ + dbOptions.snapsetPrefsSafes = []; + dbOptions.gpiiKeys = []; + dbOptions.postOptions = { + hostname: dbOptions.parsedCouchDbUrl.hostname, + port: dbOptions.parsedCouchDbUrl.port, + path: "/gpii/_bulk_docs", + auth: dbOptions.parsedCouchDbUrl.auth, + method: "POST", + headers: { + "Accept": "application/json", + "Content-Length": 0, // IMPORTANT: FILL IN PER REQUEST + "Content-Type": "application/json" + } + }; + fluid.log("COUCHDB_URL: '" + + dbOptions.parsedCouchDbUrl.protocol + "//" + + dbOptions.parsedCouchDbUrl.hostname + ":" + + dbOptions.parsedCouchDbUrl.port + + dbOptions.parsedCouchDbUrl.pathname + "'" + ); + fluid.log("STATIC_DATA_DIR: '" + dbOptions.staticDataDir + "'"); + fluid.log("BUILD_DATA_DIR: '" + dbOptions.buildDataDir + "'"); + + return dbOptions; +}; + +/** + * Reads the static data from disk, and creates a separate reference to the + * views document. Two fields are added to the `options` parameter on return: + * options.staticData {Array} - Array of Objects to be put in the database. + * options.newViews {Object} - The data used to update the `_design/views` + * record. + * @param {Object} options - The source of the static data: + * @param {String} options.staticDataDir - The path to the directory containing + * the static data. + */ +gpii.dataLoader.loadStaticDataFromDisk = function (options) { + var data = gpii.dataLoader.getDataFromDirectory(options.staticDataDir); + var views = fluid.find(data, function (anElement) { + if (anElement._id && anElement._id === "_design/views") { + return anElement; + } else { + return undefined; + } + }); + options.staticData = data; + options.newViews = views; + fluid.log("Retrieved static data from: '" + options.staticDataDir + "'"); + fluid.log("\tViews data " + ( views ? "read." : "missing." )); +}; + +/** + * Create the step that loads the static data into the database. + * @param {Object} options - The static data: + * @param {Array} options.staticData - The static data to load. + * @return {Promise} - A promise that resolves loading the static data. + */ +gpii.dataLoader.createStaticDataStep = function (options) { + var details = { + dataToPost: options.staticData, + responseDataHandler: function (responseString, options) { + fluid.log("Loading static data from '" + options.staticDataDir + "'"); + return "Uploaded static data."; + }, + responseErrMsg: "Error loading static data into database: " + }; + return gpii.dbRequest.configureStep(details, options); +}; + +/** + * Create the step that retrieves the current views from the database. + * @param {Object} options - The URL to query the database with: + * @param {String} options.viewsUrl - The `_design/views` URL. + * @return {Promise} - A promise that resolves retrieving the old views. + */ +gpii.dataLoader.createFetchOldViewsStep = function (options) { + var details = { + requestUrl: options.viewsUrl, + requestErrMsg: "Error requesting old views from database: ", + responseDataHandler: function (responseString, options) { + fluid.log("Retrieving old views from database."); + var oldViews = JSON.parse(responseString); + options.oldViews = oldViews; + return oldViews; + }, + responseErrMsg: "Error retrieving old views from database: " + }; + return gpii.dbRequest.configureStep(details, options); +}; + +/** + * Create the step that updates the views in the database. If the new views + * are the same as the old views, the `_design/views` record is not updated. + * @param {Object} options - New and old `_design/views` data: + * @param {Array} options.oldViews - the old views currently in the database. + * Its `id` and `rev` fields are needed for + * any update. + * @param {Object} options.newViews - the new views data to update with. + * @return {Promise} - A promise that resolves updating the views. + */ +gpii.dataLoader.createUpdateViewsStep = function (options) { + // Check to see if the views need updating. + // JS: Not sure how useful this is. + var oldViews = JSON.stringify(options.oldViews.views); + var newViews = JSON.stringify(options.newViews.views); + if (newViews === oldViews) { + var togo = fluid.promise(); + fluid.log("New views match old views, no change."); + togo.resolve("Updated views: no change"); + return togo; + } + else { + var viewsDataToPost = options.oldViews; // id and rev + viewsDataToPost.views = options.newViews.views; // new data. + var details = { + dataToPost: [viewsDataToPost], + responseDataHandler: function (responseString) { + var result = JSON.parse(responseString)[0]; + fluid.log("Updated views: '" + JSON.stringify(result) + "'"); + return JSON.stringify(result); + }, + responseErrMsg: "Error updating views: " + }; + return gpii.dbRequest.configureStep(details, options); + } +}; + +/** + * Find the Prefs Safes of type "snapset", mark them to be deleted, and add + * them to an array of records to remove. + * @param {String} responseString - The response from the database query -- the + * retrieved snapset PrefsSafes records. + * @param {Object} options - Used to store the snapsets: + * @param {Array} options.snapsetPrefsSafes - On output, contains the "snapset" + * PrefsSafes marked for deletion. + * @return {Array} - The snapset PrefsSafes records marked for deletion. + */ +gpii.dataLoader.processSnapsets = function (responseString, options) { + fluid.log("Processing the snapset Prefs Safes records..."); + var snapSetRecords = JSON.parse(responseString); + fluid.each(snapSetRecords.rows, function (aSnapset) { + aSnapset.value._deleted = true; + options.snapsetPrefsSafes.push(aSnapset.value); + }); + fluid.log("\tSnapset Prefs Safes marked for deletion."); + return options.snapsetPrefsSafes; +}; + +/** + * Find the GPII Key records that are associated with a snapset PrefsSafe, mark + * them for deletion, and add them to array of records to delete. + * @param {String} responseString - The response from the database query -- all + * of the GPII Keys in the database. + * @param {Object} options - Used to find and store the snapset GPII Keys: + * @param {Array} options.snapsetPrefsSafes - Contains the relevant PrefsSafes + * to use to find their associated + * GPII Keys. + * @param {Array} options.gpiiKeys - On output, contains the snapset GPII Key + * records marked for deletion. + * @return {Array} - The GPII Key records marked for deletion. + */ +gpii.dataLoader.processGpiiKeys = function (responseString, options) { + fluid.log("Processing the GPII Keys..."); + var gpiiKeyRecords = JSON.parse(responseString); + options.gpiiKeys = gpii.dataLoader.markPrefsSafesGpiiKeysForDeletion( + gpiiKeyRecords, options.snapsetPrefsSafes + ); + fluid.log("\tGPII Keys associated with snapset Prefs Safes marked for deletion."); + return options.gpiiKeys; +}; + +/** + * Given all the GPII Keys records in the database, find the ones that reference + * a snapset PrefsSafe. As each GPII Key is found it is marked for + * deletion. + * @param {Array} gpiiKeyRecords - Array of GPII Key records from the database. + * @param {Array} snapSets - Array of snapset Prefs Safes whose id references + * its associated GPII Key record. + * @return {Array} - the values from the gpiiKeyRecords that are snapset GPII + * Keys, marked for deletion. + */ +gpii.dataLoader.markPrefsSafesGpiiKeysForDeletion = function (gpiiKeyRecords, snapSets) { + var gpiiKeysToDelete = []; + fluid.each(gpiiKeyRecords.rows, function (gpiiKeyRecord) { + var gpiiKey = fluid.find(snapSets, function (aSnapSet) { + if (gpiiKeyRecord.value.prefsSafeId === aSnapSet._id) { + return gpiiKeyRecord.value; + } + }, null); + if (gpiiKey !== null) { + gpiiKey._deleted = true; + gpiiKeysToDelete.push(gpiiKey); + } + }); + return gpiiKeysToDelete; +}; + +/** + * Read all the json files from the given directory, then loop to put their + * contents into an array of Objects. + * @param {String} dataDir - Directory containing the files to load. + * @return {Array} - Each element of the array is an Object based on the + * contents of each file loaded. + */ +gpii.dataLoader.getDataFromDirectory = function (dataDir) { + var contentArray = []; + var files = fs.readdirSync(dataDir); + files.forEach(function (aFile) { + if (aFile.endsWith(".json")) { + var fileContent = fs.readFileSync(dataDir + "/" + aFile, "utf-8"); + contentArray = contentArray.concat(JSON.parse(fileContent)); + } + }); + return contentArray; +}; + +/** + * Create the step that fetches the current "snapset" Prefs Safes from the + * database. + * @param {Object} options - Object for querying the database: + * @param {String} options.prefsSafesViewUrl - Views URL for finding all the + * "snapset" PrefsSafes records. + * @return {Promise} - A promise that resolves to the set of "snapset" PrefsSafes + * currently in the database. + */ +gpii.dataLoader.createFetchSnapsetsStep = function (options) { + var details = { + requestUrl: options.prefsSafesViewUrl, + requestErrMsg: "Error requesting snapsets Prefs Safes: ", + responseDataHandler: gpii.dataLoader.processSnapsets, + responseErrMsg: "Error retrieving snapsets Prefs Safes: " + }; + return gpii.dbRequest.configureStep(details, options); +}; + +/** + * Create the step that fetches the current GPII keys associate with the snapset + * Prefs Safes. + * @param {Object} options - Object for querying the database: + * @param {String} options.gpiiKeysViewUrl - Views URL for finding all GPII Key + * records in the database. + * @return {Promise} - A promise that resolves to the set of GPII keys in the + * database that correspond to "snapset" PrefsSafes. + */ +gpii.dataLoader.createFetchGpiiKeysStep = function (options) { + var details = { + requestUrl: options.gpiiKeysViewUrl, + requestErrMsg: "Error requesting GPII Keys: ", + responseDataHandler: gpii.dataLoader.processGpiiKeys, + responseErrMsg: "Error finding snapset Prefs Safes associated GPII Keys: " + }; + return gpii.dbRequest.configureStep(details, options); +}; + +/** + * Log how many snapset Prefs Safes and GPII Keys were deleted. + * @param {String} responseString - Response from the database (ignored) + * @param {Object} options - Object containing the sets of Prefs Safes and + * their GPII keys: + * @param {Array} options.snapsetPrefsSafes - The set of Prefs Safes. + * @param {Array} options.gpiiKeys - The set of associated GPII Keys. + * @return {Object} - An object with properties "snapsets" and "gpiiKeys" that + * are the number of snapsets and gpiiKeys deleted. + */ +gpii.dataLoader.logSnapsetDeletion = function (responseString, options) { + fluid.log( "Deleted " + + options.snapsetPrefsSafes.length + " Prefs Safes and " + + options.gpiiKeys.length + " associated GPII Keys, " + ); + return { + snapsets: options.snapsetPrefsSafes.length, + gpiiKeys: options.gpiiKeys.length + }; +}; + +/** + * Create the step that deletes, in batch, the current snapset Prefs Safes and + * their associated GPII keys. + * @param {Object} options - The records to be deleted: + * @param {Array} options.snapsetPrefsSafes - The "snapset" PrefsSafe records to + * delete. + * @param {Array} options.gpiiKeys - The GPII Key records to delete. + * @return {Promise} - The promise that resolves the deletion. + */ +gpii.dataLoader.createBatchDeleteStep = function (options) { + var details = { + dataToPost: options.snapsetPrefsSafes.concat(options.gpiiKeys), + responseDataHandler: gpii.dataLoader.logSnapsetDeletion + }; + return gpii.dbRequest.configureStep(details, options); +}; + +/** + * Log the uploading of all the "snapset" Prefs Safes and their GPII Keys. + * @param {String} responseString - Response from the database (ignored) + * @param {Object} options - The directory containing the data: + * @param {String} options.buildDataDir - The directory from which the Prefs + * Safes and GPII keys were loaded. + * @return {String} - A message to indicate that the upload is complete. + */ +gpii.dataLoader.logSnapsetsUpload = function (responseString, options) { + fluid.log("Bulk loading of build data from '" + options.buildDataDir + "'"); + return "Uploaded latest snapsets preferences"; +}; + +/** + * Create the step that uploads, in batch, the new "snapset" Prefs Safes, and + * their associated GPII keys. + * @param {Object} options - The directory containing the data: + * @param {String} options.buildDataDir - The directory from which to load the + * Prefs Safes and GPII keys data. + * @return {Promise} - A promise that resolves the upload. + */ +gpii.dataLoader.createBatchUploadStep = function (options) { + var newSnapsetsData = gpii.dataLoader.getDataFromDirectory(options.buildDataDir); + var details = { + dataToPost: newSnapsetsData, + responseDataHandler: gpii.dataLoader.logSnapsetsUpload + }; + return gpii.dbRequest.configureStep(details, options); +}; + +/** + * Create and execute the steps to update the database. + */ +gpii.dataLoader.orchestrate = function () { + var options = gpii.dataLoader.initOptions(process.argv); + gpii.dataLoader.loadStaticDataFromDisk(options); + var sequence = [ + gpii.dataLoader.createStaticDataStep, + gpii.dataLoader.createFetchOldViewsStep, + gpii.dataLoader.createUpdateViewsStep, + gpii.dataLoader.createFetchSnapsetsStep, + gpii.dataLoader.createFetchGpiiKeysStep, + gpii.dataLoader.createBatchDeleteStep + ]; + if (!options.justDelete) { + sequence.push(gpii.dataLoader.createBatchUploadStep); + } + fluid.promise.sequence(sequence, options).then( + function (/*result*/) { + fluid.log("Done."); + process.exit(0); + }, + function (error) { + fluid.log(error); + process.exit(1); + } + ); +}; +gpii.dataLoader.orchestrate(); diff --git a/scripts/deleteAndLoadSnapsets.sh b/scripts/deleteAndLoadSnapsets.sh new file mode 100755 index 000000000..79f14bca3 --- /dev/null +++ b/scripts/deleteAndLoadSnapsets.sh @@ -0,0 +1,96 @@ +#!/bin/sh +GPII_APP_DIR=${GPII_APP_DIR:-"/app"} + +GPII_STATIC_DATA_DIR=${GPII_STATIC_DATA_DIR:-"${GPII_APP_DIR}/testData/dbData"} +GPII_PREFERENCES_DATA_DIR=${GPII_PREFERENCES_DATA_DIR:-"${GPII_APP_DIR}/testData/preferences"} +GPII_SNAPSET_DATA_DIR=${GPII_SNAPSET_DATA_DIR:-"${GPII_APP_DIR}/build/dbData/snapset"} + +DATALOADER_JS="${GPII_APP_DIR}/scripts/deleteAndLoadSnapsets.js" +CONVERT_JS="${GPII_APP_DIR}/scripts/convertPrefs.js" + +log() { + echo "$(date +'%Y-%m-%d %H:%M:%S') - $1" +} + +warm_indices(){ + log "Warming indices..." + + for view in $(curl -s "${GPII_COUCHDB_URL}/_design/views/" | jq -r '.views | keys[]'); do + curl -fsS "${GPII_COUCHDB_URL}/_design/views/_view/${view}" >/dev/null + done + + log "Finished warming indices..." +} + +# Verify variables +if [ -z "${GPII_COUCHDB_URL}" ]; then + echo "GPII_COUCHDB_URL environment variable must be defined" + exit 1 +fi + +GPII_COUCHDB_URL_SANITIZED=$(echo "${GPII_COUCHDB_URL}" | sed -e 's,\(://\)[^/]*\(@\),\1\2,g') + +log 'Starting' +log "CouchDB: ${GPII_COUCHDB_URL_SANITIZED}" +log "Clear index: ${GPII_CLEAR_INDEX}" +log "Static: ${GPII_STATIC_DATA_DIR}" +log "Build: ${GPII_SNAPSET_DATA_DIR}" +log "Working directory: $(pwd)" + +# Check we can connect to CouchDB +GPII_COUCHDB_URL_ROOT=$(echo "${GPII_COUCHDB_URL}" | sed 's/[^\/]*$//g') +COUCHDB_HEALTHCHECK_RETRIES=30 +COUCHDB_HEALTHCHECK_DELAY=2 +log "Checking that CouchDB is ready..." +for i in `seq 1 $COUCHDB_HEALTHCHECK_RETRIES` +do + RET_CODE=$(curl --write-out '%{http_code}' --silent --output /dev/null "${GPII_COUCHDB_URL_ROOT}/_up") + if [ "$RET_CODE" = '200' ]; then + break + fi + sleep $COUCHDB_HEALTHCHECK_DELAY +done +if [ "$RET_CODE" != '200' ]; then + log "[ERROR] Failed to connect to CouchDB: ${GPII_COUCHDB_URL_SANITIZED}" + exit 1 +fi + +# Create build dir if it does not exist +if [ ! -d "${GPII_SNAPSET_DATA_DIR}" ]; then + mkdir -p "${GPII_SNAPSET_DATA_DIR}" +fi + +# Convert preferences json5 to GPII keys and preferences safes +if [ -d "${GPII_PREFERENCES_DATA_DIR}" ]; then + node "${CONVERT_JS}" "${GPII_PREFERENCES_DATA_DIR}" "${GPII_SNAPSET_DATA_DIR}" snapset + if [ "$?" != '0' ]; then + log "[ERROR] ${CONVERT_JS} failed (exit code: $?)" + exit 1 + fi +else + log "GPII_PREFERENCES_DATA_DIR ($GPII_PREFERENCES_DATA_DIR) does not exist, nothing to convert" +fi + +# Initialize (possibly clear) data base +if [ "${GPII_CLEAR_INDEX}" = 'true' ]; then + log "Deleting database at ${GPII_COUCHDB_URL_SANITIZED}" + if ! curl -fsS -X DELETE "${GPII_COUCHDB_URL}"; then + log "Error deleting database" + fi +fi + +log "Creating database at ${GPII_COUCHDB_URL_SANITIZED}" +if ! curl -fsS -X PUT "${GPII_COUCHDB_URL}"; then + log "Database already exists at ${GPII_COUCHDB_URL_SANITIZED}" +fi + +# Submit data +node "${DATALOADER_JS}" "${GPII_COUCHDB_URL}" "${GPII_STATIC_DATA_DIR}" "${GPII_SNAPSET_DATA_DIR}" +err=$? +if [ "${err}" != '0' ]; then + log "${DATALOADER_JS} failed with exit status ${err}" + exit "${err}" +fi + +# Warm Data +warm_indices diff --git a/scripts/deleteExpiredAccessTokens.js b/scripts/deleteExpiredAccessTokens.js new file mode 100644 index 000000000..02d2fa021 --- /dev/null +++ b/scripts/deleteExpiredAccessTokens.js @@ -0,0 +1,198 @@ +/*! +Copyright 2019 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +// This script modifies the preferences database: +// 1. Gather the given number of the expired documents of type "gpiiAppInstallationAuthorization" from the database; +// 2. Delete them from the database; +// 3. Repeat step 1 & 2 untial all expired authorization documents are removed from the database. + +// Usage: node scripts/deleteExpiredAccessTokens.js CouchDB-url [maxDocsInBatchPerRequest] +// @param {String} CouchDB-url - The url to the CouchDB where docoments should be migrated. +// @param {Number} maxDocsInBatchPerRequest - [optional] Limit the number of documents to be deleted in a batch. +// Default to 100 if not provided. + +// A sample command that runs this script in the universal root directory: +// node scripts/deleteExpiredAccessTokens.js http://localhost:25984/gpii 10 + +"use strict"; + +var url = require("url"), + fluid = require("infusion"); + +fluid.require("%gpii-universal/scripts/shared/dbRequestUtils.js"); + +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.accessTokens"); + +fluid.setLogging(fluid.logLevel.INFO); + +// Handle command line +if (process.argv.length < 3) { + console.log("Usage: node deleteExpiredAccessTokens.js $COUCHDB_URL [maxDocsInBatchPerRequest]"); + process.exit(1); +} + +gpii.accessTokens.defaultMaxDocsInBatchPerRequest = 100; + +/** + * Create a set of options for this script. + * The options are based on the command line parameters and a set of database + * constants. + * @param {Array} processArgv - The command line arguments. + * @return {Object} - The options. + */ +gpii.accessTokens.initOptions = function (processArgv) { + var options = {}; + options.couchDbUrl = processArgv[2]; + options.maxDocsInBatchPerRequest = Number(processArgv[3]) || gpii.accessTokens.defaultMaxDocsInBatchPerRequest; + + // Set up database specific options + + // Note that the comparison of timestamp needs to use startkey and endkey rather than performing it in + // the CouchDB view because calling Date.now() in the view will set the "now" to a static timestamp when + // the view index is created. See: + // https://stackoverflow.com/questions/29854776/couchdb-filter-timestamps-in-a-reduce-function-some-sort-of-date-now + var currentTime = Date.now(); + options.accessTokensUrl = options.couchDbUrl + "/_design/views/_view/findAccessTokenByExpires?descending=true&startkey=" + currentTime + "&endkey=0&limit=" + options.maxDocsInBatchPerRequest; + options.accessTokens = []; + options.totalDeleted = 0; + options.parsedCouchDbUrl = url.parse(options.couchDbUrl); + options.postOptions = { + hostname: options.parsedCouchDbUrl.hostname, + port: options.parsedCouchDbUrl.port, + path: "/gpii/_bulk_docs", + auth: options.parsedCouchDbUrl.auth, + method: "POST", + headers: { + "Accept": "application/json", + "Content-Length": 0, // IMPORTANT: FILL IN PER REQUEST + "Content-Type": "application/json" + } + }; + fluid.log("COUCHDB_URL: '" + + options.parsedCouchDbUrl.protocol + "//" + + options.parsedCouchDbUrl.hostname + ":" + + options.parsedCouchDbUrl.port + + options.parsedCouchDbUrl.pathname + "'" + ); + return options; +}; + +/** + * Create the step that retrieves access tokens from the database: either + * only the expired tokens, or all of them. + * @param {Object} options - Access tokens URL and whether to filter: + * @param {String} options.accessTokensUrl - The url for retrieving all of the + * access tokens in the database. + * @return {Promise} - A promise that resolves retrieving the tokens. + */ +gpii.accessTokens.retrieveExpiredAccessTokens = function (options) { + var details = { + requestUrl: options.accessTokensUrl, + requestErrMsg: "Error retrieving access tokens from the database: ", + responseDataHandler: gpii.accessTokens.findExpiredAccessTokens, + responseErrMsg: "Error retrieving access tokens from database: " + }; + return gpii.dbRequest.configureStep(details, options); +}; + +/** + * Given all the access tokens from the database, filter out only the ones that + * have expired and store them in an array. + * @param {String} responseString - the response from the request to get all the + * acess tokens. + * @param {Object} options - Where to store the to-be-deleted access tokens and + * whether to delete regardless of time of expiration: + * @param {Array} options.accessTokens - The access tokens sought. + * @param {Number} options.totalExpiredInThisBatch - The total number of expired access tokens in this batch. + * @return {Promise} - The resolved value contains an array of expired access tokens to be deleted in this batch, + * or 0 when no more tokens to delete. + */ +gpii.accessTokens.findExpiredAccessTokens = function (responseString, options) { + var togo = fluid.promise(); + var tokens = JSON.parse(responseString); + var expiredTokens = []; + options.totalExpiredInThisBatch = tokens.total_rows; + + if (tokens.rows.length === 0) { + // No more expired access tokens + options.accessTokens = 0; + } else { + fluid.each(tokens.rows, function (aRow) { + var aToken = aRow.value; + aToken._deleted = true; + expiredTokens.push(aToken); + options.totalTokens++; + }); + options.accessTokens = expiredTokens; + } + togo.resolve(options.accessTokens); + + return togo; +}; + +/** + * Log how many accesss tokens were deleted. + * @param {String} responseString - Response from the database (ignored) + * @param {Object} options - Object containing the set of access tokens: + * @param {Array} options.accessTokens - The tokens to delete. + * @param {Number} options.totalExpiredInThisBatch - The total number of expired access tokens in this batch. + * @return {Number} - the number of access tokens deleted. + */ +gpii.accessTokens.logDeletion = function (responseString, options) { + options.totalDeleted = options.totalDeleted + options.accessTokens.length; + fluid.log("Deleted ", options.accessTokens.length, " of ", options.totalExpiredInThisBatch, " access tokens."); + return options.accessTokens.length; +}; + +/** + * Configure deletion, in batch, of the access tokens. + * @param {Object} options - The records to be deleted: + * @param {Array} options.accessTokens - The access token records to delete. + * @return {Promise} - A promise whose resolved value is the number of deleted access tokens, + * or 0 when all has been deleted. + */ +gpii.accessTokens.flush = function (options) { + var togo = fluid.promise(); + if (options.accessTokens === 0) { + togo.resolve(0); + } else { + var details = { + dataToPost: options.accessTokens, + responseDataHandler: gpii.accessTokens.logDeletion + }; + togo = gpii.dbRequest.configureStep(details, options); + } + return togo; +}; + +/** + * Create and execute the steps to delete the access tokens. + */ +gpii.accessTokens.deleteAccessTokens = function () { + var options = gpii.accessTokens.initOptions(process.argv); + var actionFunc = function (options) { + return fluid.promise.sequence([ + gpii.accessTokens.retrieveExpiredAccessTokens, + gpii.accessTokens.flush + ], options); + }; + + var finalPromise = gpii.dbRequest.processRecursive(options, actionFunc); + finalPromise.then(function () { + console.log("Done: Deleted " + options.totalDeleted + " expired access tokens in total."); + process.exit(0); + }, function (error) { + console.log(error); + process.exit(1); + }); +}; + +gpii.accessTokens.deleteAccessTokens(); diff --git a/scripts/dockerDeleteExpiredAccessTokens.sh b/scripts/dockerDeleteExpiredAccessTokens.sh new file mode 100755 index 000000000..744d040db --- /dev/null +++ b/scripts/dockerDeleteExpiredAccessTokens.sh @@ -0,0 +1,48 @@ +#!/bin/sh + +# Copyright 2019 OCAD University +# +# Licensed under the New BSD license. You may not use this file except in +# compliance with this License. +# +# You may obtain a copy of the License at +# https://github.com/GPII/universal/blob/master/LICENSE.txt + +# This script provide as way to run the node script that deletes expired access +# tokens (GPII App Installation Authorization records) from CouchDB using a +# docker image of gpii-universal + +GPII_APP_DIR=${GPII_APP_DIR:-"/app"} +GPII_COUCHDB_URL=${GPII_COUCHDB_URL:-"http://couchdb:5984/gpii"} +MAX_DOCS_IN_BATCH_PER_REQUEST=${MAX_DOCS_IN_BATCH_PER_REQUEST:-100} + +GPII_COUCHDB_URL_SANITIZED=$(echo "${GPII_COUCHDB_URL}" | sed -e 's,\(://\)[^/]*\(@\),\1\2,g') +GPII_COUCHDB_URL_ROOT=$(echo "${GPII_COUCHDB_URL}" | sed 's/[^\/]*$//g') +COUCHDB_HEALTHCHECK_DELAY=2 +COUCHDB_HEALTHCHECK_RETRIES=30 + +DELETE_ACCESS_TOKENS_JS="${GPII_APP_DIR}/scripts/deleteExpiredAccessTokens.js" + +log() { + echo "$(date +'%Y-%m-%d %H:%M:%S') - $1" +} + +log "Delete expired access tokens: starting..." +log "CouchDB: ${GPII_COUCHDB_URL_SANITIZED}" + +log "Checking that CouchDB is ready..." +for i in `seq 1 $COUCHDB_HEALTHCHECK_RETRIES` +do + RET_CODE=$(curl --write-out '%{http_code}' --silent --output /dev/null "${GPII_COUCHDB_URL_ROOT}/_up") + if [ "$RET_CODE" = '200' ]; then + break + fi + sleep $COUCHDB_HEALTHCHECK_DELAY +done + +node ${DELETE_ACCESS_TOKENS_JS} ${GPII_COUCHDB_URL} ${MAX_DOCS_IN_BATCH_PER_REQUEST} +RET_CODE=$? +if [ "${RET_CODE}" != '0' ]; then + log "${DELETE_ACCESS_TOKENS_JS} failed with exit code ${RET_CODE}" +fi +exit "${RET_CODE}" diff --git a/scripts/generateCredentials.js b/scripts/generateCredentials.js new file mode 100644 index 000000000..e67de274f --- /dev/null +++ b/scripts/generateCredentials.js @@ -0,0 +1,162 @@ +/*! +Copyright 2019 Raising the Floor US + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +As part of GPII-3714, we need a script to generate the credentials +for both the client and the server in order for the instances of GPII to be +authenticated when performing read and save actions against the cloud-based flowManager. + +These credentials consist in: + +1. The secret file - what goes in the client side + +{ + "site": "example.gpii.net", + "clientCredentials": { + "client_id": "UUID-pilot-computer", + "client_secret": "UUID-pilot-computer-secret" + } +} + +2. The credentials - what goes into the database + +2.1 The client credentials + +{ + "_id": "UUID-clientCredentials", + "type": "clientCredential", + "schemaVersion": "0.2", + "clientId": "UUID-gpiiAppInstallationClient", + "oauth2ClientId": "UUID-pilot-computer", + "oauth2ClientSecret": "UUID-pilot-computer-secret", + "allowedIPBlocks": null, + "allowedPrefsToWrite": null, + "isCreateGpiiKeyAllowed": false, + "isCreatePrefsSafeAllowed": false, + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampRevoked": null +} + +2.2 The GPII App Installation Client + +{ + "_id": "UUID-gpiiAppInstallationClient", + "type": "gpiiAppInstallationClient", + "schemaVersion": "0.2", + "name": "Pilot Computers", + "computerType": "public", + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null +} + +The options must be passed as environmental variables, they are: + +* GPII_CREDENTIALS_NAME [String] [required]: The site name +* GPII_CREDENTIALS_SITE [String] [required]: The site domain + +Example usage: + +GPII_CREDENTIALS_NAME="GPII Testers" GPII_CREDENTIALS_SITE=testers.gpii.net node generateCredentials.js + +After running the script, you can find the files in the folder "testers.gpii.net-credentials". +The script generates two files containing: +1. secret.txt: The file to be provided when building the "Secret Blower" installer +2. couchDBData.json: The JSON-format documents that go into the production +database and ready to be inserted into couchDB + +Note that, depending on the deployment, you might want to manually adjust the following fields in the client +credentials block inside the couchDBData.json file: + +* allowedIPBlocks: An array of allowed IP addresses, e.g.: ["125.19.23.0/24", "2001:cdba::3257:9652", "62.230.58.1"] +* allowedPrefsToWrite: An array of allowed prefs to write, e.g.: ["http://registry.gpii.net/common/language", "http://registry.gpii.net/common/DPIScale"] +* isCreateGpiiKeyAllowed: true|false +* isCreatePrefsSafeAllowed: true|false + +After you're done, you can use the following command to load the data using an HTTP POST request like this: + +* curl -d @couchDBData.json -H "Content-type: application/json" -X POST http://localhost:25984/gpii/_bulk_docs + +*/ +"use strict"; + +var process = require("process"), + fs = require("fs"), + path = require("path"), + uuid = uuid || require("node-uuid"); + +var credentialsName = process.env.GPII_CREDENTIALS_NAME; +var credentialsSite = process.env.GPII_CREDENTIALS_SITE; + +if (!credentialsName || !credentialsSite) { + console.log("Usage: GPII_CREDENTIALS_NAME= GPII_CREDENTIALS_SITE= node generateCredentials.js"); + process.exit(1); +} + +var generateCredentials = function () { + var currentTime = new Date().toISOString(); + + var clientId = uuid.v4(); + var clientSecret = uuid.v4(); + var clientCredentialsId = uuid.v4(); + var gpiiAppInstallationClientId = uuid.v4(); + + var secret = { + "site": credentialsSite, + "clientCredentials": { + "client_id": clientId, + "client_secret": clientSecret + } + }; + + var clientCredential = { + "_id": clientCredentialsId, + "type": "clientCredential", + "schemaVersion": "0.2", + "clientId": gpiiAppInstallationClientId, + "oauth2ClientId": clientId, + "oauth2ClientSecret": clientSecret, + "allowedIPBlocks": null, + "allowedPrefsToWrite": null, + "isCreateGpiiKeyAllowed": false, + "isCreatePrefsSafeAllowed": false, + "revoked": false, + "revokedReason": null, + "timestampCreated": currentTime, + "timestampRevoked": null + }; + + var gpiiAppInstallationClient = { + "_id": gpiiAppInstallationClientId, + "type": "gpiiAppInstallationClient", + "schemaVersion": "0.2", + "name": credentialsName, + "computerType": "public", + "timestampCreated": currentTime, + "timestampUpdated": null + }; + + return { + secret: secret, + couchDBData: { + "docs": [clientCredential, gpiiAppInstallationClient] + } + }; +}; + +var outputFolder = credentialsSite + "-credentials"; +fs.mkdir(outputFolder, function (err) { + if (err) { + throw err; + process.exit(1); + } + var credentials = generateCredentials(); + fs.writeFileSync(path.join(outputFolder, "secret.txt"), JSON.stringify(credentials.secret, null, 2)); + fs.writeFileSync(path.join(outputFolder, "couchDBData.json"), JSON.stringify(credentials.couchDBData, null, 2)); +}); diff --git a/scripts/loadEmptyKeys.js b/scripts/loadEmptyKeys.js new file mode 100644 index 000000000..3c11a4853 --- /dev/null +++ b/scripts/loadEmptyKeys.js @@ -0,0 +1,134 @@ +/*! +Copyright 2018 Raising the Floor US + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +// This script generates unique keys and loads them into CouchDB. +// +// To create 20 empty keys, the script needs to be called like this: +// * COUCHDB_URL=http://localhost:25984/gpii NUM_OF_KEYS=20 node scripts/loadEmptyKeys.js +// +// The options to this script must be passed as environment variables, and they are: +// * COUCHDB_URL [String] [required]: This needs to be in the form "http://host/dbname", but in case +// you need to provide the credentials to CouchDB, you need to use the form +// "http://user:pass@host:port/dbname". +// * NUM_OF_KEYS [Integer] [required)]: The amount of new empty preferences sets you want to create. +// * OUTPUT_FILENAME [String] [optional]: The file to save the list of keys created. By default, +// it will generate a file called "generated-keys-.txt", just provide a +// different value if you want it to be a different file. +// * HIDE_PROGRESS [Integer] [optional]: Whether you want to hide the keys as they are created. Default is +// 0 (show progress), set it to 1 if you want it to be the other way round. +// +// See this JIRA for more information: https://issues.gpii.net/browse/GPII-3381 +// +"use strict"; + +var process = require("process"), + fs = require("fs"), + fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + request = require("request"), + uuid = uuid || require("node-uuid"); + +require("./shared/prefsSetsDbUtils.js"); + +fluid.defaults("gpii.uuidLoader", { + gradeNames: ["fluid.component"], + outputFileName: process.env.OUTPUT_FILENAME || "generated-keys-" + new Date().toISOString() + ".txt", + totalNumOfKeys: parseInt(process.env.NUM_OF_KEYS), + hideProgress: process.env.HIDE_PROGRESS, + members: { + count: 1 + }, + couchUrl: process.env.COUCHDB_URL, + recordUrlTemplate: "%baseUrl/%id", + invokers: { + saveSingleCouchRecord: { + funcName: "gpii.uuidLoader.saveSingleCouchRecord", + args: ["{that}", "{arguments}.0"] // recordToPost + }, + saveSingleKey: { + funcName: "gpii.uuidLoader.saveSingleKey", + args: ["{that}", "{arguments}.0"] // keyToSave + } + }, + listeners: { + "onCreate.createKeys": { + funcName: "gpii.uuidLoader.createNewKeys", + args: [ + "{that}", + "{that}.options.totalNumOfKeys", + "{that}.options.outputFileName", + "{that}.options.hideProgress" + ] + } + } +}); + +gpii.uuidLoader.saveSingleKey = function (that, keyData) { + return function () { + var promises = [ + that.saveSingleCouchRecord(keyData.gpiiKey), + that.saveSingleCouchRecord(keyData.prefsSafe) + ]; + + return fluid.promise.sequence(promises); + }; +}; + +gpii.uuidLoader.saveSingleCouchRecord = function (that, recordToPost) { + return function () { + var couchPutPromise = fluid.promise(); + + var requestOptions = { + url: that.options.couchUrl, + body: recordToPost, + json: true + }; + + request.post(requestOptions, function (error, response, body) { + if (error) { + couchPutPromise.reject(error); + } + else if (response.statusCode !== 201) { + couchPutPromise.reject(body); + } + else { + couchPutPromise.resolve(body); + } + }); + + return couchPutPromise; + }; +}; + +gpii.uuidLoader.createSingleKey = function () { + return gpii.prefsSetsDbUtils.generateKeyData(uuid.v4()); +}; + +// The real action +// +gpii.uuidLoader.createNewKeys = function (that, totalNumOfKeys, outputFileName) { + var allKeys = fluid.generate(totalNumOfKeys, gpii.uuidLoader.createSingleKey, true); + fs.writeFileSync(outputFileName, JSON.stringify(allKeys, null, 2)); + var recordSavePromises = fluid.transform(allKeys, that.saveSingleKey); + var sequence = fluid.promise.sequence(recordSavePromises); + + sequence.then( + function () { + console.log("We're done adding new keys! :)"); + console.log("Remember, you have a list of the created keys here:", outputFileName); + }, + function (error) { + console.log("Got an error, let's stop it here. :/ Error was:", error); + console.log("You can find the keys we have created before crashing here:", outputFileName); + } + ); +}; + +gpii.uuidLoader(); diff --git a/scripts/migration/schema-0.2-GPII-4014/README.md b/scripts/migration/schema-0.2-GPII-4014/README.md new file mode 100644 index 000000000..32ec2f03e --- /dev/null +++ b/scripts/migration/schema-0.2-GPII-4014/README.md @@ -0,0 +1,43 @@ +# Data Migration and Verification for GPII-4014 Deployment + +The scripts in this directory are used during GPII-4014 deployment to perform the data migration and the data verification afterwards. See [GPII-4014](https://issues.gpii.net/browse/schema-0.2-GPII-4014) for deploy details. + +## How to use these scripts + +### Scripts for testing + +1. createSimulatedGpiiKeys.js + + This script creates simulated GPII keys and their corresponding prefs safes in schema version 0.1 format. It + creates documents in batches of a given number. For example, for creating 1000 GPII keys in batches of 100, this + script will generate 100 new GPII keys and their corresponding prefs safes each time to send to CouchDB /_bulk_docs + to create. This is to avoid the potential memory overflow at creating a large number of GPII keys in one shot. An example to create 50K GPII keys in batches of 50: + ``` + node scripts/migration/schema-0.2-GPII-4014/createSimulatedGpiiKeys.js http://localhost:25984 50000 50 + ``` + +### Scripts for deployment + +Assuming the document IDs for NOVA client credentials are "clientCredential-nova1" and "clientCredential-nova2": + +1. migration-step1.js + + Run this script **before** deploying the new universal docker image from the universal root directory. It migrates all client credential documents. An example: +``` +node scripts/migration/schema-0.2-GPII-4014/migration-step1.js http://localhost:25984 "clientCredential-nova1" "clientCredential-nova2" +``` + +2. migration-step2.js + + Run this script **after** deploying the new universal docker image from the universal root directory. It updates all "schemaVersion" and "timestampUpdated" values in batches. An example of migrating in batches of 5000 documents in one batch: +``` +node scripts/migration/schema-0.2-GPII-4014/migration-step2.js http://localhost:25984 5000 +``` + +3. verify.js + + Run this script **after** the data migration completes. It checks if all documents have been migrated. An example of verifying + in batches of 5000 documents in one batch: +``` +node scripts/migration/schema-0.2-GPII-4014/verify.js http://localhost:25984 5000 "clientCredential-nova1" "clientCredential-nova2" +``` diff --git a/scripts/migration/schema-0.2-GPII-4014/createSimulatedGpiiKeys.js b/scripts/migration/schema-0.2-GPII-4014/createSimulatedGpiiKeys.js new file mode 100644 index 000000000..52c895b76 --- /dev/null +++ b/scripts/migration/schema-0.2-GPII-4014/createSimulatedGpiiKeys.js @@ -0,0 +1,186 @@ +/*! +Copyright 2019 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +// This script creates simulated GPII keys and their corresponding prefs safes in schema version 0.1 format. +// It creates new documents in batches of a given number. Note that creating number n of GPII keys will end up +// with creating n * 2 documents in the database since each key has a corresponding prefs safe document created. + +// Usage: node scripts/migration/schema-0.2-GPII-4014/createSimulatedGpiiKeys.js CouchDB-url numOfKeysToCreate maxDocsInBatchPerRequest +// @param {String} CouchDB-url - The url to the CouchDB where docoments should be created. +// @param {Number} numOfKeysToCreate - The number of GPII keys to be created. +// @param {Number} maxDocsInBatchPerRequest - [optional] Limit the number of documents to be created in a batch. +// Default to 100 if not provided. + +// A sample command that runs this script in the universal root directory: +// node scripts/migration/schema-0.2-GPII-4014/createSimulatedGpiiKeys.js http://localhost:25984 100000 300 + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + url = require("url"), + uuid = uuid || require("node-uuid"); + +fluid.registerNamespace("gpii.migration.GPII4014"); + +require("./shared/migratedValues.js"); +require("../../shared/dbRequestUtils.js"); +require("../../../gpii/node_modules/gpii-db-operation/src/DbUtils.js"); + +// Handle command line +if (process.argv.length < 4) { + console.log("Usage: node createSimulatedGpiiKeys.js COUCHDB_URL numOfKeysToCreate [maxDocsInBatchPerRequest]"); + process.exit(1); +} + +gpii.migration.GPII4014.defaultMaxDocsInBatchPerRequest = 100; + +/** + * Create a set of options for this script. + * The options are based on the command line parameters and a set of database constants. + * @param {Array} processArgv - The command line arguments. + * @return {Object} - The options. + */ +gpii.migration.GPII4014.initOptions = function (processArgv) { + var options = {}; + options.couchDbUrl = processArgv[2] + "/gpii"; + options.numOfKeysToCreate = Number(processArgv[3]); + options.maxDocsInBatchPerRequest = Number(processArgv[4]) || gpii.migration.GPII4014.defaultMaxDocsInBatchPerRequest; + + // Set up database specific options + options.newDocs = []; + options.numOfCreatedKeys = 0; + options.parsedCouchDbUrl = url.parse(options.couchDbUrl); + options.postOptions = { + hostname: options.parsedCouchDbUrl.hostname, + port: options.parsedCouchDbUrl.port, + path: "/gpii/_bulk_docs", + auth: options.parsedCouchDbUrl.auth, + method: "POST", + headers: { + "Accept": "application/json", + "Content-Length": 0, // IMPORTANT: FILL IN PER REQUEST + "Content-Type": "application/json" + } + }; + console.log("COUCHDB_URL: '" + + options.parsedCouchDbUrl.protocol + "//" + + options.parsedCouchDbUrl.hostname + ":" + + options.parsedCouchDbUrl.port + + options.parsedCouchDbUrl.pathname + "'" + ); + return options; +}; + +/** + * Generate an array of new documents to create. + * @param {Number} numOfKeys - The number of new GPII keys to create + * @return {Array} - An array of GPII keys and corresponding prefs safe documents to create. + */ +gpii.migration.GPII4014.generateKeyData = function (numOfKeys) { + var newDocs = []; + for (var i = 0; i < numOfKeys; i++) { + var currentTime = new Date().toISOString(); + var gpiiKeyId = uuid.v4(); + var prefsSafeId = "prefsSafe-" + gpiiKeyId; + var prefs = { + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/language": "en-US" + } + } + } + } + }; + + var newGpiiKey = { + "_id": gpiiKeyId, + "type": "gpiiKey", + "schemaVersion": "0.1", + "prefsSafeId": prefsSafeId, + "prefsSetId": "gpii-default", + "revoked": false, + "revokedReason": null, + "timestampCreated": currentTime, + "timestampUpdated": null + }; + + var newPrefsSafe = { + "_id": prefsSafeId, + "type": "prefsSafe", + "schemaVersion": "0.1", + "prefsSafeType": "user", + "name": gpiiKeyId, + "password": null, + "email": null, + "preferences": prefs, + "timestampCreated": currentTime, + "timestampUpdated": null + }; + newDocs.push(newGpiiKey); + newDocs.push(newPrefsSafe); + }; + return newDocs; +}; + +/** + * Log how many GPII documents were updated. + * @param {String} responseString - Response from the database (ignored) + * @param {Object} options - Object containing the set of documents: + * @param {Number} options.numOfKeysToCreate - Total number of GPII keys to create. + * @param {Number} options.numOfCreatedKeys - Total number of GPII keys that have been created. + * @return {Promise} - The resolved value is options.numOfCreatedKeys, or 0 when all has been created. + */ +gpii.migration.GPII4014.logUpdateDB = function (responseString, options) { + var togo = fluid.promise(); + options.numOfCreatedKeys = options.numOfCreatedKeys + options.numToCreateInThisBatch; + console.log("Created " + options.numOfCreatedKeys + " of requested " + options.numOfKeysToCreate + " GPII keys."); + togo.resolve(options.numOfCreatedKeys < options.numOfKeysToCreate ? options.numOfCreatedKeys : 0); + return togo; +}; + +/** + * Configure one batch of creation. + * @param {Object} options - Object containing the set of documents: + * @param {Array} options.newDocs - The documents to create. + * @param {Number} options.numOfKeysToCreate - Total number of GPII keys to create. + * @param {Number} options.numOfCreatedKeys - Total number of GPII keys that have been created. This option will be + * written by a call to this function. + * @return {Promise} - A promise whose resolved value is the number of created documents, or 0 when all has been created. + */ +gpii.migration.GPII4014.createOneBatch = function (options) { + var numOfNeeded = options.numOfKeysToCreate - options.numOfCreatedKeys; + options.numToCreateInThisBatch = numOfNeeded > options.maxDocsInBatchPerRequest ? options.maxDocsInBatchPerRequest : numOfNeeded; + + options.newDocs = gpii.migration.GPII4014.generateKeyData(options.numToCreateInThisBatch); + + var details = { + dataToPost: options.newDocs, + responseDataHandler: gpii.migration.GPII4014.logUpdateDB + }; + return gpii.dbRequest.configureStep(details, options); +}; + +/** + * Create and execute the steps to create GPII keys. + */ +gpii.migration.GPII4014.createKeys = function () { + var options = gpii.migration.GPII4014.initOptions(process.argv); + var finalPromise = gpii.dbRequest.processRecursive(options, gpii.migration.GPII4014.createOneBatch); + + finalPromise.then(function () { + console.log("Done: " + options.numOfCreatedKeys + " GPII keys have been created."); + }, console.log); +}; + +gpii.migration.GPII4014.createKeys(); diff --git a/scripts/migration/schema-0.2-GPII-4014/migration-step1.js b/scripts/migration/schema-0.2-GPII-4014/migration-step1.js new file mode 100644 index 000000000..63ec461ea --- /dev/null +++ b/scripts/migration/schema-0.2-GPII-4014/migration-step1.js @@ -0,0 +1,168 @@ +/*! +Copyright 2019 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +// This script performs the first step of the data migration for GPII-4014 deployment. +// It updates all client credential documents by adding 4 new fields to all "clientCredential" documents: +// allowedIPBlocks, allowedPrefsToWrite, isCreateGpiiKeyAllowed, isCreatePrefsSafeAllowed. +// These 4 values are different for public client credentials and NOVA privileged client credentials. + +// Usage: node scripts/migration/schema-0.2-GPII-4014/migration-step1.js CouchDB-url clientCredentialId-for-NOVA ... +// @param {String} CouchDB-url - The url to the CouchDB where docoments should be migrated. +// @param {Strings} clientCredentialIds-for-NOVA - The "_id" value of the NOVA client credential. There could be any +// number of client credential parameters from here onwards. + +// A sample command that runs this script in the universal root directory: +// node scripts/migration/schema-0.2-GPII-4014/migration-step1.js http://localhost:25984 "clientCredential-nova1" "clientCredential-nova2" + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + url = require("url"); + +fluid.registerNamespace("gpii.migration.GPII4014"); + +require("./shared/migratedValues.js"); +require("../../shared/dbRequestUtils.js"); +require("../../../gpii/node_modules/gpii-db-operation/src/DbUtils.js"); + +/** + * Create a set of options for this script. + * The options are based on the command line parameters and a set of database constants. + * @param {Array} processArgv - The command line arguments. + * @return {Object} - The options. + */ +gpii.migration.GPII4014.initOptions = function (processArgv) { + var options = {}; + options.couchDbUrl = processArgv[2] + "/gpii"; + options.novaClientCredentials = process.argv.splice(3); + + // Set up database specific options + options.allDocsUrl = options.couchDbUrl + "/_all_docs?include_docs=true"; + options.clientCredentials = []; + options.parsedCouchDbUrl = url.parse(options.couchDbUrl); + options.postOptions = { + hostname: options.parsedCouchDbUrl.hostname, + port: options.parsedCouchDbUrl.port, + path: "/gpii/_bulk_docs", + auth: options.parsedCouchDbUrl.auth, + method: "POST", + headers: { + "Accept": "application/json", + "Content-Length": 0, // IMPORTANT: FILL IN PER REQUEST + "Content-Type": "application/json" + } + }; + console.log("COUCHDB_URL: '" + + options.parsedCouchDbUrl.protocol + "//" + + options.parsedCouchDbUrl.hostname + ":" + + options.parsedCouchDbUrl.port + + options.parsedCouchDbUrl.pathname + "'" + ); + return options; +}; + +/** + * Create the step that retrieves all documents from the database + * @param {Object} options - All docs URL and whether to filter: + * @param {Array} options.allDocsUrl - The url for retrieving all documents in the database. + * @return {Promise} - A promise that resolves retrieved documents. + */ +gpii.migration.GPII4014.retrieveClientCredentials = function (options) { + var details = { + requestUrl: options.allDocsUrl, + requestErrMsg: "Error retrieving client credentials from the database: ", + responseDataHandler: gpii.migration.GPII4014.updateDocsData, + responseErrMsg: "Error retrieving client credentials from database: " + }; + + return gpii.dbRequest.configureStep(details, options); +}; + +/** + * Given all the documents from the database, update their data according to the new data model. + * @param {String} responseString - the response from the request to get all documents. + * @param {Object} options - Where to store the to-be-updated documents: + * @param {Array} options.novaClientCredentials - NOVA client credential IDs read from input arguments. + * @param {Array} options.clientCredentials - Accumulated documents. + * @return {Array} - The updated documents in the new data structure with the new schema version. + */ +gpii.migration.GPII4014.updateDocsData = function (responseString, options) { + var allDocs = JSON.parse(responseString); + var updatedDocs = []; + options.totalNumOfDocs = allDocs.total_rows; + if (allDocs.rows) { + fluid.each(allDocs.rows, function (aRow) { + var aDoc = aRow.doc; + // To filter out the "_design/views" doc that doesn't have the "schemaVersion" field + if (aDoc.type === "clientCredential") { + aDoc.schemaVersion = gpii.migration.GPII4014.newSchemaVersion; + aDoc = fluid.extend( + {}, + aDoc, + options.novaClientCredentials.includes(aDoc._id) ? gpii.migration.GPII4014.newValuesForNovaClientCredential : gpii.migration.GPII4014.newValuesForPublicClientCredential + ); + console.log("Updating the client credential ID: ", aDoc._id); + updatedDocs.push(aDoc); + } + }); + options.updatedDocs = updatedDocs; + } + return updatedDocs; +}; + +/** + * Log how many GPII documents were updated. + * @param {String} responseString - Response from the database (ignored) + * @param {Object} options - Object containing the set of documents: + * @param {Array} options.updatedDocs - The documents to update. + * @return {Number} - the number of documents updated. + */ +gpii.migration.GPII4014.logUpdateDB = function (responseString, options) { + console.log("Updated ", options.updatedDocs.length, " of ", options.totalNumOfDocs, " GPII documents."); + return options.updatedDocs.length; +}; + +/** + * Configure update, in batch, of the documents. + * @param {Object} options - The documents to be updated: + * @param {Array} options.updatedDocs - The documents to update. + * @return {Promise} - The promise that resolves the update. + */ +gpii.migration.GPII4014.updateDB = function (options) { + var details = { + dataToPost: options.updatedDocs, + responseDataHandler: gpii.migration.GPII4014.logUpdateDB + }; + return gpii.dbRequest.configureStep(details, options); +}; + +/** + * Create and execute the steps to migrate documents. + */ +gpii.migration.GPII4014.migrateStep1 = function () { + var options = gpii.migration.GPII4014.initOptions(process.argv); + var sequence = [ + gpii.migration.GPII4014.retrieveClientCredentials, + gpii.migration.GPII4014.updateDB + ]; + fluid.promise.sequence(sequence, options).then( + function (/*result*/) { + console.log("Done."); + process.exit(0); + }, + function (error) { + console.log(error); + process.exit(1); + } + ); +}; + +gpii.migration.GPII4014.migrateStep1(); diff --git a/scripts/migration/schema-0.2-GPII-4014/migration-step2.js b/scripts/migration/schema-0.2-GPII-4014/migration-step2.js new file mode 100644 index 000000000..52328d52f --- /dev/null +++ b/scripts/migration/schema-0.2-GPII-4014/migration-step2.js @@ -0,0 +1,183 @@ +/*! +Copyright 2019 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +// This script performs the second step of the data migration for GPII-4014 deployment. It includes: +// 1. Bump schemaVersion value from 0.1 to 0.2 for all documents that have schemaVersion field. +// Note that "_design/views" doc doesn't have this field. +// 2. if the document has "timestampUpdated" field, set it to the time that the migration runs. +// Note that this script migrates documents in batches. Each batch will migrate a limited number of documents +// to avoid hitting the out-of-memory issue when migrating a large number of documents. + +// Usage: node scripts/migration/schema-0.2-GPII-4014/migration-step2.js CouchDB-url [maxDocsInBatchPerRequest] +// @param {String} CouchDB-url - The url to the CouchDB where docoments should be migrated. +// @param {Number} maxDocsInBatchPerRequest - [optional] Limit the number of documents to be migrated in a batch. +// Default to 100 if not provided. + +// A sample command that runs this script in the universal root directory: +// node scripts/migration/schema-0.2-GPII-4014/migration-step2.js http://localhost:25984 10 + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + url = require("url"); + +fluid.registerNamespace("gpii.migration.GPII4014"); + +require("./shared/migratedValues.js"); +require("../../shared/dbRequestUtils.js"); +require("../../../gpii/node_modules/gpii-db-operation/src/DbUtils.js"); + +gpii.migration.GPII4014.defaultMaxDocsInBatchPerRequest = 100; + +/** + * Create a set of options for this script. + * The options are based on the command line parameters and a set of database constants. + * @param {Array} processArgv - The command line arguments. + * @return {Object} - The options. + */ +gpii.migration.GPII4014.initOptions = function (processArgv) { + var options = {}; + options.couchDbUrl = processArgv[2] + "/gpii"; + options.maxDocsInBatchPerRequest = Number(processArgv[3]) || gpii.migration.GPII4014.defaultMaxDocsInBatchPerRequest; + options.numOfUpdated = 0; + + // Set up database specific options + options.allDocsUrl = options.couchDbUrl + "/_design/views/_view/findDocsBySchemaVersion?key=%22" + gpii.migration.GPII4014.oldSchemaVersion + "%22&limit=" + options.maxDocsInBatchPerRequest; + options.allDocs = []; + options.parsedCouchDbUrl = url.parse(options.couchDbUrl); + options.postOptions = { + hostname: options.parsedCouchDbUrl.hostname, + port: options.parsedCouchDbUrl.port, + path: "/gpii/_bulk_docs", + auth: options.parsedCouchDbUrl.auth, + method: "POST", + headers: { + "Accept": "application/json", + "Content-Length": 0, // IMPORTANT: FILL IN PER REQUEST + "Content-Type": "application/json" + } + }; + console.log("COUCHDB_URL: '" + + options.parsedCouchDbUrl.protocol + "//" + + options.parsedCouchDbUrl.hostname + ":" + + options.parsedCouchDbUrl.port + + options.parsedCouchDbUrl.pathname + "'" + ); + return options; +}; + +/** + * Create the step that retrieves all documents from the database + * @param {Object} options - All docs URL and whether to filter: + * @param {String} options.allDocsUrl - The url for retrieving all documents in the database. + * @return {Promise} - A promise that resolves retrieved documents. + */ +gpii.migration.GPII4014.retrieveAllDocs = function (options) { + var details = { + requestUrl: options.allDocsUrl, + requestErrMsg: "Error retrieving documents from the database: ", + responseDataHandler: gpii.migration.GPII4014.updateDocsData, + responseErrMsg: "Error retrieving documents from database: " + }; + return gpii.dbRequest.configureStep(details, options); +}; + +/** + * Given all the documents from the database, update their data according to the new data model. + * @param {String} responseString - the response from the request to get all documents. + * @param {Object} options - Where to store the to-be-updated documents: + * @param {Array} options.allDocs - Accumulated documents. + * @return {Promise} - The resolved value contains an array of documents to be migrated, or 0 + * when no more document to migrate. + */ +gpii.migration.GPII4014.updateDocsData = function (responseString, options) { + var allDocs = JSON.parse(responseString); + var updatedDocs = []; + var togo = fluid.promise(); + + options.totalNumOfDocs = allDocs.total_rows; + + if (allDocs.rows.length === 0) { + options.updatedDocs = 0; + } else { + fluid.each(allDocs.rows, function (aRow) { + var aDoc = aRow.value; + // To filter out the "_design/views" doc that doesn't have the "schemaVersion" field + if (aDoc.schemaVersion) { + aDoc.schemaVersion = gpii.migration.GPII4014.newSchemaVersion; + if (aDoc.timestampUpdated === null) { + aDoc.timestampUpdated = gpii.dbOperation.getCurrentTimestamp(); + } + updatedDocs.push(aDoc); + } + }); + options.updatedDocs = updatedDocs; + } + togo.resolve(options.updatedDocs); + + return togo; +}; + +/** + * Log how many GPII documents were updated. + * @param {String} responseString - Response from the database (ignored) + * @param {Object} options - Object containing the set of documents: + * @param {Array} options.updatedDocs - The documents to update. + * @param {Number} options.numOfUpdated - The total number of migrated documents. + * @return {Number} - the number of documents updated. + */ +gpii.migration.GPII4014.logUpdateDB = function (responseString, options) { + options.numOfUpdated = options.numOfUpdated + options.updatedDocs.length; + console.log("Updated ", options.numOfUpdated, " of ", options.totalNumOfDocs, " GPII documents."); + return options.updatedDocs.length; +}; + +/** + * Configure update, in batch, of the documents. + * @param {Object} options - The documents to be updated: + * @param {Array} options.updatedDocs - The documents to update. + * @return {Promise} - A promise whose resolved value is the number of migrated documents, + * or 0 when all has been migrated. + */ +gpii.migration.GPII4014.updateDB = function (options) { + var togo = fluid.promise(); + if (options.updatedDocs === 0) { + togo.resolve(0); + } else { + var details = { + dataToPost: options.updatedDocs, + responseDataHandler: gpii.migration.GPII4014.logUpdateDB + }; + togo = gpii.dbRequest.configureStep(details, options); + } + return togo; +}; + +/** + * Create and execute the steps to migrate documents. + */ +gpii.migration.GPII4014.migrateStep2 = function () { + var options = gpii.migration.GPII4014.initOptions(process.argv); + var actionFunc = function (options) { + return fluid.promise.sequence([ + gpii.migration.GPII4014.retrieveAllDocs, + gpii.migration.GPII4014.updateDB + ], options); + }; + + var finalPromise = gpii.dbRequest.processRecursive(options, actionFunc); + + finalPromise.then(function () { + console.log("Done: Migrated " + options.numOfUpdated + " documents in total."); + }, console.log); +}; + +gpii.migration.GPII4014.migrateStep2(); diff --git a/scripts/migration/schema-0.2-GPII-4014/shared/migratedValues.js b/scripts/migration/schema-0.2-GPII-4014/shared/migratedValues.js new file mode 100644 index 000000000..9c95eae10 --- /dev/null +++ b/scripts/migration/schema-0.2-GPII-4014/shared/migratedValues.js @@ -0,0 +1,43 @@ +/*! +Copyright 2019 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +// This shared script defines migrated values for the GPII-4014 deployment. + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.migration.GPII4014"); + +// Migrated values +gpii.migration.GPII4014.oldSchemaVersion = "0.1"; +gpii.migration.GPII4014.newSchemaVersion = "0.2"; + +gpii.migration.GPII4014.newValuesForNovaClientCredential = { + "allowedIPBlocks": null, + "allowedPrefsToWrite": [ + "http://registry.gpii.net/common/language", + "http://registry.gpii.net/common/DPIScale", + "http://registry.gpii.net/common/highContrast/enabled", + "http://registry.gpii.net/common/highContrastTheme", + "http://registry.gpii.net/common/selfVoicing/enabled", + "http://registry.gpii.net/applications/com.microsoft.office" + ], + "isCreateGpiiKeyAllowed": true, + "isCreatePrefsSafeAllowed": true +}; + +gpii.migration.GPII4014.newValuesForPublicClientCredential = { + allowedIPBlocks: null, + allowedPrefsToWrite: null, + isCreateGpiiKeyAllowed: false, + isCreatePrefsSafeAllowed: false +}; diff --git a/scripts/migration/schema-0.2-GPII-4014/verify.js b/scripts/migration/schema-0.2-GPII-4014/verify.js new file mode 100644 index 000000000..75576bf3e --- /dev/null +++ b/scripts/migration/schema-0.2-GPII-4014/verify.js @@ -0,0 +1,168 @@ +/*! +Copyright 2019 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +// This script verifies updated CouchDB documents after the GPII-4014 deployment. It includes: +// 1. Client credential documents have new fields added with proper values; +// 2. All "schemaVersion" in documents have been set to 0.2. Note that "_design/views" doc doesn't have this field. +// 3. All "timestampUpdated" have been set. + +// Usage: node scripts/migration/schema-0.2-GPII-4014/verify.js CouchDB-url maxDocsInBatchPerRequest clientCredentialId-for-NOVA ... +// @param {String} CouchDB-url - The url to the CouchDB where docoments should be verified. +// @param {Number} maxDocsInBatchPerRequest - Limit the number of documents to be verified in a batch. +// @param {Strings} clientCredentialIds-for-NOVA - The "_id" value of the NOVA client credential. There could be any +// number of client credential parameters from here onwards. + +// A sample command that runs this script in the universal root directory: +// node scripts/migration/schema-0.2-GPII-4014/verify.js 500 http://localhost:25984 100 "clientCredential-nova1" "clientCredential-nova2" + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + url = require("url"); + +fluid.registerNamespace("gpii.migration.GPII4014"); + +require("./shared/migratedValues.js"); +require("../../shared/dbRequestUtils.js"); +require("../../../gpii/node_modules/gpii-db-operation/src/DbUtils.js"); + +/** + * Create a set of options for this script. + * The options are based on the command line parameters and a set of database constants. + * @param {Array} processArgv - The command line arguments. + * @return {Object} - The options. + */ +gpii.migration.GPII4014.initOptions = function (processArgv) { + var options = {}; + options.couchDbUrl = processArgv[2] + "/gpii"; + options.maxDocsInBatchPerRequest = Number(processArgv[3]); + options.novaClientCredentials = process.argv.splice(4); + options.numOfVerified = 0; + options.numOfErrorDocs = 0; + + // Set up database specific options + options.allDocsUrl = options.couchDbUrl + "/_all_docs?include_docs=true&descending=true&limit=" + options.maxDocsInBatchPerRequest; + options.allDocs = []; + options.parsedCouchDbUrl = url.parse(options.couchDbUrl); + options.postOptions = { + hostname: options.parsedCouchDbUrl.hostname, + port: options.parsedCouchDbUrl.port, + path: "/gpii/_bulk_docs", + auth: options.parsedCouchDbUrl.auth, + method: "POST", + headers: { + "Accept": "application/json", + "Content-Length": 0, // IMPORTANT: FILL IN PER REQUEST + "Content-Type": "application/json" + } + }; + console.log("COUCHDB_URL: '" + + options.parsedCouchDbUrl.protocol + "//" + + options.parsedCouchDbUrl.hostname + ":" + + options.parsedCouchDbUrl.port + + options.parsedCouchDbUrl.pathname + "'" + ); + return options; +}; + +/** + * Create the step that retrieves all documents from the database + * @param {Object} options - All docs URL and whether to filter: + * @param {String} options.allDocsUrl - The url for retrieving all documents in the database. + * @return {Promise} - A promise whose resolved value is the verification result. + */ +gpii.migration.GPII4014.verifyDocsInBatch = function (options) { + var details = { + requestUrl: options.allDocsUrl, + requestErrMsg: "Error retrieving documents from the database: ", + responseDataHandler: gpii.migration.GPII4014.verifyDocsData, + responseErrMsg: "Error verifying documents from database: " + }; + return gpii.dbRequest.configureStep(details, options); +}; + +/** + * Given all the documents from the database, update their data according to the new data model. + * @param {String} responseString - the response from the request to get all documents. + * @param {Object} options - Where to store the to-be-updated documents: + * @param {Array} options.novaClientCredentials - An array of NOVA client credential IDs. + * @param {Number} options.numOfVerified - The number of verified documents. + * @param {Number} options.numOfErrorDocs - The number of verified documents that have verification errors. + * @return {Promise} - A promise whose resolved value is the number of verified documents, or 0 when all has been verified. + */ +gpii.migration.GPII4014.verifyDocsData = function (responseString, options) { + var allDocs = JSON.parse(responseString); + options.totalNumOfDocs = allDocs.total_rows; + var togo = fluid.promise(); + + if (allDocs.rows.length === 0) { + togo.resolve(0); + } else { + fluid.each(allDocs.rows, function (aRow) { + var hasError = false; + var aDoc = aRow.doc; + // To filter out the "_design/views" doc that doesn't have the "schemaVersion" field + if (aDoc.schemaVersion) { + if (aDoc.schemaVersion !== gpii.migration.GPII4014.newSchemaVersion) { + console.log("Error with the document _id \"" + aDoc._id + "\": schema version is ", aDoc.schemaVersion, ", not ", gpii.migration.GPII4014.newSchemaVersion); + hasError = true; + } + if (aDoc.timestampUpdated === null) { + console.log("Error with the document _id \"" + aDoc._id + "\": the value of timestampUpdated is empty"); + hasError = true; + } + if (aDoc.type === "clientCredential") { + var docFields = { + allowedIPBlocks: aDoc.allowedIPBlocks, + allowedPrefsToWrite: aDoc.allowedPrefsToWrite, + isCreateGpiiKeyAllowed: aDoc.isCreateGpiiKeyAllowed, + isCreatePrefsSafeAllowed: aDoc.isCreatePrefsSafeAllowed + }; + var diffResult = fluid.model.diff( + docFields, + options.novaClientCredentials.includes(aDoc._id) ? gpii.migration.GPII4014.newValuesForNovaClientCredential : gpii.migration.GPII4014.newValuesForPublicClientCredential + ); + if (!diffResult) { + console.log("Error with the document _id \"" + aDoc._id + "\": new field values for client credential are incorrect - ", docFields); + hasError = true; + } + } + } + if (hasError) { + options.numOfErrorDocs++; + } + }); + options.numOfVerified = options.numOfVerified + allDocs.rows.length; + console.log("Verified " + options.numOfVerified + " of " + options.totalNumOfDocs + " documents."); + options.allDocsUrl = options.couchDbUrl + "/_all_docs?include_docs=true&descending=true&skip=" + options.numOfVerified + "&limit=" + options.maxDocsInBatchPerRequest; + togo.resolve(allDocs.rows.length); + } + return togo; +}; + +/** + * Create and execute the steps to verify documents. + */ +gpii.migration.GPII4014.verify = function () { + var options = gpii.migration.GPII4014.initOptions(process.argv); + var finalPromise = gpii.dbRequest.processRecursive(options, gpii.migration.GPII4014.verifyDocsInBatch); + + finalPromise.then(function () { + if (options.numOfErrorDocs > 0) { + console.log("Fail: " + options.numOfErrorDocs + " documents have errors."); + } else { + console.log("All passed."); + } + console.log("Done: Verified " + options.totalNumOfDocs + " documents in total."); + }, console.log); +}; + +gpii.migration.GPII4014.verify(); diff --git a/scripts/shared/dbRequestUtils.js b/scripts/shared/dbRequestUtils.js new file mode 100644 index 000000000..fd086ac43 --- /dev/null +++ b/scripts/shared/dbRequestUtils.js @@ -0,0 +1,219 @@ +/*! +Copyright 2019 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var http = require("http"), + fluid = require("infusion"); + +var gpii = fluid.registerNamespace("gpii"); +fluid.registerNamespace("gpii.dbRequest"); + +/** + * Response handler function used for the callback argument of an + * {http.ClientRequest}. + * @callback ResponseCallback + * @param {http.IncomingMessage} response - Response object. + */ + +/** + * Function that processes the data passed via the {ResponseCallback}. + * @callback ResponseDataHandler + * @param {String} responseString - The raw response data. + * @param {Object} options - Other information used by this handler; documented + * by specific data handler functions. + * @return {String|Promise} - Returns a string describing the handling result, or + * a promise whose resolved value is a string that describes + * the handling result. + */ + +/** + * POST request headers. + * @typedef {Object} PostRequestHeaders + * @property {String} Accept - "application/json" (constant). + * @property {String} Content-Length - Computed and filled in per request (variable). + * @property {String} Content-Type - "application/json" (constant). + */ + +/** + * The POST request options for bulk updates. + * @typedef {Object} PostRequestOptions + * @property {String} hostname - The database host name (constant). + * @property {String} port - The port associated with the URL (constant). + * @property {String} path - The bulk documents command: "/gpii/_bulk_docs" (constant). + * @property {String} auth - Authorization for access (constant). + * @property {String} method - "POST" (constant). + * @property {PostRequestHeaders} headers - The POST headers. + */ + +/** + * Utility to configure a step: Creates a response callback, binds it to an + * http database request, and configures a promise to resolve/reject when the + * response callback finishes or fails. + * @param {Object} details - Specific information for the request and response. + * These details are set appropriately by the caller: + * @param {String} details.requestErrMsg - Error message to display on a request + * error. + * @param {ResponseDataHandler} details.responseDataHandler - + * Function for processing the data returned in + * the response. + * @param {String} details.responseErrMsg - Error message to display on a + * response error. + * @param {Array} details.dataToPost - Optional: if present, a POST request is + * used. + * @param {String} details.requestUrl - If not a POST request, the URL for a GET + * request. + * @param {Object} options - Post request: + * @param {PostRequestOptions} options.postOptions - If a POST request is used, + * contains the specifics of + * the request. + * @return {Promise} - A promise that resolves the configured step. + */ +gpii.dbRequest.configureStep = function (details, options) { + var togo = fluid.promise(); + var response = gpii.dbRequest.createResponseHandler( + details.responseDataHandler, + options, + togo, + details.responseErrMsg + ); + var request; + if (details.dataToPost) { + request = gpii.dbRequest.createPostRequest( + details.dataToPost, response, options, togo + ); + } else { + request = gpii.dbRequest.queryDatabase( + details.requestUrl, response, details.requestErrMsg, togo + ); + } + request.end(); + return togo; +}; + +/** + * Create an http request for a bulk docs POST request using the given data. + * @param {Object} dataToPost - JSON data to POST and process in bulk. + * @param {ResponseCallback} responseHandler - http response callback for the + * request. + * @param {Object} options - Post request options: + * @param {PostRequestOptions} options.postOptions - the POST request specifics. + * @param {Promise} promise - promise to reject on a request error. + * @return {http.ClientRequest} - An http request object. + */ +gpii.dbRequest.createPostRequest = function (dataToPost, responseHandler, options, promise) { + var batchPostData = JSON.stringify({"docs": dataToPost}); + options.postOptions.headers["Content-Length"] = Buffer.byteLength(batchPostData); + var batchDocsRequest = http.request(options.postOptions, responseHandler); + batchDocsRequest.on("error", function (e) { + fluid.log("Error with bulk 'docs' POST request: " + e); + promise.reject(e); + }); + batchDocsRequest.write(batchPostData); + return batchDocsRequest; +}; + +/** + * Generate a response handler, setting up the given promise to resolve/reject + * at the correct time. + * @param {ResponseDataHandler} handleEnd - Function that processes the response + * data when the response receives an + * "end" event. + * @param {Object} options - Data loader options passed to `handleEnd()`. + * @param {Promise} promise - Promise to resolve/reject on a response "end" or + * "error" event. + * @param {String} errorMsg - Optional error message to prepend to the error + * received from a response "error" event. + * @return {ResponseCallback} - Reponse callback function suitable for an http + * request. + */ +gpii.dbRequest.createResponseHandler = function (handleEnd, options, promise, errorMsg) { + errorMsg = (errorMsg || ""); + return function (response) { + var responseString = ""; + + response.setEncoding("utf8"); + response.on("data", function (chunk) { + responseString += chunk; + }); + response.on("end", function () { + if (response.statusCode >= 400) { // error + var fullErrorMsg = errorMsg + + response.statusCode + " - " + + response.statusMessage; + // Document-not-found or 404 errors include a reason in the + // response. + // http://docs.couchdb.org/en/stable/api/basics.html#http-status-codes + if (response.statusCode === 404) { + fullErrorMsg = fullErrorMsg + ", " + + JSON.parse(responseString).reason; + } + promise.reject(fullErrorMsg); + } + else { + var handlingResult = handleEnd(responseString, options); + if (fluid.isPromise(handlingResult)) { + fluid.promise.follow(handlingResult, promise); + } else { + promise.resolve(handlingResult); + } + } + }); + response.on("error", function (e) { + fluid.log(errorMsg + e.message); + promise.reject(e); + }); + }; +}; + +/** + * General mechanism to create a database request, set up an error handler and + * return. It is up to the caller to trigger the request by calling its end() + * function. + * @param {String} databaseURL - URL to query the database with. + * @param {ResponseCallback} handleResponse - callback that processes the + * response from the request. + * @param {String} errorMsg - optional error message for request errors. + * @param {Promise} promise - promise to reject on a request error. + * @return {http.ClientRequest} - The http request object. + */ +gpii.dbRequest.queryDatabase = function (databaseURL, handleResponse, errorMsg, promise) { + var aRequest = http.request(databaseURL, handleResponse); + aRequest.on("error", function (e) { + fluid.log(errorMsg + e.message); + promise.reject(e); + }); + return aRequest; +}; + +/** + * Process recursively. + * @param {Object} options - Object of elements required for processing. It also tracks information that need to be passed + * along during the processing. + * @param {Function} actionFunc - The action function that returns a promise whose resolved value is a number of + * documents to process. If the number is 0, the recursion stops. If more than 0, the recursion continues. + * @return {Promise} - Hold the processing result. + */ +gpii.dbRequest.processRecursive = function (options, actionFunc) { + var togo = fluid.promise(); + var actionPromise = actionFunc(options); + + actionPromise.then(function (docCount) { + if (docCount === 0 || docCount[0] === 0) { + // No more documents to process + togo.resolve(); + } else { + // Continue to process + var subsequentProcess = gpii.dbRequest.processRecursive(options, actionFunc); + fluid.promise.follow(subsequentProcess, togo); + } + }, togo.reject); + return togo; +}; diff --git a/scripts/shared/prefsSetsDbUtils.js b/scripts/shared/prefsSetsDbUtils.js new file mode 100644 index 000000000..3e868f5a2 --- /dev/null +++ b/scripts/shared/prefsSetsDbUtils.js @@ -0,0 +1,81 @@ +/*! +Copyright 2018 Raising the Floor US + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +/** + * This file contains utilities that are useful to generate keys data that + * can be inserted into a CouchDB. + * + * Actually, these utils are used in: + * * convertPrefs.js + * * loadEmptyKeys.js + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.prefsSetsDbUtils"); + +/** + * This is an empty preferences set. + */ +gpii.prefsSetsDbUtils.emptyPreferencesBlock = { + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": {} + } + } + } +}; + +/** + * Generate the data for a key creation. + * + * The data contains both the gpiiKey and the preferences safe. + * + * @param {String} gpiiKeyId - The identifier of the prefsSet (usually, a uuid). + * @param {Object} preferences - The preferences to include in the prefsSet. + * @param {String} prefsSafeType - The type of the prefsSafe, either "user" or "snapset". + * @return {Object} A JSON object containing the documents ready to be inserted into DB. + */ +gpii.prefsSetsDbUtils.generateKeyData = function (gpiiKeyId, preferences, prefsSafeType) { + var currentTime = new Date().toISOString(); + var prefsSafeId = "prefsSafe-" + gpiiKeyId; + + var newGpiiKey = { + "_id": gpiiKeyId, + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": prefsSafeId, + "prefsSetId": "gpii-default", + "revoked": false, + "revokedReason": null, + "timestampCreated": currentTime, + "timestampUpdated": null + }; + + var newPrefsSafe = { + "_id": prefsSafeId, + "type": "prefsSafe", + "schemaVersion": "0.2", + "prefsSafeType": prefsSafeType || "user", + "name": gpiiKeyId, + "password": null, + "email": null, + "preferences": preferences || gpii.prefsSetsDbUtils.emptyPreferencesBlock, + "timestampCreated": currentTime, + "timestampUpdated": null + }; + + return { gpiiKey: newGpiiKey, prefsSafe: newPrefsSafe }; +}; diff --git a/scripts/vagrantCloudBasedContainers.sh b/scripts/vagrantCloudBasedContainers.sh new file mode 100755 index 000000000..a13604b61 --- /dev/null +++ b/scripts/vagrantCloudBasedContainers.sh @@ -0,0 +1,105 @@ +# +# This scripts deploys an environment for end-to-end tests based on Docker +# containers. It was initially developed to support CI. +# +# It builds a Docker image for GPII/universal and uses it to start two +# components: the Preferences Server and the Flow Manager. +# +# It also starts a CouchDB container and loads the CouchDB data into +# it, so tests running against the GPII components will have access to the +# latest test data. +# +# This script can be executed multiple times to create a new Docker image +# and deploy fresh containers every time. +# +# https://issues.gpii.net/browse/GPII-2470 + +# This script accepts an option "--no-rebuild" indicating to only start +# docker containers without rebuilding the universal docker image. If this +# option is not provided, the old docker containers will be removed and the +# universal docker image will be rebuilt. +# Example command to build from the scratch: ./vagrantCloudBasedContainers.sh +# Example command not to rebuild: ./vagrantCloudBasedContainers.sh --no-rebuild + +if [ "$1" == "--no-rebuild" ]; then + NO_REBUILD=true +fi + +UNIVERSAL_IMAGE=vagrant-universal + +COUCHDB_IMAGE=couchdb:2.3.1 +COUCHDB_PORT=5984 +COUCHDB_HEALTHCHECK_DELAY=2 +COUCHDB_HEALTHCHECK_TIMEOUT=30 +if [ "$NO_REBUILD" == "true" ] ; then + CLEAR_INDEX= +else + CLEAR_INDEX='true' +fi + +UNIVERSAL_DIR="/home/vagrant/sync/universal" +STATIC_DATA_DIR="$UNIVERSAL_DIR/testData/dbData" +BUILD_DATA_DIR="$UNIVERSAL_DIR/build/dbData/snapset" + +DATALOADER_COUCHDB_URL="http://couchdb:${COUCHDB_PORT}/gpii" +DATASOURCE_HOSTNAME="http://couchdb" +DATALOADER_CMD="/app/scripts/deleteAndLoadSnapsets.sh" +PRODTESTS_CMD="npm run test:productionConfig" + +GPII_PREFERENCES_CONFIG="gpii.config.preferencesServer.standalone.production" +GPII_PREFERENCES_PORT=9081 + +GPII_FLOWMANAGER_CONFIG="gpii.config.cloudBased.flowManager.production" +GPII_CLOUD_FLOWMANAGER_PORT=9082 +GPII_LOCAL_FLOWMANAGER_PORT=8080 +GPII_FLOWMANAGER_TO_PREFERENCESSERVER_URL="http://preferences:${GPII_PREFERENCES_PORT}" + +# The URL to point to the flow manager docker container, only used by running the production config tests +GPII_CLOUD_URL="http://flowmanager:9082" + +# The URLs to test the readiness of each docker container +COUCHDB_VIEW_URL="http://localhost:$COUCHDB_PORT/gpii/_design/views/_view/findPrefsSafeByGpiiKey?key=%22carla%22&include_docs=true" +CARLA_PREFERENCES_URL="http://localhost:$GPII_PREFERENCES_PORT/preferences/carla" +GPII_FLOWMANAGER_READY_URL="http://localhost:$GPII_CLOUD_FLOWMANAGER_PORT/ready" + +# Remove old containers (exit code is ignored) +docker rm -f couchdb 2>/dev/null || true +docker rm -f preferences 2>/dev/null || true +docker rm -f flowmanager 2>/dev/null || true +docker rm -f productionConfigTests 2>/dev/null || true + +if [ "$NO_REBUILD" != "true" ] ; then + # Remove old image (exit code is ignored) + docker rmi -f $UNIVERSAL_IMAGE 2>/dev/null || true + + # Build image + docker build -t $UNIVERSAL_IMAGE . +fi + +# Start the CouchDB container +docker run -d -p $COUCHDB_PORT:$COUCHDB_PORT --name couchdb $COUCHDB_IMAGE + +# Wait for CouchDB +wget -O /dev/null --retry-connrefused --waitretry=$COUCHDB_HEALTHCHECK_DELAY --read-timeout=20 --timeout=1 --tries=$COUCHDB_HEALTHCHECK_TIMEOUT http://localhost:$COUCHDB_PORT + +# Load the CouchDB data +docker run --rm --link couchdb -v $STATIC_DATA_DIR:/static_data -e GPII_STATIC_DATA_DIR=/static_data -v $BUILD_DATA_DIR:/build_data -e GPII_SNAPSET_DATA_DIR=/build_data -e GPII_COUCHDB_URL=$DATALOADER_COUCHDB_URL -e GPII_CLEAR_INDEX=$CLEAR_INDEX $UNIVERSAL_IMAGE $DATALOADER_CMD + +# Wait for the CouchDB views become accessible. Accessing the view URL forced the view index to build which take time. +# The URL returns 500 when the index is not ready, so use "--retry-on-http-error" option to continue retries at 500 response code. +wget -O /dev/null --retry-connrefused --waitretry=10 --read-timeout=20 --timeout=1 --tries=30 --retry-on-http-error=500 $COUCHDB_VIEW_URL + +# Start the preferences server container +docker run -d -p $GPII_PREFERENCES_PORT:$GPII_PREFERENCES_PORT --name preferences --link couchdb -e NODE_ENV=$GPII_PREFERENCES_CONFIG -e GPII_PREFERENCESSERVER_LISTEN_PORT=$GPII_PREFERENCES_PORT -e GPII_DATASOURCE_HOSTNAME=$DATASOURCE_HOSTNAME -e GPII_DATASOURCE_PORT=$COUCHDB_PORT $UNIVERSAL_IMAGE + +# Wait for the preferences server container to be ready +wget -O /dev/null --retry-connrefused --waitretry=10 --read-timeout=20 --timeout=1 --tries=30 $CARLA_PREFERENCES_URL + +# Start the flow manager container (the CBFM) +docker run -d -p $GPII_CLOUD_FLOWMANAGER_PORT:$GPII_CLOUD_FLOWMANAGER_PORT --name flowmanager --link couchdb --link preferences -e NODE_ENV=$GPII_FLOWMANAGER_CONFIG -e GPII_FLOWMANAGER_LISTEN_PORT=$GPII_CLOUD_FLOWMANAGER_PORT -e GPII_DATASOURCE_HOSTNAME=$DATASOURCE_HOSTNAME -e GPII_DATASOURCE_PORT=$COUCHDB_PORT -e GPII_FLOWMANAGER_TO_PREFERENCESSERVER_URL=$GPII_FLOWMANAGER_TO_PREFERENCESSERVER_URL $UNIVERSAL_IMAGE + +# Wait for the flow manager container to be ready +wget -O /dev/null --retry-connrefused --waitretry=10 --read-timeout=20 --timeout=1 --tries=30 $GPII_FLOWMANAGER_READY_URL + +# Start the container to run production config tests +docker run --name productionConfigTests --link flowmanager --link couchdb -e GPII_CLOUD_URL=$GPII_CLOUD_URL -e GPII_FLOWMANAGER_LISTEN_PORT=$GPII_LOCAL_FLOWMANAGER_PORT -e GPII_COUCHDB_URL=$DATALOADER_COUCHDB_URL $UNIVERSAL_IMAGE $PRODTESTS_CMD diff --git a/testData/clientCredentials/pilot.json b/testData/clientCredentials/pilot.json new file mode 100644 index 000000000..c8a964a69 --- /dev/null +++ b/testData/clientCredentials/pilot.json @@ -0,0 +1,4 @@ +{ + "client_id": "pilot-computer", + "client_secret": "pilot-computer-secret" +} diff --git a/testData/dbData/README.md b/testData/dbData/README.md new file mode 100644 index 000000000..d3511570b --- /dev/null +++ b/testData/dbData/README.md @@ -0,0 +1,25 @@ +This folder contains the DB data that is used for running GPII in different configurations. They are loaded into CouchDB: + +1. When GPII runs in a development configuration or when running GPII integration tests. +2. When GPII runs in a production or staging configuration. + +Each data file corresponds to a document structure specified in the [GPII Data Model](https://wiki.gpii.net/w/Keys,_KeyTokens,_and_Preferences) documentation: + +* clientCredentials.json: Contains OAuth2 client credentials. +* gpiiAppInstallationClients.json: Contains the information of all registered GPII app installations. +* views.json: The CouchDB "_design/views" functions. + +Additional preferences DB data are found elsewhere and used in conjunction with the files in this folder. The +preferences files located in %universal/testData/preferences/ are converted into both "snapset" and "user" preferences. +The following "snapset" folder contains snapset PrefsSafes and GPII keys for (1) running GPII in production and staging +GPII configurations where the data is loaded into CouchDB, and (2) running GPII in its development configuration where +the data is loaded into the local CouchDB: + +* %universal/build/dbData/snapset/gpiiKeys.json +* %universal/build/dbData/snapset/prefsSafes.json + +The following "user" folder contains writable "user" data, loaded into a local CouchDB instance when running GPII +integration tests: + +* %universal/build/dbData/user/gpiiKeys.json +* %universal/build/dbData/user/prefsSafes.json diff --git a/testData/dbData/clientCredentials.json b/testData/dbData/clientCredentials.json new file mode 100644 index 000000000..f66e408de --- /dev/null +++ b/testData/dbData/clientCredentials.json @@ -0,0 +1,18 @@ +[ + { + "_id": "clientCredential-1", + "type": "clientCredential", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-1", + "oauth2ClientId": "pilot-computer", + "oauth2ClientSecret": "pilot-computer-secret", + "allowedIPBlocks": null, + "allowedPrefsToWrite": null, + "isCreateGpiiKeyAllowed": false, + "isCreatePrefsSafeAllowed": false, + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampRevoked": null + } +] diff --git a/testData/dbData/gpiiAppInstallationClients.json b/testData/dbData/gpiiAppInstallationClients.json new file mode 100644 index 000000000..4c6e76f4b --- /dev/null +++ b/testData/dbData/gpiiAppInstallationClients.json @@ -0,0 +1,11 @@ +[ + { + "_id": "gpiiAppInstallationClient-1", + "type": "gpiiAppInstallationClient", + "schemaVersion": "0.2", + "name": "Pilot Computers", + "computerType": "public", + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + } +] diff --git a/testData/dbData/views.json b/testData/dbData/views.json new file mode 100644 index 000000000..6c189697f --- /dev/null +++ b/testData/dbData/views.json @@ -0,0 +1,28 @@ +[ + { + "_id": "_design/views", + "views": { + "findPrefsSafeByGpiiKey": { + "map": "function(doc) {\n if (doc.type === 'gpiiKey') {\nvar record = { gpiiKey: doc};\nif (doc.prefsSafeId) { record._id = doc.prefsSafeId; };\nemit(doc._id, record);\n}\n}" + }, + "findClientByOauth2ClientId": { + "map": "function(doc) {\n if (doc.type === 'clientCredential' && doc.revoked === false) {\n var record = { clientCredential: doc };\n if (doc.clientId) {\n record._id = doc.clientId;\n }\n emit(doc.oauth2ClientId, record);\n} \n}" + }, + "findInfoByAccessToken": { + "map": "function(doc) {\n if (doc.type === 'gpiiAppInstallationAuthorization' && doc.revoked === false) {\n var record = { authorization: doc };\n if (doc.clientCredentialId) {\n record._id = doc.clientCredentialId;\n } \n emit(doc.accessToken, record);\n }\n }" + }, + "findSnapsetPrefsSafes": { + "map": "function(doc) {if (doc.type === 'prefsSafe' && doc.prefsSafeType === 'snapset') { emit(doc._id, doc); }}" + }, + "findAllGpiiKeys": { + "map": "function(doc) {if (doc.type === 'gpiiKey') { emit(doc._id, doc); }}" + }, + "findAccessTokenByExpires": { + "map": "function(doc) {if (doc.type === 'gpiiAppInstallationAuthorization') emit(Date.parse(doc.timestampExpires), doc); }" + }, + "findDocsBySchemaVersion": { + "map": "function(doc) {emit(doc.schemaVersion, doc); }" + } + } + } +] diff --git a/testData/defaultSettings/defaultSettings.win32.json5 b/testData/defaultSettings/defaultSettings.win32.json5 new file mode 100644 index 000000000..d8ea20804 --- /dev/null +++ b/testData/defaultSettings/defaultSettings.win32.json5 @@ -0,0 +1,25 @@ +{ + "contexts": { + "gpii-default": { + "preferences": { + "http://registry.gpii.net/common/language": "en-US", + "http://registry.gpii.net/common/DPIScale": 0, + "http://registry.gpii.net/common/highContrast/enabled": false, + "http://registry.gpii.net/common/highContrastTheme": "regular-contrast", + "http://registry.gpii.net/common/selfVoicing/enabled": false, + "http://registry.gpii.net/applications/com.microsoft.office": { + "word-ribbon": "StandardSet" + }, + "http://registry.gpii.net/applications/com.microsoft.windows.colorFilters": { + "FilterType": 0 + }, + "http://registry.gpii.net/applications/com.microsoft.windows.mouseSettings": { + "PointerSpeed": 10, + "SwapMouseButtons": 0, + "DoubleClickTime": 500 + }, + "http://registry.gpii.net/common/cursorSize": 0 + } + } + } +} diff --git a/testData/deviceReporter/acceptanceTests/android_builtIn.json b/testData/deviceReporter/acceptanceTests/android_builtIn.json new file mode 100644 index 000000000..3310a5a9d --- /dev/null +++ b/testData/deviceReporter/acceptanceTests/android_builtIn.json @@ -0,0 +1,11 @@ +[ + { + "id": "com.android.persistentConfiguration" + }, + { + "id": "com.android.audioManager" + }, + { + "id": "com.android.settings.system" + } +] diff --git a/testData/deviceReporter/acceptanceTests/jaws.json b/testData/deviceReporter/acceptanceTests/jaws.json new file mode 100644 index 000000000..a60797e53 --- /dev/null +++ b/testData/deviceReporter/acceptanceTests/jaws.json @@ -0,0 +1,3 @@ +[ + { "id": "com.freedomscientific.jaws" } +] diff --git a/testData/deviceReporter/acceptanceTests/linux_builtIn.json b/testData/deviceReporter/acceptanceTests/linux_builtIn.json new file mode 100644 index 000000000..e715db15c --- /dev/null +++ b/testData/deviceReporter/acceptanceTests/linux_builtIn.json @@ -0,0 +1,20 @@ +[ + { + "id": "org.gnome.desktop.interface" + }, + { + "id": "org.gnome.desktop.a11y.magnifier" + }, + { + "id": "org.alsa-project" + }, + { + "id": "org.gnome.desktop.a11y.keyboard" + }, + { + "id": "org.gnome.desktop.a11y.applications.onscreen-keyboard" + }, + { + "id": "org.gnome.orca" + } +] diff --git a/testData/deviceReporter/acceptanceTests/magic.json b/testData/deviceReporter/acceptanceTests/magic.json new file mode 100644 index 000000000..d68c01192 --- /dev/null +++ b/testData/deviceReporter/acceptanceTests/magic.json @@ -0,0 +1,3 @@ +[ + { "id": "com.freedomscientific.magic" } +] diff --git a/testData/deviceReporter/acceptanceTests/nvda.json b/testData/deviceReporter/acceptanceTests/nvda.json new file mode 100644 index 000000000..c23a27c5a --- /dev/null +++ b/testData/deviceReporter/acceptanceTests/nvda.json @@ -0,0 +1,5 @@ +[ + { + "id": "org.nvda-project" + } +] diff --git a/testData/deviceReporter/acceptanceTests/oneNoteLearningTools.json b/testData/deviceReporter/acceptanceTests/oneNoteLearningTools.json new file mode 100644 index 000000000..7994b8855 --- /dev/null +++ b/testData/deviceReporter/acceptanceTests/oneNoteLearningTools.json @@ -0,0 +1,5 @@ +[ + { + "id": "com.office.windowsOneNoteLearningTools" + } +] diff --git a/testData/deviceReporter/acceptanceTests/orca.json b/testData/deviceReporter/acceptanceTests/orca.json new file mode 100644 index 000000000..060caa1d4 --- /dev/null +++ b/testData/deviceReporter/acceptanceTests/orca.json @@ -0,0 +1,5 @@ +[ + { + "id": "org.gnome.orca" + } +] diff --git a/testData/deviceReporter/acceptanceTests/readWrite.json b/testData/deviceReporter/acceptanceTests/readWrite.json new file mode 100644 index 000000000..08a7f346e --- /dev/null +++ b/testData/deviceReporter/acceptanceTests/readWrite.json @@ -0,0 +1,5 @@ +[ + { + "id": "com.texthelp.readWriteGold" + } +] diff --git a/testData/deviceReporter/acceptanceTests/talkback.json b/testData/deviceReporter/acceptanceTests/talkback.json new file mode 100644 index 000000000..dffc1acd8 --- /dev/null +++ b/testData/deviceReporter/acceptanceTests/talkback.json @@ -0,0 +1,11 @@ +[ + { + "id": "com.android.talkback" + }, + { + "id": "com.android.audioManager" + }, + { + "id": "com.android.settings.secure" + } +] diff --git a/testData/deviceReporter/acceptanceTests/uioPlus.json b/testData/deviceReporter/acceptanceTests/uioPlus.json new file mode 100644 index 000000000..30658cff6 --- /dev/null +++ b/testData/deviceReporter/acceptanceTests/uioPlus.json @@ -0,0 +1,5 @@ +[ + { + "id": "net.gpii.uioPlus" + } +] diff --git a/testData/deviceReporter/acceptanceTests/win7_builtIn.json b/testData/deviceReporter/acceptanceTests/win7_builtIn.json new file mode 100644 index 000000000..7468d1866 --- /dev/null +++ b/testData/deviceReporter/acceptanceTests/win7_builtIn.json @@ -0,0 +1,97 @@ +[ + { + "id": "com.microsoft.windows.magnifier" + }, + + { + "id": "com.microsoft.windows.onscreenKeyboard" + }, + + { + "id": "com.microsoft.windows.highContrast" + }, + + { + "id": "com.microsoft.windows.mouseTrailing" + }, + + { + "id": "com.microsoft.windows.screenResolution" + }, + + { + "id": "com.microsoft.windows.screenDPI" + }, + + { + "id": "com.microsoft.windows.cursors" + }, + + { + "id": "com.microsoft.windows.stickyKeys" + }, + + { + "id": "com.microsoft.windows.toggleKeys" + }, + + { + "id": "com.microsoft.windows.underlineMenuShortcuts" + }, + + { + "id": "com.microsoft.windows.shortcutWarningMessage" + }, + + { + "id": "com.microsoft.windows.shortcutWarningSound" + }, + + { + "id": "com.microsoft.windows.filterKeys" + }, + + { + "id": "com.microsoft.windows.mouseKeys" + }, + + { + "id": "com.microsoft.windows.mouseSettings" + }, + + { + "id": "com.microsoft.windows.narrator" + }, + + { + "id": "com.microsoft.windows.nightScreen" + }, + + { + "id": "com.microsoft.windows.typingEnhancement" + }, + + { + "id": "com.microsoft.windows.volumeControl" + }, + + { + "id": "com.microsoft.windows.language" + }, + + { + "id": "com.microsoft.windows.touchPadSettings" + }, + + { + "id": "com.microsoft.windows.desktopBackground" + }, + + { + "id": "com.microsoft.windows.desktopBackgroundColor" + }, + + { + "id": "com.microsoft.windows.soundSentry" + } +] diff --git a/testData/deviceReporter/acceptanceTests/win_brightness.json b/testData/deviceReporter/acceptanceTests/win_brightness.json new file mode 100644 index 000000000..0885ca2eb --- /dev/null +++ b/testData/deviceReporter/acceptanceTests/win_brightness.json @@ -0,0 +1,5 @@ +[ + { + "id": "com.microsoft.windows.brightness" + } +] diff --git a/testData/deviceReporter/acceptanceTests/wordHome365LearningTools.json b/testData/deviceReporter/acceptanceTests/wordHome365LearningTools.json new file mode 100644 index 000000000..8bf599836 --- /dev/null +++ b/testData/deviceReporter/acceptanceTests/wordHome365LearningTools.json @@ -0,0 +1,5 @@ +[ + { + "id": "com.office.windowsWordHome365LearningTools" + } +] diff --git a/testData/deviceReporter/acceptanceTests/wordPro365LearningTools.json b/testData/deviceReporter/acceptanceTests/wordPro365LearningTools.json new file mode 100644 index 000000000..f622c0a50 --- /dev/null +++ b/testData/deviceReporter/acceptanceTests/wordPro365LearningTools.json @@ -0,0 +1,5 @@ +[ + { + "id": "com.office.windowsWordPro365LearningTools" + } +] diff --git a/testData/deviceReporter/acceptanceTests/xrandr.json b/testData/deviceReporter/acceptanceTests/xrandr.json new file mode 100644 index 000000000..c0e254ce5 --- /dev/null +++ b/testData/deviceReporter/acceptanceTests/xrandr.json @@ -0,0 +1,5 @@ +[ + { + "id": "org.freedesktop.xrandr" + } +] diff --git a/testData/deviceReporter/acceptanceTests/zoomtext.json b/testData/deviceReporter/acceptanceTests/zoomtext.json new file mode 100644 index 000000000..4eaeb442b --- /dev/null +++ b/testData/deviceReporter/acceptanceTests/zoomtext.json @@ -0,0 +1,3 @@ +[ + { "id": "com.aisquared.zoomtext" } +] diff --git a/testData/deviceReporter/installedSolutions.json b/testData/deviceReporter/installedSolutions.json index 599a03952..f915e9bc8 100644 --- a/testData/deviceReporter/installedSolutions.json +++ b/testData/deviceReporter/installedSolutions.json @@ -1,109 +1,214 @@ [ + { - "id": "org.gnome.desktop.interface" + "id": "com.android.activitymanager" }, - + { - "id": "org.gnome.nautilus" + "id": "com.android.audioManager" }, - + { - "id": "org.gnome.desktop.a11y.keyboard" + "id": "com.android.freespeech" }, - + { - "id": "org.gnome.desktop.a11y.caribou-keyboard" + "id": "com.android.persistentConfiguration" }, - + { - "id": "org.gnome.orca" + "id": "com.android.settings.secure" + }, + + { + "id": "com.android.settings.system" }, - + { - "id": "org.gnome.desktop.a11y.magnifier" + "id": "com.android.talkback" + }, + + { + "id": "com.freedomscientific.jaws" + }, + + { + "id": "com.freedomscientific.magic" + }, + + { + "id": "com.microsoft.windows.cursors" + }, + + { + "id": "com.microsoft.windows.colorFilters" + }, + + { + "id": "com.microsoft.windows.filterKeys" + }, + + { + "id": "com.microsoft.windows.highContrast" + }, + + { + "id": "com.microsoft.windows.highContrastTheme" }, - + + { + "id": "com.microsoft.windows.language" + }, + { "id": "com.microsoft.windows.magnifier" }, - + + { + "id": "com.microsoft.windows.audioDescription" + }, + + { + "id": "com.microsoft.windows.notificationDuration" + }, + + { + "id": "com.microsoft.windows.mouseKeys" + }, + + { + "id": "com.microsoft.windows.mouseSettings" + }, + + { + "id": "com.microsoft.windows.mouseTrailing" + }, + + { + "id": "com.microsoft.windows.toggleKeys" + }, + + { + "id": "com.microsoft.windows.underlineMenuShortcuts" + }, + + { + "id": "com.microsoft.windows.shortcutWarningMessage" + }, + + { + "id": "com.microsoft.windows.shortcutWarningSound" + }, + + { + "id": "com.microsoft.windows.narrator" + }, + + { + "id": "com.microsoft.windows.nightScreen" + }, + { "id": "com.microsoft.windows.onscreenKeyboard" }, - + { - "id": "org.nvda-project" + "id": "com.microsoft.windows.screenDPI" }, - + { - "id": "fluid.uiOptions.windows" + "id": "com.microsoft.windows.screenResolution" }, - + { - "id": "fluid.uiOptions.linux" + "id": "com.microsoft.windows.stickyKeys" }, - + { - "id": "org.gnome.desktop.interface" + "id": "com.microsoft.windows.touchPadSettings" }, - + { - "id": "org.gnome.nautilus" + "id": "com.microsoft.windows.typingEnhancement" }, - + { - "id": "trace.easyOne.communicator.windows" + "id": "com.microsoft.windows.volumeControl" }, - + { - "id": "trace.easyOne.communicator.linux" + "id": "com.microsoft.windows.desktopBackground" }, - + { - "id": "trace.easyOne.sudan.windows" + "id": "com.microsoft.windows.desktopBackgroundColor" }, - + { - "id": "trace.easyOne.sudan.linux" + "id": "com.microsoft.windows.brightness" }, - + { - "id": "webinsight.webAnywhere.windows" + "id": "com.microsoft.windows.soundSentry" }, - + { - "id": "webinsight.webAnywhere.linux" + "id": "fakemag2" }, - + { - "id": "net.opendirective.maavis" + "id": "fakescreenreader1" }, { - "id": "com.microsoft.windows.highContrast" + "id": "net.gpii.explode" }, - + { - "id": "com.microsoft.windows.nonClientMetrics" + "id": "net.gpii.test.speechControl" }, - + { - "id": "com.microsoft.windows.mouseTrailing" + "id": "net.gpii.uioPlus" }, - + { - "id": "com.microsoft.windows.cursors" + "id": "org.alsa-project" }, { - "id": "com.android.activitymanager" + "id": "org.freedesktop.xrandr" }, { - "id": "com.android.talkback" + "id": "org.gnome.desktop.a11y.applications.onscreen-keyboard" }, { - "id": "com.android.freespeech" + "id": "org.gnome.desktop.a11y.keyboard" + }, + + { + "id": "org.gnome.desktop.a11y.magnifier" + }, + + { + "id": "org.gnome.desktop.interface" + }, + + { + "id": "org.gnome.nautilus" + }, + + { + "id": "org.gnome.orca" + }, + + { + "id": "org.nvda-project" + }, + + { + "id": "com.microsoft.office" } ] diff --git a/testData/deviceReporter/installedSolutions.txt b/testData/deviceReporter/installedSolutions.txt new file mode 100644 index 000000000..bd1bdc54d --- /dev/null +++ b/testData/deviceReporter/installedSolutions.txt @@ -0,0 +1,8 @@ +installedSolutions.json +========================== + +This is the file describing what solutions the 'deviceReporter' reports as installed when +the system is run in 'development' mode. It contains a list of objects, each reporting the +ID of a solution. To modify what solutions are reported as installed, simply add/remove entries +to/from this file. + diff --git a/testData/deviceReporter/osx_installedSolutions.json b/testData/deviceReporter/osx_installedSolutions.json deleted file mode 100644 index deeec6286..000000000 --- a/testData/deviceReporter/osx_installedSolutions.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "OS": { - "id": "darwin", - "version": "11.3.0" - }, - "solutions": [ - { - "id": "org.gnome.desktop.interface" - }, - - { - "id": "org.gnome.nautilus" - }, - - { - "id": "org.gnome.desktop.a11y.keyboard" - }, - - { - "id": "org.gnome.desktop.a11y.caribou-keyboard" - }, - - { - "id": "org.gnome.orca" - }, - - { - "id": "org.gnome.desktop.a11y.magnifier" - }, - - { - "id": "com.microsoft.windows.magnifier" - }, - - { - "id": "com.microsoft.windows.onscreenKeyboard" - }, - - { - "id": "nvda.screenReader" - }, - - { - "id": "fluid.uiOptions.windows" - }, - - { - "id": "fluid.uiOptions.linux" - }, - - { - "id": "org.gnome.desktop.interface" - }, - - { - "id": "org.gnome.nautilus" - }, - - { - "id": "trace.easyOne.communicator.windows" - }, - - { - "id": "trace.easyOne.communicator.linux" - }, - - { - "id": "trace.easyOne.sudan.windows" - }, - - { - "id": "trace.easyOne.sudan.linux" - }, - - { - "id": "webinsight.webAnywhere.windows" - }, - - { - "id": "webinsight.webAnywhere.linux" - } - ] -} \ No newline at end of file diff --git a/testData/deviceReporter/review3/installedSolutionsClassroomAndroid.json b/testData/deviceReporter/review3/installedSolutionsClassroomAndroid.json new file mode 100644 index 000000000..e15fc37c0 --- /dev/null +++ b/testData/deviceReporter/review3/installedSolutionsClassroomAndroid.json @@ -0,0 +1,5 @@ +[ + { + "id": "es.codefactory.android.app.ma" + } +] diff --git a/testData/deviceReporter/review3/installedSolutionsClassroomWindows.json b/testData/deviceReporter/review3/installedSolutionsClassroomWindows.json new file mode 100644 index 000000000..aa375b361 --- /dev/null +++ b/testData/deviceReporter/review3/installedSolutionsClassroomWindows.json @@ -0,0 +1,23 @@ +[ + { + "id": "com.microsoft.windows.onscreenKeyboard" + }, + { + "id": "com.microsoft.windows.magnifier" + }, + { + "id": "com.microsoft.windows.displaySettings" + }, + { + "id": "com.microsoft.windows.highContrast" + }, + { + "id": "org.nvda-project" + }, + { + "id": "com.yourdolphin.supernova-as" + }, + { + "id": "com.texthelp.readWriteGold" + } +] diff --git a/testData/deviceReporter/review3/installedSolutionsVladimirHotel.json b/testData/deviceReporter/review3/installedSolutionsVladimirHotel.json new file mode 100644 index 000000000..e4fb00e2e --- /dev/null +++ b/testData/deviceReporter/review3/installedSolutionsVladimirHotel.json @@ -0,0 +1,5 @@ +[ + { + "id": "com.tpvision.gpiitvproxy" + } +] diff --git a/testData/deviceReporter/review3/installedSolutionsVladimirLobby.json b/testData/deviceReporter/review3/installedSolutionsVladimirLobby.json new file mode 100644 index 000000000..b05af329f --- /dev/null +++ b/testData/deviceReporter/review3/installedSolutionsVladimirLobby.json @@ -0,0 +1,17 @@ +[ + { + "id": "com.microsoft.windows.cursors" + }, + { + "id": "com.microsoft.windows.highContrast" + }, + { + "id": "com.microsoft.windows.mouseTrailing" + }, + { + "id": "org.chrome.cloud4chrome" + }, + { + "id": "org.nvda-project" + } +] diff --git a/testData/deviceReporter/review3/installedSolutionsVladimirSubway.json b/testData/deviceReporter/review3/installedSolutionsVladimirSubway.json new file mode 100644 index 000000000..62d891d5c --- /dev/null +++ b/testData/deviceReporter/review3/installedSolutionsVladimirSubway.json @@ -0,0 +1,17 @@ +[ + { + "id": "com.android.talkback" + }, + { + "id": "com.android.persistentConfiguration" + }, + { + "id": "com.android.audioManager" + }, + { + "id": "com.android.settings.system" + }, + { + "id": "com.android.settings.secure" + } +] diff --git a/testData/deviceReporter/review3/installedSolutionsVladimirVending.json b/testData/deviceReporter/review3/installedSolutionsVladimirVending.json new file mode 100644 index 000000000..4a67f1817 --- /dev/null +++ b/testData/deviceReporter/review3/installedSolutionsVladimirVending.json @@ -0,0 +1,5 @@ +[ + { + "id": "de.fraunhofer.iao.C4A-TVM" + } +] diff --git a/testData/deviceReporter/review3/review3.md b/testData/deviceReporter/review3/review3.md new file mode 100644 index 000000000..0ec864ed4 --- /dev/null +++ b/testData/deviceReporter/review3/review3.md @@ -0,0 +1,5 @@ +== Device reporter files used for 3. review + +The files in this folder are the ones used for the review meeting, Jan. 2015, using this script: https://docs.google.com/document/d/13fqxn2kpmLcdDkSnHKKpW3DtHaKVxP_zyA92-NqxWLU/edit . To activate one of these configuration, you need to rename the file in question to "installedSolutions.json" and place in the "testData/deviceReporter" folder. + +**Warning: These device reporter files are not part of any tests that is being run on a regular basis and can hence not be trusted to always work. They _were_ tested leading up to and during the review meeting on January, 2015** diff --git a/testData/deviceReporter/secondPilots/demo_GoogleChrome.json b/testData/deviceReporter/secondPilots/demo_GoogleChrome.json new file mode 100644 index 000000000..79dde7e5d --- /dev/null +++ b/testData/deviceReporter/secondPilots/demo_GoogleChrome.json @@ -0,0 +1,5 @@ +[ + { + "id": "org.chrome.cloud4chrome" + } +] diff --git a/testData/deviceReporter/secondPilots/demo_Library.json b/testData/deviceReporter/secondPilots/demo_Library.json new file mode 100644 index 000000000..365c03c32 --- /dev/null +++ b/testData/deviceReporter/secondPilots/demo_Library.json @@ -0,0 +1,9 @@ +[ + { + "id": "webinsight.webAnywhere.windows" + }, + + { + "id": "webinsight.webAnywhere.linux" + } +] diff --git a/testData/deviceReporter/secondPilots/demo_MultipleSolutions.json b/testData/deviceReporter/secondPilots/demo_MultipleSolutions.json new file mode 100644 index 000000000..7a8a4ddac --- /dev/null +++ b/testData/deviceReporter/secondPilots/demo_MultipleSolutions.json @@ -0,0 +1,8 @@ +[ + { + "id": "org.nvda-project" + }, + { + "id": "com.yourdolphin.supernova-as" + } +] diff --git a/testData/deviceReporter/secondPilots/demo_SmartHouse.json b/testData/deviceReporter/secondPilots/demo_SmartHouse.json new file mode 100644 index 000000000..f2f0970ea --- /dev/null +++ b/testData/deviceReporter/secondPilots/demo_SmartHouse.json @@ -0,0 +1,5 @@ +[ + { + "id": "net.gpii.smarthouses" + } +] diff --git a/testData/deviceReporter/secondPilots/platformAB_onAndroid_MobileAccessibility.json b/testData/deviceReporter/secondPilots/platformAB_onAndroid_MobileAccessibility.json new file mode 100644 index 000000000..e5623ed86 --- /dev/null +++ b/testData/deviceReporter/secondPilots/platformAB_onAndroid_MobileAccessibility.json @@ -0,0 +1,97 @@ +[ + { + "id": "org.gnome.desktop.interface" + }, + + { + "id": "org.gnome.shell.overrides" + }, + + { + "id": "org.gnome.desktop.wm.preferences" + }, + + { + "id": "org.gnome.nautilus" + }, + + { + "id": "org.gnome.desktop.a11y.keyboard" + }, + + { + "id": "org.gnome.desktop.a11y.applications.onscreen-keyboard" + }, + + { + "id": "org.gnome.orca" + }, + + { + "id": "org.gnome.desktop.a11y.magnifier" + }, + + { + "id": "com.microsoft.windows.magnifier" + }, + + { + "id": "com.microsoft.windows.onscreenKeyboard" + }, + + { + "id": "org.nvda-project" + }, + + { + "id": "org.gnome.desktop.interface" + }, + + { + "id": "org.gnome.nautilus" + }, + + { + "id": "com.microsoft.windows.highContrast" + }, + + { + "id": "com.microsoft.windows.mouseTrailing" + }, + + { + "id": "com.microsoft.windows.cursors" + }, + + { + "id": "com.android.activitymanager" + }, + + { + "id": "com.android.freespeech" + }, + + { + "id": "com.android.audioManager" + }, + + { + "id": "com.android.persistentConfiguration" + }, + + { + "id": "org.alsa-project" + }, + + { + "id": "org.freedesktop.xrandr" + }, + + { + "id": "com.android.settings.system" + }, + + { + "id": "es.codefactory.android.app.ma" + } +] diff --git a/testData/deviceReporter/secondPilots/platformAB_onAndroid_TalkBack.json b/testData/deviceReporter/secondPilots/platformAB_onAndroid_TalkBack.json new file mode 100644 index 000000000..700578ec0 --- /dev/null +++ b/testData/deviceReporter/secondPilots/platformAB_onAndroid_TalkBack.json @@ -0,0 +1,101 @@ +[ + { + "id": "org.gnome.desktop.interface" + }, + + { + "id": "org.gnome.shell.overrides" + }, + + { + "id": "org.gnome.desktop.wm.preferences" + }, + + { + "id": "org.gnome.nautilus" + }, + + { + "id": "org.gnome.desktop.a11y.keyboard" + }, + + { + "id": "org.gnome.desktop.a11y.applications.onscreen-keyboard" + }, + + { + "id": "org.gnome.orca" + }, + + { + "id": "org.gnome.desktop.a11y.magnifier" + }, + + { + "id": "com.microsoft.windows.magnifier" + }, + + { + "id": "com.microsoft.windows.onscreenKeyboard" + }, + + { + "id": "org.nvda-project" + }, + + { + "id": "org.gnome.desktop.interface" + }, + + { + "id": "org.gnome.nautilus" + }, + + { + "id": "com.microsoft.windows.highContrast" + }, + + { + "id": "com.microsoft.windows.mouseTrailing" + }, + + { + "id": "com.microsoft.windows.cursors" + }, + + { + "id": "com.android.activitymanager" + }, + + { + "id": "com.android.talkback" + }, + + { + "id": "com.android.freespeech" + }, + + { + "id": "com.android.settings.secure" + }, + + { + "id": "com.android.audioManager" + }, + + { + "id": "com.android.persistentConfiguration" + }, + + { + "id": "org.alsa-project" + }, + + { + "id": "org.freedesktop.xrandr" + }, + + { + "id": "com.android.settings.system" + } +] diff --git a/testData/deviceReporter/secondPilots/platformAB_onWindows_NVDA.json b/testData/deviceReporter/secondPilots/platformAB_onWindows_NVDA.json new file mode 100644 index 000000000..4c2f29bf3 --- /dev/null +++ b/testData/deviceReporter/secondPilots/platformAB_onWindows_NVDA.json @@ -0,0 +1,105 @@ +[ + { + "id": "org.gnome.desktop.interface" + }, + + { + "id": "org.gnome.shell.overrides" + }, + + { + "id": "org.gnome.desktop.wm.preferences" + }, + + { + "id": "org.gnome.nautilus" + }, + + { + "id": "org.gnome.desktop.a11y.keyboard" + }, + + { + "id": "org.gnome.desktop.a11y.applications.onscreen-keyboard" + }, + + { + "id": "org.gnome.orca" + }, + + { + "id": "org.gnome.desktop.a11y.magnifier" + }, + + { + "id": "com.microsoft.windows.magnifier" + }, + + { + "id": "com.microsoft.windows.onscreenKeyboard" + }, + + { + "id": "org.nvda-project" + }, + + { + "id": "org.gnome.desktop.interface" + }, + + { + "id": "org.gnome.nautilus" + }, + + { + "id": "com.microsoft.windows.highContrast" + }, + + { + "id": "com.microsoft.windows.mouseTrailing" + }, + + { + "id": "com.microsoft.windows.cursors" + }, + + { + "id": "com.android.activitymanager" + }, + + { + "id": "com.android.talkback" + }, + + { + "id": "com.android.freespeech" + }, + + { + "id": "com.android.settings.secure" + }, + + { + "id": "com.android.audioManager" + }, + + { + "id": "com.android.persistentConfiguration" + }, + + { + "id": "org.alsa-project" + }, + + { + "id": "org.freedesktop.xrandr" + }, + + { + "id": "com.android.settings.system" + }, + + { + "id": "info.cloud4all.JME" + } +] diff --git a/testData/deviceReporter/secondPilots/platformAB_onWindows_Supernova.json b/testData/deviceReporter/secondPilots/platformAB_onWindows_Supernova.json new file mode 100644 index 000000000..647e94d76 --- /dev/null +++ b/testData/deviceReporter/secondPilots/platformAB_onWindows_Supernova.json @@ -0,0 +1,97 @@ +[ + { + "id": "org.gnome.desktop.interface" + }, + + { + "id": "org.gnome.shell.overrides" + }, + + { + "id": "org.gnome.desktop.wm.preferences" + }, + + { + "id": "org.gnome.nautilus" + }, + + { + "id": "org.gnome.desktop.a11y.keyboard" + }, + + { + "id": "org.gnome.desktop.a11y.applications.onscreen-keyboard" + }, + + { + "id": "org.gnome.orca" + }, + + { + "id": "org.gnome.desktop.a11y.magnifier" + }, + + { + "id": "com.microsoft.windows.onscreenKeyboard" + }, + + { + "id": "com.yourdolphin.supernova-as" + }, + + { + "id": "org.gnome.desktop.interface" + }, + + { + "id": "org.gnome.nautilus" + }, + + { + "id": "com.microsoft.windows.highContrast" + }, + + { + "id": "com.microsoft.windows.mouseTrailing" + }, + + { + "id": "com.microsoft.windows.cursors" + }, + + { + "id": "com.android.activitymanager" + }, + + { + "id": "com.android.talkback" + }, + + { + "id": "com.android.freespeech" + }, + + { + "id": "com.android.settings.secure" + }, + + { + "id": "com.android.audioManager" + }, + + { + "id": "com.android.persistentConfiguration" + }, + + { + "id": "org.alsa-project" + }, + + { + "id": "org.freedesktop.xrandr" + }, + + { + "id": "com.android.settings.system" + } +] diff --git a/testData/deviceReporter/secondPilots/secondPilots.txt b/testData/deviceReporter/secondPilots/secondPilots.txt new file mode 100644 index 000000000..388d91916 --- /dev/null +++ b/testData/deviceReporter/secondPilots/secondPilots.txt @@ -0,0 +1,14 @@ +The configuration files in this folder were used for the second round of pilot testing in the Cloud4All project. + +Each configuration file sets up a machine according to some scenario that should be tested in the second pilots phase - these files are used instead of the dynamic device reporter to statically report what's "installed" on the machine. + +The machines were 'configured' by using a configuration tool (web page - https://github.com/Cloud4AllTUD/pilotsConfig), which would replace the static device reporter file that was used by GPII with the relevant one from this folder. + +To make sense of the use case of each device reporter file, see +* https://docs.google.com/document/d/1f4s6EUpLRR9ZMqQyJufmwx0l7HfzxdSNYusZa8vysSc/ + +Page with all documents related to the second pilot phase: +* http://wiki.gpii.net/w/Cloud4all_Second_Pilot_Phase + +Setup and installation instructions: +* http://wiki.gpii.net/w/Cloud4all_Pilot_2_-_Set_up_and_installation diff --git a/testData/deviceReporter/win32_installedSolutions.json b/testData/deviceReporter/win32_installedSolutions.json deleted file mode 100644 index 0daff4ae9..000000000 --- a/testData/deviceReporter/win32_installedSolutions.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "OS": { - "id": "win32", - "version": "5.0.0" - }, - "solutions": [ - { - "id": "org.gnome.desktop.interface" - }, - - { - "id": "org.gnome.nautilus" - }, - - { - "id": "org.gnome.desktop.a11y.keyboard" - }, - - { - "id": "org.gnome.desktop.a11y.caribou-keyboard" - }, - - { - "id": "org.gnome.orca" - }, - - { - "id": "org.gnome.desktop.a11y.magnifier" - }, - - { - "id": "com.microsoft.windows.magnifier" - }, - - { - "id": "com.microsoft.windows.onscreenKeyboard" - }, - - { - "id": "org.nvda-project" - }, - - { - "id": "fluid.uiOptions.windows" - }, - - { - "id": "fluid.uiOptions.linux" - }, - - { - "id": "org.gnome.desktop.interface" - }, - - { - "id": "org.gnome.nautilus" - }, - - { - "id": "trace.easyOne.communicator.windows" - }, - - { - "id": "trace.easyOne.communicator.linux" - }, - - { - "id": "trace.easyOne.sudan.windows" - }, - - { - "id": "trace.easyOne.sudan.linux" - }, - - { - "id": "webinsight.webAnywhere.windows" - }, - - { - "id": "webinsight.webAnywhere.linux" - }, - - { - "id": "net.opendirective.maavis" - }, - - { - "id": "com.microsoft.windows.highContrast" - }, - - { - "id": "com.microsoft.windows.nonClientMetrics" - }, - - { - "id": "com.microsoft.windows.mouseTracking" - }, - - { - "id": "com.microsoft.windows.cursors" - } - ] -} \ No newline at end of file diff --git a/testData/ontologies/ISO24751-2/flat.json b/testData/ontologies/ISO24751-2/flat.json deleted file mode 100644 index 6577e14cd..000000000 --- a/testData/ontologies/ISO24751-2/flat.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "transformations": { - "display.screenEnhancement.fontSize": "{http://registry.gpii.org/common/fontSize}.0.value", - "display.screenEnhancement.foregroundColor": "{http://registry.gpii.org/common/foregroundColor}.0.value", - "display.screenEnhancement.backgroundColor": "{http://registry.gpii.org/common/backgroundColor}.0.value", - "display.screenEnhancement.fontFaceFontName": "{http://registry.gpii.org/common/fontFaceFontName}.0.value", - "display.screenEnhancement.fontFaceGenericFontFace": "{http://registry.gpii.org/common/fontFaceGenericFontFace}.0.value", - "display.screenEnhancement.magnification": "{http://registry.gpii.org/common/magnification}.0.value", - "display.screenEnhancement.-provisional-magnifierPosition": "{http://registry.gpii.org/common/magnifierPosition}.0.value", - "display.screenEnhancement.tracking": "{http://registry.gpii.org/common/tracking}.0.value", - "display.screenEnhancement.trackingTTS": "{http://registry.gpii.org/common/trackingTTS}.0.value", - "display.screenEnhancement.invertImages": "{http://registry.gpii.org/common/invertImages}.0.value", - "display.screenEnhancement.cursorSize": "{http://registry.gpii.org/common/cursorSize}.0.value", - "display.screenEnhancement.highContrast": "{http://registry.gpii.org/common/highContrast}.0.value", - "display.screenEnhancement.mouseTrailing": "{http://registry.gpii.org/common/mouseTrailing}.0.value", - - "display.screenReader.-provisional-screenReaderBrailleOutput": "{http://registry.gpii.org/common/screenReaderBrailleOutput}.0.value", - "display.screenReader.-provisional-auditoryOutLanguage": "{http://registry.gpii.org/common/auditoryOutLanguage}.0.value", - "display.screenReader.speechRate": "{http://registry.gpii.org/common/speechRate}.0.value", - "display.screenReader.-provisional-speakTutorialMessages": "{http://registry.gpii.org/common/speakTutorialMessages}.0.value", - "display.screenReader.-provisional-keyEcho": "{http://registry.gpii.org/common/keyEcho}.0.value", - "display.screenReader.-provisional-wordEcho": "{http://registry.gpii.org/common/wordEcho}.0.value", - "display.screenReader.-provisional-announceCapitals": "{http://registry.gpii.org/common/announceCapitals}.0.value", - "display.screenReader.-provisional-brailleOutput": "{http://registry.gpii.org/common/screenReaderBrailleOutput}.0.value", - "display.screenReader.-provisional-punctuationVerbosity": "{http://registry.gpii.org/common/punctuationVerbosity}.0.value", - "display.screenReader.-provisional-screenReaderTTSEnabled": "{http://registry.gpii.org/common/screenReaderTTSEnabled}.0.value", - - "display.textReadingHighlight.readingUnit": "{http://registry.gpii.org/common/readingUnit}.0.value", - - "display.braille.-provisional-brailleMode": "{http://registry.gpii.org/common/brailleMode}.0.value", - - "control.onscreenKeyboard": "{http://registry.gpii.org/common/onscreenKeyboard}.0.value", - "control.mouseEmulation.-provisional-initDelay": "{http://registry.gpii.org/common/-provisional-initDelay}.0.value", - "control.mouseEmulation.cursorSpeed": "{http://registry.gpii.org/common/cursorSpeed}.0.value", - "control.mouseEmulation.cursorAcceleration": "{http://registry.gpii.org/common/cursorAcceleration}.0.value", - "control.mouseEmulation.-provisional-mouseEmulationEnabled": "{http://registry.gpii.org/common/-provisional-mouseEmulationEnabled}.0.value", - "control.keyboardEnhancement.stickyKeys": "{http://registry.gpii.org/common/stickyKeys}.0.value", - "control.keyboardEnhancement.slowKeys.slowKeysInterval": "{http://registry.gpii.org/common/slowKeysInterval}.0.value", - "control.keyboardEnhancement.slowKeys.-provisional-slowKeysEnable": "{http://registry.gpii.org/common/-provisional-slowKeysEnable}.0.value", - "control.keyboardEnhancement.debounceKeys.-provisional-debounceEnable": "{http://registry.gpii.org/common/-provisional-debounceEnable}.0.value", - "control.keyboardEnhancement.debounceKeys.debounceInterval": "{http://registry.gpii.org/common/debounceInterval}.0.value", - - "content.adaptationPreference": "{http://registry.gpii.org/common/adaptationPreference}.0.value", - "control.structuralNavigation.tableOfContents": "{http://registry.gpii.org/common/tableOfContents}.0.value", - - "": { - "transform": { - "type": "gpii.ontologyServer.transform.application", - "inputPath": "", - "outputPath": "applications" - } - } - } -} \ No newline at end of file diff --git a/testData/ontologies/flat.json5 b/testData/ontologies/flat.json5 new file mode 100644 index 000000000..33b7e2f01 --- /dev/null +++ b/testData/ontologies/flat.json5 @@ -0,0 +1,954 @@ +{ + "http://registry.gpii.net/common/announceCapitals": { + "schema": { + "title": "Announce capitals", + "description": "Whether to announce capitals", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/audioDescription/enabled": { + "schema": { + "title": "Audio Description", + "description": "Whether to enable/disable audio description on videos", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/auditoryOutLanguage": { + "schema": { + "title": "TTS output language", + "description": "Language in which the text to speech must be produced", + "type": "string", + "default": "en", //Not sure whether this should be specified and fallback to the currently used language + "enum": [ + "en", + "en-GB", + "en-US", + "en-scotland", + "en-BZ", + "en-BS", + "en-AG", + "en-AI", + "af", + "bg", + "bs", + "ca", + "cs", + "cy", + "da", + "de", + "el", + "grc", + "eo", + "es", + "es-419", + "et", + "fi", + "fr", + "fr-BE", + "hi", + "hr", + "hu", + "hy", + "hy-arevmda", + "id", + "is", + "it", + "jbo", + "ka", + "kn", + "ku", + "la", + "lv", + "mk", + "ml", + "nl", + "no", + "pap", + "pl", + "pt-BR", + "pt-PT", + "ro", + "ru", + "sk", + "sq", + "sr", + "sv", + "sw", + "ta", + "tr", + "vi", + "zh-cmn", + "cmn", + "zh-yue", + "yue" + ], + "enumLabels": [ + "English", + "British English", + "US English", + "en-scotland", + "en-BZ", + "en-BS", + "en-AG", + "en-AI", + "Afrikaans", + "Bulgarian", + "Bosnian", + "Catalan Spanish", + "Czech", + "Welsh", + "Danish", + "German", + "Greek", + "grc", + "eo", + "Spanish", + "es-419", + "Estonian", + "Finnish", + "French", + "French (Belgium)", + "Hindi", + "Croatian", + "Hungarian", + "Armenian", + "hy-arevmda", + "Indonesian", + "Icelandic", + "Italian", + "jbo", + "Georgian", + "Kannada", + "ku", + "la", + "Latvian", + "Macedonian", + "Malayalam", + "Dutch", + "no", + "pap", + "Polish", + "Portuguese (Brazil)", + "Portuguese (Portugal)", + "Romanian", + "Russian", + "Slovak", + "Albanian", + "Serbian", + "Swedish", + "Kiswahili", + "Tamil", + "Turkish", + "Vienamese", + "zh-cmn", + "cmn", + "zh-yue", + "yue" + ] + } + }, + "http://registry.gpii.net/common/captions/enabled": { + "schema": { + "title": "Captions", + "description": "Whether to enable/disable captions on videos", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/characterSpace": { + "schema": { + "title": "Character Space", + "description": "Increases the distance between characters.", + "type": "number", + "default": 1, + "minimum": 1, + "maximum": 2, + "multipleOf": 0.1 + } + }, + "http://registry.gpii.net/common/contrast": { + "schema": { + "title": "Contrast", + "description": "Screen contrast", + "type": "integer", + "minimum": 0, // 0 means black - should this be 1 instead? + "maximum": 100 + } + }, + "http://registry.gpii.net/common/cursorColor": { + "schema": { + "title": "Cursor Color", + "description": "Cursor color", + enum: ["White", "Black", "ReverseBlack"], + enumLabels: ["White", "Black", "Black (reversed)"], + default: "White" + } + }, + "http://registry.gpii.net/common/cursorSize": { + "schema": { + "title": "Cursor Size", + "description": "Cursor size", + "type": "number", + "default": 0.5, + "minimum": 0, + "maximum": 1, + "multipleOf": 0.1 + } + }, + "http://registry.gpii.net/common/cursorSpeed": { + "schema": { + "title": "Cursor speed", + "description": "The speed of the mouse cursor", + "type": "number", + "minimum": 0, + "maximum": 1, + "default": 0.5 // TODO: Discuss why this is needed for the transform checks and if there are alternative approaches. + } + }, + "http://registry.gpii.net/common/debounce/enabled": { + "schema": { + "title": "Debounce", + "description": "Whether to enable/disable debounce", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/DPIScale": { + "schema": { + "title": "DPI Scale", + "description": "DPI scale factor on default monitor", + "type": "integer", + "default": 0, + "minimum": -2, + "maximum": 4 + } + }, + "http://registry.gpii.net/common/fontSize": { + "schema": { + "title": "Font Size", + "description": "Font size of the text", + "type": "number", + "default": 12, + "minimum": 3, + //TODO: Do we need to define a maximum value to avoid the user to leave the computer unusable? + "multipleOf": 0.1 + } + }, + "http://registry.gpii.net/common/highContrast/enabled": { + "schema": { + "title": "High Contrast", + "description": "Whether to enable/disable High Contrast", + "type": "boolean", + "default": false + } + }, + // TODO: These are heavily skewed towards windows, we will have to think about how to make truly "common" values. + "http://registry.gpii.net/common/highContrastTheme": { + "schema": { + "title": "High Contrast theme", + "description": "High Contrast Theme", + "type": "string", + "default": "black-white", + "enum": [ + "regular-contrast", + "black-white", + "white-black", + "yellow-black", + "black-yellow", + "lime-black", + "black-brown", + "grey-black", + "grey-white" + ], + "enumLabels": [ + "Regular Contrast", + "Black on White", + "White on Black", + "Yellow on Black", + "Black on Yellow", + "Lime on Black", + "Black on Brown", + "Grey on Dark", + "Grey on White" + ] + } + }, + "http://registry.gpii.net/common/highlightColor": { + "schema": { + "title": "Highlight Color", + "description": "The color used to highlight a user's selection.", + "type": "string", + "default": "default", + "enum": [ + "default", + "yellow", + "green", + "pink" + ], + "enumLabels": [ + "Default", + "Yellow", + "Green", + "Pink" + ] + } + }, + "http://registry.gpii.net/common/initDelay": { + "schema": { + "title": "Keyboard init delay", // needs to be defined + "description": "Keyboard's init delay", + "type": "integer", + "default": 0, + "minimum": 0 // in milliseconds + } + }, + "http://registry.gpii.net/common/inputsLarger/enabled": { + "schema": { + "title": "Inputs Larger", + "description": "Increases the size of inputs to make them more discoverable.", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/invertColours": { + "schema": { + "title": "Invert colours", + "description": "Whether to invert colours", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/keyEcho": { + "schema": { + "title": "Key echo", + "description": "Whether to speak each key as it is introduced", + "type": "boolean", + "default": true + } + }, + // Currently adapted from: https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/available-language-packs-for-windows + // TODO: This should be platform independent and pinned to some reasonable standard (such as RFC-5646). + "http://registry.gpii.net/common/language": { + "schema": { + "title": "Language", + "description": "Language of the system", + "type": "string", + "default": "en-US", + "enum": [ + "af-ZA", + "sq-AL", + "am-ET", + "ar-SA", + "hy-AM", + "as-IN", + "az-Latn-AZ", + "bn-BD", + "bn-IN", + "eu-ES", + "be-BY", + "bs-Latn-BA", + "bg-BG", + "ca-ES", + "ku-ARAB-IQ", + "chr-CHER-US", + "zh-HK", + "zh-CN", + "zh-TW", + "hr-HR", + "cs-CZ", + "da-DK", + "prs-AF", + "nl-NL", + "en-GB", + "en-US", + "et-EE", + "fil-PH", + "fi-FI", + "fr-CA", + "fr-FR", + "gl-ES", + "ka-GE", + "de-DE", + "el-GR", + "gu-IN", + "ha-Latn-NG", + "he-IL", + "hi-IN", + "hu-HU", + "is-IS", + "ig-NG", + "id-ID", + "iu-Latn-CA", + "ga-IE", + "xh-ZA", + "zu-ZA", + "it-IT", + "ja-JP", + "quc-Latn-GT", + "qut-GT", + "kn-IN", + "kk-KZ", + "km-KH", + "rw-RW", + "sw-KE", + "kok-IN", + "ko-KR", + "ky-KG", + "lo-LA", + "lv-LV", + "lt-LT", + "lb-LU", + "mk-MK", + "ms-MY", + "ml-IN", + "mt-MT", + "mi-NZ", + "mr-IN", + "mn-MN", + "ne-NP", + "nb-NO", + "nn-NO", + "or-IN", + "fa-IR", + "pl-PL", + "pt-BR", + "pt-PT", + "pa-Arab-PK", + "pa-IN", + "quz-PE", + "ro-RO", + "ru-RU", + "gd-GB", + "sr-Cyrl-BA", + "sr-Cyrl-CS", + "sr-Cyrl-RS", + "sr-Latn-CS", + "sr-Latn-RS", + "nso-ZA", + "tn-ZA", + "sd-Arab-PK", + "si-LK", + "sk-SK", + "sl-SI", + "es-MX", + "es-ES", + "sv-SE", + "tg-Cyrl-TJ", + "ta-IN", + "tt-RU", + "te-IN", + "th-TH", + "ti-ET", + "tr-TR", + "tk-TM", + "uk-UA", + "ur-PK", + "ug-CN", + "uz-Latn-UZ", + "ca-ES-valencia", + "vi-VN", + "cy-GB", + "wo-SN", + "yo-NG" + ], + "enumLabels": [ + "Afrikaans (South Africa)", + "Albanian (Albania)", + "Amharic (Ethiopia)", + "Arabic (Saudi Arabia)", + "Armenian (Armenia)", + "Assamese (India)", + "Azerbaijan", + "Bangla (Bangladesh)", + "Bangla (India)", + "Basque (Basque)", + "Belarusian", + "Bosnian (Latin)", + "Bulgarian (Bulgaria)", + "Catalan", + "Central Kurdish", + "Cherokee", + "Chinese (Hong Kong SAR)", + "Chinese (PRC)", + "Chinese (Taiwan)", + "Croatian (Croatia)", + "Czech (Czech Republic)", + "Danish (Denmark)", + "Dari", + "Dutch (Netherlands)", + "English (United Kingdom)", + "English (United States)", + "Estonian (Estonia)", + "Filipino", + "Finnish (Finland)", + "French (Canada)", + "French (France)", + "Galician", + "Georgian (Georgia)", + "German (Germany)", + "Greek (Greece)", + "Gujarati (India)", + "Hausa (Latin, Nigeria)", + "Hebrew (Israel)", + "Hindi (India)", + "Hungarian (Hungary)", + "Icelandic (Iceland)", + "Igbo (Nigeria)", + "Indonesian (Indonesia)", + "Inuktitut (Latin, Canada)", + "Irish (Ireland)", + "isiXhosa (South Africa)", + "isiZulu (South Africa)", + "Italian (Italy)", + "Japanese (Japan)", + "K'iche' (Guatemala)", + "K'iche' (Guatemala)", + "Kannada (India)", + "Kazakh (Kazakhstan)", + "Khmer (Cambodia)", + "Kinyarwanda", + "Kiswahili (Kenya)", + "Konkani (India)", + "Korean (Korea)", + "Kyrgyz (Kyrgyzstan)", + "Lao (Laos)", + "Latvian (Latvia)", + "Lithuanian (Lithuania)", + "Luxembourgish (Luxembourg)", + "Macedonian (FYROM)", + "Malay (Malaysia, Brunei, and Singapore)", + "Malayalam (India)", + "Maltese (Malta)", + "Maori (New Zealand)", + "Marathi (India)", + "Mongolian (Cyrillic)", + "Nepali (Federal Democratic Republic of Nepal)", + "Norwegian, Bokmål (Norway)", + "Norwegian, Nynorsk (Norway)", + "Odia (India)", + "Persian", + "Polish (Poland)", + "Portuguese (Brazil)", + "Portuguese (Portugal)", + "Punjabi (Arabic)", + "Punjabi (India)", + "Quechua (Peru)", + "Romanian (Romania)", + "Russian (Russia)", + "Scottish Gaelic", + "Serbian (Cyrillic, Bosnia and Herzegovina)", + "Serbian (Cyrillic, Serbia)", + "Serbian (Cyrillic, Serbia)", + "Serbian (Latin, Serbia)", + "Serbian (Latin, Serbia)", + "Sesotho sa Leboa (South Africa)", + "Setswana (South Africa)", + "Sindhi (Arabic)", + "Sinhala (Sri Lanka)", + "Slovak (Slovakia)", + "Slovenian (Slovenia)", + "Spanish (Mexico)", + "Spanish (Spain)", + "Swedish (Sweden)", + "Tajik (Cyrillic)", + "Tamil (India)", + "Tatar (Russia)", + "Telugu (India)", + "Thai (Thailand)", + "Tigrinya", + "Turkish (Turkey)", + "Turkmen", + "Ukrainian (Ukraine)", + "Urdu", + "Uyghur", + "Uzbek (Latin)", + "Valencian", + "Vietnamese", + "Welsh (Great Britain)", + "Wolof", + "Yoruba (Nigeria)" + ] + } + }, + "http://registry.gpii.net/common/lineSpace": { + "schema": { + "title": "Line Space", + "description": "Increases the distance between lines of text.", + "type": "number", + "default": 1, + "minimum": 1, + "maximum": 3, + "multipleOf": 0.1 + } + }, + "http://registry.gpii.net/common/magnification/enabled": { + "schema": { + "title": "Magnifier Enabled", + "description": "Whether to enable/disable magnification", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/magnification": { + "schema": { + "title": "Magnification", + "description": "Level of magnification", + "type": "number", + "default": 1, + "minimum": 1, + "multipleOf": 0.1 + } + }, + "http://registry.gpii.net/common/magnifierPosition": { + "schema": { + "title": "Magnifier position", + "description": "Position of the magnified area", + "type": "string", + "default": "TopHalf", + "enum": [ + "FullScreen", + "Lens", + "LeftHalf", + "RightHalf", + "TopHalf", + "BottomHalf", + "Custom" + ], + "enumLabels": [ + "Full Screen", + "Lens", + "Left Half", + "Right Half", + "Top Half", + "Bottom Half", + "Custom" + ] + } + }, + "http://registry.gpii.net/common/mouseEmulation/enabled": { + "schema": { + "title": "Mouse emulation", + "description": "Whether to enable/disable the mouse emulation", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/mouseTrailing": { + "schema": { + "title": "Mouse Trailing", + "description": "Amount of mouse trailing", + "type": "integer", + "minimum": 0, + "maximum": 10, + "default": 0 + } + }, + "http://registry.gpii.net/common/nightScreen": { + "schema": { + "title": "Night Screen", + "description": "Reduce the blue emitted from the screen.", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/onScreenKeyboard/enabled": { + "schema": { + "title": "On-Screen keyboard", + "description": "Whether to enable/disable the on-screen keyboard", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/pitch": { + "schema": { + "title": "Pitch rate", + "description": "Text to speech output pitch rate", + "type": "number", + "default": 0.5, + "minimum": 0, + "maximum": 1, + "multipleOf": 0.1 + } + }, + "http://registry.gpii.net/common/punctuationVerbosity": { + "schema": { + "title": "Punctuation verbosity", + "description": "Level of punctuation verbosity when producing text to speech", + "default": "some", + "enum": [ + "none", + "some", + "most", + "all" + ], + "enumLabels": [ + "None", + "Some", + "Most", + "All" + ] + } + }, + "http://registry.gpii.net/common/readingUnit": { + "schema": { + "title": "Reading Unit", + "description": "Reading mode when producing text to speech", + "default": "sentence", + "enum": [ + "word", + "line", + "sentence", + "paragraph" + ], + "enumLabels": [ + "Word", + "Line", + "Sentence", + "Paragraph" + ] + } + }, + "http://registry.gpii.net/common/screenBrightness": { + "schema": { + "title": "Screen brightness", + "description": "Main monitor screen brightness", + "type": "number", + "minimum": 0, + "maximum": 100 + } + }, + "http://registry.gpii.net/common/screenReaderBrailleOutput": { + "schema": { + "title": "Enable braille output", + "description": "Whether to enable/disable braille output", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/screenReaderTTS/enabled": { + "schema": { + "title": "TTS enabled", + "description": "Whether to enable/disable text to speech from screen reader", + "type": "boolean", + "default": true + } + }, + "http://registry.gpii.net/common/screenResolution": { + "schema": { + "title": "Screen Resolution", + "description": "Screen resolution of the default display", + "type": "string", + "default": "normal", + "enum": [ + "high", + "normal", + "low", + "very low" + ], + "enumLabels": [ + "High", + "Normal", + "Low", + "Very Low" + ] + } + }, + "http://registry.gpii.net/common/selfVoicing/enabled": { + "schema": { + "title": "Self Voicing", + "description": "Whether to enable/disable self voicing", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/showCrosshairs": { + "schema": { + "title": "Show crosshairs", + "description": "Whether to show crosshairs", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/simplifiedUi/enabled": { + "schema": { + "title": "Simplified User Interface", + "description": "Removes extraneous content to reduce the cognitive load and improve comprehension and/or focus.", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/slowKeys/enabled": { + "schema": { + "title": "Slow Keys", + "description": "Whether to enable/disable slow keys", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/slowKeysInterval": { + "schema": { + "title": "Slow Keys Interval", + "description": "Slow keys interval", // needs to be defined + "type": "number" + } + }, + "http://registry.gpii.net/common/speakTutorialMessages": { + "schema": { + "title": "Speak Tutorial Messages", + "description": "Whether to speak tutorial messages or not", + "default": false, + "enum": [true, false], + "enumLabels": [ "On", "Off"] + } + }, + "http://registry.gpii.net/common/speechControl": { // This is not currently supported by any real solutions, but rather used for user testing/demoing + "schema": { + "title": "Talk to the Computer", + "description": "Whether to enable/disable voice commands for computer", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/speechRate": { + "schema": { + "title": "Speech rate", + "description": "Text to speech output speech rate in words per minute.", + "type": "integer", + "default": 150, + "minimum": 1, + "maximum": 1000 + } + }, + "http://registry.gpii.net/common/stickyKeys": { + "schema": { + "title": "Sticky Keys", + "description": "Whether to enable/disable sticky keys", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/toggleKeys": { + "schema": { + "title": "Toggle Keys", + "description": "Whether to enable/disable toggle keys", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/subtitles/enabled": { + "schema": { + "title": "Subtitles", + "description": "Whether to enable/disable subtitles on videos", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/supportTool": { + "schema": { + "title": "Support Tools", + "description": "Whether to enable/disable certain support tools", + "type": "array", + "default": [], + "items": { + "enum": ["dictionary"], + "enumLabels": ["Dictionary"] + } + } + }, + "http://registry.gpii.net/common/syllabification/enabled": { + "schema": { + "title": "Syllabification", + "description": "Separate words into their syllables.", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/tableOfContents": { + "schema": { + "title": "Table of Contents", + "description": "Displays a Table of Contents for the sections of a document.", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/tracking": { + "schema": { + "title": "Tracking", + "description": "Tracking mode of the screen magnifier", + "type": "array", + "default": ["mouse"], + "items": { + "enum": [ + "mouse", + "caret", + "focus" + ], + "enumLabels": [ + "Mouse", + "Caret", + "Focus" + ] + } + } + }, + "http://registry.gpii.net/common/trackingTTS": { + "schema": { + "title": "TTS tracking mode", + "description": "Tracking mode for TTS.", + "type": "array", + "default": ["focus"], + "items": { + "enum": [ + "mouse", + "caret", + "focus" + ], + "enumLabels": [ + "Mouse", + "Caret", + "Focus" + ] + } + } + }, + "http://registry.gpii.net/common/volume": { + "schema": { + "title": "Volume", + "description": "General volume of the operating system", + "type": "number", + "minimum": 0, + "maximum": 1 + } + }, + "http://registry.gpii.net/common/volumeTTS": { + "schema": { + "title": "TTS Volume", + "description": "Output volume of TTS", + "type": "number", + "minimum": 0, + "maximum": 1, + "multipleOf": 0.1 + } + }, + "http://registry.gpii.net/common/wordEcho": { + "schema": { + "title": "Word echo", + "description": "Whether to speak each word as it is typed", + "type": "boolean", + "default": false + } + }, + "http://registry.gpii.net/common/wordSpace": { + "schema": { + "title": "Word spacing", + "description": "The relative spacing between words", + "type": "number", + "default": -1, + "minimum": -1, + "maximum": 2, + "multipleOf": 0.1 + } + } +} diff --git a/testData/ontologies/mappings/ISO24751-flat.json5 b/testData/ontologies/mappings/ISO24751-flat.json5 new file mode 100644 index 000000000..70169d4d7 --- /dev/null +++ b/testData/ontologies/mappings/ISO24751-flat.json5 @@ -0,0 +1,78 @@ +{ + "http://registry\\.gpii\\.net/common/fontSize": "display.screenEnhancement.fontSize", + "http://registry\\.gpii\\.net/common/lineSpace": "display.screenEnhancement.-provisional-lineSpace", + "http://registry\\.gpii\\.net/common/characterSpace": "display.screenEnhancement.-provisional-characterSpace", + "http://registry\\.gpii\\.net/common/inputsLarger/enabled": "display.screenEnhancement.-provisional-inputsLarger/enabled", + "http://registry\\.gpii\\.net/common/foregroundColor": "display.screenEnhancement.foregroundColor", + "http://registry\\.gpii\\.net/common/backgroundColor": "display.screenEnhancement.backgroundColor", + "http://registry\\.gpii\\.net/common/highlightColor": "display.screenEnhancement.highlightColor", + "http://registry\\.gpii\\.net/common/fontFaceFontName": "display.screenEnhancement.fontFaceFontName", + "http://registry\\.gpii\\.net/common/fontFaceGenericFontFace": "display.screenEnhancement.fontFaceGenericFontFace", + "http://registry\\.gpii\\.net/common/magnification/enabled": "display.screenEnhancement.-provisional-magnification/enabled", + "http://registry\\.gpii\\.net/common/magnification": "display.screenEnhancement.magnification", + "http://registry\\.gpii\\.net/common/magnifierPosition": "display.screenEnhancement.-provisional-magnifierPosition", + "http://registry\\.gpii\\.net/common/tracking": "display.screenEnhancement.tracking", + "http://registry\\.gpii\\.net/common/trackingTTS": "display.screenEnhancement.trackingTTS", + "http://registry\\.gpii\\.net/common/invertImages": "display.screenEnhancement.invertImages", + "http://registry\\.gpii\\.net/common/invertColours": "display.screenEnhancement.-provisional-invertColours", + "http://registry\\.gpii\\.net/common/cursorSize": "display.screenEnhancement.cursorSize", + "http://registry\\.gpii\\.net/common/highContrast/enabled": "display.screenEnhancement.-provisional-highContrast/enabled", + "http://registry\\.gpii\\.net/common/highContrastTheme": "display.screenEnhancement.-provisional-highContrastTheme", + "http://registry\\.gpii\\.net/common/mouseTrailing": "display.screenEnhancement.mouseTrailing", + "http://registry\\.gpii\\.net/common/screenReaderBrailleOutput": "display.screenReader.-provisional-screenReaderBrailleOutput", + "http://registry\\.gpii\\.net/common/auditoryOutLanguage": "display.screenReader.-provisional-auditoryOutLanguage", + "http://registry\\.gpii\\.net/common/speechRate": "display.screenReader.speechRate", + "http://registry\\.gpii\\.net/common/speakTutorialMessages": "display.screenReader.-provisional-speakTutorialMessages", + "http://registry\\.gpii\\.net/common/keyEcho": "display.screenReader.-provisional-keyEcho", + "http://registry\\.gpii\\.net/common/wordEcho": "display.screenReader.-provisional-wordEcho", + "http://registry\\.gpii\\.net/common/announceCapitals": "display.screenReader.-provisional-announceCapitals", + "http://registry\\.gpii\\.net/common/punctuationVerbosity": "display.screenReader.-provisional-punctuationVerbosity", + "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled": "display.screenReader.-provisional-screenReaderTTS/enabled", + "http://registry\\.gpii\\.net/common/pitch": "display.textReadingHighlight.pitch", + "http://registry\\.gpii\\.net/common/readingUnit": "display.textReadingHighlight.readingUnit", + "http://registry\\.gpii\\.net/common/volumeTTS": "display.textReadingHighlight.-provisional-volumeTTS", + "http://registry\\.gpii\\.net/common/selfVoicing/enabled": "display.textReadingHighlight.-provisional-selfVoicing/enabled", + "http://registry\\.gpii\\.net/common/brailleMode": "display.braille.-provisional-brailleMode", + "http://registry\\.gpii\\.net/common/screenOffTime": "display.-provisional-screenOffTime", + "http://registry\\.gpii\\.net/common/screenRotation": "display.-provisional-screenRotation", + "http://registry\\.gpii\\.net/common/screenDefaultRotation": "display.-provisional-screenDefaultRotation", + "http://registry\\.gpii\\.net/common/screenDim": "display.-provisional-screenDim", + "http://registry\\.gpii\\.net/common/onScreenKeyboard/enabled": "control.onscreenKeyboard", + "http://registry\\.gpii\\.net/common/initDelay": "control.mouseEmulation.-provisional-initDelay", + "http://registry\\.gpii\\.net/common/cursorSpeed": "control.mouseEmulation.cursorSpeed", + "http://registry\\.gpii\\.net/common/mouseEmulation/enabled": "control.mouseEmulation.-provisional-mouseEmulation/enabled", + "http://registry\\.gpii\\.net/common/stickyKeys": "control.keyboardEnhancement.stickyKeys", + "http://registry\\.gpii\\.net/common/toggleKeys": "control.keyboardEnhancement.toggleKeys", + "http://registry\\.gpii\\.net/common/slowKeysInterval": "control.keyboardEnhancement.slowKeys.slowKeysInterval", + "http://registry\\.gpii\\.net/common/slowKeys/enabled": "control.keyboardEnhancement.slowKeys.-provisional-slowKeys/enabled", + "http://registry\\.gpii\\.net/common/debounce/enabled": "control.keyboardEnhancement.debounceKeys.-provisional-debounce/enabled", + "http://registry\\.gpii\\.net/common/hapticFeedback": "control.-provisional-hapticFeedback", + "http://registry\\.gpii\\.net/common/tableOfContents": "control.structuralNavigation.tableOfContents", + "http://registry\\.gpii\\.net/common/adaptationPreference": "content.adaptationPreference", + "http://registry\\.gpii\\.net/common/supportTool": "content.supportTool", + "http://registry\\.gpii\\.net/common/simplifiedUi/enabled": "-provisional-cognitiveSupport.-provisional-simplifiedUi/enabled", + "http://registry\\.gpii\\.net/common/syllabification/enabled": "-provisional-cognitiveSupport.-provisional-syllabification/enabled", + "http://registry\\.gpii\\.net/common/volume": "-provisional-general.-provisional-volume", + "http://registry\\.gpii\\.net/common/screenBrightness": "-provisional-general.-provisional-screenBrightness", + "http://registry\\.gpii\\.net/common/language": "language", + "http://registry\\.gpii\\.net/common/autocorrectionEnabled": "keyboard.typingEnhancement.autocorrection", + "http://registry\\.gpii\\.net/common/spellCheckEnabled": "keyboard.typingEnhancement.spellCheck", + "http://registry\\.gpii\\.net/common/textPredictionEnabled": "keyboard.typingEnhancement.textPrediction", + "http://registry\\.gpii\\.net/common/textPredictionWithSpaceEnabled": "keyboard.typingEnhancement.textPredictionWithSpace", + "http://registry\\.gpii\\.net/common/doubleTabSpaceEnabled": "keyboard.typingEnhancement.doubleTabSpace", + "http://registry\\.gpii\\.net/common/keyAudioFeedbackEnabled": "keyboard.typingEnhancement.keyAudioFeedback", + "http://registry\\.gpii\\.net/common/autoShiftEngageEnabled": "keyboard.typingEnhancement.autoShiftEngage", + "http://registry\\.gpii\\.net/common/shiftLockEnabled": "keyboard.typingEnhancement.shiftLock", + "http://registry\\.gpii\\.net/common/compatibilityKeyboardEnabled": "keyboard.typingEnhancement.compatibilityKeyboard", + "http://registry\\.gpii\\.net/common/autoScreenKeyboardEnabled": "keyboard.typingEnhancement.autoScreenKeyboard", + "http://registry\\.gpii\\.net/common/DPIScale": "display.screenEnhancement.-provisional-DPIScale", + "http://registry\\.gpii\\.net/common/speechControl": "control.-provisional-speechControl", // This is not currently supported by any real solutions, but rather used for user testing/demoing + "http://registry\\.gpii\\.net/common/nightScreen": "display.screenEnhancement.-provisional-nightScreen", + "": { + "transform": { + "type": "gpii.ontologyHandler.transforms.applicationISOToFlat", + "inputPath": "applications", + "outputPath": "" + } + } +} diff --git a/testData/ontologies/mappings/flat-apptology.json5 b/testData/ontologies/mappings/flat-apptology.json5 new file mode 100644 index 000000000..691b4937a --- /dev/null +++ b/testData/ontologies/mappings/flat-apptology.json5 @@ -0,0 +1,18 @@ +{ + "transform": [ + { + "type": "fluid.transforms.value", + "inputPath": "http://registry\\.gpii\\.net/common/magnification/enabled", + "outputPath": "http://registry\\.gpii\\.net/common/magnifierApplication" + }, { + "type": "fluid.transforms.value", + "inputPath": "http://registry\\.gpii\\.net/common/magnification", + "outputPath": "http://registry\\.gpii\\.net/common/magnifierApplication" + } + ], + "http://registry\\.gpii\\.net/common/screenReaderApplication": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled", + "http://registry\\.gpii\\.net/common/stickyKeysApplication": "http://registry\\.gpii\\.net/common/stickyKeys", + "http://registry\\.gpii\\.net/common/slowKeysApplication": "http://registry\\.gpii\\.net/common/slowKeys/enabled", + "http://registry\\.gpii\\.net/common/debounceKeysApplication": "http://registry\\.gpii\\.net/common/debounce/enabled", + "http://registry\\.gpii\\.net/common/onScreenKeyboardApplication": "http://registry\\.gpii\\.net/common/onScreenKeyboard/enabled" +} diff --git a/testData/ontologies/ontologies.json b/testData/ontologies/ontologies.json deleted file mode 100644 index 3b9683154..000000000 --- a/testData/ontologies/ontologies.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "ISO24751-2/flat": { - "transformations": { - "display.screenEnhancement.fontSize": "{http://registry.gpii.org/common/fontSize}.0.value", - "display.screenEnhancement.foregroundColor": "{http://registry.gpii.org/common/foregroundColor}.0.value", - "display.screenEnhancement.backgroundColor": "{http://registry.gpii.org/common/backgroundColor}.0.value", - "display.screenEnhancement.fontFaceFontName": "{http://registry.gpii.org/common/fontFaceFontName}.0.value", - "display.screenEnhancement.fontFaceGenericFontFace": "{http://registry.gpii.org/common/fontFaceGenericFontFace}.0.value", - "display.screenEnhancement.magnification": "{http://registry.gpii.org/common/magnification}.0.value", - "display.screenEnhancement.-provisional-magnifierPosition": "{http://registry.gpii.org/common/magnifierPosition}.0.value", - "display.screenEnhancement.tracking": "{http://registry.gpii.org/common/tracking}.0.value", - "display.screenEnhancement.trackingTTS": "{http://registry.gpii.org/common/trackingTTS}.0.value", - "display.screenEnhancement.invertImages": "{http://registry.gpii.org/common/invertImages}.0.value", - "display.screenEnhancement.cursorSize": "{http://registry.gpii.org/common/cursorSize}.0.value", - "display.screenEnhancement.highContrast": "{http://registry.gpii.org/common/highContrast}.0.value", - "display.screenEnhancement.mouseTrailing": "{http://registry.gpii.org/common/mouseTrailing}.0.value", - - "display.screenReader.-provisional-screenReaderBrailleOutput": "{http://registry.gpii.org/common/screenReaderBrailleOutput}.0.value", - "display.screenReader.-provisional-auditoryOutLanguage": "{http://registry.gpii.org/common/auditoryOutLanguage}.0.value", - "display.screenReader.speechRate": "{http://registry.gpii.org/common/speechRate}.0.value", - "display.screenReader.-provisional-speakTutorialMessages": "{http://registry.gpii.org/common/speakTutorialMessages}.0.value", - "display.screenReader.-provisional-keyEcho": "{http://registry.gpii.org/common/keyEcho}.0.value", - "display.screenReader.-provisional-wordEcho": "{http://registry.gpii.org/common/wordEcho}.0.value", - "display.screenReader.-provisional-announceCapitals": "{http://registry.gpii.org/common/announceCapitals}.0.value", - "display.screenReader.-provisional-brailleOutput": "{http://registry.gpii.org/common/screenReaderBrailleOutput}.0.value", - "display.screenReader.-provisional-punctuationVerbosity": "{http://registry.gpii.org/common/punctuationVerbosity}.0.value", - "display.screenReader.-provisional-screenReaderTTSEnabled": "{http://registry.gpii.org/common/screenReaderTTSEnabled}.0.value", - - "display.textReadingHighlight.readingUnit": "{http://registry.gpii.org/common/readingUnit}.0.value", - - "display.braille.-provisional-brailleMode": "{http://registry.gpii.org/common/brailleMode}.0.value", - - "control.onscreenKeyboard": "{http://registry.gpii.org/common/onscreenKeyboard}.0.value", - "control.mouseEmulation.-provisional-initDelay": "{http://registry.gpii.org/common/-provisional-initDelay}.0.value", - "control.mouseEmulation.cursorSpeed": "{http://registry.gpii.org/common/cursorSpeed}.0.value", - "control.mouseEmulation.cursorAcceleration": "{http://registry.gpii.org/common/cursorAcceleration}.0.value", - "control.mouseEmulation.-provisional-mouseEmulationEnabled": "{http://registry.gpii.org/common/-provisional-mouseEmulationEnabled}.0.value", - "control.keyboardEnhancement.stickyKeys": "{http://registry.gpii.org/common/stickyKeys}.0.value", - "control.keyboardEnhancement.slowKeys.slowKeysInterval": "{http://registry.gpii.org/common/slowKeysInterval}.0.value", - "control.keyboardEnhancement.slowKeys.-provisional-slowKeysEnable": "{http://registry.gpii.org/common/-provisional-slowKeysEnable}.0.value", - "control.keyboardEnhancement.debounceKeys.-provisional-debounceEnable": "{http://registry.gpii.org/common/-provisional-debounceEnable}.0.value", - "control.keyboardEnhancement.debounceKeys.debounceInterval": "{http://registry.gpii.org/common/debounceInterval}.0.value", - - "content.adaptationPreference": "{http://registry.gpii.org/common/adaptationPreference}.0.value", - "control.structuralNavigation.tableOfContents": "{http://registry.gpii.org/common/tableOfContents}.0.value", - "": { - "transform": { - "type": "gpii.ontologyServer.transform.application", - "inputPath": "", - "outputPath": "applications" - } - } - } - } -} \ No newline at end of file diff --git a/testData/preferences/GPII-270-rbmm-demo.json5 b/testData/preferences/GPII-270-rbmm-demo.json5 new file mode 100644 index 000000000..088e039e1 --- /dev/null +++ b/testData/preferences/GPII-270-rbmm-demo.json5 @@ -0,0 +1,30 @@ +{ + "flat": { + "name": "GPII 270 RBMM Demo data set", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 24, + "http://registry.gpii.net/common/magnification": 2.0, + "http://registry.gpii.net/common/tracking": ["mouse"], + "http://registry.gpii.net/common/speechRate": 42, + "http://registry.gpii.net/common/trackingTTS": ["mouse"], + "http://registry.gpii.net/common/speakTutorialMessages": true, + "http://registry.gpii.net/common/keyEcho": true, + "http://registry.gpii.net/common/wordEcho": true, + "http://registry.gpii.net/common/announceCapitals": false, + "http://registry.gpii.net/common/screenReaderBrailleOutput": false, + "http://registry.gpii.net/common/punctuationVerbosity": "some", + "http://registry.gpii.net/common/readingUnit": "word", + "http://registry.gpii.net/common/auditoryOutLanguage": "en-GB", + "http://registry.gpii.net/common/screenReaderTTS/enabled": true, + "http://registry.gpii.net/common/pitch": 0.4, + "http://registry.gpii.net/common/volumeTTS": 0.5, + + "http://registry.gpii.net/applications/com.microsoft.windows.highContrast/enabled": true + } + } + } + } +} diff --git a/testData/preferences/MikelVargas.json b/testData/preferences/MikelVargas.json deleted file mode 100644 index 9fc2daf71..000000000 --- a/testData/preferences/MikelVargas.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "http://registry.gpii.org/common/onscreenKeyboard": [{ "value": true }], - "http://registry.gpii.org/common/-provisional-initDelay": [{ "value": 0.120 }], - "http://registry.gpii.org/common/cursorSpeed": [{ "value": 0.850 }], - "http://registry.gpii.org/common/cursorAcceleration": [{ "value": 0.800 }], - "http://registry.gpii.org/common/-provisional-mouseEmulationEnabled": [{ "value": true }], - "http://registry.gpii.org/common/stickyKeys": [{ "value": true }], - "http://registry.gpii.org/common/-provisional-slowKeysEnable": [{ "value": true }], - "http://registry.gpii.org/common/slowKeysInterval": [{ "value": 0.4 }], - "http://registry.gpii.org/common/-provisional-debounceEnable": [{ "value": true }], - "http://registry.gpii.org/common/debounceInterval": [{ "value": 0.20 }] -} \ No newline at end of file diff --git a/testData/preferences/MikelVargas.json5 b/testData/preferences/MikelVargas.json5 new file mode 100644 index 000000000..be2bc499c --- /dev/null +++ b/testData/preferences/MikelVargas.json5 @@ -0,0 +1,20 @@ +{ + "flat": { + "name": "Mikel Vargas", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/stickyKeys": true, + "http://registry.gpii.net/common/slowKeys/enabled": true, + "http://registry.gpii.net/common/slowKeysInterval": 0.4, + "http://registry.gpii.net/common/debounce/enabled": true + } + } + } + } +} diff --git a/testData/preferences/alice.json5 b/testData/preferences/alice.json5 new file mode 100644 index 000000000..b9f288f1b --- /dev/null +++ b/testData/preferences/alice.json5 @@ -0,0 +1,26 @@ +// # alice.json5 +// +// ## Demo Personas with User Stories +// +// This preference set is part of our set of personas used for the demo install, +// presentations, and in general conveying an array of compelling user stories for +// the GPII. +// +// ## Details +// +// Alice — On screen Keyboard +// +// Alice has a high spinal cord injury and cannot control anything below her chin. She uses a head-controlled mouse point. At the library she will ask anyone nearby to touch the GPII card to the computer for her and she is all set. +{ + "flat": { + "name": "Alice", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.onscreenKeyboard": {} + } + } + } + } +} diff --git a/testData/preferences/alsa.json5 b/testData/preferences/alsa.json5 new file mode 100644 index 000000000..4fab03d12 --- /dev/null +++ b/testData/preferences/alsa.json5 @@ -0,0 +1,15 @@ +{ + "flat": { + "name": "Alsa", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 0 + } + } + } + } + } +} diff --git a/testData/preferences/andrei.json b/testData/preferences/andrei.json deleted file mode 100644 index 8a0e7f777..000000000 --- a/testData/preferences/andrei.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "http://registry.gpii.org/applications/com.texthelp.readWriteGold": [{ - "value": { - "ApplicationSettings.AppBar.Width.$t": 788, - "ApplicationSettings.AppBar.LargeIcons.$t": false, - "ApplicationSettings.AppBar.optToolbarLargeIcons.$t": false, - "ApplicationSettings.AppBar.ShowText.$t": false, - "ApplicationSettings.AppBar.optToolbarShowText.$t": false, - "ApplicationSettings.Speech.optSAPI5Speed.$t": 90, - "ApplicationSettings.Speech.optAutoUseScreenReading.$t": true - } - }], - "http://registry.gpii.org/applications/org.gnome.orca": [{ "value": {}}], - "http://registry.gpii.org/applications/nvda.screenReader": [{ "value": {}}], - "http://registry.gpii.org/applications/webinsight.webAnywhere.windows": [{ "value": {}}], - "http://registry.gpii.org/applications/webinsight.webAnywhere.linux": [{ "value": {}}], - "http://registry.gpii.org/applications/fluid.uiOptions.windows": [{ "value": {}}], - "http://registry.gpii.org/applications/fluid.uiOptions.linux": [{ "value": {}}], - "http://registry.gpii.org/applications/fluid.uiOptions": [{ - "value": { - "lineSpacing": "1", - "links": false, - "inputsLarger": false, - "captions": false, - "transcripts": false, - "volume": "50" - } - }], - "http://registry.gpii.org/applications/org.gnome.desktop.a11y.keyboard": [{ - "value": { - "feature-state-change-beep": true - } - }], - "http://registry.gpii.org/common/fontSize": [{ "value": 12 }], - "http://registry.gpii.org/common/foregroundColor": [{ "value": "white" }], - "http://registry.gpii.org/common/backgroundColor": [{ "value": "black" }], - "http://registry.gpii.org/common/fontFaceFontName": [{ "value": ["Comic Sans"] }], - "http://registry.gpii.org/common/fontFaceGenericFontFace": [{ "value": "sans serif" }], - "http://registry.gpii.org/common/adaptationPreference": [{ "value": [ - { - "adaptationType": "caption", - "language": "en" - }, - {} - ]}], - "http://registry.gpii.org/common/tableOfContents": [{ "value": false }] -} \ No newline at end of file diff --git a/testData/preferences/andrei.json5 b/testData/preferences/andrei.json5 new file mode 100644 index 000000000..0e6b50598 --- /dev/null +++ b/testData/preferences/andrei.json5 @@ -0,0 +1,15 @@ +{ + "flat": { + "name": "Andrei", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/org.gnome.orca": {}, + "http://registry.gpii.net/common/fontSize": 12 , + "http://registry.gpii.net/common/tableOfContents": false + } + } + } + } +} diff --git a/testData/preferences/audio.json5 b/testData/preferences/audio.json5 new file mode 100644 index 000000000..46d7fbc36 --- /dev/null +++ b/testData/preferences/audio.json5 @@ -0,0 +1,16 @@ +{ + "flat": { + "name": "Android Audio", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.android.audioManager": { + "STREAM_SYSTEM": 14.5, + "STREAM_MUSIC": 15 + } + } + } + } + } +} diff --git a/testData/preferences/ben.json5 b/testData/preferences/ben.json5 new file mode 100644 index 000000000..f417b98a3 --- /dev/null +++ b/testData/preferences/ben.json5 @@ -0,0 +1,22 @@ +// # Ben.json5 +// +// This preference sets the screen brightness to full brightness. +// +// ## Testing +// +// If the screen brightness is supported in the device, the value should be setted +// to the maximum possible value. +// +{ + "flat": { + "name": "Ben", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/screenBrightness": 100 + } + } + } + } +} diff --git a/testData/preferences/carla.json b/testData/preferences/carla.json deleted file mode 100644 index 7ff7a6b08..000000000 --- a/testData/preferences/carla.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "http://registry.gpii.org/applications/com.texthelp.readWriteGold": [{ - "value": { - "ApplicationSettings.AppBar.Width.$t": 788, - "ApplicationSettings.AppBar.ShowText.$t": true, - "ApplicationSettings.AppBar.optToolbarShowText.$t": true, - "ApplicationSettings.AppBar.LargeIcons.$t": true, - "ApplicationSettings.AppBar.optToolbarLargeIcons.$t": true, - "ApplicationSettings.Speech.optSAPI5Speed.$t": 50, - "ApplicationSettings.Speech.optAutoUseScreenReading.$t": false - } - }], - "http://registry.gpii.org/applications/fluid.uiOptions.windows": [{ "value": {}}], - "http://registry.gpii.org/applications/fluid.uiOptions.linux": [{ "value": {}}], - "http://registry.gpii.org/applications/fluid.uiOptions": [{ - "value": { - "lineSpacing": "2", - "links": true, - "inputsLarger": true, - "captions": false, - "transcripts": false, - "volume": "50" - } - }], - "http://registry.gpii.org/applications/org.gnome.desktop.a11y.magnifier": [{ - "value": { - "show-cross-hairs": true, - "lens-mode": false, - "mag-factor": 2, - "mouse-tracking": "proportional", - "screen-position": "right-half", - "scroll-at-edges": true - } - }], - "http://registry.gpii.org/applications/com.microsoft.windows.magnifier": [{ - "value": { - "Magnification": { - "value": 200, - "dataType": "REG_DWORD" - }, - "ZoomIncrement": { - "value": 50, - "dataType": "REG_DWORD" - }, - "Invert": { - "value": 0, - "dataType": "REG_DWORD" - }, - "FollowMouse": { - "value": 1, - "dataType": "REG_DWORD" - }, - "FollowFocus": { - "value": 1, - "dataType": "REG_DWORD" - }, - "FollowCaret": { - "value": 1, - "dataType": "REG_DWORD" - }, - "MagnificationModel": { - "value": 1, - "dataType": "REG_DWORD" - } - } - }], - "http://registry.gpii.org/common/fontSize": [{ "value": 24 }], - "http://registry.gpii.org/common/foregroundColor": [{ "value": "white" }], - "http://registry.gpii.org/common/backgroundColor": [{ "value": "black" }], - "http://registry.gpii.org/common/fontFaceFontName": [{ "value": ["Comic Sans"] }], - "http://registry.gpii.org/common/fontFaceGenericFontFace": [{ "value": "sans serif" }], - "http://registry.gpii.org/common/magnification": [{ "value": 2.0 }], - "http://registry.gpii.org/common/tracking": [{ "value": ["mouse"] }], - "http://registry.gpii.org/common/invertImages": [{ "value": true }], - "http://registry.gpii.org/common/adaptationPreference": [{ "value": [ - { - "adaptationType": "caption", - "language": "en" - }, - {} - ]}], - "http://registry.gpii.org/common/tableOfContents": [{ "value": false }] -} diff --git a/testData/preferences/carla.json5 b/testData/preferences/carla.json5 new file mode 100644 index 000000000..0a7b1ce9a --- /dev/null +++ b/testData/preferences/carla.json5 @@ -0,0 +1,34 @@ +{ + "flat": { + "name": "Carla", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier": { + "show-cross-hairs": true, + "lens-mode": false, + "mag-factor": 2, + "mouse-tracking": "proportional", + "screen-position": "right-half", + "scroll-at-edges": true + }, + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "Magnification": 200, + "ZoomIncrement": 50, + "Invert": 0, + "FollowMouse": 1, + "FollowFocus": 1, + "FollowCaret": 1, + "MagnificationMode": 1 + }, + "http://registry.gpii.net/common/fontSize": 24, + "http://registry.gpii.net/common/magnification": 2.0, + "http://registry.gpii.net/common/tracking": ["mouse"], + "http://registry.gpii.net/common/invertColours": true, + "http://registry.gpii.net/common/tableOfContents": false + } + } + } + } +} diff --git a/testData/preferences/carla_24751.json b/testData/preferences/carla_24751.json deleted file mode 100644 index bf5e044e7..000000000 --- a/testData/preferences/carla_24751.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "languageAssistance": { - "applications": { - "com.texthelp.readWriteGold": { - "name": "Read Write Gold", - "priority": 100, - "parameters": { - "ApplicationSettings.AppBar.Width.$t": 788, - "ApplicationSettings.AppBar.ShowText.$t": true, - "ApplicationSettings.AppBar.optToolbarShowText.$t": true, - "ApplicationSettings.AppBar.LargeIcons.$t": true, - "ApplicationSettings.AppBar.optToolbarLargeIcons.$t": true, - "ApplicationSettings.Speech.optSAPI5Speed.$t": 50, - "ApplicationSettings.Speech.optAutoUseScreenReading.$t": false - } - } - } - }, - "display": { - "applications": { - "fluid.uiOptions.windows": { - "name": "UI Options" - }, - "fluid.uiOptions.linux": { - "name": "UI Options" - }, - "fluid.uiOptions": { - "name": "UI Options", - "parameters": { - "lineSpacing": "2", - "links": true, - "inputsLarger": true, - "captions": false, - "transcripts": false, - "volume": "50" - } - } - }, - "screenEnhancement": { - "fontSize": 24, - "foregroundColor": "white", - "backgroundColor": "black", - "fontFace": { - "fontName": [ - "Comic Sans" - ], - "genericFontFace": "sans serif" - }, - - "screenMagnification": { - "applications": { - "org.gnome.desktop.a11y.magnifier": { - "name": "GNOME Shell Magnifier", - "priority": 100, - "parameters": { - "show-cross-hairs": true, - "lens-mode": false, - "mag-factor": 2, - "mouse-tracking": "proportional", - "screen-position": "right-half", - "scroll-at-edges": true - } - }, - "com.microsoft.windows.magnifier": { - "name": "Windows Built-in Screen Magnifier", - "priority": 100, - "parameters": { - "Magnification": { - "value": 200, - "dataType": "REG_DWORD" - }, - "ZoomIncrement": { - "value": 50, - "dataType": "REG_DWORD" - }, - "Invert": { - "value": 0, - "dataType": "REG_DWORD" - }, - "FollowMouse": { - "value": 1, - "dataType": "REG_DWORD" - }, - "FollowFocus": { - "value": 1, - "dataType": "REG_DWORD" - }, - "FollowCaret": { - "value": 1, - "dataType": "REG_DWORD" - }, - "MagnificationModel": { - "value": 1, - "dataType": "REG_DWORD" - } - } - } - } - } - } - }, - "content": { - "adaptationPreference": [ - { - "adaptationType": "caption", - "language": "en" - }, - {} - ] - }, - "control": { - "structuralNavigation": { - "tableOfContents": false - } - } -} diff --git a/testData/preferences/carlos.json5 b/testData/preferences/carlos.json5 new file mode 100644 index 000000000..f78a657fe --- /dev/null +++ b/testData/preferences/carlos.json5 @@ -0,0 +1,25 @@ +//# Carlos +// +// This preference set sets the Windows color filters to 'Grayscale inverted', and also +// enables the feature for toggling the color filters using the shortcut: "Windows + Ctrl+ C". +// +//## Testing +// +// Color should change when the user is logged in, the screen color should change to 'Grayscale inverted', +// and the setting 'Allow the shortcut key to toggle filter on or off' should be turned on. +{ + "flat": { + "name": "Carlos", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.colorFilters": { + "FilterType": 2, + "ShortcutKeyEnable": true + } + } + } + } + } +} diff --git a/testData/preferences/caroline.json5 b/testData/preferences/caroline.json5 new file mode 100644 index 000000000..6e6980396 --- /dev/null +++ b/testData/preferences/caroline.json5 @@ -0,0 +1,22 @@ +//# Caroline +// +// This preference set sets the Windows color filters for a user with deuteranopia. +// +//## Testing +// +// Color should change when the user is logged in, green color should be weaker than before. +{ + "flat": { + "name": "Caroline", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.colorFilters": { + "FilterType": 3 + } + } + } + } + } +} diff --git a/testData/preferences/catalina.json5 b/testData/preferences/catalina.json5 new file mode 100644 index 000000000..7a09e0798 --- /dev/null +++ b/testData/preferences/catalina.json5 @@ -0,0 +1,20 @@ +//# catalina +// +//This preference set sets the OS language to Spanish (Spain), es-ES. +// +//## Testing +// +//For this to work correctly, the es-ES language pack needs to be installed on Windows. See GPII-2212. +{ + "flat": { + "name": "Catalina", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/language": "es-ES" + } + } + } + } +} diff --git a/testData/preferences/chris.json5 b/testData/preferences/chris.json5 new file mode 100644 index 000000000..23935bd4a --- /dev/null +++ b/testData/preferences/chris.json5 @@ -0,0 +1,42 @@ +// # chris.json5 +// +// ## Weekly Integration Tests: Preference Sets +// +// This preference set is used for the +// [weekly integration tests](http://wiki.gpii.net/w/Weekly_Integration_Test_Plan) and is manually tested on a regular basis. The expected system behaviour on login is described here. +// +// It has also been used in the [third review of Cloud4all](https://github.com/GPII/universal/blob/master/testData/preferences/review3/review3-preferences.md). +// +// ## Details +// `chris.json5`: preference set with a combination of common and application-specific terms for the following settings: +// * screen reader with TTS using a speech rate of 350 wpm, +// * Braille output, +// * a lower screen resolution on Windows, +// * NVDA is prioritised on Windows, +// * Mobile Accessibility for Android is prioritised over other Android accessibility features, +// * Rule-Based Matchmaker. +{ + "flat": { + "name": "Chris", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/screenReaderTTS/enabled": true, + "http://registry.gpii.net/common/screenReaderBrailleOutput": true, + "http://registry.gpii.net/common/speechRate": 350 + }, + "metadata": [ + { + "type": "priority", + "scope": [ + "http://registry.gpii.net/applications/org.nvda-project", + "http://registry.gpii.net/applications/es.codefactory.android.app.ma" + ], + "value": 1024 + } + ] + } + } + } +} diff --git a/testData/preferences/condTest.json5 b/testData/preferences/condTest.json5 new file mode 100644 index 000000000..925820769 --- /dev/null +++ b/testData/preferences/condTest.json5 @@ -0,0 +1,39 @@ +{ + "flat": { + "name": "Conditional Test", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 12 + }, + "metadata" : [ + { + "message" : { + "de" : { + "learnMore" : "http://cloud4all.info", + "message" : "Ihr bevorzugtes technisches Hilfsmittel ist auf diesem Gerät nicht vorhanden. Als Ersatz haben wir ein anderes Hilfsmittel gestartet." + }, + "en" : { + "learnMore" : "http://cloud4all.info", + "message" : "Your preferred Assistive Technology is not available on the device. We have launched this alternative solution for you." + }, + "gr" : { + "learnMore" : "http://cloud4all.info", + "message" : "Bla Bla Bla in Greek." + } + }, + "scope" : [ "com.yourdolphin.supernova-as" ], + "type" : "helpMessage" + } + ] + }, + "really-bright": { + "name": "Really bright out", + "preferences": { + "http://registry.gpii.net/common/fontSize": 16 + } + } + } + } +} diff --git a/testData/preferences/condTest2.json5 b/testData/preferences/condTest2.json5 new file mode 100644 index 000000000..eb960691a --- /dev/null +++ b/testData/preferences/condTest2.json5 @@ -0,0 +1,19 @@ +{ + "flat": { + "name": "Conditional Test 2", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 12 + } + }, + "really-bright": { + "name": "Really bright out", + "preferences": { + "http://registry.gpii.net/common/fontSize": 16 + } + } + } + } +} diff --git a/testData/preferences/davey.json5 b/testData/preferences/davey.json5 new file mode 100644 index 000000000..1c38acbc7 --- /dev/null +++ b/testData/preferences/davey.json5 @@ -0,0 +1,42 @@ +// # Davey.json5 +// +// This preference set changes several preferences from Read&Write Gold 12 +// +// ## Testing +// +// This changes the following settings from the AT software: +// Translation: +// + From Language: Czech +// + To Language: Danish +// +// Vocabulary List: +// + Show vocabulary List window: true +// + Include pictures: false +// + Include definitions: true +// +// Voice Note: +// + Insert directly in the document: false +{ + "flat": { + "name": "Davey", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.texthelp.readWriteGold": { + // Translation + "ApplicationSettings.Translation.FromLanguage.$t": "cs", + "ApplicationSettings.Translation.ToLanguage.$t": "da", + // Vocabulary List + // TODO: Verify with Microsoft word installation + "ApplicationSettings.Vocabulary.VocabListWindow.$t": true, + "ApplicationSettings.Vocabulary.VocabListImages.$t": false, + "ApplicationSettings.Vocabulary.VocabListDefinitions.$t": true, + // Voice Note + "ApplicationSettings.VoiceNote.InsertInDoc.$t": true + } + } + } + } + } +} diff --git a/testData/preferences/david.json5 b/testData/preferences/david.json5 new file mode 100644 index 000000000..4a07d0b39 --- /dev/null +++ b/testData/preferences/david.json5 @@ -0,0 +1,32 @@ +// # David.json5 +// +// This preference set changes several preferences from Read&Write Gold 12 +// +// ## Testing +// +// This changes the following settings from the AT software: +// General: +// + Button size: Small +// + Button style: Color +// + Show text on toolbar: true +// + Toolbar color: "#008080" (Need to verify by looking to the panel) +// + Launch on Windows startup: false +{ + "flat": { + "name": "David", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.texthelp.readWriteGold": { + "ApplicationSettings.AppBar.IconSize.$t": "Small", + "ApplicationSettings.AppBar.ToolbarIconSet.$t": "Color", + "ApplicationSettings.AppBar.ShowText.$t": true, + "ApplicationSettings.AppBar.optToolbarBackColour.$t": "#008080", + "ApplicationSettings.AppBar.RunOnStartUp.$t": false + } + } + } + } + } +} diff --git a/testData/preferences/debbie.json5 b/testData/preferences/debbie.json5 new file mode 100644 index 000000000..b30cf0dc6 --- /dev/null +++ b/testData/preferences/debbie.json5 @@ -0,0 +1,22 @@ +// # debbie +// +// This preference set enables de-bounce keys. +// +// This makes the system ignore repeated key presses. +// +// ## Testing +// +// In something like Notepad, quickly press the same key twice. The key press should only be accepted once. +{ + "flat": { + "name": "Debbie", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/debounce/enabled": true + } + } + } + } +} diff --git a/testData/preferences/easit1.json b/testData/preferences/easit1.json deleted file mode 100644 index 8ab5be8f3..000000000 --- a/testData/preferences/easit1.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "http://registry.gpii.org/common/fontSize": [{ "value": 11 }], - "http://registry.gpii.org/common/foregroundColor": [{ "value": "yellow" }], - "http://registry.gpii.org/common/backgroundColor": [{ "value": "black" }], - "http://registry.gpii.org/common/fontFaceFontName": [{ "value": [ "ARIAL" ] }], - "http://registry.gpii.org/common/fontFaceGenericFontFace": [{ "value": "SERIF" }], - "http://registry.gpii.org/common/magnification": [{ "value": 1 }], - "http://registry.gpii.org/common/tracking": [{ "value": ["mouse"] }], - "http://registry.gpii.org/common/invertImages": [{ "value": false }], - "http://registry.gpii.org/common/links": [{ "value": true }], - "http://registry.gpii.org/common/toc": [{ "value": false }], - "http://registry.gpii.org/common/inputsLarger": [{ "value": true }], - "http://registry.gpii.org/common/layout": [{ "value": false }], - "http://registry.gpii.org/common/lineSpacing": [{ "value": 1.2 }] -} \ No newline at end of file diff --git a/testData/preferences/easit1.json5 b/testData/preferences/easit1.json5 new file mode 100644 index 000000000..b5550bea4 --- /dev/null +++ b/testData/preferences/easit1.json5 @@ -0,0 +1,15 @@ +{ + "flat": { + "name": "easit1", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 11, + "http://registry.gpii.net/common/magnification": 1, + "http://registry.gpii.net/common/tracking": ["mouse"] + } + } + } + } +} diff --git a/testData/preferences/easit2.json b/testData/preferences/easit2.json deleted file mode 100644 index a338acad6..000000000 --- a/testData/preferences/easit2.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "http://registry.gpii.org/common/fontSize": [{ "value": 10 }], - "http://registry.gpii.org/common/foregroundColor": [{ "value": "default" }], - "http://registry.gpii.org/common/backgroundColor": [{ "value": "default" }], - "http://registry.gpii.org/common/fontFaceFontName": [{ "value": [ "default" ] }], - "http://registry.gpii.org/common/fontFaceGenericFontFace": [{ "value": "default" }], - "http://registry.gpii.org/common/magnification": [{ "value": 1 }], - "http://registry.gpii.org/common/tracking": [{ "value": ["mouse"] }], - "http://registry.gpii.org/common/invertImages": [{ "value": false }], - "http://registry.gpii.org/common/links": [{ "value": false }], - "http://registry.gpii.org/common/toc": [{ "value": false }], - "http://registry.gpii.org/common/inputsLarger": [{ "value": false }], - "http://registry.gpii.org/common/layout": [{ "value": false }], - "http://registry.gpii.org/common/lineSpacing": [{ "value": 1 }] -} \ No newline at end of file diff --git a/testData/preferences/easit2.json5 b/testData/preferences/easit2.json5 new file mode 100644 index 000000000..047f04481 --- /dev/null +++ b/testData/preferences/easit2.json5 @@ -0,0 +1,16 @@ +{ + "flat": { + "name": "easit2", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 10, + "http://registry.gpii.net/common/magnification": 1, + "http://registry.gpii.net/common/tracking": ["mouse"], + "http://registry.gpii.net/common/tableOfContents": false + } + } + } + } +} diff --git a/testData/preferences/elaine.json b/testData/preferences/elaine.json deleted file mode 100644 index 3c025c0c3..000000000 --- a/testData/preferences/elaine.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "http://registry.gpii.org/applications/trace.easyOne.communicator.windows": [{ "value": {} }], - "http://registry.gpii.org/applications/trace.easyOne.communicator.linux": [{ "value": {} }] -} \ No newline at end of file diff --git a/testData/preferences/elaine.json5 b/testData/preferences/elaine.json5 new file mode 100644 index 000000000..d1da33cc3 --- /dev/null +++ b/testData/preferences/elaine.json5 @@ -0,0 +1,14 @@ +{ + "flat": { + "name": "Elaine", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/trace.easyOne.communicator.windows": {}, + "http://registry.gpii.net/applications/trace.easyOne.communicator.linux": {} + } + } + } + } +} diff --git a/testData/preferences/elmer.json5 b/testData/preferences/elmer.json5 new file mode 100644 index 000000000..b932d582c --- /dev/null +++ b/testData/preferences/elmer.json5 @@ -0,0 +1,31 @@ +// # elmer.json5 +// +// ## Demo Personas with User Stories +// +// This preference set is part of our set of personas used for the demo install, +// presentations, and in general conveying an array of compelling user stories for +// the GPII. +// +// ## Details +// +// Elmer — Low Screen Resolution +// +// Elmer is older and his sight is failing — but magnifiers confuse him. So he just uses a low screen resolution and everything is larger for him, but nothing moves offscreen like a magnifier would do. +{ + "flat": { + "name": "Elmer", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.screenResolution": { + "screen-resolution": { + "width": 800, + "height": 600 + } + } + } + } + } + } +} diff --git a/testData/preferences/elmerv.json5 b/testData/preferences/elmerv.json5 new file mode 100644 index 000000000..a88afc803 --- /dev/null +++ b/testData/preferences/elmerv.json5 @@ -0,0 +1,33 @@ +// # elmerv.json5 +// +// ## Demo Personas with User Stories +// +// This preference set is part of our set of personas used for the demo install, +// presentations, and in general conveying an array of compelling user stories for +// the GPII. +// +// elmerv is identical to elmer except that it sets the screen to 1280 x 720 (instead of 800 x 600 that ELMER does) which works very well for demoing on the Sony Vaio (which is also the reason for the "v" in elmerv) +// +// ## Details +// +// Elmer — Low Screen Resolution +// +// Elmer is older and his sight is failing — but magnifiers confuse him. So he just uses a low screen resolution and everything is larger for him, but nothing moves offscreen like a magnifier would do. +{ + "flat": { + "name": "Elmer's VAIO", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.screenResolution": { + "screen-resolution": { + "width": 1280, + "height": 720 + } + } + } + } + } + } +} diff --git a/testData/preferences/elod.json5 b/testData/preferences/elod.json5 new file mode 100644 index 000000000..39bb9c144 --- /dev/null +++ b/testData/preferences/elod.json5 @@ -0,0 +1,26 @@ +// # elod.json5 +// +// ## Demo Personas with User Stories +// +// This preference set is part of our set of personas used for the demo install, +// presentations, and in general conveying an array of compelling user stories for +// the GPII. +// +// ## Details +// +// Elod - Magnifier 200 +// +// Brings up a floating magnifying window. Elod has low vision and can’t see anything unless magnified 200 percent. +{ + "flat": { + "name": "Elod", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/magnification": 2.0 + } + } + } + } +} diff --git a/testData/preferences/empty.json5 b/testData/preferences/empty.json5 new file mode 100644 index 000000000..e57290ec3 --- /dev/null +++ b/testData/preferences/empty.json5 @@ -0,0 +1,12 @@ +// An empty snapset for testing purposes +{ + "flat": { + "name": "Empty", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": {} + } + } + } +} diff --git a/testData/preferences/explodeLaunchHandlerStart.json5 b/testData/preferences/explodeLaunchHandlerStart.json5 new file mode 100644 index 000000000..a74517a52 --- /dev/null +++ b/testData/preferences/explodeLaunchHandlerStart.json5 @@ -0,0 +1,16 @@ +{ + "flat": { + "name": "Exploding Settings Handler Start", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/net.gpii.explode": { + "explodeOn": "launch", + "explodeMethod": "reject" + } + } + } + } + } +} diff --git a/testData/preferences/explodeLaunchHandlerStop.json5 b/testData/preferences/explodeLaunchHandlerStop.json5 new file mode 100644 index 000000000..72be5f2ba --- /dev/null +++ b/testData/preferences/explodeLaunchHandlerStop.json5 @@ -0,0 +1,16 @@ +{ + "flat": { + "name": "Exploding Settings Handler Stop", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/net.gpii.explode": { + "explodeOn": "stop", + "explodeMethod": "reject" + } + } + } + } + } +} diff --git a/testData/preferences/explodeSettingsHandlerGet.json5 b/testData/preferences/explodeSettingsHandlerGet.json5 new file mode 100644 index 000000000..eca3412d0 --- /dev/null +++ b/testData/preferences/explodeSettingsHandlerGet.json5 @@ -0,0 +1,16 @@ +{ + "flat": { + "name": "Exploding Settings Handler Get", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/net.gpii.explode": { + "explodeOn": "get", + "explodeMethod": "reject" + } + } + } + } + } +} diff --git a/testData/preferences/explodeSettingsHandlerSet.json5 b/testData/preferences/explodeSettingsHandlerSet.json5 new file mode 100644 index 000000000..3bbb1f641 --- /dev/null +++ b/testData/preferences/explodeSettingsHandlerSet.json5 @@ -0,0 +1,16 @@ +{ + "flat": { + "name": "Exploding Settings Handler Set", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/net.gpii.explode": { + "explodeOn": "set", + "explodeMethod": "reject" + } + } + } + } + } +} diff --git a/testData/preferences/franklin.json5 b/testData/preferences/franklin.json5 new file mode 100644 index 000000000..8e3222509 --- /dev/null +++ b/testData/preferences/franklin.json5 @@ -0,0 +1,33 @@ +// # franklin.json5 +// +// ## Weekly Integration Tests: Preference Sets +// +// This preference set is used for the +// [weekly integration tests](http://wiki.gpii.net/w/Weekly_Integration_Test_Plan) and is manually tested on a regular basis. The expected system behaviour on login is described here. +// +// It has also been used in the [third review of Cloud4all](https://github.com/GPII/universal/blob/master/testData/preferences/review3/review3-preferences.md). +// +// ## Details +// `franklin.json5`: preference set with a combination of common and application-specific terms for the following settings: +// * Read&Write Gold settings: +// * App bar width, +// * show text on the app bar, +// * optional tool bar shows large icons and text, +// * SAPI5 speed set to 50, +// * Rule-Based Matchmaker. +{ + "flat": { + "name": "Franklin", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.texthelp.readWriteGold": { + "ApplicationSettings.AppBar.ShowText.$t": true, + "ApplicationSettings.AppBar.IconSize.$t": "Large" + } + } + } + } + } +} diff --git a/testData/preferences/gert.json b/testData/preferences/gert.json deleted file mode 100644 index 92677d454..000000000 --- a/testData/preferences/gert.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "http://registry.gpii.org/common/fontSize": [{ "value": 24 }] -} diff --git a/testData/preferences/gert.json5 b/testData/preferences/gert.json5 new file mode 100644 index 000000000..d2e768575 --- /dev/null +++ b/testData/preferences/gert.json5 @@ -0,0 +1,13 @@ +{ + "flat": { + "name": "gert", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 24 + } + } + } + } +} diff --git a/testData/preferences/intra_application.json5 b/testData/preferences/intra_application.json5 new file mode 100644 index 000000000..bcf3dc314 --- /dev/null +++ b/testData/preferences/intra_application.json5 @@ -0,0 +1,25 @@ +// Test user for "intra application" transforms, including "aliases" and other remappings of application settings. +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.desktopBackground": { + "Image": "%SystemRoot%\\Web\\Wallpaper\\Windows\\img0.jpg", // Transformed to "ImageConfig" setting + "SolidColor": { r: 128, g: 128, b: 128 }, // Transformed to "SolidColorConfig" setting + "WallpaperStyle": "6" // Transformed to "Scaling" setting + }, + "http://registry.gpii.net/applications/com.microsoft.windows.mouseSettings": { + "DoubleClickTime": 500, // Transformed to "DoubleClickTimeConfig" setting + "ScrollChars": 5, // Transformed to "ScrollCharsConfig" setting + "SwapMouseButtons": 1 // Transformed to "SwapMouseButtonsConfig" setting + }, + "http://registry.gpii.net/applications/com.microsoft.windows.notificationDuration": { + "NotificationDuration": 15 // Transformed to "Duration" setting. + } + } + } + } + } +} diff --git a/testData/preferences/james.json5 b/testData/preferences/james.json5 new file mode 100644 index 000000000..177be1865 --- /dev/null +++ b/testData/preferences/james.json5 @@ -0,0 +1,36 @@ +/* + * A fixture to test many JAWS settings at once by setting them to something other than the default. + */ +{ + "flat": { + "name": "James", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.freedomscientific.jaws": { + "Options.PrimaryLanguage": "ENU", + + "ENU-Message.Rate": 140, + "ENU-Message.Punctuation": 1, + + "ENU-PCCursor.Rate": 140, + "ENU-PCCursor.Punctuation": 1, + + "ENU-JAWSCursor.Rate": 140, + "ENU-JAWSCursor.Punctuation": 1, + + "ENU-Keyboard.Rate": 140, + "ENU-Keyboard.Punctuation": 1, + + "ENU-Global.Rate": 140, + "ENU-Global.Punctuation": 1, + + "ENU-MenuAndDialog.Rate": 140, + "ENU-MenuAndDialog.Punctuation": 1 + } + } + } + } + } +} diff --git a/testData/preferences/javier.json5 b/testData/preferences/javier.json5 new file mode 100644 index 000000000..53f83d09f --- /dev/null +++ b/testData/preferences/javier.json5 @@ -0,0 +1,53 @@ +/* + * A fixture to test many JAWS settings at once by setting them to something other than the default. + */ +{ + "flat": { + "name": "Javier", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.freedomscientific.jaws": { + "Options.PrimaryLanguage": "ESN", + + "ESM-Message.Rate": 90, + "ESM-Message.Punctuation": 1, + "ESM-PCCursor.Rate": 90, + "ESM-PCCursor.Punctuation": 1, + + "ESM-JAWSCursor.Rate": 90, + "ESM-JAWSCursor.Punctuation": 1, + + "ESM-Keyboard.Rate": 90, + "ESM-Keyboard.Punctuation": 1, + + "ESM-Global.Rate": 90, + "ESM-Global.Punctuation": 1, + + "ESM-MenuAndDialog.Rate": 90, + "ESM-MenuAndDialog.Punctuation": 1, + + "ESN-Message.Rate": 90, + "ESN-Message.Punctuation": 1, + + "ESN-PCCursor.Rate": 90, + "ESN-PCCursor.Punctuation": 1, + + "ESN-JAWSCursor.Rate": 90, + "ESN-JAWSCursor.Punctuation": 1, + + "ESN-Keyboard.Rate": 90, + "ESN-Keyboard.Punctuation": 1, + + "ESN-Global.Rate": 90, + "ESN-Global.Punctuation": 1, + + "ESN-MenuAndDialog.Rate": 90, + "ESN-MenuAndDialog.Punctuation": 1 + } + } + } + } + } +} diff --git a/testData/preferences/jaws.json5 b/testData/preferences/jaws.json5 new file mode 100644 index 000000000..6ef2ec015 --- /dev/null +++ b/testData/preferences/jaws.json5 @@ -0,0 +1,151 @@ +/* + + A fixture to test many JAWS settings at once by setting them to something other than the default. + +*/ +{ + "flat": { + "name": "JAWS", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.freedomscientific.jaws": { + "Braille.AttributeRotationDelay": 500, + "Braille.AutoPanMode": 2, + "Braille.BrailleMessages": 0, + "Braille.BrailleMode": 2, + "Braille.BrailleSleepMode": 1, + // TODO: Figure out default. + "Braille.BrailleVerbosity": 2, + "Braille.EightDotBraille": 0, + "Braille.MessagePrefixes": 0, + "Braille.MessageTime": 2500, + "Braille.ReversePanningButtons": 1, + "Braille.StructuredModeReverseOrder": 0, + "Braille.WordWrap": 0, + + "FSCasts.EnableNotifications": 0, + + "HTML.Abbreviations": 1, + "HTML.AccessKeys": 0, + "HTML.Acronyms": 1, + "HTML.ExpandAbbreviations": 1, + "HTML.ExpandAcronyms": 1, + "HTML.FilterConsecutiveDuplicateLinks": 0, + "HTML.FormFieldPromptOptions": 1, + "HTML.FormsModeAutoOff": 0, + "HTML.IgnoreInlineFrames": 1, + "HTML.IncludeGraphics": 0, + "HTML.IndicateColSpan": 0, + "HTML.SayAllOnDocumentLoad": 0, + "HTML.SmartNavigation": 1, + "HTML.TextBlockLength": 15, + + "Options.AllCapsIndicator": "definitely all cap", + "Options.AllowMouseEchoWhenMuted": 1, + "Options.AllowSpeechOnDemandWhenMuted": 0, + "Options.AllowTypingEchoWhenMuted": 1, + "Options.CapIndicator": "definitely cap", + "Options.CaretBlinkRate": 106, + "Options.CaretDetect": 5, + "Options.CaretDetectTimeOut": 500, + "Options.Case": 0, + "Options.CustomPageSummary": 2, + "Options.DetectKeyboardInputLanguage": 0, + "Options.Dictionary": 1, + "Options.Filter": 5, + "Options.GeneralizeDialect": 0, + "Options.Indentation": 1, + "Options.IndicateAttributesInDialogsAndMenus": 1, + "Options.IndicateCaps": 3, + "Options.IndicateNewlinesAndParagraphs": 2, + "Options.IndicateSelected": 1, + "Options.InitialNumlockState": 2, + "Options.InsertKeyMode": 1, + "Options.JAWSInsertKey": 0, + "Options.JAWSPunctuationEnabled": 3, + "Options.KeyRepeat": 0, + "Options.LanguageDetection": 0, + "Options.LeftShiftSkipBack": 0, + // TODO: Verify default + "Options.LessSpeechMode": 1, + "Options.LinePauses": 1, + "Options.LowerOtherAppsVolumeWhileJAWSIsRunning": 1, + "Options.MixedCase": 0, + "Options.MouseEchoSpeaksControlTypeAndState": 0, + "Options.MouseEchoSpeaksHelpAndDescription": 1, + "Options.MouseMovementStopsSpeech": 0, + "Options.MouseSpeechDelay": 250, + "Options.MouseSpeechEchoUnit": 3, + "Options.MouseSpeechEnabled": 1, + "Options.Numbers": 3, + "Options.OnScreenKeyboard": 1, + "Options.ProcessToolTipEvent": 0, + "Options.ProgressBarUpdateInterval": 2500, + "Options.QuickKeyNavigationMode": 1, + "Options.ReadingInterrupt": 0, + "Options.Repetitions": 1, + "Options.SayAllIgnoreShiftKeys": 1, + "Options.SayAllIndicateCaps": 1, + "Options.SayAllMode": 2, + "Options.SayBlankLineCount": 1, + "Options.SayCursorShapeChange": 1, + "Options.SayDollars": 1, + "Options.SayNumericDates": 2, + "Options.SaySelectedFirst": 1, + "Options.SayStateFirst": 1, + "Options.SayWindowTypeFirst": 1, + "Options.Scheme": "Classic", + "Options.ScreenEcho": 2, + "Options.SimultaneousSynthAndWave": 1, + "Options.SingleDigitThreshold": 7, + "Options.SkimReadingIndication": 0, + "Options.SmartWordReading": 0, + "Options.SpeakANSIChars": 0, + "Options.SpeakCharacterValueAsMultibyteSequence": 1, + "Options.SpeakCharacterValueInHex": 1, + "Options.SpeakNumbersSepByDashesAsDigits": 1, + "Options.SpeechHistory": 0, + "Options.SpeechMode": 1, + "Options.SpellAlphanumericData": 1, + "Options.SpellPhonetic": 1, + "Options.SynthesizerResetFrequency": 5000, + "Options.TetherJawsToPC": 1, + "Options.TextAnalyser": 2, + "Options.TouchKeyboardChildPanelNotification": 1, + "Options.TouchKeyboardNotification": 1, + "Options.TouchTypingEcho": 2, + "Options.TouchTypingMode": 0, + "Options.TypingEcho": 3, + "Options.TypingInterrupt": 0, + "Options.UseExtendedKeys": 1, + "Options.UseVirtualInfoInFormsMode": 1, + "Options.Verbosity": 2, + "Options.VTcolor": 11393254, + "Options.VTspacing": 8, + "Options.VTstyle": 2, + "Options.VTthickness": 10, + "Options.VTtransparency": 50, + "Options.VTUseTouchCursor": 1, + "Options.VTUseVirtualPCCursor": 0, + "Options.VTUseVirtualRibbon": 1, + "Options.VIRTUALMSAAREFRESH": -1, + + "OSM.PixelsPerSpace": 4, + "OSM.PixelsPerTab": 5, + "OSM.TableDetection": 1, + "OSM.TrackFocusRect": 0, + "OSM.UnderlineProximity": 5, + + "Touch.FlickVelocityMin": 99, + "Touch.TapEventDurationMax": 170, + "Touch.TapInterEventDurationMax": 300, + "Touch.TapTranslationMax": 50 + } + }, + "http://registry.gpii.net/applications/com.freedomscientific.jaws/enabled": true + } + } + } +} diff --git a/testData/preferences/jme_common.json5 b/testData/preferences/jme_common.json5 new file mode 100644 index 000000000..dbc15bf02 --- /dev/null +++ b/testData/preferences/jme_common.json5 @@ -0,0 +1,16 @@ +{ + "flat": { + "name": "JME Common", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 24, + "http://registry.gpii.net/common/highContrastTheme": "yellow-black", + "http://registry.gpii.net/common/volume": 0.25, + "http://registry.gpii.net/common/language": "es-ES" + } + } + } + } +} diff --git a/testData/preferences/li.json5 b/testData/preferences/li.json5 new file mode 100644 index 000000000..b70b5b9da --- /dev/null +++ b/testData/preferences/li.json5 @@ -0,0 +1,63 @@ +// # li.json5 +// +// ## Weekly Integration Tests: Preference Sets +// +// This preference set is used for the +// [weekly integration tests](http://wiki.gpii.net/w/Weekly_Integration_Test_Plan) and is manually tested on a regular basis. The expected system beahvior on login is described here. +// +// It has also been used in the [third review of Cloud4all](https://github.com/GPII/universal/blob/master/testData/preferences/review3/review3-preferences.md). +// +// ## Details +// `li.json5`: preference set with only common format terms for the following settings: +// * default preferences: +// * screen reader with TTS using a speech rate of 200 wpm, +// * magnifier with 150% magnification (the magnifier's position is not defined), +// * SuperNova is prioritised on Windows, +// * Mobile Accessibility for Android is prioritised over other Android accessibility features, +// * Rule-Based Matchmaker. +{ + "flat": { + "name": "Li", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/screenReaderTTS/enabled": true, + "http://registry.gpii.net/common/speechRate": 200, + "http://registry.gpii.net/common/magnification/enabled": true, + "http://registry.gpii.net/common/magnification": 1.5 + }, + "metadata": [ + { + "type": "priority", + "scope": [ + "http://registry.gpii.net/applications/com.yourdolphin.supernova-as", + "http://registry.gpii.net/applications/es.codefactory.android.app.ma" + ], + "value": 1024 + } + ] + }, + "turn-down-light": { + "name": "turn down light", + "preferences": { + "http://registry.gpii.net/common/screenReaderTTS/enabled": true, + "http://registry.gpii.net/common/speechRate": 200, + "http://registry.gpii.net/common/magnification/enabled": true, + "http://registry.gpii.net/common/magnification": 2, + "http://registry.gpii.net/common/highContrast/enabled": true + }, + "metadata": [ + { + "type": "priority", + "scope": [ + "http://registry.gpii.net/applications/com.yourdolphin.supernova-as", + "http://registry.gpii.net/applications/es.codefactory.android.app.ma" + ], + "value": 1024 + } + ] + } + } + } +} diff --git a/testData/preferences/liam.json5 b/testData/preferences/liam.json5 new file mode 100644 index 000000000..b99c2e771 --- /dev/null +++ b/testData/preferences/liam.json5 @@ -0,0 +1,58 @@ +/* # liam.json5 + +It has been tested with the Learning Tools addin features for OneNote 2016. Available with Office 2016 and Learning Tools Addin. + +Website: https://www.onenote.com/learningtools + +This settings can be found in the following menu of "OneNote 2016" UI: + +- Open OneNote 2016. +- Go to "Learning Tools" mode in the top ToolBar. +- -> The "Dictate" language setting is in the dropdown menu from "Dictate". +- -> For the other settings go to the "Immersive Reader". +- -> Under "aA" dropdown-menu the following settings can be found: +- - * Text Size. +- - * Spacing. +- - * Font. +- - * Themes. +- -> Under "Voice Settings", represented with a "human speaking" icon. The following settings can be found. +- - * Voice Speed. +- - * Voice Selection. + +User settings are: + +* Dictation Language = English + +* Text Size = Large. +* ->> Text Size Description = Real value is 52, but it's not represented in any way that the number can be verfified. +* Spacing = Wide. +* ->> Spacing Description = The right option. +* Font = Sitka Small +* Themes = Yellow + +* Voice Speed = 2 +* ->> Voice Speed Description = The slider should be two point to the right of the center. +* Voice Selection = Zira + +*/ +{ + "flat": { + "name": "Liam", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.office.windowsOneNoteLearningTools": { + "DictationLanguage": 1033, + "ReadingComprehensionDefaultFont": "Sitka Small", + "IsReadingComprehensionFontWide": 1, + "ReadingComprehensionTheme": 2, + "ReadingComprehensionFontSize": 52, + "SynthRate": 2, + "SynthVoices": "[{\"Lang\":\"en\",\"Voice\":\"Microsoft Zira Desktop\"}]" + } + } + } + } + } +} diff --git a/testData/preferences/livia.json5 b/testData/preferences/livia.json5 new file mode 100644 index 000000000..129c079ff --- /dev/null +++ b/testData/preferences/livia.json5 @@ -0,0 +1,26 @@ +// # livia.json5 +// +// ## Demo Personas with User Stories +// +// This preference set is part of our set of personas used for the demo install, +// presentations, and in general conveying an array of compelling user stories for +// the GPII. +// +// ## Details +// +// Livia - NVDA +// +// Livia is blind. She feels the GPII logo, and touches it with her card to it to launch her screen reader. +{ + "flat": { + "name": "Livia", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/org.nvda-project": {} + } + } + } + } +} diff --git a/testData/preferences/lorie.json5 b/testData/preferences/lorie.json5 new file mode 100644 index 000000000..a33e4d080 --- /dev/null +++ b/testData/preferences/lorie.json5 @@ -0,0 +1,63 @@ +/* # lorie.json5 + +It has been tested with the Learning Tools features for Word from Office 16. +Available with Microsoft Office 2016 Word Pro/Home 365 edition for Windows 10. + +Website: https://www.office.com/ + +This settings can be found in the following menu of word UI: + +- Open Microsoft Word. +- Go to "View" mode in the top ToolBar. +- Open "Reading mode". +- Go to "View" menu. +- The settings are placed in that dropdown menu. + +For seeing the speech settings is mandatory enable the "Read Aloud" feature. For doing this: + +- In the "Reading mode", go to the "View" menu. +- Trigger the feature "Read Aloud". +- In the dropdown menu that is enabled at the top right of the screen open the "Settings" dropdown menu. It's represented with a "human speaking" icon. +- The settings are placed in that dropdown menu. + +User settings are: + +* Column Width = Default +* Page Color = Reverse +* Design = Column Design +* Voice = Zira +* Speech Rate = 2 +* Syllables = Activated +* Text Spacing = Activated + +*/ +{ + "flat": { + "name": "Lorie", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.office.windowsWordHome365LearningTools": { + "ReadingModeColumnWidth": 2, + "ReadingModePageColor": 2, + "ReadingModePrintedPage": 0, + "ReadAloudVoiceId": "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech_OneCore\\Voices\\Tokens\\MSTTS_V110_enUS_ZiraM", + "ReadAloudVoiceRate": 2, + "ReadingModeSyllables": 1, + "ReadingModeTextSpacing": 1 + }, + "http://registry.gpii.net/applications/com.office.windowsWordPro365LearningTools": { + "ReadingModeColumnWidth": 2, + "ReadingModePageColor": 2, + "ReadingModePrintedPage": 0, + "ReadAloudVoiceId": "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\TTS_MS_EN-US_ZIRA_11.0", + "ReadAloudVoiceRate": 2, + "ReadingModeSyllables": 1, + "ReadingModeTextSpacing": 1 + } + } + } + } + } +} diff --git a/testData/preferences/maavis1.json b/testData/preferences/maavis1.json deleted file mode 100644 index 6c8b27969..000000000 --- a/testData/preferences/maavis1.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "http://registry.gpii.org/applications/net.opendirective.maavis": [{ - "value": { - "theme": "bw", - "playStartSound":"yes", - "speakOnActivate":"yes", - "speakTitles":"yes", - "speakLabels":"yes", - "showLabels":"yes", - "showImages":"no", - "useSkype":"no", - "userType":"touch", - "splashTime":2, - "scanRate":"1000", - "scanMode":"AUTO1SWITCHAUTOSTART", - - "selectionsSetSize":"2x3", - - "passwordSetSize":"4x3" - } - }] -} diff --git a/testData/preferences/maavis2.json b/testData/preferences/maavis2.json deleted file mode 100644 index 8a33bd62d..000000000 --- a/testData/preferences/maavis2.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "http://registry.gpii.org/applications/net.opendirective.maavis": [{ - "value": { - "name":"Ethel", - "theme":"colour", - "playStartSound":"no", - "speakTitles":"no", - "speakLabels":"no", - "showLabels":"no", - "showImages":"yes", - "useSkype":"no", - "userType":"scan", - "splashTime":0, - "scanRate":"600", - "scanMode":"AUTO1SWITCHAUTOSTART", - - "selectionsSetSize":"3x3", - - "passwordItems":"complete" - } - }] -} diff --git a/testData/preferences/maggie.json5 b/testData/preferences/maggie.json5 new file mode 100644 index 000000000..82fd8c21b --- /dev/null +++ b/testData/preferences/maggie.json5 @@ -0,0 +1,22 @@ +// # Maggie +// +// The preference set enables the magnifier, making it follow the text insertion point (caret) and docks the magnifier pane to the top of the screen. +// +// ## Testing +// +// The magnifier should be looking at the caret when you type some text. +{ + "flat": { + "name": "Maggie", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/magnification": 2.0, + "http://registry.gpii.net/common/tracking": ["caret"], + "http://registry.gpii.net/common/magnifierPosition": "TopHalf" + } + } + } + } +} diff --git a/testData/preferences/magic.json5 b/testData/preferences/magic.json5 new file mode 100644 index 000000000..f5594d810 --- /dev/null +++ b/testData/preferences/magic.json5 @@ -0,0 +1,94 @@ +/* + + A fixture to test all supported MAGic settings at once by setting them to something other than the default. + +*/ +{ + "flat": { + "name": "MAGic", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.freedomscientific.magic": { + "mag.caretbutton": 1, + "mag.colorbutton": 1, + "mag.cursorbutton": 1, + "mag.detectKeyboardInputLanguage": 0, + "mag.DisabledScreenSaver": 0, + "mag.dynlensbordercolor": 9, + "mag.dynlensborderwidth": 5, + "mag.enablebilingual": 1, + "mag.enablemagicfrmsystray": 1, + "mag.Focus Enhancement Enabled": 1, + "mag.IndicateCaps": 2, + "mag.IndicateCapsBy": 2, + "mag.lensbordercolor": 9, + "mag.lensborderwidth": 5, + "mag.LowerOtherAppsVolumeWhileMagicIsRunning": 1, + "mag.overlaybordercolor": 9, + "mag.overlayborderwidth": 5, + "mag.promptmagictosave": 1, + "mag.promptoverwriteschemes": 0, + // This may be problematic, as it is associated with a common term. + "mag.size": 2, + "mag.speechbutton": 0, + "mag.splitlocation": 2, + "mag.splitsize": 0, + "mag.splitbordercolor": 9, + "mag.splitframesize": 5, + "mag.splitwindowheight": 20, + "mag.splitwindowwidth": 20, + "mag.startmagicminimized": 1, + // This may be problematic, as it is associated with a common term. + "mag.startmagnified": 1, + "mag.TextViewerCharSpace": 1, + "mag.TextViewerFontBkgColorIndex": 13, + "mag.TextViewerFontColorIndex": 14, + // This may be problematic, as it is associated with a common term. + "mag.TextViewerFontFaceName": "Comic Sans", + // This may be problematic, as it is associated with a common term. + "mag.TextViewerFontSize": 45, + "mag.TextViewerFontStyle": 2, + "mag.TextViewerFontEffect": 1, + "mag.TextViewerOn": 0, + "mag.TextViewerPosition": 1, + "mag.TextViewerSpotlightStyle": 1, + "mag.TextViewerSpotlightTransparency": 25, + "mag.TextViewerSpotlightColorIndex": 9, + "mag.TextViewerSpotlightWordColorIndex": 14, + "mag.TextViewerSpotlightThickness": 5, + "mag.TextViewerTrackingMode": 1, + "mag.TextViewerWordSpace": 1, + // This is problematic because it is transformed into a JAWS setting via the common term. + // TODO: Discuss the matchmaker's current behaviour and whether it can change. + // "mag.typingecho": 3, + "mag.typeechobutton": 0, + "mag.viewtype": 1, + "Options.SkipILM": 1, + "Options.AllowMouseEchoWhenMuted": 1, + "Options.AllowSpeechOnDemandWhenMuted": 0, + "Options.AllowTypingEchoWhenMuted": 1, + "Options.IndicateCaps": 1, + "Options.InitialNumlockState": 2, + "Options.MouseMovementStopsSpeech": 0, + "Options.MouseSpeechDelay": 2, + "Options.MouseSpeechEchoUnit": 1, + "Options.MouseSpeechEnabled": 1, + // This is problematic because it is transformed into a JAWS setting via the common term. + // TODO: Discuss the matchmaker's current behaviour and whether it can change. + // "Options.TypingEcho": 2, + "scheme.ColorSchemeModified": 1, + "scheme.Color_Scheme": "Grayscale (Inverted)+Color", + "scheme.CursorSchemeModified": 1, + "scheme.Cursor_Scheme": "Purple Long Box+Cursor", + "scheme.MouseSchemeModified": 1, + // This may be problematic, as it is associated with a common term (crosshairs). + "scheme.Mouse_Scheme": "Scope When Moving+Mouse" + }, + "http://registry.gpii.net/applications/com.freedomscientific.magic/enabled": true + } + } + } + } +} diff --git a/testData/preferences/maguro.json5 b/testData/preferences/maguro.json5 new file mode 100644 index 000000000..0c9918853 --- /dev/null +++ b/testData/preferences/maguro.json5 @@ -0,0 +1,16 @@ +{ + "flat": { + "name": "Maguro", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.android.persistentConfiguration": { + "fontScale": 1.5, + "locale": "en_GB" + } + } + } + } + } +} diff --git a/testData/preferences/mary.json5 b/testData/preferences/mary.json5 new file mode 100644 index 000000000..07d4353e2 --- /dev/null +++ b/testData/preferences/mary.json5 @@ -0,0 +1,26 @@ +// # mary.json5 +// +// ## Weekly Integration Tests: Preference Sets +// +// This preference set is used for the +// [weekly integration tests](http://wiki.gpii.net/w/Weekly_Integration_Test_Plan) and is manually tested on a regular basis. The expected system behaviour on login is described here. +// +// It has also been used in the [third review of Cloud4all](https://github.com/GPII/universal/blob/master/testData/preferences/review3/review3-preferences.md). +// +// ## Details +// `mary.json5`: preference set with only common format terms for the following settings: +// * enable the on-screen keyboard, +// * Rule-Based Matchmaker. +{ + "flat": { + "name": "Mary", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true + } + } + } + } +} diff --git a/testData/preferences/mickey.json5 b/testData/preferences/mickey.json5 new file mode 100644 index 000000000..078dac62a --- /dev/null +++ b/testData/preferences/mickey.json5 @@ -0,0 +1,28 @@ +// # Mickey +// +// This preference set enables mouse emulation. +// +// With this setting, the user can control the mouse using the number pad. +// +// ## Testing +// +// The mouse cursor should move when pressing the directional keys on the number pad (depending on if Num Lock is on). +// +// For testing in a Virtual Machine, mouse integration should be turned off (keyboard focus can be toggled using the "host key", usually the right ctrl). +// +// On a laptop keyboard, Fn may need to be held down or put in keypad mode. +{ + "flat": { + "name": "Mickey", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/initDelay": 120, + "http://registry.gpii.net/common/cursorSpeed": 0.850 + } + } + } + } +} diff --git a/testData/preferences/mobileaccessibility1.json5 b/testData/preferences/mobileaccessibility1.json5 new file mode 100644 index 000000000..6a6c674af --- /dev/null +++ b/testData/preferences/mobileaccessibility1.json5 @@ -0,0 +1,43 @@ +{ + "flat": { + "name": "Mobile Accessibility 1", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/es.codefactory.android.app.ma": { + // TODO: There are no schemas for these, disabling and leaving around for reference. +// "MA_CLOUD4ALL_USER":{ +// "type":"class java.lang.String", +// "value":"mobileaccessibility1" +// }, +// "access_commonprefs_editingkeyboardecho":{ +// "type":"class java.lang.String", +// "value":"3" +// }, +// "access_commonprefs_speechpitch":{ +// "type":"class java.lang.String", +// "value":"6" +// }, +// "access_commonprefs_speechrate":{ +// "type":"class java.lang.String", +// "value":"10" +// }, +// "access_commonprefs_spellphonetically":{ +// "type":"class java.lang.Boolean", +// "value":"false" +// }, +// "access_commonprefs_punctuation":{ +// "type":"class java.lang.String", +// "value":"2" +// }, +// "access_commonprefs_capitalization":{ +// "type":"class java.lang.Boolean", +// "value":"true" +// } + } + } + } + } + } +} diff --git a/testData/preferences/mobileaccessibility2.json5 b/testData/preferences/mobileaccessibility2.json5 new file mode 100644 index 000000000..2a8c4d720 --- /dev/null +++ b/testData/preferences/mobileaccessibility2.json5 @@ -0,0 +1,43 @@ +{ + "flat": { + "name": "Mobile Accessibility 2", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/es.codefactory.android.app.ma": { + // TODO: There are no schemas for these, disabling and leaving around for reference. +// "MA_CLOUD4ALL_USER":{ +// "type":"class java.lang.String", +// "value":"mobileaccessibility2" +// }, +// "access_commonprefs_editingkeyboardecho":{ +// "type":"class java.lang.String", +// "value":"3" +// }, +// "access_commonprefs_speechpitch":{ +// "type":"class java.lang.String", +// "value":"4" +// }, +// "access_commonprefs_speechrate":{ +// "type":"class java.lang.String", +// "value":"4" +// }, +// "access_commonprefs_spellphonetically":{ +// "type":"class java.lang.Boolean", +// "value":"false" +// }, +// "access_commonprefs_punctuation":{ +// "type":"class java.lang.String", +// "value":"2" +// }, +// "access_commonprefs_capitalization":{ +// "type":"class java.lang.Boolean", +// "value":"true" +// } + } + } + } + } + } +} diff --git a/testData/preferences/multi_context.json5 b/testData/preferences/multi_context.json5 new file mode 100644 index 000000000..0a2119d7c --- /dev/null +++ b/testData/preferences/multi_context.json5 @@ -0,0 +1,38 @@ +// # multi_context +// +// A preference set for testing the PSP's multiple preference sets functionality. +// +// Provides control over `Magnification`, `Volume Control` or both (depending on the selected preference set). +{ + "flat": { + "name": "Multiple Contexts", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/magnification": 1.5, + "http://registry.gpii.net/common/volume": 0.5 + } + }, + "bright": { + "name": "bright", + "preferences": { + "http://registry.gpii.net/common/magnification": 2 + } + }, + "noise": { + "name": "noise", + "preferences": { + "http://registry.gpii.net/common/volume": 1 + } + }, + "brightandnoise": { + "name": "bright and noise", + "preferences": { + "http://registry.gpii.net/common/volume": 1, + "http://registry.gpii.net/common/magnification": 2 + } + } + } + } +} diff --git a/testData/preferences/naomi.json5 b/testData/preferences/naomi.json5 new file mode 100644 index 000000000..1b854037a --- /dev/null +++ b/testData/preferences/naomi.json5 @@ -0,0 +1,55 @@ +// # Naomi.json5 +// +// This preference set configure several Windows 10 built in narrator features. +// +// ## Testing +// +// The setting is supposed to trigger the following settings from the Windows 10 +// Narrator, with the following values: +// - "IntonationPause": Enabled, +// - "ReadHints": Enabled, +// - "PlayAudioCues": Enabled, +// - "NarratorCursorHighlight": Enabled, +// - "FastKeyEntryEnabled": Enabled, +// - "ReadingWithIntent": Enabled, +// - "ErrorNotificationType": Enabled, +// - "CoupleNarratorCursorMouse": Enabled, +// - "LockNarratorKeys": Enabled +// - "SpeechVolume": 80, +// - "SpeechVoice": "Microsoft Zira - English (United States)", +// - "ControlContextVerbosity": "4 - Full context of new control", +// - "ReadContextBeforeControls": Enabled, +// - "LowerAppsVolume": Enabled, +// - "StartWithShortcut": Enabled, +// - "TextVerbosityLevel": "3 - Other annotations", +// - "DetailedFeedback": Enabled, +// - "EnableAutoStart": Enabled, +// - "EnableSystemAutoStart": Enabled +// +// The settings changes can be observed in the Windows 10 "Settings" app. +{ + "flat": { + "name": "naomi", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.narrator": { + "IntonationPause": 1, + "ReadHints": 1, + "PlayAudioCues": 1, + "NarratorCursorHighlight": 1, + "FastKeyEntryEnabled": 1, + "ReadingWithIntent": 1, + "ErrorNotificationType": 1, + "CoupleNarratorCursorMouse": 1, + "LockNarratorKeys": 1, + "SpeechVolume": .8, + "SpeechVoice": "Microsoft Zira - English (United States)", + "DetailedFeedback": 1 + } + } + } + } + } +} diff --git a/testData/preferences/nisha.json b/testData/preferences/nisha.json deleted file mode 100644 index bf3cc1a8a..000000000 --- a/testData/preferences/nisha.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "http://registry.gpii.org/applications/com.texthelp.readWriteGold": [{ - "value": { - "ApplicationSettings.AppBar.Width.$t": 788, - "ApplicationSettings.AppBar.ShowText.$t": false, - "ApplicationSettings.AppBar.optToolbarShowText.$t": false, - "ApplicationSettings.AppBar.LargeIcons.$t": true, - "ApplicationSettings.AppBar.optToolbarShowText.$t": true, - "ApplicationSettings.AppBar.optToolbarLargeIcons.$t": true, - "ApplicationSettings.Speech.optSAPI5Speed.$t": 50, - "ApplicationSettings.Speech.optAutoUseScreenReading.$t": false - } - }], - "http://registry.gpii.org/applications/fluid.uiOptions.windows": [{ "value": {}}], - "http://registry.gpii.org/applications/fluid.uiOptions.linux": [{ "value": {}}], - "http://registry.gpii.org/applications/fluid.uiOptions": [{ - "value": { - "lineSpacing": "1", - "links": false, - "inputsLarger": false, - "captions": false, - "transcripts": false, - "volume": "50" - } - }], - "http://registry.gpii.org/applications/org.gnome.desktop.a11y.caribou-keyboard": [{ "value": {}}], - "http://registry.gpii.org/applications/com.microsoft.windows.onscreenKeyboard": [{ "value": {}}], - "http://registry.gpii.org/applications/org.gnome.desktop.a11y.keyboard": [{ - "value": { - "stickykeys-enable": true, - "enable": true, - "bouncekeys-enable": true, - "bouncekeys-delay": 300, - "mousekeys-enable": true, - "mousekeys-init-delay": 120, - "mousekeys-max-speed": 850, - "mousekeys-accel-time": 800, - "slowkeys-enable": true, - "togglekeys-enable": true - } - }], - "http://registry.gpii.org/common/fontSize": [{ "value": 12 }], - "http://registry.gpii.org/common/foregroundColor": [{ "value": "white" }], - "http://registry.gpii.org/common/backgroundColor": [{ "value": "black" }], - "http://registry.gpii.org/common/fontFaceFontName": [{ "value": ["Comic Sans"] }], - "http://registry.gpii.org/common/fontFaceGenericFontFace": [{ "value": "sans serif" }], - "http://registry.gpii.org/common/adaptationPreference": [{ "value": [ - { - "adaptationType": "caption", - "language": "en" - }, - {} - ]}], - "http://registry.gpii.org/common/tableOfContents": [{ "value": false }] -} \ No newline at end of file diff --git a/testData/preferences/nisha.json5 b/testData/preferences/nisha.json5 new file mode 100644 index 000000000..6065584ca --- /dev/null +++ b/testData/preferences/nisha.json5 @@ -0,0 +1,29 @@ +{ + "flat": { + "name": "Nisha", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.applications.onscreen-keyboard": {}, + "http://registry.gpii.net/applications/com.microsoft.windows.onscreenKeyboard": {}, + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.keyboard": { + // These lack schemas and should not be used. Commenting out and leaving for future reference. +// "stickykeys-enable": true, +// "enable": true, +// "bouncekeys-enable": true, +// "bouncekeys-delay": 300, +// "mousekeys-enable": true, +// "mousekeys-init-delay": 120, +// "mousekeys-max-speed": 850, +// "mousekeys-accel-time": 800, +// "slowkeys-enable": true, +// "togglekeys-enable": true + }, + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/tableOfContents": false + } + } + } + } +} diff --git a/testData/preferences/nvda.json5 b/testData/preferences/nvda.json5 new file mode 100644 index 000000000..f3f0e7139 --- /dev/null +++ b/testData/preferences/nvda.json5 @@ -0,0 +1,168 @@ +/* + + A fixture to test many NVDA settings at once by setting them to something other than the default. + +*/ +{ + "flat": { + "name": "NVDA", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/org.nvda-project": { + "audio.audioDuckingMode":2, + "braille.autoTether": "False", + "braille.cursorBlink": "False", + "braille.cursorBlinkRate": 750, + "braille.cursorShapeFocus": 255, + "braille.cursorShapeReview": 255, + "braille.display": "auto", + "braille.expandAtCursor": "False", + "braille.focusContextPresentation": "fill", + "braille.inputTable": "en-us-comp8-ext.utb", + "braille.messageTimeout": 12, + "braille.noMessageTimeout": "True", + "braille.readByParagraph": "True", + "braille.showCursor": "False", + "braille.tetherTo": "autoTether", + "braille.translationTable": "en-gb-comp8.ctb", + "braille.wordWrap": "False", + "documentFormatting.detectFormatAfterCursor": "True", + "documentFormatting.includeLayoutTables": "True", + "documentFormatting.reportAlignment": "True", + "documentFormatting.reportBlockQuotes": "False", + "documentFormatting.reportBorderColor": "True", + "documentFormatting.reportBorderStyle": "True", + "documentFormatting.reportClickable": "False", + "documentFormatting.reportColor": "True", + "documentFormatting.reportComments": "False", + "documentFormatting.reportEmphasis": "True", + "documentFormatting.reportFontAttributes": "True", + "documentFormatting.reportFontName": "True", + "documentFormatting.reportFontSize": "True", + "documentFormatting.reportFrames": "False", + "documentFormatting.reportHeadings": "False", + "documentFormatting.reportLandmarks": "False", + "documentFormatting.reportLineIndentation": "True", + "documentFormatting.reportLineIndentationWithTones": "True", + "documentFormatting.reportLineNumber": "True", + "documentFormatting.reportLineSpacing": "True", + "documentFormatting.reportLinks": "False", + "documentFormatting.reportLists": "False", + "documentFormatting.reportPage": "False", + "documentFormatting.reportParagraphIndentation": "True", + "documentFormatting.reportRevisions": "False", + "documentFormatting.reportSpellingErrors": "False", + "documentFormatting.reportStyle": "True", + "documentFormatting.reportTableCellCoords": "False", + "documentFormatting.reportTableHeaders": "False", + "documentFormatting.reportTables": "False", + "general.askToExit": "False", + "general.language": "nl", + "general.playStartAndExitSounds": "False", + "general.saveConfigurationOnExit": "False", + "general.showWelcomeDialogAtStartup": "False", + "inputComposition.alwaysIncludeShortCharacterDescriptionInCandidateName": "False", + "inputComposition.announceSelectedCandidate": "False", + "inputComposition.autoReportAllCandidates": "False", + "inputComposition.reportCompositionStringChanges": "False", + "inputComposition.reportReadingStringChanges": "False", + "keyboard.alertForSpellingErrors": "False", + "keyboard.allowSkimReadingInSayAll": "True", + "keyboard.beepForLowercaseWithCapslock": "False", + "keyboard.handleInjectedKeys": "False", + "keyboard.keyboardLayout": "laptop", + "keyboard.speakCommandKeys": "True", + "keyboard.speakTypedCharacters": "True", + "keyboard.speakTypedWords": "True", + "keyboard.speechInterruptForCharacters": "False", + "keyboard.speechInterruptForEnter": "False", + "keyboard.useCapsLockAsNVDAModifierKey": "True", + "keyboard.useExtendedInsertAsNVDAModifierKey": "False", + "keyboard.useNumpadInsertAsNVDAModifierKey": "False", + "mouse.audioCoordinates_detectBrightness": "True", + "mouse.audioCoordinatesOnMouseMove": "True", + "mouse.enableMouseTracking": "False", + "mouse.mouseTextUnit": "line", + "mouse.reportMouseShapeChanges": "True", + "mouse.reportObjectRoleOnMouseEnter": "True", + "presentation.guessObjectPositionInformationWhenUnavailable": "True", + "presentation.reportAutoSuggestionsWithSound": "False", + "presentation.reportDynamicContentChanges": "False", + "presentation.reportHelpBalloons": "True", + "presentation.reportKeyboardShortcuts": "False", + "presentation.reportObjectDescriptions": "False", + "presentation.reportObjectPositionInformation": "False", + "presentation.reportTooltips": "True", + "presentation.progressBarUpdates.progressBarOutputMode": "off", + "presentation.progressBarUpdates.reportBackgroundProgressBars": "True", + "reviewCursor.followCaret": "True", + "reviewCursor.followFocus": "True", + "reviewCursor.followMouse": "True", + "reviewCursor.simpleReviewMode": "False", + "speech.beepSpeechModePitch": 5000, + "speech.espeak.autoDialectSwitching": "True", + "speech.espeak.autoLanguageSwitching": "False", + "speech.espeak.beepForCapitals": "True", + "speech.espeak.capPitchChange": -30, + "speech.espeak.inflection": 25, + "speech.espeak.pitch": 80, + "speech.espeak.rate": 70, + "speech.espeak.rateBoost": "True", + "speech.espeak.sayCapForCapitals": "True", + "speech.espeak.trustVoiceLanguage": "False", + "speech.espeak.useSpellingFunctionality": "False", + "speech.espeak.voice": "en-gb-scotland", + "speech.espeak.volume": 70, + "speech.oneCore.autoDialectSwitching": "True", + "speech.oneCore.autoLanguageSwitching": "False", + "speech.oneCore.beepForCapitals": "True", + "speech.oneCore.capPitchChange": -30, + "speech.oneCore.pitch": 75, + "speech.oneCore.rate": 75, + "speech.oneCore.sayCapForCapitals": "True", + "speech.oneCore.trustVoiceLanguage": "False", + "speech.oneCore.useSpellingFunctionality": "False", + "speech.oneCore.voice": "Microsoft Zira", + "speech.oneCore.volume": 75, + "speech.outputDevice": "Microsoft Sound Mapper", + "speech.sapi5.beepForCapitals": "True", + "speech.sapi5.capPitchChange": -30, + "speech.sapi5.pitch": 75, + "speech.sapi5.rate": 75, + "speech.sapi5.sayCapForCapitals": "True", + "speech.sapi5.trustVoiceLanguage": "False", + "speech.sapi5.useSpellingFunctionality": "False", + "speech.sapi5.voice": "Microsoft Zira Desktop - English (United States)", + "speech.sapi5.volume": 75, + "speech.silence.autoDialectSwitching": "True", + "speech.silence.autoLanguageSwitching": "False", + "speech.silence.beepForCapitals": "True", + "speech.silence.capPitchChange": -30, + "speech.silence.sayCapForCapitals": "True", + "speech.silence.trustVoiceLanguage": "False", + "speech.silence.useSpellingFunctionality": "False", + "speech.symbolLevel": 300, // "All" + "speech.synth": "silence", + "speechViewer.autoPositionWindow": "False", + "speechViewer.showSpeechViewerAtStartup": "True", + "touch.touchTyping": "True", + "update.autoCheck": "False", + "update.startupNotification": "False", + "uwpOcr.language": "English(United States)", + "virtualBuffers.autoPassThroughOnCaretMove": "True", + "virtualBuffers.autoPassThroughOnFocusChange": "False", + "virtualBuffers.autoSayAllOnPageLoad": "False", + "virtualBuffers.linesPerPage": 50, + "virtualBuffers.maxLineLength": 150, + "virtualBuffers.passThroughAudioIndication": "False", + "virtualBuffers.trapNonCommandGestures": "False", + "virtualBuffers.useScreenLayout": "False" + } + }, + "http://registry.gpii.net/applications/org.nvda-project/enabled": true + } + } + } +} diff --git a/testData/preferences/nyx.json5 b/testData/preferences/nyx.json5 new file mode 100644 index 000000000..2647f2d09 --- /dev/null +++ b/testData/preferences/nyx.json5 @@ -0,0 +1,25 @@ +/* # nyx.json5 + +The preference set enables the night screen, reducing the blue from the screen. + +## Testing + +To see the results, a physical machine is required. When enabled, the blue is reduced (making white turn yellow). + +For a VM, search for "night light" in System Settings. The button will say `Turn on now` if this setting is disabled, +otherwise `Turn off now`. + +*/ +{ + "flat": { + "name": "Nyx", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/nightScreen": true + } + } + } + } +} diff --git a/testData/preferences/olga.json5 b/testData/preferences/olga.json5 new file mode 100644 index 000000000..a88fe6114 --- /dev/null +++ b/testData/preferences/olga.json5 @@ -0,0 +1,45 @@ +// # Olga.json5 +// +// This preference set changes the Windows 10 OSK preferences settings. +// +// ## Testing +// +// This changes the following settings from the system: +// + ClickSound: Disable click sound. +// + ShowPositionKeys: Enable OSK position keys. +// + ShowNumPad: Shows numeric pad. +// + KeyPressMode: Scan keys mode. +// + HoverPeriod: 1.5 seconds. +// + ScanInterval: 0.5 seconds. +// + UseDevice: Don't use a gaming device for key scanning. +// + UseKB: Use keyboard key for key scanning. +// + ScanKey: Set "F4" as scan key. +// + UseMouse: Disable mouse click to scan keys. +// + UseTextPrediction: Enable text prediction. +// + InsertSpace: Enable space insertion after predicted word. +// +{ + "flat": { + "name": "olga", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.onscreenKeyboard": { + "ClickSound": 0, + "ShowNumPad": 1, + "HoverPeriod": 1500, + "ScanInterval": 500, + "UseDevice": 0, + "UseKB": 1, + "ScanKey": 115, + "UseMouse": 0, + "UseTextPrediction": 1, + "InsertSpace": 1, + "Dock": 1 + } + } + } + } + } +} diff --git a/testData/preferences/oliver.json5 b/testData/preferences/oliver.json5 new file mode 100644 index 000000000..79d5914a0 --- /dev/null +++ b/testData/preferences/oliver.json5 @@ -0,0 +1,45 @@ +// # Oliver.json5 +// +// This preference set changes the Windows 10 OSK preferences settings. +// +// ## Testing +// +// This changes the following settings from the system: +// + ClickSound: Disable click sound. +// + ShowPositionKeys: Disable OSK position keys. +// + ShowNumPad: Shows numeric pad. +// + KeyPressMode: Hover over keys. +// + HoverPeriod: 1.5 seconds. +// + ScanInterval: 1.5 seconds. +// + UseDevice: Don't use a gaming device for key scanning. +// + UseKB: Don't use keyboard key for key scanning. +// + ScanKey: Set "F2" as scan key. +// + UseMouse: Use mouse click to scan keys. +// + UseTextPrediction: Disable text prediction. +// + InsertSpace: Disable space insertion after predicted word. +// +{ + "flat": { + "name": "oliver", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.onscreenKeyboard": { + "ClickSound": 0, + "ShowNumPad": 1, + "HoverPeriod": 1500, + "ScanInterval": 1500, + "UseDevice": 0, + "UseKB": 0, + "ScanKey": 113, + "UseMouse": 1, + "UseTextPrediction": 0, + "InsertSpace": 0, + "Dock": 0 + } + } + } + } + } +} diff --git a/testData/preferences/omar.json b/testData/preferences/omar.json deleted file mode 100644 index 764a61202..000000000 --- a/testData/preferences/omar.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "http://registry.gpii.org/applications/trace.easyOne.sudan.windows": [{ "value": {} }], - "http://registry.gpii.org/applications/trace.easyOne.sudan.linux": [{ "value": {} }] -} diff --git a/testData/preferences/omar.json5 b/testData/preferences/omar.json5 new file mode 100644 index 000000000..dab9bccc8 --- /dev/null +++ b/testData/preferences/omar.json5 @@ -0,0 +1,14 @@ +{ + "flat": { + "name": "Omar", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/trace.easyOne.sudan.windows": {}, + "http://registry.gpii.net/applications/trace.easyOne.sudan.linux": {} + } + } + } + } +} diff --git a/testData/preferences/omnitor1.json5 b/testData/preferences/omnitor1.json5 new file mode 100644 index 000000000..a225b80dc --- /dev/null +++ b/testData/preferences/omnitor1.json5 @@ -0,0 +1,24 @@ +// # Omnitor's eCmobile/eCtouch preference sets +// +// * `omnitor1.json5`: eCmobile's factory settings as common terms +// * default font size +// * high contrast off +// +// When logging in/out with this preference sets into a device where eCmobile/eCtouch is installed a user will observe the following sequence: +// * eCmobile/eCtouch will be launched +// * It will be configured according to the used preference set +// * When logging out, the user will be taken to the home screen (even if eCmobile/eCtouch is still running in the background) +{ + "flat": { + "name": "Omnitor 1", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/highContrast/enabled": false + } + } + } + } +} diff --git a/testData/preferences/omnitor2.json5 b/testData/preferences/omnitor2.json5 new file mode 100644 index 000000000..dbcd72c60 --- /dev/null +++ b/testData/preferences/omnitor2.json5 @@ -0,0 +1,26 @@ +// # Omnitor's eCmobile/eCtouch preference sets +// +// * `omnitor2.json5`: custom settings as common terms +// * larger text +// * high contrast on +// * use the yellow-black high contrast theme +// +// When logging in/out with this preference sets into a device where eCmobile/eCtouch is installed a user will observe the following sequence: +// * eCmobile/eCtouch will be launched +// * It will be configured according to the used preference set +// * When logging out, the user will be taken to the home screen (even if eCmobile/eCtouch is still running in the background) +{ + "flat": { + "name": "Omnitor 2", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 25, + "http://registry.gpii.net/common/highContrast/enabled": true, + "http://registry.gpii.net/common/highContrastTheme": "yellow-black" + } + } + } + } +} diff --git a/testData/preferences/os_android.json5 b/testData/preferences/os_android.json5 new file mode 100644 index 000000000..9f2ced582 --- /dev/null +++ b/testData/preferences/os_android.json5 @@ -0,0 +1,26 @@ +{ + "flat": { + "name": "Android OS", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.android.settings.system": { + "dim_screen": true, + "haptic_feedback_enabled": true, + "accelerometer_rotation": false, + "user_rotation": 2, + "screen_off_timeout": 10000 + }, + "http://registry.gpii.net/applications/com.android.audioManager": { + "STREAM_SYSTEM": 0.5 + }, + "http://registry.gpii.net/applications/com.android.persistentConfiguration": { + "fontScale": 1.5, + "locale": "en" + } + } + } + } + } +} diff --git a/testData/preferences/os_android_common.json5 b/testData/preferences/os_android_common.json5 new file mode 100644 index 000000000..4cbd1ff81 --- /dev/null +++ b/testData/preferences/os_android_common.json5 @@ -0,0 +1,15 @@ +{ + "flat": { + "name": "Android OS Common", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/volume": 0.5, + "http://registry.gpii.net/common/fontSize": 24, + "http://registry.gpii.net/common/language": "en-GB" + } + } + } + } +} diff --git a/testData/preferences/os_common.json b/testData/preferences/os_common.json deleted file mode 100644 index 9ee6c2603..000000000 --- a/testData/preferences/os_common.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "http://registry.gpii.org/common/magnifierPosition": [{ "value": "Lens" }], - "http://registry.gpii.org/common/magnification": [{ "value": 1.5 }], - "http://registry.gpii.org/common/tracking": [{ "value": [ "mouse", "caret" ] }], - "http://registry.gpii.org/common/invertImages": [{ "value": true }], - "http://registry.gpii.org/common/cursorSize": [{ "value": 0.9 }], - "http://registry.gpii.org/common/fontSize": [{ "value": 9 }], - "http://registry.gpii.org/common/mouseTrailing": [{ "value": 10 }], - "http://registry.gpii.org/common/highContrast": [{ "value": true }] -} diff --git a/testData/preferences/os_gnome.json b/testData/preferences/os_gnome.json deleted file mode 100644 index 68181ff5b..000000000 --- a/testData/preferences/os_gnome.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "http://registry.gpii.org/applications/org.gnome.desktop.a11y.magnifier": [{ - "value": { - "mag-factor": 1.5, - "screen-position": "full-screen" - } - }], - "http://registry.gpii.org/applications/org.gnome.desktop.interface": [{ - "value": { - "cursor-size": 90, - "text-scaling-factor": 0.75 - } - }] -} diff --git a/testData/preferences/os_win7.json b/testData/preferences/os_win7.json deleted file mode 100644 index 1db9bf0b2..000000000 --- a/testData/preferences/os_win7.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "http://registry.gpii.org/applications/com.microsoft.windows.mouseTrailing": [{ - "value": { - "MouseTrails": { - "path": { - "get": "pvParam", - "set": "uiParam" - }, - "value": 10 - } - } - }], - "http://registry.gpii.org/applications/com.microsoft.windows.highContrast": [{ - "value": { - "HighContrastOn": { - "path": "pvParam.dwFlags.HCF_HIGHCONTRASTON", - "value": true - } - } - }], - "http://registry.gpii.org/applications/com.microsoft.windows.cursors": [{ - "value": { - "No": { - "value": "%SystemRoot%\\cursors\\aero_unavail_xl.cur", - "dataType": "REG_SZ" - }, - "Hand": { - "value": "%SystemRoot%\\cursors\\aero_link_xl.cur", - "dataType": "REG_SZ" - }, - "Help": { - "value": "%SystemRoot%\\cursors\\aero_helpsel_xl.cur", - "dataType": "REG_SZ" - }, - "Wait": { - "value": "%SystemRoot%\\cursors\\aero_busy_xl.ani", - "dataType": "REG_SZ" - }, - "Arrow": { - "value": "%SystemRoot%\\cursors\\aero_arrow_xl.cur", - "dataType": "REG_SZ" - }, - "NWPen": { - "value": "%SystemRoot%\\cursors\\aero_pen_xl.cur", - "dataType": "REG_SZ" - }, - "SizeNS": { - "value": "%SystemRoot%\\cursors\\aero_ns_xl.cur", - "dataType": "REG_SZ" - }, - "SizeWE": { - "value": "%SystemRoot%\\cursors\\aero_ew_xl.cur", - "dataType": "REG_SZ" - }, - "SizeAll": { - "value": "%SystemRoot%\\cursors\\aero_move_xl.cur", - "dataType": "REG_SZ" - }, - "UpArrow": { - "value": "%SystemRoot%\\cursors\\aero_up_xl.cur", - "dataType": "REG_SZ" - }, - "SizeNESW": { - "value": "%SystemRoot%\\cursors\\aero_nesw_xl.cur", - "dataType": "REG_SZ" - }, - "SizeNWSE": { - "value": "%SystemRoot%\\cursors\\aero_nwse_xl.cur", - "dataType": "REG_SZ" - }, - "AppStarting": { - "value": "%SystemRoot%\\cursors\\aero_working_xl.ani", - "dataType": "REG_SZ" - } - } - }], - "http://registry.gpii.org/applications/com.microsoft.windows.magnifier": [{ - "value": { - "FollowFocus": { - "value": 0, - "dataType": "REG_DWORD" - }, - "FollowCaret": { - "value": 1, - "dataType": "REG_DWORD" - }, - "FollowMouse": { - "value": 1, - "dataType": "REG_DWORD" - }, - "Invert": { - "value": true, - "dataType": "REG_DWORD" - }, - "Magnification": { - "value": 150, - "dataType": "REG_DWORD" - }, - "MagnificationMode": { - "value": 3, - "dataType": "REG_DWORD" - } - } - }] -} diff --git a/testData/preferences/otis.json5 b/testData/preferences/otis.json5 new file mode 100644 index 000000000..724b81402 --- /dev/null +++ b/testData/preferences/otis.json5 @@ -0,0 +1,17 @@ +// # otis.json5 +// Test user for Office UI customisations. +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.office": { + "word-ribbon": "Basics+StandardSet", + "OverrideTabletMode": 2 + } + } + } + } + } +} diff --git a/testData/preferences/paulina.json b/testData/preferences/paulina.json deleted file mode 100644 index e19f80b8a..000000000 --- a/testData/preferences/paulina.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "http://registry.gpii.org/applications/com.android.talkback": [{ "value": {}}] -} diff --git a/testData/preferences/phil.json5 b/testData/preferences/phil.json5 new file mode 100644 index 000000000..478faa961 --- /dev/null +++ b/testData/preferences/phil.json5 @@ -0,0 +1,23 @@ +// # phil +// +// This preference set enables slow keys. +// +// This setting makes the system ignore brief key presses. +// +// ## Testing +// +// In something like Notepad, quickly press and release a letter key. The key press will not be registered unless the key is held down for a longer time. +{ + "flat": { + "name": "Phil", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/slowKeys/enabled": true, + "http://registry.gpii.net/common/slowKeysInterval": 0.4 + } + } + } + } +} diff --git a/testData/preferences/rachel.json5 b/testData/preferences/rachel.json5 new file mode 100644 index 000000000..cc114c40e --- /dev/null +++ b/testData/preferences/rachel.json5 @@ -0,0 +1,43 @@ +// # Rachel.json5 +// +// This preference set changes several preferences from Read&Write Gold 12 +// +// ## Testing +// +// This changes the following settings from the AT software: +// Screen Masking: +// + Screen Masking mode: Underline typing line +// +// Underline typing line: +// + Underline color: "#a7efce" (Need to visually check the line under the writing cursor) +// + Underline opacity: 0.3 (In the third dot of the slider control) +// + Underline height: 15 (Bigger than 'Default', but smaller than 'Medium') +// +// Screenshot Reader: +// + Capture by: Drawing Freehand +// + Schreenshot to: Window +// + Add to active Microsoft Word document: false +{ + "flat": { + "name": "Rachel", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.texthelp.readWriteGold": { + // Screen Masking + "ApplicationSettings.ScreenMasking.ScreenMask.$t": "UnderlineTypingLine", + // Screen Masking -> Underline typing line + "ApplicationSettings.ScreenMasking.UnderlineTypingLineColour.$t": "#a7efce", + "ApplicationSettings.ScreenMasking.UnderlineTypingLineOpacity.$t": 0.3, + "ApplicationSettings.ScreenMasking.UnderlineTypingLineHeight.$t": 15, + // Screenshot Reader + "ApplicationSettings.Screenshot.CaptureBy.$t": "Drawing Freehand", + "ApplicationSettings.Screenshot.ScreenshotTo.$t": "Window", + "ApplicationSettings.Screenshot.AppendToMsWordDoc.$t": false + } + } + } + } + } +} diff --git a/testData/preferences/randy.json5 b/testData/preferences/randy.json5 new file mode 100644 index 000000000..d4a76f524 --- /dev/null +++ b/testData/preferences/randy.json5 @@ -0,0 +1,18 @@ +{ + "flat": { + "name": "Randy", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/org.freedesktop.xrandr": { + "screen-resolution": [{ + "width": 800, + "height": 600 + }] + } + } + } + } + } +} diff --git a/testData/preferences/rebecca.json5 b/testData/preferences/rebecca.json5 new file mode 100644 index 000000000..552ce927e --- /dev/null +++ b/testData/preferences/rebecca.json5 @@ -0,0 +1,95 @@ +// # Rebecca.json5 +// +// This preference set changes several preferences from Read&Write Gold 12 +// +// ## Testing +// +// This changes the following settings from the AT software: +// Prediction Window Behavior: +// + Display words alpahbetically: true +// + Follow the cursor while typing: true +// + Auto height adjust: true +// + Predict one word ahead: true +// + Insert space after predictions: false +// + Use spelling suggestions for the prediction list: false +// +// Prediction Speech: +// + Speak when mouse pointer hovers over a word: false +// + Speak the word when clicked: true +// + Insert the word when clicked: false +// +// Prediction Learning: +// + Spell check each word before learning: false +// + Automatically learn as I type: false +// +// Scanning: +// + Resolution: 600 DPI +// + Color: GrayScale +// + Hide device settings: false +// +// Device Setup: +// + Using Countdown: true +// + Countdown interval: 10 +// + Using ADF: true +// + Using ADF with Duplex: true +// +// Save Options: +// + Always use save location below: true +// + File path: "C:\\Users\\vagrant\\Download" +// + File name: ScannedDoc +// + Text only PDF Output: true +// + Plain text Word Output: true +// + Treat heading as book title: false +// +// Output Types: +// + Enable ePup: true +// + Enable Web: true +{ + "flat": { + "name": "Rebecca", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.texthelp.readWriteGold": { + // Prediction + "ApplicationSettings.Prediction.DisplayAlphabetic.$t": true, + "ApplicationSettings.Prediction.FollowCursor.$t": true, + "ApplicationSettings.Prediction.AutoHeightAdjust.$t": true, + "ApplicationSettings.Prediction.winPredictOneWordAhead.$t": true, + "ApplicationSettings.Prediction.InsertSpaceAfterPred.$t": false, + "ApplicationSettings.Prediction.UseSpellingForPredictions.$t": false, + // Prediction Speech + "ApplicationSettings.Prediction.SpeakPredictionOnMouseHover.$t": false, + // This two settings are excluyent, need to encode that + "ApplicationSettings.Prediction.SpeakOnWordClick.$t": true, + "ApplicationSettings.Prediction.InsertOnWordClick.$t": false, + // Prediction Learning + "ApplicationSettings.Prediction.SpellCheckBeforeLearn.$t": false, + "ApplicationSettings.Prediction.AutoLearnAsType.$t": false, + // Scanning + "ApplicationSettings.Scanning.dpi.$t": 600, + "ApplicationSettings.Scanning.colormode.$t": "GrayScale", + "ApplicationSettings.Scanning.HideInterface.$t": false, + // Device Setup (Advanced settigns) + "ApplicationSettings.Scanning.Countdown.$t": true, + "ApplicationSettings.Scanning.CountdownInterval.$t": 10, + "ApplicationSettings.Scanning.ADF.$t": true, + "ApplicationSettings.Scanning.Duplex.$t": true, + // Save Options + "ApplicationSettings.Scanning.AutoSave.$t": true, + "ApplicationSettings.Scanning.DefaultFilePath.$t": "C:\\Users\\vagrant\\Download", + "ApplicationSettings.Scanning.DefaultFileName.$t": "ScannedDoc", + // PDF Output + "ApplicationSettings.Scanning.PdfTextOnly.$t": true, + "ApplicationSettings.Scanning.MSWordRetainFormat.$t": true, + "ApplicationSettings.Scanning.CoverFirstPage.$t": false, + // Output Types + "ApplicationSettings.Scanning.EnableEpub.$t": true, + "ApplicationSettings.Scanning.EnableWeb.$t": true + } + } + } + } + } +} diff --git a/testData/preferences/review3_chrome_high_contrast.json5 b/testData/preferences/review3_chrome_high_contrast.json5 new file mode 100644 index 000000000..4caeefab0 --- /dev/null +++ b/testData/preferences/review3_chrome_high_contrast.json5 @@ -0,0 +1,19 @@ +{ + "flat": { + "name": "Review 3 Chrome High Contrast", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/screenReaderTTS/enabled": false, + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1, + "http://registry.gpii.net/common/highContrast/enabled": true, + "http://registry.gpii.net/common/highContrastTheme": "white-black", + "http://registry.gpii.net/common/invertColours": false + } + } + } + } +} diff --git a/testData/preferences/review3_ma1.json5 b/testData/preferences/review3_ma1.json5 new file mode 100644 index 000000000..735d64cc3 --- /dev/null +++ b/testData/preferences/review3_ma1.json5 @@ -0,0 +1,18 @@ +{ + "flat": { + "name": "Review 3 M.A. 1", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/speechRate": 200, + "http://registry.gpii.net/common/pitch": 0.5, + "http://registry.gpii.net/common/punctuationVerbosity": "all", + "http://registry.gpii.net/common/announceCapitals": true, + "http://registry.gpii.net/common/keyEcho": true, + "http://registry.gpii.net/common/wordEcho": true + } + } + } + } +} diff --git a/testData/preferences/review3_ma2.json5 b/testData/preferences/review3_ma2.json5 new file mode 100644 index 000000000..f82fb7d76 --- /dev/null +++ b/testData/preferences/review3_ma2.json5 @@ -0,0 +1,18 @@ +{ + "flat": { + "name": "Review 3 M.A. 1", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/speechRate": 400, + "http://registry.gpii.net/common/pitch": 0.6, + "http://registry.gpii.net/common/punctuationVerbosity": "all", + "http://registry.gpii.net/common/announceCapitals": false, + "http://registry.gpii.net/common/keyEcho": true, + "http://registry.gpii.net/common/wordEcho": false + } + } + } + } +} diff --git a/testData/preferences/review3_user_1.json5 b/testData/preferences/review3_user_1.json5 new file mode 100644 index 000000000..8cb7aa2e6 --- /dev/null +++ b/testData/preferences/review3_user_1.json5 @@ -0,0 +1,16 @@ +{ + "flat": { + "name": "Review 3 User 1", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/magnification": 1, + "http://registry.gpii.net/common/invertColours": false, + "http://registry.gpii.net/common/tableOfContents": false + } + } + } + } +} diff --git a/testData/preferences/review3_user_2.json5 b/testData/preferences/review3_user_2.json5 new file mode 100644 index 000000000..a5a90afac --- /dev/null +++ b/testData/preferences/review3_user_2.json5 @@ -0,0 +1,16 @@ +{ + "flat": { + "name": "Review 3 User 2", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/magnification": 1, + "http://registry.gpii.net/common/invertColours": false, + "http://registry.gpii.net/common/tableOfContents": false + } + } + } + } +} diff --git a/testData/preferences/review3_user_3.json5 b/testData/preferences/review3_user_3.json5 new file mode 100644 index 000000000..1c82587db --- /dev/null +++ b/testData/preferences/review3_user_3.json5 @@ -0,0 +1,16 @@ +{ + "flat": { + "name": "Review 3 User 3", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/magnification": 1, + "http://registry.gpii.net/common/invertColours": false, + "http://registry.gpii.net/common/tableOfContents": false + } + } + } + } +} diff --git a/testData/preferences/review3_user_4.json5 b/testData/preferences/review3_user_4.json5 new file mode 100644 index 000000000..f4d93afa6 --- /dev/null +++ b/testData/preferences/review3_user_4.json5 @@ -0,0 +1,16 @@ +{ + "flat": { + "name": "Review 3 User 4", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/magnification": 1, + "http://registry.gpii.net/common/invertColours": false, + "http://registry.gpii.net/common/tableOfContents": false + } + } + } + } +} diff --git a/testData/preferences/roger.json b/testData/preferences/roger.json deleted file mode 100644 index 43e4e9c2a..000000000 --- a/testData/preferences/roger.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "http://registry.gpii.org/common/cursorSize": [{"value": "extralarge"}], - "http://registry.gpii.org/common/fontSize": [{"value": 20}], - "http://registry.gpii.org/common/mouseTrailing": [{"value": 16}], - - "http://registry.gpii.org/common/magnification": [{"value": 2.0}], - "http://registry.gpii.org/common/tracking": [{"value": ["focus"]}], - "http://registry.gpii.org/common/invertImages": [{"value": false}] -} diff --git a/testData/preferences/roger.json5 b/testData/preferences/roger.json5 new file mode 100644 index 000000000..5beabe7e9 --- /dev/null +++ b/testData/preferences/roger.json5 @@ -0,0 +1,19 @@ +{ + "flat": { + "name": "Roger", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/cursorSize": 0.9, + "http://registry.gpii.net/common/fontSize": 20, + "http://registry.gpii.net/common/mouseTrailing": 9, + + "http://registry.gpii.net/common/magnification": 2.0, + "http://registry.gpii.net/common/tracking": ["focus"], + "http://registry.gpii.net/common/invertColours": false + } + } + } + } +} diff --git a/testData/preferences/rose.json5 b/testData/preferences/rose.json5 new file mode 100644 index 000000000..87ff759ca --- /dev/null +++ b/testData/preferences/rose.json5 @@ -0,0 +1,60 @@ +// # Rose.json5 +// +// This preference set changes several preferences from Read&Write Gold 12 +// +// ## Testing +// +// This changes the following settings from the AT software: +// Check It: +// + Order suggestions by context: false +// + Speak when mouse pointer hovers over a word: false +// + Classic Spell Check: true +// + Allow right click Read&Write spell check in Microsoft Word: true +// +// Dictionary: +// + Web Dictionary: Bing +// + Pop-up Dictionary: true +// + Toogle images: true +// +// Hightlights: +// + Colors to collect: +// - Yellow: false +// - Blue: true +// - Green: false +// - Pink: false +// + Order highlights by: "Position" +// + Separate colors by: "Page" +// + Separate highlights by: "Tab" +// + Collect from multiple documents: false +{ + "flat": { + "name": "Rose", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.texthelp.readWriteGold": { + // Check It + "ApplicationSettings.Spelling.OrderByContext.$t": false, + "ApplicationSettings.Spelling.SpeakSpellingOnHover.$t": false, + "ApplicationSettings.Spelling.ShowCheckIt.$t": true, + "ApplicationSettings.Spelling.RightClickMSWord.$t": true, + // Dictionary + "ApplicationSettings.Dictionary.WebDictionary.$t": "Bing", + "ApplicationSettings.Dictionary.PopupDictionary.$t": true, + "ApplicationSettings.Dictionary.ToggleImages.$t": true, + // Highlights + "ApplicationSettings.StudySkills.CollectYellowHighlight.$t": false, + "ApplicationSettings.StudySkills.CollectBlueHighlight.$t": true, + "ApplicationSettings.StudySkills.CollectGreenHighlight.$t": false, + "ApplicationSettings.StudySkills.CollectPinkHighlight.$t": false, + "ApplicationSettings.StudySkills.CollectOrder.$t": "Position", + "ApplicationSettings.StudySkills.ColorSeparator.$t": "Page", + "ApplicationSettings.StudySkills.HighlightSeparator.$t": "Tab", + "ApplicationSettings.StudySkills.MultiDocHighlighting.$t": false + } + } + } + } + } +} diff --git a/testData/preferences/ryan.json5 b/testData/preferences/ryan.json5 new file mode 100644 index 000000000..8d4e88281 --- /dev/null +++ b/testData/preferences/ryan.json5 @@ -0,0 +1,81 @@ +// # Ryan.json5 +// +// This preference set changes several preferences from Read&Write Gold 12 +// +// ## Testing +// +// This changes the following settings from the AT software: +// Speech: +// + Voice: Mexican Spanish Paulina - Vocalizer +// + Voice Speed: 70 +// + Use arrow keys to change speed: false +// + Word Pause: 0.2 +// + Voice Pitch: 40 +// +// Auto read: +// + Speak as I type: true +// + Speak on each letter: true +// + Speak on each word: false +// + Speak on each sentence: true +// +// Screen reading: +// + Read by: Words +// + Continuous reading: false +// + Read the web: true +// +// Speech highlighting: +// + Speech highlighting method: Speak using one word display +// + Font: Times New Roman +// + Font size: 16 +// + Highlighting colors: Bright green on violet +{ + "flat": { + "name": "Ryan", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.texthelp.readWriteGold": { + // Speech + "ApplicationSettings.Speech.CurrentVoice.$t": "Mexican Spanish Paulina - Vocalizer", + "ApplicationSettings.Speech.VoiceSpeed.$t": 70, + "ApplicationSettings.Speech.UseVoiceSpeedHotKey.$t": false, + "ApplicationSettings.Speech.WordPause.$t": 200, + "ApplicationSettings.Speech.VoicePitch.$t": 40, + // Speech -> Auto Read + "ApplicationSettings.Speech.SpeakAsIType.$t": true, + // Speech -> Auto Read -> Speak as I type + "ApplicationSettings.Speech.SpeakOnEachLetter.$t": true, + // Speech -> Auto Read -> Speak as I type + "ApplicationSettings.Speech.SpeakOnEachWord.$t": false, + // Speech -> Auto Read -> Speak as I type + "ApplicationSettings.Speech.SpeakOnEachSentence.$t": false, + // Speech -> Screen reading + "ApplicationSettings.Speech.ScreenReadingEnabled.$t": true, + // Speech -> Screen reading -> Items to read + // TODO: Setting is stored in an array format in XML, we need to check how + // XMLSettingsHandler handles this case. + // "ApplicationSettings.Speech.ScreenReadingItems.$t":, + // Speech -> Read by + "ApplicationSettings.Speech.SelectionType.$t": 0, + // Speech -> Continuous reading + "ApplicationSettings.Speech.ContinuousReading.$t": false, + // Speech -> Read the web + "ApplicationSettings.Speech.ReadTheWeb.$t": true, + // Speech -> Speech highlighting + "ApplicationSettings.Speech.HighlightingMethod.$t": 2, + // Speech -> Speech highlighting -> Font + "ApplicationSettings.Speech.TextReaderOneWordFontName.$t": "Times New Roman", + "ApplicationSettings.Speech.optSpeechHighTRFontName.$t": "Times New Roman", + // Speech -> Speech highlighting -> Font size + "ApplicationSettings.Speech.TextReaderOneWordFontSize.$t": 16, + "ApplicationSettings.Speech.optSpeechHighTRFontSize.$t": 16, + // Speech -> Speech highlighting -> Highlighting colors + "ApplicationSettings.Speech.HighlightingBackColour.$t": "Violet", + "ApplicationSettings.Speech.HighlightingForeColour.$t": "Chartreuse" + } + } + } + } + } +} diff --git a/testData/preferences/salem.json5 b/testData/preferences/salem.json5 new file mode 100644 index 000000000..bee43c33d --- /dev/null +++ b/testData/preferences/salem.json5 @@ -0,0 +1,25 @@ +// # salem +// +// This preference set changes the screen scaling (DPI). +// +// ## Testing +// +// The screen scale should be set to 125% (or, 1 value above the display's recommended setting). This is under the +// "Change the size of text, apps, and other items" setting. +// +// If the resolution is lower than 1024x768 then it should do nothing. +{ + "flat": { + "name": "Salem", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.screenDPI": { + "screen-dpi": 1 + } + } + } + } + } +} diff --git a/testData/preferences/sally.json5 b/testData/preferences/sally.json5 new file mode 100644 index 000000000..94fe69b70 --- /dev/null +++ b/testData/preferences/sally.json5 @@ -0,0 +1,27 @@ +// # Sally.json5 +// +// This preference set changes the SoundSentry feature from Windows 10. Changing the visual +// notification for sounds. +// +// ## Testing +// +// The setting this preference set changes is located in: +// + Ease Of Access -> Audio -> Show audio alerts visually. +// and it's changed to a value of: +// + "Flash the active window". +// +{ + "flat": { + "name": "sally", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.soundSentry": { + "WindowsEffect": "2" + } + } + } + } + } +} diff --git a/testData/preferences/sammy.json b/testData/preferences/sammy.json deleted file mode 100644 index 1898b57d1..000000000 --- a/testData/preferences/sammy.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "http://registry.gpii.org/common/fontSize": [{ "value": 24 }], - "http://registry.gpii.org/common/foregroundColor": [{"value": "white"}], - "http://registry.gpii.org/common/backgroundColor": [{"value": "black"}], - "http://registry.gpii.org/common/fontFaceFontName": [{"value": ["Comic Sans"]}], - "http://registry.gpii.org/common/fontFaceGenericFontFace": [{"value": "sans serif"}], - "http://registry.gpii.org/common/magnification": [{"value": 2.0}], - "http://registry.gpii.org/common/tracking": [{"value": ["mouse"]}], - "http://registry.gpii.org/common/invertImages": [{"value": true}] -} \ No newline at end of file diff --git a/testData/preferences/sammy.json5 b/testData/preferences/sammy.json5 new file mode 100644 index 000000000..bd19bde6c --- /dev/null +++ b/testData/preferences/sammy.json5 @@ -0,0 +1,16 @@ +{ + "flat": { + "name": "Sammy", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 24, + "http://registry.gpii.net/common/magnification": 2.0, + "http://registry.gpii.net/common/tracking": ["mouse"], + "http://registry.gpii.net/common/invertColours": true + } + } + } + } +} diff --git a/testData/preferences/samuel.json5 b/testData/preferences/samuel.json5 new file mode 100644 index 000000000..d1c40366f --- /dev/null +++ b/testData/preferences/samuel.json5 @@ -0,0 +1,79 @@ +// # Samuel.json5 +// +// This preference set changes several preferences from ZoomText 2019 +// +// ## Testing +// +// This changes the following settings from the AT software: +// +// - Voice Language: Spanish +// - Synthesizer: Vocalizer Expressive +// - Voice Settings: +// + Rate: 120 WPM. +// + Volume: 70%. +// + Pitch: 35%. +// +// - Color Scheme: Black and White +// +// - Keyboard Echo: Words +// - Mouse Echo: +// + Hover: Enabled +// + Echo the whole line: Enabled +// +// - Pointer Scheme: Giant Green +// - Zoom Level: 1.4 +// - Navigation Settings - Mouse - "Router Pointer into view when it is moved": Enabled. +// - Navigation Settings - Mouse - "Route pointer over the active control": Enabled +// - Navigation Settings - Mouse - "Hold Shit key to move only horizontally or vertically": Enabled +// - Navigation Settings - Mouse - "Hold Ctrl key to stay within the active window": Enabled +{ + "flat": { + "name": "Samuel", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.aisquared.zoomtext": { + "ZtSpeech.SapiProductName": "VocalizerExpressive-Spanish", + "ZtSpeech.Language": 10, + + "ColorScheme.ColorSchEnable": 1, + "ColorScheme.ColorSchIndex": 5, + + // [VocalizerExpressive-Spanish Speech - Screen Reader] + // ================================================================== + "VocalizerExpressive-Spanish Speech - Screen Reader.Valid": 1, + "VocalizerExpressive-Spanish Speech - Screen Reader.SettingsValid": 1, + "VocalizerExpressive-Spanish Speech - Screen Reader.Muted": 0, + "VocalizerExpressive-Spanish Speech - Screen Reader.Voice": 0, + "VocalizerExpressive-Spanish Speech - Screen Reader.Rate": 120, + "VocalizerExpressive-Spanish Speech - Screen Reader.Volume": 85, + "VocalizerExpressive-Spanish Speech - Screen Reader.Pitch": 40, + + // [VocalizerExpressive-Spanish Speech - DocReader] + "VocalizerExpressive-Spanish Speech - DocReader.Valid": 1, + "VocalizerExpressive-Spanish Speech - DocReader.SettingsValid": 1, + "VocalizerExpressive-Spanish Speech - DocReader.Muted": 0, + "VocalizerExpressive-Spanish Speech - DocReader.Voice": 0, + "VocalizerExpressive-Spanish Speech - DocReader.Rate": 120, + "VocalizerExpressive-Spanish Speech - DocReader.Volume": 85, + "VocalizerExpressive-Spanish Speech - DocReader.Pitch": 40, + + "EchoTyping.Mode": 32770, + "EchoMouse.HoverMode": 1, + "EchoMouse.LineMode": 1, + "EchoMouse.Enabled": 1, + + "PointerScheme.PtrEnhEnable": 1, + "PointerScheme.PtrEnhIndex": 6, + + "PRIMARY.magPower": 225, + "STATIC 1.magPower": 225, + + "MagEngine.MouseConstraint": 3 + } + } + } + } + } +} diff --git a/testData/preferences/screenreader_common.json b/testData/preferences/screenreader_common.json deleted file mode 100644 index a5643ac13..000000000 --- a/testData/preferences/screenreader_common.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "http://registry.gpii.org/common/speakTutorialMessages": [{ "value": false }], - "http://registry.gpii.org/common/keyEcho": [{ "value": false }], - "http://registry.gpii.org/common/wordEcho": [{ "value": true }], - "http://registry.gpii.org/common/announceCapitals": [{ "value": true }], - "http://registry.gpii.org/common/readingUnit": [{ "value": "word" }], - "http://registry.gpii.org/common/punctuationVerbosity": [{ "value": "all" }], - "http://registry.gpii.org/common/screenReaderTTSEnabled": [{ "value": true }], - "http://registry.gpii.org/common/trackingTTS": [{ "value": ["mouse", "caret"] }], - "http://registry.gpii.org/common/speechRate": [{ "value": 400 }], - "http://registry.gpii.org/common/auditoryOutLanguage": [{ "value": "en-BS" }] -} diff --git a/testData/preferences/screenreader_nvda.json b/testData/preferences/screenreader_nvda.json deleted file mode 100644 index f6bb48ada..000000000 --- a/testData/preferences/screenreader_nvda.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "http://registry.gpii.org/applications/org.nvda-project": [{ - "value": { - "speech.synth": "espeak", - "speech.outputDevice": "Microsoft Sound Mapper", - "speech.symbolLevel": "300", - "speech.espeak.rate": 17.20430107526882, - "speech.espeak.voice": "en\\en-wi", - "reviewCursor.followFocus": false, - "reviewCursor.followCaret": true, - "reviewCursor.followMouse": true, - "keyboard.speakTypedWords": true, - "speech.espeak.rateBoost": true, - "keyboard.speakTypedCharacters": false, - "presentation.reportHelpBalloons": false, - "speech.espeak.sayCapForCapitals": true, - "virtualBuffers.autoSayAllOnPageLoad": false - } - }] -} diff --git a/testData/preferences/screenreader_orca.json b/testData/preferences/screenreader_orca.json deleted file mode 100644 index 1b5fa944a..000000000 --- a/testData/preferences/screenreader_orca.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "http://registry.gpii.org/applications/org.gnome.orca": [{ - "value": { - "sayAllStyle": 1, - "enableEchoByWord": true, - "enableEchoByCharacter": false, - "voices.default.rate": 102.27272727272727, - "enableTutorialMessages": false, - "voices.default.family": { - "locale": "en", - "name": "en-westindies" - }, - "verbalizePunctuationStyle": 0 - } - }] -} diff --git a/testData/preferences/simon.json5 b/testData/preferences/simon.json5 new file mode 100644 index 000000000..3cdd65aa6 --- /dev/null +++ b/testData/preferences/simon.json5 @@ -0,0 +1,26 @@ +// # Simon.json5 +// +// This preference set changes touchpad sensitivity to low. +// This way it's more difficult to trigger mouse accidentally while typing. +// +// This setting makes the system ignore brief key presses. +// +// ## Testing +// +// In any text writing app, this should decrease the probability mouse being moved by accidentally touching touchpad. +// +{ + "flat": { + "name": "Simon", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.touchPadSettings": { + "Sensitivity": "Low sensitivity" + } + } + } + } + } +} diff --git a/testData/preferences/slater.json5 b/testData/preferences/slater.json5 new file mode 100644 index 000000000..5630c93d5 --- /dev/null +++ b/testData/preferences/slater.json5 @@ -0,0 +1,18 @@ +{ + "flat": { + "name": "Slater", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/screenReaderTTS/enabled": true, + "http://registry.gpii.net/common/speechRate": 140, + "http://registry.gpii.net/common/trackingTTS": ["focus"], + "http://registry.gpii.net/common/keyEcho": true, + "http://registry.gpii.net/common/wordEcho": true, + "http://registry.gpii.net/common/pitch": 0.6 + } + } + } + } +} diff --git a/testData/preferences/snapset_1a.json5 b/testData/preferences/snapset_1a.json5 new file mode 100644 index 000000000..f709d2194 --- /dev/null +++ b/testData/preferences/snapset_1a.json5 @@ -0,0 +1,23 @@ +// # Snapsets for APCP pilots +// +// ## Description +// +// These snapsets have been created in order to test the GPII in the APCP pilots. +// A full description of them can be found here: https://docs.google.com/document/d/1B3uIonWJNqbzoMgboVc7b-T3UJYuikB2FKJmIIp4DYE/edit# +// In addition to the specifications from the original document: +// +// * **snapset_1a**: Win10 Display scaling (125%) and Win10 Mouse cursor size (1.0) +{ + "flat": { + "name": "Larger 125%", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/DPIScale": 1, + "http://registry.gpii.net/common/cursorSize": 1.0 + } + } + } + } +} diff --git a/testData/preferences/snapset_1b.json5 b/testData/preferences/snapset_1b.json5 new file mode 100644 index 000000000..476f0ccc3 --- /dev/null +++ b/testData/preferences/snapset_1b.json5 @@ -0,0 +1,23 @@ +// # Snapsets for APCP pilots +// +// ## Description +// +// These snapsets have been created in order to test the GPII in the APCP pilots. +// A full description of them can be found here: https://docs.google.com/document/d/1B3uIonWJNqbzoMgboVc7b-T3UJYuikB2FKJmIIp4DYE/edit# +// In addition to the specifications from the original document: +// +// * **snapset_1b**: Win10 Display scaling (150%) and Win10 Mouse cursor size (1.0) +{ + "flat": { + "name": "Larger 150%", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/DPIScale": 2, + "http://registry.gpii.net/common/cursorSize": 1.0 + } + } + } + } +} diff --git a/testData/preferences/snapset_1c.json5 b/testData/preferences/snapset_1c.json5 new file mode 100644 index 000000000..c9e0d5d9c --- /dev/null +++ b/testData/preferences/snapset_1c.json5 @@ -0,0 +1,23 @@ +// # Snapsets for APCP pilots +// +// ## Description +// +// These snapsets have been created in order to test the GPII in the APCP pilots. +// A full description of them can be found here: https://docs.google.com/document/d/1B3uIonWJNqbzoMgboVc7b-T3UJYuikB2FKJmIIp4DYE/edit# +// In addition to the specifications from the original document: +// +// * **snapset_1c**: Win10 Display scaling (175%) and Win10 Mouse cursor size (1.0) +{ + "flat": { + "name": "Larger 175%", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/DPIScale": 3, + "http://registry.gpii.net/common/cursorSize": 1.0 + } + } + } + } +} diff --git a/testData/preferences/snapset_2a.json5 b/testData/preferences/snapset_2a.json5 new file mode 100644 index 000000000..23a6fb522 --- /dev/null +++ b/testData/preferences/snapset_2a.json5 @@ -0,0 +1,24 @@ +// # Snapsets for APCP pilots +// +// ## Description +// +// These snapsets have been created in order to test the GPII in the APCP pilots. +// A full description of them can be found here: https://docs.google.com/document/d/1B3uIonWJNqbzoMgboVc7b-T3UJYuikB2FKJmIIp4DYE/edit# +// In addition to the specifications from the original document: +// +// * **snapset_2a**: Win10 Display scaling (125%), Win10 High Contrast Theme white-black Win10 Mouse cursor size (1.0) and Chrome extensions high contrast (Enabled) +{ + "flat": { + "name": "Dark & Larger 125%", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/cursorSize": 1.0, + "http://registry.gpii.net/common/DPIScale": 1, + "http://registry.gpii.net/common/highContrastTheme": "white-black" + } + } + } + } +} diff --git a/testData/preferences/snapset_2b.json5 b/testData/preferences/snapset_2b.json5 new file mode 100644 index 000000000..fcf3d8c72 --- /dev/null +++ b/testData/preferences/snapset_2b.json5 @@ -0,0 +1,24 @@ +// # Snapsets for APCP pilots +// +// ## Description +// +// These snapsets have been created in order to test the GPII in the APCP pilots. +// A full description of them can be found here: https://docs.google.com/document/d/1B3uIonWJNqbzoMgboVc7b-T3UJYuikB2FKJmIIp4DYE/edit# +// In addition to the specifications from the original document: +// +// * **snapset_2b**: Win10 Display scaling (150%), Win10 High Contrast Theme white-black, Win10 Mouse cursor size (1.0) and Chrome extensions high contrast (Enabled) +{ + "flat": { + "name": "Dark & Larger 150%", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/cursorSize": 1.0, + "http://registry.gpii.net/common/DPIScale": 2, + "http://registry.gpii.net/common/highContrastTheme": "white-black" + } + } + } + } +} diff --git a/testData/preferences/snapset_2c.json5 b/testData/preferences/snapset_2c.json5 new file mode 100644 index 000000000..128d5a3ec --- /dev/null +++ b/testData/preferences/snapset_2c.json5 @@ -0,0 +1,24 @@ +// # Snapsets for APCP pilots +// +// ## Description +// +// These snapsets have been created in order to test the GPII in the APCP pilots. +// A full description of them can be found here: https://docs.google.com/document/d/1B3uIonWJNqbzoMgboVc7b-T3UJYuikB2FKJmIIp4DYE/edit# +// In addition to the specifications from the original document: +// +// * **snapset_2c**: Win10 Display scaling (175%), Win10 High Contrast Theme white-black, Win10 Mouse cursor size (1.0) and Chrome extensions high contrast (Enabled) +{ + "flat": { + "name": "Dark & Larger 175%", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/cursorSize": 1.0, + "http://registry.gpii.net/common/DPIScale": 3, + "http://registry.gpii.net/common/highContrastTheme": "white-black" + } + } + } + } +} diff --git a/testData/preferences/snapset_3.json5 b/testData/preferences/snapset_3.json5 new file mode 100644 index 000000000..1115879e5 --- /dev/null +++ b/testData/preferences/snapset_3.json5 @@ -0,0 +1,24 @@ +// # Snapsets for APCP pilots +// +// ## Description +// +// These snapsets have been created in order to test the GPII in the APCP pilots. +// A full description of them can be found here: https://docs.google.com/document/d/1B3uIonWJNqbzoMgboVc7b-T3UJYuikB2FKJmIIp4DYE/edit# +// In addition to the specifications from the original document: +// +// * **snapset_3**: Win10 Display scaling (150%), Win10 Mouse cursor size (1.0) and Chrome extensions selfVoicing (Enabled) +{ + "flat": { + "name": "Read To Me", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/DPIScale": 2, + "http://registry.gpii.net/common/cursorSize": 1.0, + "http://registry.gpii.net/common/selfVoicing/enabled": true + } + } + } + } +} diff --git a/testData/preferences/snapset_4a.json5 b/testData/preferences/snapset_4a.json5 new file mode 100644 index 000000000..2fee9d3f6 --- /dev/null +++ b/testData/preferences/snapset_4a.json5 @@ -0,0 +1,23 @@ +// # Snapsets for APCP pilots +// +// ## Description +// +// These snapsets have been created in order to test the GPII in the APCP pilots. +// A full description of them can be found here: https://docs.google.com/document/d/1B3uIonWJNqbzoMgboVc7b-T3UJYuikB2FKJmIIp4DYE/edit# +// In addition to the specifications from the original document: +// +// * **snapset_4a**: Win10 Magnifier Lens view (200%) +{ + "flat": { + "name": "Magnifier 200%", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/magnification": 2, + "http://registry.gpii.net/common/magnification/enabled": false + } + } + } + } +} diff --git a/testData/preferences/snapset_4b.json5 b/testData/preferences/snapset_4b.json5 new file mode 100644 index 000000000..e3b36141d --- /dev/null +++ b/testData/preferences/snapset_4b.json5 @@ -0,0 +1,23 @@ +// # Snapsets for APCP pilots +// +// ## Description +// +// These snapsets have been created in order to test the GPII in the APCP pilots. +// A full description of them can be found here: https://docs.google.com/document/d/1B3uIonWJNqbzoMgboVc7b-T3UJYuikB2FKJmIIp4DYE/edit# +// In addition to the specifications from the original document: +// +// * **snapset_4b**: Win10 Magnifier Lens view (400%) +{ + "flat": { + "name": "Magnifier 400%", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/magnification": 4, + "http://registry.gpii.net/common/magnifierPosition": "Lens" + } + } + } + } +} diff --git a/testData/preferences/snapset_4c.json5 b/testData/preferences/snapset_4c.json5 new file mode 100644 index 000000000..f6cfd73e9 --- /dev/null +++ b/testData/preferences/snapset_4c.json5 @@ -0,0 +1,24 @@ +// # Snapsets for APCP pilots +// +// ## Description +// +// These snapsets have been created in order to test the GPII in the APCP pilots. +// A full description of them can be found here: https://docs.google.com/document/d/1B3uIonWJNqbzoMgboVc7b-T3UJYuikB2FKJmIIp4DYE/edit# +// In addition to the specifications from the original document: +// +// * **snapset_4c**: Win10 Magnifier Lens view (200%), Win10 Display scaling (175%) +{ + "flat": { + "name": "Large + Magnifier 200%", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/magnification": 2, + "http://registry.gpii.net/common/magnifierPosition": "Lens", + "http://registry.gpii.net/common/DPIScale": 3 + } + } + } + } +} diff --git a/testData/preferences/snapset_4d.json5 b/testData/preferences/snapset_4d.json5 new file mode 100644 index 000000000..81f68489c --- /dev/null +++ b/testData/preferences/snapset_4d.json5 @@ -0,0 +1,24 @@ +// # Snapsets for APCP pilots +// +// ## Description +// +// These snapsets have been created in order to test the GPII in the APCP pilots. +// A full description of them can be found here: https://docs.google.com/document/d/1B3uIonWJNqbzoMgboVc7b-T3UJYuikB2FKJmIIp4DYE/edit# +// In addition to the specifications from the original document: +// +// * **snapset_4d**: Win10 Magnifier Lens view (200%), Win10 High Contrast Theme white-black, and Chrome extensions high contrast (Enabled) +{ + "flat": { + "name": "Magnifier Dark 200%", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/magnification": 2, + "http://registry.gpii.net/common/magnifierPosition": "Lens", + "http://registry.gpii.net/common/highContrastTheme": "white-black" + } + } + } + } +} diff --git a/testData/preferences/snapset_5.json5 b/testData/preferences/snapset_5.json5 new file mode 100644 index 000000000..9d44c71b8 --- /dev/null +++ b/testData/preferences/snapset_5.json5 @@ -0,0 +1,15 @@ +{ + "flat": { + "name": "Voice Control with Increased Size", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/DPIScale": 2, + "http://registry.gpii.net/common/cursorSize": 1.0, + "http://registry.gpii.net/common/speechControl": true + } + } + } + } +} diff --git a/testData/preferences/talkback1.json5 b/testData/preferences/talkback1.json5 new file mode 100644 index 000000000..74e29366a --- /dev/null +++ b/testData/preferences/talkback1.json5 @@ -0,0 +1,20 @@ +{ + "flat": { + "name": "TallBack1", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.android.talkback": {}, + "http://registry.gpii.net/applications/com.android.audioManager": { + "STREAM_MUSIC": 14.5 + }, + "http://registry.gpii.net/applications/com.android.settings.secure": { + "tts_default_rate": 450.99, + "tts_default_pitch": 450.99 + } + } + } + } + } +} diff --git a/testData/preferences/talkback2.json5 b/testData/preferences/talkback2.json5 new file mode 100644 index 000000000..b1de7b981 --- /dev/null +++ b/testData/preferences/talkback2.json5 @@ -0,0 +1,16 @@ +{ + "flat": { + "name": "TallBack2", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.android.talkback/enabled": true, + "http://registry.gpii.net/common/pitch": 1, + "http://registry.gpii.net/common/speechRate": 340, + "http://registry.gpii.net/common/volumeTTS": 0.5 + } + } + } + } +} diff --git a/testData/preferences/testUser1.json5 b/testData/preferences/testUser1.json5 new file mode 100644 index 000000000..e32ee7010 --- /dev/null +++ b/testData/preferences/testUser1.json5 @@ -0,0 +1,18 @@ +{ + "flat": { + "name": "Test User 1", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry\\.gpii\\.net/common/setting1": 12 , + "http://registry\\.gpii\\.net/common/setting2": "white", + "http://registry\\.gpii\\.net/common/setting3": "black", + "http://registry\\.gpii\\.net/common/setting4": ["Comic Sans"], + "http://registry\\.gpii\\.net/common/setting5": "sans serif", + "http://registry\\.gpii\\.net/common/setting6": false + } + } + } + } +} diff --git a/testData/preferences/timothy.json b/testData/preferences/timothy.json deleted file mode 100644 index 89976f1d5..000000000 --- a/testData/preferences/timothy.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "http://registry.gpii.org/applications/com.texthelp.readWriteGold": [{ - "value": { - "ApplicationSettings.AppBar.Width.$t": 788, - "ApplicationSettings.AppBar.Width.$t": 1072, - "ApplicationSettings.AppBar.ShowText.$t": true, - "ApplicationSettings.AppBar.optToolbarShowText.$t": true, - "ApplicationSettings.AppBar.LargeIcons.$t": true, - "ApplicationSettings.AppBar.optToolbarLargeIcons.$t": true, - "ApplicationSettings.Speech.optSAPI5Speed.$t": 50, - "ApplicationSettings.Speech.optAutoUseScreenReading.$t": false - } - }], - "http://registry.gpii.org/applications/fluid.uiOptions.windows": [{ "value": {}}], - "http://registry.gpii.org/applications/fluid.uiOptions.linux": [{ "value": {}}], - "http://registry.gpii.org/applications/fluid.uiOptions": [{ - "value": { - "lineSpacing": "1.5", - "links": true, - "inputsLarger": true, - "captions": false, - "transcripts": false, - "volume": "50" - } - }], - "http://registry.gpii.org/applications/org.gnome.desktop.interface": [{ - "value": { - "text-scaling-factor": 1.6 - } - }], - "http://registry.gpii.org/applications/org.gnome.nautilus": [{ - "value": { - "font": "Cantarell 18" - } - }], - "http://registry.gpii.org/common/fontSize": [{ "value": 18 }], - "http://registry.gpii.org/common/foregroundColor": [{ "value": "white" }], - "http://registry.gpii.org/common/backgroundColor": [{ "value": "black" }], - "http://registry.gpii.org/common/fontFaceFontName": [{ "value": ["Comic Sans"] }], - "http://registry.gpii.org/common/fontFaceGenericFontFace": [{ "value": "sans serif" }], - "http://registry.gpii.org/common/adaptationPreference": [{ "value": [ - { - "adaptationType": "caption", - "language": "en" - }, - {} - ]}], - "http://registry.gpii.org/common/tableOfContents": [{ "value": false }] -} diff --git a/testData/preferences/timothy.json5 b/testData/preferences/timothy.json5 new file mode 100644 index 000000000..b0e1e0ceb --- /dev/null +++ b/testData/preferences/timothy.json5 @@ -0,0 +1,20 @@ +{ + "flat": { + "name": "Timothy", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/org.gnome.desktop.interface": { + "text-scaling-factor": 1.6 + }, + "http://registry.gpii.net/applications/org.gnome.nautilus": { + "font": "Cantarell 18" + }, + "http://registry.gpii.net/common/fontSize": 18, + "http://registry.gpii.net/common/tableOfContents": false + } + } + } + } +} diff --git a/testData/preferences/tom.json5 b/testData/preferences/tom.json5 new file mode 100644 index 000000000..2149305dd --- /dev/null +++ b/testData/preferences/tom.json5 @@ -0,0 +1,16 @@ +//# tom +// +//This preference set sets the OS language to English (US), en-US. +{ + "flat": { + "name": "Tom", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/language": "en-US" + } + } + } + } +} diff --git a/testData/preferences/tony.json5 b/testData/preferences/tony.json5 new file mode 100644 index 000000000..dd826250a --- /dev/null +++ b/testData/preferences/tony.json5 @@ -0,0 +1,30 @@ +// tony.json5 +// +// Settings: +// - shortcutWarningMessage: Activate Windows 10 warning message when enabling settings through shortcuts. +// - shortcutWarningSounds: Activate Windows 10 warning sounds when changing settings through shortcuts. +// - toggleKeys: Activates Windows 10 toggle keys feature. +// - underlineMenuShortcuts: Enable underlining for menu shortcuts and sets keyboard as preferred input method. +{ + "flat": { + "name": "Tony", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/toggleKeys": true, + "http://registry.gpii.net/applications/com.microsoft.windows.underlineMenuShortcuts": { + "Enabled": true, + "KeyboardPrefEnabled": true + }, + "http://registry.gpii.net/applications/com.microsoft.windows.shortcutWarningMessage": { + "Enabled": true + }, + "http://registry.gpii.net/applications/com.microsoft.windows.shortcutWarningSound": { + "Sound on Activation": 1 + } + } + } + } + } +} diff --git a/testData/preferences/uioPlusCommon.json5 b/testData/preferences/uioPlusCommon.json5 new file mode 100644 index 000000000..8151ca4a0 --- /dev/null +++ b/testData/preferences/uioPlusCommon.json5 @@ -0,0 +1,43 @@ +// # uioPlusCommon +// +// A preference set for testing preference settings in UIO+. +// +// Attempts to set all of the UIO+ supported preferences. +// +// ## Preferences +// +// * Character Space: increased (0.2) +// * Font Size: doubled (2) +// * High Contrast: Enabled +// * High Contrast Theme: white-black +// * Highlight Color: green (text selection is highlighted in green) +// * Inputs Larger: enabled (form controls increase in size) +// * Line Space: doubled (2) +// * Self Voicing: enabled +// * Simplified UI: enabled +// * Table of Contents: enabled +// * Word Space: increased (0.2) +{ + "flat": { + "name": "UIO+ Common", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/characterSpace": 1, + "http://registry.gpii.net/common/fontSize": 24, + "http://registry.gpii.net/common/highContrast/enabled": true, + "http://registry.gpii.net/common/highContrastTheme": "white-black", + "http://registry.gpii.net/common/highlightColor": "green", + "http://registry.gpii.net/common/inputsLarger/enabled": true, + "http://registry.gpii.net/common/lineSpace": 2, + "http://registry.gpii.net/common/selfVoicing/enabled": true, + "http://registry.gpii.net/common/simplifiedUi/enabled": true, + "http://registry.gpii.net/common/syllabification/enabled": true, + "http://registry.gpii.net/common/tableOfContents": true, + "http://registry.gpii.net/common/wordSpace": 1 + } + } + } + } +} diff --git a/testData/preferences/uioPlus_character_space.json5 b/testData/preferences/uioPlus_character_space.json5 new file mode 100644 index 000000000..95f740849 --- /dev/null +++ b/testData/preferences/uioPlus_character_space.json5 @@ -0,0 +1,28 @@ +// # uioPlus_character_space +// +// A preference set for testing preference settings in UIO+. +// +// Applies the Character Space preference. The space between characters should increase. +{ + "flat": { + "name": "UIO+ Character Spacing", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/characterSpace": 1.2, + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/highContrast/enabled": false, + "http://registry.gpii.net/common/highlightColor": "default", + "http://registry.gpii.net/common/inputsLarger/enabled": false, + "http://registry.gpii.net/common/lineSpace": 1, + "http://registry.gpii.net/common/selfVoicing/enabled": false, + "http://registry.gpii.net/common/simplifiedUi/enabled": false, + "http://registry.gpii.net/common/syllabification/enabled": false, + "http://registry.gpii.net/common/tableOfContents": false, + "http://registry.gpii.net/common/wordSpace": 0 + } + } + } + } +} diff --git a/testData/preferences/uioPlus_defaults.json5 b/testData/preferences/uioPlus_defaults.json5 new file mode 100644 index 000000000..1681282fb --- /dev/null +++ b/testData/preferences/uioPlus_defaults.json5 @@ -0,0 +1,28 @@ +// # uioPlus_defaults +// +// A preference set for testing preference settings in UIO+. +// +// Uses only the default settings, which should have no affect on UIO+. +{ + "flat": { + "name": "UIO+ Defaults", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/characterSpace": 1, + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/highContrast/enabled": false, + "http://registry.gpii.net/common/highlightColor": "default", + "http://registry.gpii.net/common/inputsLarger/enabled": false, + "http://registry.gpii.net/common/lineSpace": 1, + "http://registry.gpii.net/common/selfVoicing/enabled": false, + "http://registry.gpii.net/common/simplifiedUi/enabled": false, + "http://registry.gpii.net/common/syllabification/enabled": false, + "http://registry.gpii.net/common/tableOfContents": false, + "http://registry.gpii.net/common/wordSpace": 0 + } + } + } + } +} diff --git a/testData/preferences/uioPlus_font_size.json5 b/testData/preferences/uioPlus_font_size.json5 new file mode 100644 index 000000000..31d446628 --- /dev/null +++ b/testData/preferences/uioPlus_font_size.json5 @@ -0,0 +1,28 @@ +// # uioPlus_font_size +// +// A preference set for testing preference settings in UIO+. +// +// Applies the Font Size preference. The text should increase in size. +{ + "flat": { + "name": "UIO+ Font Size", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/characterSpace": 1, + "http://registry.gpii.net/common/fontSize": 24, + "http://registry.gpii.net/common/highContrast/enabled": false, + "http://registry.gpii.net/common/highlightColor": "default", + "http://registry.gpii.net/common/inputsLarger/enabled": false, + "http://registry.gpii.net/common/lineSpace": 1, + "http://registry.gpii.net/common/selfVoicing/enabled": false, + "http://registry.gpii.net/common/simplifiedUi/enabled": false, + "http://registry.gpii.net/common/syllabification/enabled": false, + "http://registry.gpii.net/common/tableOfContents": false, + "http://registry.gpii.net/common/wordSpace": 0 + } + } + } + } +} diff --git a/testData/preferences/uioPlus_high_contrast.json5 b/testData/preferences/uioPlus_high_contrast.json5 new file mode 100644 index 000000000..98e19ccd4 --- /dev/null +++ b/testData/preferences/uioPlus_high_contrast.json5 @@ -0,0 +1,29 @@ +// # uioPlus_high_contrast +// +// A preference set for testing preference settings in UIO+. +// +// Applies the Highlight Contrast theme preference. The contrast of the page will be set to match the specified contrast theme. Unless a site provides it's own themed versions (most likely only pages that already support UIO), images will remain unaffected. +{ + "flat": { + "name": "UIO+ High Contrast", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/characterSpace": 1, + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/highContrast/enabled": true, + "http://registry.gpii.net/common/highContrastTheme": "white-black", + "http://registry.gpii.net/common/highlightColor": "default", + "http://registry.gpii.net/common/inputsLarger/enabled": false, + "http://registry.gpii.net/common/lineSpace": 1, + "http://registry.gpii.net/common/selfVoicing/enabled": false, + "http://registry.gpii.net/common/simplifiedUi/enabled": false, + "http://registry.gpii.net/common/syllabification/enabled": false, + "http://registry.gpii.net/common/tableOfContents": false, + "http://registry.gpii.net/common/wordSpace": 0 + } + } + } + } +} diff --git a/testData/preferences/uioPlus_highlight_colour.json5 b/testData/preferences/uioPlus_highlight_colour.json5 new file mode 100644 index 000000000..8e2653f20 --- /dev/null +++ b/testData/preferences/uioPlus_highlight_colour.json5 @@ -0,0 +1,28 @@ +// # uioPlus_highlight_colour +// +// A preference set for testing preference settings in UIO+. +// +// Applies the Highlight Colour preference. The colour of the text selection highlight will be changed to match the preference. +{ + "flat": { + "name": "UIO+ Highlight Colour", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/characterSpace": 1, + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/highContrast/enabled": false, + "http://registry.gpii.net/common/highlightColor": "yellow", + "http://registry.gpii.net/common/inputsLarger/enabled": false, + "http://registry.gpii.net/common/lineSpace": 1, + "http://registry.gpii.net/common/selfVoicing/enabled": false, + "http://registry.gpii.net/common/simplifiedUi/enabled": false, + "http://registry.gpii.net/common/syllabification/enabled": false, + "http://registry.gpii.net/common/tableOfContents": false, + "http://registry.gpii.net/common/wordSpace": 0 + } + } + } + } +} diff --git a/testData/preferences/uioPlus_inputs_larger.json5 b/testData/preferences/uioPlus_inputs_larger.json5 new file mode 100644 index 000000000..a5942984f --- /dev/null +++ b/testData/preferences/uioPlus_inputs_larger.json5 @@ -0,0 +1,28 @@ +// # uioPlus_inputs_larger +// +// A preference set for testing preference settings in UIO+. +// +// Applies the Inputs Larger preference. Form controls should increase in size. +{ + "flat": { + "name": "UIO+ Larger Inputs", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/characterSpace": 1, + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/highContrast/enabled": false, + "http://registry.gpii.net/common/highlightColor": "default", + "http://registry.gpii.net/common/inputsLarger/enabled": true, + "http://registry.gpii.net/common/lineSpace": 1, + "http://registry.gpii.net/common/selfVoicing/enabled": false, + "http://registry.gpii.net/common/simplifiedUi/enabled": false, + "http://registry.gpii.net/common/syllabification/enabled": false, + "http://registry.gpii.net/common/tableOfContents": false, + "http://registry.gpii.net/common/wordSpace": 0 + } + } + } + } +} diff --git a/testData/preferences/uioPlus_line_space.json5 b/testData/preferences/uioPlus_line_space.json5 new file mode 100644 index 000000000..b10c40d9c --- /dev/null +++ b/testData/preferences/uioPlus_line_space.json5 @@ -0,0 +1,28 @@ +// # uioPlus_line_space +// +// A preference set for testing preference settings in UIO+. +// +// Applies the Line Space preference. The line space should increase in size. +{ + "flat": { + "name": "UIO+ Line Spacing", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/characterSpace": 1, + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/highContrast/enabled": false, + "http://registry.gpii.net/common/highlightColor": "default", + "http://registry.gpii.net/common/inputsLarger/enabled": false, + "http://registry.gpii.net/common/lineSpace": 2, + "http://registry.gpii.net/common/selfVoicing/enabled": false, + "http://registry.gpii.net/common/simplifiedUi/enabled": false, + "http://registry.gpii.net/common/syllabification/enabled": false, + "http://registry.gpii.net/common/tableOfContents": false, + "http://registry.gpii.net/common/wordSpace": 0 + } + } + } + } +} diff --git a/testData/preferences/uioPlus_multiple_settings.json5 b/testData/preferences/uioPlus_multiple_settings.json5 new file mode 100644 index 000000000..c8fc0cee8 --- /dev/null +++ b/testData/preferences/uioPlus_multiple_settings.json5 @@ -0,0 +1,44 @@ +// # uioPlus_multiple_settings +// +// A preference set for testing preference settings in UIO+. +// +// Attempts to set all of the UIO+ preferences. The Syllabification and CharacterSpace preferences are currently not supported by UIO+ and will be ignored. +// +// ## Preferences +// +// * Character Space: increased (1) +// * Font Size: increased (1.3) +// * High Contrast: Enabled +// * High Contrast Theme: yellow-black +// * Highlight Color: green (text selection is highlighted in green) +// * Inputs Larger: enabled (form controls increase in size) +// * Line Space: increased (1.3) +// * Self Voicing: enabled +// * Simplified UI: enabled +// * Syllabification: enabled +// * Table of Contents: enabled +// * wordSpace: increased (1) +{ + "flat": { + "name": "UIO+ Multiple Settings", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/characterSpace": 1, + "http://registry.gpii.net/common/fontSize": 16, + "http://registry.gpii.net/common/highContrast/enabled": true, + "http://registry.gpii.net/common/highContrastTheme": "yellow-black", + "http://registry.gpii.net/common/highlightColor": "green", + "http://registry.gpii.net/common/inputsLarger/enabled": true, + "http://registry.gpii.net/common/lineSpace": 1.3, + "http://registry.gpii.net/common/selfVoicing/enabled": true, + "http://registry.gpii.net/common/simplifiedUi/enabled": true, + "http://registry.gpii.net/common/syllabification/enabled": true, + "http://registry.gpii.net/common/tableOfContents": true, + "http://registry.gpii.net/common/wordSpace": 1 + } + } + } + } +} diff --git a/testData/preferences/uioPlus_self_voicing.json5 b/testData/preferences/uioPlus_self_voicing.json5 new file mode 100644 index 000000000..7def68a82 --- /dev/null +++ b/testData/preferences/uioPlus_self_voicing.json5 @@ -0,0 +1,28 @@ +// # uioPlus_self_voicing +// +// A preference set for testing preference settings in UIO+. +// +// Enables the Self Voicing preference. The click2speech browser extension should be enabled. +{ + "flat": { + "name": "UIO+ Self Voicing", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/characterSpace": 1, + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/highContrast/enabled": false, + "http://registry.gpii.net/common/highlightColor": "default", + "http://registry.gpii.net/common/inputsLarger/enabled": false, + "http://registry.gpii.net/common/lineSpace": 1, + "http://registry.gpii.net/common/selfVoicing/enabled": true, + "http://registry.gpii.net/common/simplifiedUi/enabled": false, + "http://registry.gpii.net/common/syllabification/enabled": false, + "http://registry.gpii.net/common/tableOfContents": false, + "http://registry.gpii.net/common/wordSpace": 0 + } + } + } + } +} diff --git a/testData/preferences/uioPlus_simplified.json5 b/testData/preferences/uioPlus_simplified.json5 new file mode 100644 index 000000000..3e4c524e4 --- /dev/null +++ b/testData/preferences/uioPlus_simplified.json5 @@ -0,0 +1,28 @@ +// # uioPlus_simplified +// +// A preference set for testing preference settings in UIO+. +// +// Enables the Simplified UI preference. For pages where main content can be identified, only the portions of the page that reside within the main content will be visible. If main content cannot be identified, the page will remain unchanged. +{ + "flat": { + "name": "UIO+ Simplified", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/characterSpace": 1, + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/highContrast/enabled": false, + "http://registry.gpii.net/common/highlightColor": "default", + "http://registry.gpii.net/common/inputsLarger/enabled": false, + "http://registry.gpii.net/common/lineSpace": 1, + "http://registry.gpii.net/common/selfVoicing/enabled": false, + "http://registry.gpii.net/common/simplifiedUi/enabled": true, + "http://registry.gpii.net/common/syllabification/enabled": false, + "http://registry.gpii.net/common/tableOfContents": false, + "http://registry.gpii.net/common/wordSpace": 0 + } + } + } + } +} diff --git a/testData/preferences/uioPlus_syllabification.json5 b/testData/preferences/uioPlus_syllabification.json5 new file mode 100644 index 000000000..ea265f198 --- /dev/null +++ b/testData/preferences/uioPlus_syllabification.json5 @@ -0,0 +1,28 @@ +// # uioPlus_syllabification +// +// A preference set for testing preference settings in UIO+. +// +// While the Syllabification preference is set, there is currently no adjuster or enactor that supports this preference. No change will be reflected in UIO+ panel or the site. +{ + "flat": { + "name": "UIO+ Syllabification", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/characterSpace": 1, + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/highContrast/enabled": false, + "http://registry.gpii.net/common/highlightColor": "default", + "http://registry.gpii.net/common/inputsLarger/enabled": false, + "http://registry.gpii.net/common/lineSpace": 1, + "http://registry.gpii.net/common/selfVoicing/enabled": false, + "http://registry.gpii.net/common/simplifiedUi/enabled": false, + "http://registry.gpii.net/common/syllabification/enabled": true, + "http://registry.gpii.net/common/tableOfContents": false, + "http://registry.gpii.net/common/wordSpace": 0 + } + } + } + } +} diff --git a/testData/preferences/uioPlus_toc.json5 b/testData/preferences/uioPlus_toc.json5 new file mode 100644 index 000000000..f728351e4 --- /dev/null +++ b/testData/preferences/uioPlus_toc.json5 @@ -0,0 +1,28 @@ +// # uioPlus_toc +// +// A preference set for testing preference settings in UIO+. +// +// Should only enable the Table of Contents. The table of contents will be injected into one of the following locations depending on what is located on the page: an element with the class `flc-toc-tocContainer`, main content, or the `body` element. +{ + "flat": { + "name": "UIO+ Table of Contents", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/characterSpace": 1, + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/highContrast/enabled": false, + "http://registry.gpii.net/common/highlightColor": "default", + "http://registry.gpii.net/common/inputsLarger/enabled": false, + "http://registry.gpii.net/common/lineSpace": 1, + "http://registry.gpii.net/common/selfVoicing/enabled": false, + "http://registry.gpii.net/common/simplifiedUi/enabled": false, + "http://registry.gpii.net/common/syllabification/enabled": false, + "http://registry.gpii.net/common/tableOfContents": true, + "http://registry.gpii.net/common/wordSpace": 0 + } + } + } + } +} diff --git a/testData/preferences/uioPlus_word_space.json5 b/testData/preferences/uioPlus_word_space.json5 new file mode 100644 index 000000000..a9df7e1c6 --- /dev/null +++ b/testData/preferences/uioPlus_word_space.json5 @@ -0,0 +1,28 @@ +// # uioPlus_word_space +// +// A preference set for testing preference settings in UIO+. +// +// Applies the Word Space preference. The space between words should increase. +{ + "flat": { + "name": "UIO+ Word Spacing", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/characterSpace": 1, + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/highContrast/enabled": false, + "http://registry.gpii.net/common/highlightColor": "default", + "http://registry.gpii.net/common/inputsLarger/enabled": false, + "http://registry.gpii.net/common/lineSpace": 1, + "http://registry.gpii.net/common/selfVoicing/enabled": false, + "http://registry.gpii.net/common/simplifiedUi/enabled": false, + "http://registry.gpii.net/common/syllabification/enabled": false, + "http://registry.gpii.net/common/tableOfContents": false, + "http://registry.gpii.net/common/wordSpace": 2 + } + } + } + } +} diff --git a/testData/preferences/vicky.json5 b/testData/preferences/vicky.json5 new file mode 100644 index 000000000..fecdefe02 --- /dev/null +++ b/testData/preferences/vicky.json5 @@ -0,0 +1,22 @@ +// # vicky +// +// This preference set just enables sticky keys. +// +// This allows the user to use modifier keys without having to hold them down. +// +// ## Testing +// +// Press Alt, Ctrl, then Delete (not at the same time). The lock screen should appear. +{ + "flat": { + "name": "Vicky", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/stickyKeys": true + } + } + } + } +} diff --git a/testData/preferences/wayne.json b/testData/preferences/wayne.json deleted file mode 100644 index b41043ef0..000000000 --- a/testData/preferences/wayne.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "http://registry.gpii.org/applications/com.android.freespeech": [{ "value": {}}] -} diff --git a/testData/preferences/wayne.json5 b/testData/preferences/wayne.json5 new file mode 100644 index 000000000..63443c9b3 --- /dev/null +++ b/testData/preferences/wayne.json5 @@ -0,0 +1,13 @@ +{ + "flat": { + "name": "Wayne", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.android.freespeech": {} + } + } + } + } +} diff --git a/testData/preferences/zacarias.json5 b/testData/preferences/zacarias.json5 new file mode 100644 index 000000000..43a993f80 --- /dev/null +++ b/testData/preferences/zacarias.json5 @@ -0,0 +1,64 @@ +// # Zacarias.json5 +// +// This preference set changes several preferences from ZoomText 2019 +// +// ## Testing +// +// This changes the following settings from the AT software: +// +// - Voice Language: French +// - Synthesizer: Vocalizer Expressive +// - Voice Settings: +// + Rate: 90 WPM. +// + Volume: 80%. +// + Pitch: 30%. +// +// - Keyboard Echo: Words +// - Mouse Echo: Disabled +// - Pointer Scheme: Yellow with Full Crosshairs +// - Zoom Level: 1.6 +{ + "flat": { + "name": "Zacarias", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.aisquared.zoomtext": { + "ZtSpeech.SapiProductName": "VocalizerExpressive-French", + "ZtSpeech.Language": 12, + + // [VocalizerExpressive-French Speech - Screen Reader] + // ================================================================== + "VocalizerExpressive-French Speech - Screen Reader.Valid": 1, + "VocalizerExpressive-French Speech - Screen Reader.SettingsValid": 1, + "VocalizerExpressive-French Speech - Screen Reader.Muted": 0, + "VocalizerExpressive-French Speech - Screen Reader.Voice": 0, + "VocalizerExpressive-French Speech - Screen Reader.Rate": 90, + "VocalizerExpressive-French Speech - Screen Reader.Volume": 80, + "VocalizerExpressive-French Speech - Screen Reader.Pitch": 30, + + // [VocalizerExpressive-French Speech - DocReader] + "VocalizerExpressive-French Speech - DocReader.Valid": 1, + "VocalizerExpressive-French Speech - DocReader.SettingsValid": 1, + "VocalizerExpressive-French Speech - DocReader.Muted": 0, + "VocalizerExpressive-French Speech - DocReader.Voice": 0, + "VocalizerExpressive-French Speech - DocReader.Rate": 90, + "VocalizerExpressive-French Speech - DocReader.Volume": 80, + "VocalizerExpressive-French Speech - DocReader.Pitch": 30, + + "EchoTyping.Mode": 32770, + "EchoMouse.HoverMode": 0, + "EchoMouse.LineMode": 0, + + "PointerScheme.PtrEnhEnable": 1, + "PointerScheme.PtrEnhIndex": 1, + + "PRIMARY.magPower": 160, + "STATIC 1.magPower": 160 + } + } + } + } + } +} diff --git a/testData/preferences/zack.json5 b/testData/preferences/zack.json5 new file mode 100644 index 000000000..870c51ad2 --- /dev/null +++ b/testData/preferences/zack.json5 @@ -0,0 +1,59 @@ +// # Zack.json5 +// +// This preference set changes several preferences from ZoomText 2019 +// +// ## Testing +// +// This changes the following settings from the AT software: +// +// - Voice Language: French +// - Synthesizer: Vocalizer Expressive +// - Voice Settings: +// + Rate: 110 WPM. +// + Volume: 90%. +// + Pitch: 40%. +// +// - Keyboard Echo: Words +// - Mouse Echo: Disabled +// - Pointer Scheme: Red with Circle +// - Zoom Level: 1.6 +{ + "flat": { + "name": "Zack", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.aisquared.zoomtext": { + "ZtSpeech.SapiProductName": "Microsoft Zira Desktop - English (United States)-English", + "ZtSpeech.Language": 1033, + + // [Microsoft Zira Desktop - English (United States)-English Speech - Screen Reader] + // ================================================================== + "Microsoft Zira Desktop - English (United States)-English Speech - Screen Reader.Valid": 1, + "Microsoft Zira Desktop - English (United States)-English Speech - Screen Reader.SettingsValid": 1, + "Microsoft Zira Desktop - English (United States)-English Speech - Screen Reader.Muted": 0, + "Microsoft Zira Desktop - English (United States)-English Speech - Screen Reader.Voice": 0, + "Microsoft Zira Desktop - English (United States)-English Speech - Screen Reader.Rate": 110, + "Microsoft Zira Desktop - English (United States)-English Speech - Screen Reader.Volume": 90, + "Microsoft Zira Desktop - English (United States)-English Speech - Screen Reader.Pitch": 40, + + // [Microsoft Zira Desktop - English (United States)-English Speech - DocReader] + "Microsoft Zira Desktop - English (United States)-English Speech - DocReader.Valid": 1, + "Microsoft Zira Desktop - English (United States)-English Speech - DocReader.SettingsValid": 1, + "Microsoft Zira Desktop - English (United States)-English Speech - DocReader.Muted": 0, + "Microsoft Zira Desktop - English (United States)-English Speech - DocReader.Voice": 0, + "Microsoft Zira Desktop - English (United States)-English Speech - DocReader.Rate": 110, + "Microsoft Zira Desktop - English (United States)-English Speech - DocReader.Volume": 90, + "Microsoft Zira Desktop - English (United States)-English Speech - DocReader.Pitch": 40, + + "EchoTyping.Mode": 32771, + "PointerScheme.PtrEnhEnable": 1, + "PointerScheme.PtrEnhIndex": 2 + }, + "http://registry.gpii.net/common/magnification": 3 + } + } + } + } +} diff --git a/testData/preferences/zanya.json5 b/testData/preferences/zanya.json5 new file mode 100644 index 000000000..d961a2418 --- /dev/null +++ b/testData/preferences/zanya.json5 @@ -0,0 +1,68 @@ +// # Zanya.json5 +// +// This preference set changes several preferences from ZoomText 2019 +// +// ## Testing +// +// This changes the following settings from the AT software: +// +// - Voice Language: Dutch +// - Synthesizer: Vocalizer Expressive +// - Voice Settings: +// + Rate: 120 WPM. +// + Volume: 70%. +// + Pitch: 35%. +// +// - Keyboard Echo: Keys +// - Mouse Echo: Hover +// +// - Pointer Scheme: Giant Green +// - Zoom Level: 1.4 +// - Navigation Settings - Mouse - "Router Pointer into view when it is moved": Disabled. +{ + "flat": { + "name": "Zanya", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.aisquared.zoomtext": { + "ZtSpeech.SapiProductName": "VocalizerExpressive-Dutch", + "ZtSpeech.Language": 19, + + // [VocalizerExpressive-Dutch Speech - Screen Reader] + // ================================================================== + "VocalizerExpressive-Dutch Speech - Screen Reader.Valid": 1, + "VocalizerExpressive-Dutch Speech - Screen Reader.SettingsValid": 1, + "VocalizerExpressive-Dutch Speech - Screen Reader.Muted": 0, + "VocalizerExpressive-Dutch Speech - Screen Reader.Voice": 0, + "VocalizerExpressive-Dutch Speech - Screen Reader.Rate": 120, + "VocalizerExpressive-Dutch Speech - Screen Reader.Volume": 70, + "VocalizerExpressive-Dutch Speech - Screen Reader.Pitch": 35, + + // [VocalizerExpressive-Dutch Speech - DocReader] + "VocalizerExpressive-Dutch Speech - DocReader.Valid": 1, + "VocalizerExpressive-Dutch Speech - DocReader.SettingsValid": 1, + "VocalizerExpressive-Dutch Speech - DocReader.Muted": 0, + "VocalizerExpressive-Dutch Speech - DocReader.Voice": 0, + "VocalizerExpressive-Dutch Speech - DocReader.Rate": 120, + "VocalizerExpressive-Dutch Speech - DocReader.Volume": 70, + "VocalizerExpressive-Dutch Speech - DocReader.Pitch": 35, + + "EchoTyping.Mode": 32769, + "EchoMouse.HoverMode": 1, + "EchoMouse.LineMode": 0, + + "PointerScheme.PtrEnhEnable": 1, + "PointerScheme.PtrEnhIndex": 3, + + "PRIMARY.magPower": 140, + "STATIC 1.magPower": 140, + + "Tracking.RouteMouse": 0 + } + } + } + } + } +} diff --git a/testData/solutions/android.json b/testData/solutions/android.json deleted file mode 100644 index 25db79887..000000000 --- a/testData/solutions/android.json +++ /dev/null @@ -1,104 +0,0 @@ -[ - { - "name": "FreeSpeech", - "id": "com.android.freespeech", - "contexts": { - "OS": [{ - "id": "android", - "version": ">=0.1" - }] - }, - "settingsHandlers": [ - { - "type": "gpii.settingsHandlers.noSettings", - "capabilities": [ - "display.screenReader", - "applications.com\\.android\\.freespeech.id", - "display.screenReader.applications.com\\.android\\.freespeech.name" - ] - } - ], - "lifecycleManager": { - "start": [ - { - "type": "gpii.androidActivityManager.startFreespeech" - } - ], - "stop": [ - { - } - ] - } - }, - - { - "name": "TalkBack Screen Reader", - "id": "com.android.talkback", - "contexts": { - "OS": [{ - "id": "android", - "version": ">=0.1" - }] - }, - "settingsHandlers": [ - { - "type": "gpii.settingsHandlers.noSettings", - "capabilities": [ - "display.screenReader", - "applications.com\\.android\\.talkback.id", - "display.screenReader.applications.com\\.android\\.talkback.name" - ] - } - ], - "lifecycleManager": { - "start": [ - { - "type": "gpii.androidActivityManager.startTalkback" - } - ], - "stop": [ - { - "type": "gpii.androidActivityManager.stopTalkback" - } - ] - } - }, - - { - "name": "Android System Font Size Demo", - "id": "com.android.activitymanager", - "contexts": { - "OS": [{ - "id": "android", - "version": ">=0.1" - }] - }, - "settingsHandlers": [ - { - "type": "gpii.androidActivityManager.set", - "capabilities": [ - "display.screenEnhancement.applications.com\\.android\\.activityManager\\.interface.name", - "applications.com\\.android\\.activityManager\\.interface.id" - ], - "capabilitiesTransformations": { - "text-scaling-factor": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.fontSize", - "operator": "/", - "right": 12 - } - } - }, - "options": { - "schema": "com.android.activityManager" - } - } - ], - "lifecycleManager": { - "start": [ "setSettings" ], - "stop": [ "restoreSettings" ] - } - } - -] diff --git a/testData/solutions/android.json5 b/testData/solutions/android.json5 new file mode 100644 index 000000000..4d97d3e82 --- /dev/null +++ b/testData/solutions/android.json5 @@ -0,0 +1,579 @@ +{ + "com.android.freespeech": { + "name": "FreeSpeech", + "contexts": { + "OS": [ + { + "id": "android", + "version": ">=0.1" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled" + ], + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "liveness": "manualRestart", + "options": { + "setTrue": { + "type": "gpii.androidActivityManager.startFreespeech" + }, + "getState": [ + { + "type": "gpii.processReporter.neverRunning" + } + ] + } + } + }, + "start": [ + "launchers.launcher" + ], + "isRunning": [ + "launchers.launcher" + ] + }, + "com.android.talkback": { + "name": "TalkBack Screen Reader", + "contexts": { + "OS": [ + { + "id": "android", + "version": ">=0.1" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled" + ], + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "setTrue": { + "type": "gpii.androidActivityManager.startTalkback" + }, + "setFalse": { + "type": "gpii.androidActivityManager.stopTalkback" + }, + "getState": [ + { + "type": "gpii.processReporter.neverRunning" + } + ] + } + } + } + }, + "com.android.persistentConfiguration": { + "name": "Android Configuration", + "contexts": { + "OS": [ + { + "id": "android", + "version": ">=0.1" + } + ] + }, + "settingsHandlers": { + "configuration": { + "type": "gpii.androidPersistentConfiguration", + "supportedSettings": { + "fontScale": { + "schema": { + "type": "number" + } + }, + "locale": { + "schema": { + "type": "string" + } + } + }, + "liveness": "live", + "capabilitiesTransformations": { + "fontScale": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/fontSize", + "operator": "/", + "right": 12 + } + }, + "locale": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/language", + "match": { + "en": "en", + "en-GB": "en_GB", + "en-US": "en_US", + "en-BZ": "en_GB", + "en-BS": "en_GB", + "en-AG": "en_GB", + "en-AI": "en_GB", + "af": "af", + "bg": "bg_BG", + "bs": "bs", + "ca": "ca_ES", + "cs": "cs_CZ", + "cy": "cy", + "da": "da_DK", + "de": "de_DE", + "el": "el_GR", + "grc": "el_GR", + "eo": "eo", + "es": "es_ES", + "es-ES": "es_ES", + "es-419": "es_ES", + "et": "et", + "fi": "fi_FI", + "fr": "fr_FR", + "fr-BE": "fr_BE", + "hi": "hi_IN", + "hr": "hr_HR", + "hu": "hu_HU", + "hy": "hy", + "hy-arevmda": "hy", + "id": "id_ID", + "is": "is", + "it": "it_IT", + "ka": "ka", + "kn": "kn", + "ku": "ku", + "la": "la", + "lv": "lv_LV", + "mk": "mk", + "ml": "ml", + "nl": "nl_NL", + "no": "nb_NO", + "pl": "pl_PL", + "pt-BR": "pt_BR", + "pt-PT": "pt_PT", + "ro": "ro_RO", + "ru": "ru_RU", + "sk": "sk_SK", + "sq": "sq", + "sr": "sr_RS", + "sv": "sv_SE", + "sw": "ss", + "ta": "ta", + "tr": "tr_TR", + "vi": "vi_VN", + "zh-cmn": "zh_CN", + "cmn": "zh_CN" + } + } + } + }, + "inverseCapabilitiesTransformations": {} + } + } + }, + "com.android.audioManager": { + "name": "Android Audio Manager", + "contexts": { + "OS": [ + { + "id": "android", + "version": ">=0.1" + } + ] + }, + "settingsHandlers": { + "configuration": { + "type": "gpii.androidAudioManager.volume", + "liveness": "live", + "supportedSettings": { + "STREAM_MUSIC": { + "schema": { + "title": "Music Volume", + "description": "Music volume.", + "type": "number" + } + }, + "STREAM_SYSTEM": { + "schema": { + "title": "System Volume", + "description": "System volume.", + "type": "number" + } + + } + }, + "capabilitiesTransformations": { + "STREAM_MUSIC": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volumeTTS", + "factor": 15 + } + } + } + }, + "STREAM_SYSTEM": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volume", + "factor": 7 + } + } + } + } + }, + "inverseCapabilitiesTransformations": {} + } + } + }, + "com.android.settings.system": { + "name": "Android UI Settings.System", + "contexts": { + "OS": [ + { + "id": "android", + "version": ">=0.1" + } + ] + }, + "settingsHandlers": { + "configuration": { + "type": "gpii.androidSettings", + "liveness": "live", + "options": { + "settingType": "System" + }, + "supportedSettings": { + "dim_screen": { + "schema": { "type": "boolean" } + }, + "haptic_feedback_enabled": { + "schema": { "type": "boolean" } + }, + "accelerometer_rotation": { + "schema": { "type": "boolean" } + }, + "user_rotation": { + "schema": { "type": "integer" } + }, + "screen_off_timeout": { + "schema": { "type": "integer" } + } + }, + "capabilitiesTransformations": {}, + "inverseCapabilitiesTransformations": {} + } + } + }, + "com.android.settings.secure": { + "name": "Android UI Settings Secure", + "contexts": { + "OS": [ + { + "id": "android", + "version": ">=0.1" + } + ] + }, + "settingsHandlers": { + "configuration": { + "type": "gpii.androidSettings", + "liveness": "live", + "options": { + "settingType": "Secure" + }, + "supportedSettings": { + "tts_default_pitch": { + "schema": { "type": "number" } + }, + "tts_default_rate": { + "schema": { "type": "number" } + } + }, + "capabilitiesTransformations": { + "tts_default_pitch": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 500 + } + } + } + }, + "tts_default_rate": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": 21.753, + "operator": "-", + "right": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": 0.0317, + "operator": "*", + "rightPath": "http://registry\\.gpii\\.net/common/speechRate" + } + }, + "operator": "-", + "right": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": "0.0042", + "operator": "*", + "right": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/speechRate", + "operator": "*", + "rightPath": "http://registry\\.gpii\\.net/common/speechRate" + } + } + } + } + } + } + } + } + } + } + }, + "inverseCapabilitiesTransformations": {} + } + } + }, + "se.omnitor.ecmobile": { + "name": "Omnitor eCtouch/eCmobile", + "contexts": { + "OS": [ + { + "id": "android", + "version": ">=0.1" + } + ] + }, + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.XMLHandler", + "liveness": "manualRestart", + "supportedSettings": { + "map.string.fontsize.$t": {}, + "map.string.theme.$t": {} + }, + "options": { + "filename": "/sdcard/output.xml", + "encoding": "utf-8", + "xml-tag": "", + "rules": { + "map": "map", + "map.string": { + "transform": { + "type": "fluid.transforms.arrayToObject", + "inputPath": "map.string", + "key": "name" + } + } + } + }, + "capabilitiesTransformations": { + "map\\.string\\.fontsize\\.$t": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/fontSize", + "factor": 2 + } + }, + "map\\.string\\.theme\\.$t": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/common/highContrast/enabled", + "true": "yellow-black", + "truePath": "http://registry\\.gpii\\.net/common/highContrastTheme", + "false": "none" + } + } + }, + "inverseCapabilitiesTransformations": {} + } + }, + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "setTrue": { + "type": "gpii.androidActivityManager.startActivityByPackageName", + "packageName": "se.omnitor.ecmobile" + }, + "setFalse": [ + { + "type": "gpii.androidActivityManager.stopActivityByPackageName", + "packageName": "se.omnitor.ecmobile" + }, + { + "type": "gpii.androidActivityManager.goToHomeScreen" + } + ], + "getState": [ + { + "type": "gpii.processReporter.neverRunning" + } + ] + } + } + } + }, + "es.codefactory.android.app.ma": { + "name": "Mobile Accessibility", + "contexts": { + "OS": [ + { + "id": "android", + "version": ">=0.1" + } + ] + }, + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.noSettings", + "liveness": "live", + "supportedSettings": { + "MA_CLOUD4ALL_USER": { + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + }, + "access_commonprefs_speechrate": { + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + }, + "access_commonprefs_speechpitch": { + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + }, + "access_commonprefs_editingkeyboardecho": { + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + }, + "access_commonprefs_punctuation": { + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + }, + "access_commonprefs_capitalization": { + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + } + } + } + }, + "capabilitiesTransformations": { + "access_commonprefs_speechrate": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/speechRate", + "operator": "/", + "right": 40 + } + }, + "access_commonprefs_speechpitch": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/pitch", + "operator": "*", + "right": 10 + } + }, + "access_commonprefs_editingkeyboardecho": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/common/keyEcho", + "true": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/common/wordEcho", + "true": "3", + "false": "1" + } + }, + "false": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/common/wordEcho", + "true": "2", + "false": "0" + } + } + } + }, + "access_commonprefs_punctuation": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "access_commonprefs_capitalization": "http://registry\\.gpii\\.net/common/announceCapitals" + }, + "inverseCapabilitiesTransformations": {} + } + } + } +} diff --git a/testData/solutions/darwin.json b/testData/solutions/darwin.json deleted file mode 100644 index 0637a088a..000000000 --- a/testData/solutions/darwin.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/testData/solutions/darwin.json5 b/testData/solutions/darwin.json5 new file mode 100644 index 000000000..3e124df1b --- /dev/null +++ b/testData/solutions/darwin.json5 @@ -0,0 +1,172 @@ +{ + "fakemag1": { + "name": "Fake Magnifier 1", + "contexts": { + "OS": [ + { + "id": "darwin" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.JSONSettingsHandler", + "liveness": "live", + "options": { + "filename": "/tmp/fakemag1.settings.json" + }, + "capabilitiesTransformations": { + "magnification": "http://registry\\.gpii\\.net/common/magnification" + }, + "supportedSettings": { + "magnification": { + schema: { + title: "Magnification Level", + description: "How many times to magnify content when magnification is enabled.", + type: "number", + default: 1.10 + } + } + } + } + }, + "configure": [ + "settings.configuration" + ], + "restore": [ + "settings.configuration" + ], + "start": [ ], + "stop": [ ], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + + "fakemag2": { + "name": "Fake Magnifier 2 - fully featured", + "contexts": { + "OS": [ + { + "id": "darwin" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.JSONSettingsHandler", + "liveness": "live", + "options": { + "filename": "/tmp/fakemag2.settings.json" + }, + "capabilitiesTransformations": { + "magnification": "http://registry\\.gpii\\.net/common/magnification", + "invert": "http://registry\\.gpii\\.net/common/invertColours" + }, + "supportedSettings": { + "magnification": { + "schema": { + "title": "Magnification Level", + "description": "How many times to magnify content when magnification is enabled.", + "type": "number", + "default": 1.10 + } + }, + "invert": { + "schema": { + "title": "Invert Colours", + "description": "Enable colour inversion for Magnifier", + "type": "boolean" + } + } + } + } + }, + "configure": [ + "settings.configuration" + ], + "restore": [ + "settings.configuration" + ], + "start": [ ], + "stop": [ ], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + + "fakescreenreader1": { + "name": "fake screenreader", + "contexts": { + "OS": [ + { + "id": "darwin" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled" + ], + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.JSONSettingsHandler", + "liveness": "live", + "options": { + "filename": "/tmp/fakescreenreader1.json" + }, + "capabilitiesTransformations": { + "pitch": "http://registry\\.gpii\\.net/common/pitch", + "volumeTTS": "http://registry\\.gpii\\.net/common/volumeTTS", + "rate": "http://registry\\.gpii\\.net/common/speechRate" + }, + "supportedSettings": { + "pitch": { + "schema": { + "title": "Speech Pitch", + "description": "The pitch at which text is announced.", + "minimum": 0, + "maximum": 20 + } + }, + "volumeTTS": { + "schema": { + "title": "Volume TTS", + "description": "Volume TTS", + "type": "integer" + } + }, + "rate": { + "schema": { + "title": "Rate", + "description": "Speech rate for all announcements (scale varies by voice).", + "type": "integer" + } + } + } + } + }, + "configure": [ + "settings.configuration" + ], + "restore": [ + "settings.configuration" + ], + "start": [ ], + "stop": [ ], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + } +} diff --git a/testData/solutions/fluid.uiOptions.linux.json b/testData/solutions/fluid.uiOptions.linux.json deleted file mode 100644 index 10b102e61..000000000 --- a/testData/solutions/fluid.uiOptions.linux.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "UI Options", - "id": "fluid.uiOptions.linux", - "contexts": { - "OS": [ - { - "id": "linux", - "version": ">=2.6.25" - } - ] - }, - "settingsHandlers": [ - { - "type": "gpii.settingsHandlers.noSettings", - "capabilities": ["display"] - } - ], - "lifecycleManager": { - "start": [ - { - "type": "gpii.launch.exec", - "command": "google-chrome http://ec2-107-21-143-113.compute-1.amazonaws.com:443/demos/Mammals.html?token=${{userToken}}" - } - ], - "stop": [ - { - "type": "gpii.launch.exec", - "command": "pkill -2 chrome" - } - ] - } -} \ No newline at end of file diff --git a/testData/solutions/fluid.uiOptions.windows.json b/testData/solutions/fluid.uiOptions.windows.json deleted file mode 100644 index ce05deffa..000000000 --- a/testData/solutions/fluid.uiOptions.windows.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "UI Options", - "id": "fluid.uiOptions.windows", - "contexts": { - "OS": [ - { - "id": "win32", - "version": ">=5.0" - } - ] - }, - "settingsHandlers": [ - { - "type": "gpii.settingsHandlers.noSettings", - "capabilities": ["display"] - } - ], - "lifecycleManager": { - "start": [ - { - "type": "gpii.launch.exec", - "command": "\"${{environment}.PROGRAMFILES(x86)}\\Mozilla Firefox\\firefox.exe\" http://ec2-107-21-143-113.compute-1.amazonaws.com:443/demos/Mammals.html?token=${{userToken}}" - } - ], - "stop": [ - { - "type": "gpii.launch.exec", - "command": "${{environment}.SystemRoot}\\System32\\taskkill.exe /im firefox.exe" - } - ] - } -} \ No newline at end of file diff --git a/testData/solutions/index.js b/testData/solutions/index.js new file mode 100644 index 000000000..fe68265ae --- /dev/null +++ b/testData/solutions/index.js @@ -0,0 +1,7 @@ +/* eslint-env node */ +"use strict"; +require("json5/lib/register"); // allow us to require json5 files +exports.win32 = require("./win32.json5"); +exports.linux = require("./linux.json5"); +exports.darwin = require("./darwin.json5"); +exports.android = require("./android.json5"); diff --git a/testData/solutions/linux.json b/testData/solutions/linux.json deleted file mode 100644 index 83bc6d0e6..000000000 --- a/testData/solutions/linux.json +++ /dev/null @@ -1,1277 +0,0 @@ -[ - - { - "name": "GNOME Shell Magnifier", - "id": "org.gnome.desktop.a11y.magnifier", - "contexts": { - "OS": [ - { - "id": "linux", - "version": ">=2.6.26" - } - ] - }, - "settingsHandlers": [ - { - "type": "gpii.gsettings.set", - "options": { - "schema": "org.gnome.desktop.a11y.magnifier" - }, - "capabilities": [ - "applications.org\\.gnome\\.desktop\\.a11y\\.magnifier.id", - "display.screenEnhancement.screenMagnification.applications.org\\.gnome\\.desktop\\.a11y\\.magnifier.name" - ], - "capabilitiesTransformations": { - "mag-factor": "display.screenEnhancement.magnification", - "show-cross-hairs": "display.screenEnhancement.-provisional-showCrosshairs", - "mouse-tracking": { - "transform": { - "type": "fluid.transforms.valueMapper", - "inputPath": "display.screenEnhancement.tracking", - "options": { - "mouse": { - "outputValue": "centered" - } - } - } - }, - "screen-position": { - "transform": { - "type": "fluid.transforms.valueMapper", - "inputPath": "display.screenEnhancement.-provisional-magnifierPosition", - "defaultInputValue": "Custom", - "options": { - "FullScreen": { - "outputValue": "full-screen" - }, - "Lens": { - "outputValue": "full-screen" - }, - "LeftHalf": { - "outputValue": "left-half" - }, - "RightHalf": { - "outputValue": "right-half" - }, - "TopHalf": { - "outputValue": "top-half" - }, - "BottomHalf": { - "outputValue": "bottom-half" - }, - "Custom": { - "outputValue": "full-screen" - } - } - } - } - } - } - ], - "lifecycleManager": { - "start": [ - "setSettings", - { - "type": "gpii.launch.exec", - "command": "gsettings set org.gnome.desktop.a11y.applications screen-magnifier-enabled true" - } - ], - "stop": [ - { - "type": "gpii.launch.exec", - "command": "gsettings set org.gnome.desktop.a11y.applications screen-magnifier-enabled false" - }, - "restoreSettings" - ] - } - }, - - { - "name": "UI Options", - "id": "fluid.uiOptions.linux", - "contexts": { - "OS": [ - { - "id": "linux", - "version": ">=2.6.26" - } - ] - }, - "settingsHandlers": [ - { - "type": "gpii.settingsHandlers.noSettings", - "capabilities": [ - "applications.fluid\\.uiOptions\\.linux.id", - "display.applications.fluid\\.uiOptions\\.linux.name", - "display.screenEnhancement.fontSize", - "display.screenEnhancement.foregroundColor", - "display.screenEnhancement.backgroundColor", - "display.screenEnhancement.fontFace" - ] - } - ], - "lifecycleManager": { - "start": [ - { - "type": "gpii.launch.exec", - "command": "google-chrome http://ec2-107-21-143-113.compute-1.amazonaws.com:443/demos/Mammals.html?token=${{userToken}}" - } - ], - "stop": [ - { - "type": "gpii.launch.exec", - "command": "pkill -2 chrome" - } - ] - } - }, - - { - "name": "GNOME Interface Settings", - "id": "org.gnome.desktop.interface", - "contexts": { - "OS": [ - { - "id": "linux", - "version": ">=2.6.26" - } - ] - }, - "settingsHandlers": [ - { - "type": "gpii.gsettings.set", - "capabilities": [ - "display.screenEnhancement.applications.org\\.gnome\\.desktop\\.interface.name", - "applications.org\\.gnome\\.desktop\\.interface.id" - ], - "capabilitiesTransformations": { - "text-scaling-factor": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.fontSize", - "operator": "/", - "right": 12 - } - }, - "cursor-size": { - "transform": { - "type": "fluid.transforms.condition", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": ">", - "right": 0.666 - } - }, - "true": 41, - "false": { - "transform": { - "type": "fluid.transforms.condition", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": ">", - "right": 0.333 - } - }, - "true": 29, - "false": { - "transform": { - "type": "fluid.transforms.condition", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": ">", - "right": 0 - } - }, - "true": 20, - "false": -1 - } - } - } - } - } - }, - "gtk-theme": { - "transform": { - "type": "fluid.transforms.valueMapper", - "inputPath": "display.screenEnhancement.highContrast", - "options": { - "true": { - "outputValue": "HighContrast" - } - } - } - }, - "icon-theme": { - "transform": { - "type": "fluid.transforms.valueMapper", - "inputPath": "display.screenEnhancement.highContrast", - "options": { - "true": { - "outputValue": "HighContrast" - } - } - } - } - }, - "options": { - "schema": "org.gnome.desktop.interface" - } - } - ], - "lifecycleManager": { - "start": [ "setSettings" ], - "stop": [ "restoreSettings" ] - } - }, - - { - "name": "GNOME Nautilus Settings", - "id": "org.gnome.nautilus", - "contexts": { - "OS": [{ - "id": "linux", - "version": ">=2.6.26" - }] - }, - - "settingsHandlers": [ - { - "type": "gpii.gsettings.set", - "capabilities": [ - "applications.org\\.gnome\\.nautilus.id", - "display.screenEnhancement.applications.org\\.gnome\\.nautilus.name" - ], - "capabilitiesTransformations": { - "font": "display.screenEnhancement.fontFace.fontName.0" - }, - "options": { - "schema": "org.gnome.nautilus.desktop" - } - } - ], - - "lifecycleManager": { - "start": [ "setSettings" ], - "stop": [ "restoreSettings" ] - } - }, - - { - "name": "GNOME Caribou Onscreen Keyboard", - "id": "org.gnome.desktop.a11y.caribou-keyboard", - "contexts": { - "OS": [{ - "id": "linux", - "version": ">=2.6.26" - }] - }, - "settingsHandlers": [ - { - "type": "gpii.settingsHandlers.noSettings", - "capabilities": [ - "control.onscreenKeyboard", - "control.onscreenKeyboard.applications.org\\.gnome\\.desktop\\.a11y\\.caribou-keyboard.name", - "applications.org\\.gnome\\.desktop\\.a11y\\.caribou-keyboard.id" - ] - } - ], - "lifecycleManager": { - "start": [ - { - "type": "gpii.launch.exec", - "command": "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled true" - } - ], - "stop": [ - { - "type": "gpii.launch.exec", - "command": "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled false" - } - ] - } - }, - - { - "name": "GNOME Shell Keyboard Settings", - "id": "org.gnome.desktop.a11y.keyboard", - "contexts": { - "OS": [{ - "id": "linux", - "version": ">=2.6.26" - }] - }, - "settingsHandlers": [ { - "type": "gpii.gsettings.set", - "capabilities": [ - "applications.org\\.gnome\\.desktop\\.a11y\\.keyboard.id", - "control.onscreenKeyboard.applications.org\\.gnome\\.desktop\\.a11y\\.keyboard.name", - "control.keyboardEnhancement.applications.org\\.gnome\\.desktop\\.a11y\\.keyboard.name" - ], - "capabilitiesTransformations": { - "stickykeys-enable": "control.keyboardEnhancement.stickyKeys", - "slowkeys-enable": "control.keyboardEnhancement.slowKeys.-provisional-slowKeysEnable", - "slowkeys-delay": { - "transform": { - "type": "fluid.transforms.linearScale", - "valuePath": "control.keyboardEnhancement.slowKeys.slowKeysInterval", - "factor": 1000 - } - }, - "bouncekeys-enable": "control.keyboardEnhancement.debounceKeys.-provisional-debounceEnable", - "bouncekeys-delay": { - "transform": { - "type": "fluid.transforms.linearScale", - "valuePath": "control.keyboardEnhancement.debounceKeys.debounceInterval", - "factor": 1000 - } - }, - "mousekeys-enable": "control.mouseEmulation.-provisional-mouseEmulationEnabled", - "mousekeys-init-delay": { - "transform": { - "type": "fluid.transforms.linearScale", - "valuePath": "control.mouseEmulation.-provisional-initDelay", - "factor": 1000 - } - }, - "mousekeys-max-speed": { - "transform": { - "type": "fluid.transforms.linearScale", - "valuePath": "control.mouseEmulation.cursorSpeed", - "factor": 1000 - } - }, - "mousekeys-accel-time": { - "transform": { - "type": "fluid.transforms.linearScale", - "valuePath": "control.mouseEmulation.cursorAcceleration", - "factor": 1000 - } - } - }, - "options": { - "schema": "org.gnome.desktop.a11y.keyboard" - } - } - ], - "lifecycleManager": { - "start": [ "setSettings" ], - "stop": [ "restoreSettings" ] - } - }, - - { - "name": "ORCA Screen Reader", - "id": "org.gnome.orca", - "contexts": { - "OS": [{ - "id": "linux", - "version": ">=2.6.26" - }] - }, - "settingsHandlers": [ - { - "type": "gpii.orca.set", - "options": { - "user": "${{userToken}}" - }, - "capabilities": [ - "display.screenReader", - "applications.org\\.gnome\\.orca.id", - "display.screenReader.applications.org\\.gnome\\.orca.name" - ], - "capabilitiesTransformations": { - "enableTutorialMessages": "display.screenReader.-provisional-speakTutorialMessages", - "enableEchoByCharacter": "display.screenReader.-provisional-keyEcho", - "enableEchoByWord": "display.screenReader.-provisional-wordEcho", - "enableBraille": "display.screenReader.-provisional-screenReaderBrailleOutput", - "enableSpeech": "display.screenReader.-provisional-screenReaderTTSEnabled", - "sayAllStyle": { - "transform": { - "type": "fluid.transforms.valueMapper", - "inputPath": "display.textReadingHighlight.readingUnit", - "defaultInputValue": "sentence", - "options": { - "line": { - "outputValue": 0 - }, - "sentence": { - "outputValue": 1 - } - } - } - }, - "voices\\.default\\.rate": { - "transform": { - "type": "fluid.transforms.binaryOp", - "right": 50, - "operator": "+", - "left": { - "transform": { - "type": "fluid.transforms.binaryOp", - "operator": "/", - "left": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenReader.speechRate", - "operator": "-", - "right": 170 - } - }, - "right": { - "transform": { - "type": "fluid.transforms.condition", - "condition": { - "transform" : { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenReader.speechRate", - "operator": "<", - "right": 170 - } - }, - "true": 1.8, - "false": 4.4 - } - } - } - } - } - }, - "voices\\.default\\.family": { - "transform": { - "type": "fluid.transforms.valueMapper", - "inputPath": "display.screenReader.-provisional-auditoryOutLanguage", - "defaultInputValue": "en", - "options": { - "en": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "en", - "name": "english" - } - } - } - }, - "en-GB": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "en", - "name": "english" - } - } - } - }, - "en-US": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "en", - "name": "english-us" - } - } - } - }, - "en-scotland": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "en", - "name": "en-scottish" - } - } - } - }, - "en-BZ": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "en", - "name": "en-westindies" - } - } - } - }, - "en-BS": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "en", - "name": "en-westindies" - } - } - } - }, - "en-AG": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "en", - "name": "en-westindies" - } - } - } - }, - "en-AI": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "en", - "name": "en-westindies" - } - } - } - }, - "af": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "af", - "name": "afrikaans" - } - } - } - }, - "bg": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "bg", - "name": "bulgarian-test" - } - } - } - }, - "bs": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "bs", - "name": "bosnian" - } - } - } - }, - "ca": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "ca", - "name": "catalan" - } - } - } - }, - "cs": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "cs", - "name": "czech" - } - } - } - }, - "cy": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "cy", - "name": "welsh-test" - } - } - } - }, - "da": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "da", - "name": "danish" - } - } - } - }, - "de": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "de", - "name": "german" - } - } - } - }, - "el": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "el", - "name": "greek" - } - } - } - }, - "grc": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "grc", - "name": "greek-ancient" - } - } - } - }, - "eo": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "eo", - "name": "esperanto" - } - } - } - }, - "es": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "es", - "name": "spanish" - } - } - } - }, - "es-419": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "es", - "name": "spanish-latin-american" - } - } - } - }, - "et": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "et", - "name": "estonian" - } - } - } - }, - "fi": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "fi", - "name": "finnish" - } - } - } - }, - "fr": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "fr", - "name": "french" - } - } - } - }, - "fr-BE": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "(Belgium)", - "name": "french" - } - } - } - }, - "hi": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "hi", - "name": "hindi" - } - } - } - }, - "hr": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "hr", - "name": "croatian" - } - } - } - }, - "hu": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "hu", - "name": "hungarian" - } - } - } - }, - "hy": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "hy", - "name": "armenian" - } - } - } - }, - "hy-arevmda": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "hy", - "name": "armenian-west" - } - } - } - }, - "id": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "id", - "name": "indonesian-test" - } - } - } - }, - "is": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "is", - "name": "icelandic-test" - } - } - } - }, - "it": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "it", - "name": "italian" - } - } - } - }, - "jbo": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "jbo", - "name": "lojban" - } - } - } - }, - "ka": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "ka", - "name": "georgian-test" - } - } - } - }, - "kn": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "kn", - "name": "kannada" - } - } - } - }, - "ku": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "ku", - "name": "kurdish" - } - } - } - }, - "la": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "la", - "name": "latin" - } - } - } - }, - "lv": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "lv", - "name": "latvian" - } - } - } - }, - "mk": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "mk", - "name": "macedonian-test" - } - } - } - }, - "ml": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "ml", - "name": "malayalam" - } - } - } - }, - "nl": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "nl", - "name": "dutch-test" - } - } - } - }, - "no": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "no", - "name": "norwegian" - } - } - } - }, - "pap": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "pap", - "name": "papiamento-test" - } - } - } - }, - "pl": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "pl", - "name": "polish" - } - } - } - }, - "pt-BR": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "pt", - "name": "brazil" - } - } - } - }, - "pt-PT": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "pt", - "name": "portugal" - } - } - } - }, - "ro": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "ro", - "name": "romanian" - } - } - } - }, - "ru": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "ru", - "name": "russian_test" - } - } - } - }, - "sk": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "sk", - "name": "slovak" - } - } - } - }, - "sq": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "sq", - "name": "albanian" - } - } - } - }, - "sr": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "sr", - "name": "serbian" - } - } - } - }, - "sv": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "sv", - "name": "swedish" - } - } - } - }, - "sw": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "sw", - "name": "swahili-test" - } - } - } - }, - "ta": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "ta", - "name": "tamil" - } - } - } - }, - "tr": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "tr", - "name": "turkish" - } - } - } - }, - "vi": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "vi", - "name": "vietnam" - } - } - } - }, - "zh-cmn": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "zh", - "name": "Mandarin" - } - } - } - }, - "cmn": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "zh", - "name": "Mandarin" - } - } - } - }, - "zh-yue": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "zh", - "name": "cantonese" - } - } - } - }, - "yue": { - "outputValue": { - "transform" : { - "type": "fluid.transforms.literalValue", - "value": { - "locale": "zh", - "name": "cantonese" - } - } - } - } - } - } - }, - "verbalizePunctuationStyle": { - "transform": { - "type": "fluid.transforms.valueMapper", - "inputPath": "display.screenReader.-provisional-punctuationVerbosity", - "defaultInputValue": "most", - "options": { - "none": { - "outputValue": 3 - }, - "some": { - "outputValue": 2 - }, - "most": { - "outputValue": 1 - }, - "all": { - "outputValue": 0 - } - } - } - } - } - } - ], - "lifecycleManager": { - "start": [ - "setSettings", - { - "type": "gpii.launch.exec", - "command": "gsettings set org.gnome.desktop.a11y.applications screen-reader-enabled true" - } - ], - "stop": [ - { - "type": "gpii.launch.exec", - "command": "gsettings set org.gnome.desktop.a11y.applications screen-reader-enabled false" - }, - "restoreSettings" - ] - } - }, - - { - "name": "EasyOne Communicator Linux", - "id": "trace.easyOne.communicator.linux", - "contexts": { - "OS": [ - { - "id": "linux", - "version": ">=2.6.26" - } - ] - }, - "settingsHandlers": [ - { - "type": "gpii.settingsHandlers.noSettings", - "capabilities": [ - "content.simplification", - "applications.trace\\.easyOne\\.communicator\\.linux.id", - "content.simplification.applications.trace\\.easyOne\\.communicator\\.linux.name" - ] - } - ], - "lifecycleManager" : { - "start": [ - { - "type": "gpii.launch.exec", - "command": "google-chrome http://easy1234.org/user/${{userToken}}" - } - ], - "stop": [ - { - "type": "gpii.launch.exec", - "command": "pkill -2 chrome" - } - ] - } - }, - - { - "name": "EasyOne Communicator Sudan", - "id": "trace.easyOne.sudan.linux", - "contexts": { - "OS": [ - { - "id": "linux", - "version": ">=2.6.26" - } - ] - }, - "settingsHandlers": [ - { - "type": "gpii.settingsHandlers.noSettings", - "capabilities": [ - "content.pictorialSimplification", - "applications.trace\\.easyOne\\.sudan\\.linux.id", - "content.pictorialSimplification.applications.trace\\.easyOne\\.sudan\\.linux.name" - ] - } - ], - "lifecycleManager" : { - "start": [ - { - "type": "gpii.launch.exec", - "command": "google-chrome http://easy1234.org/sudan" - } - ], - "stop": [ - { - "type": "gpii.launch.exec", - "command": "pkill -2 chrome" - } - ] - } - }, - - { - "name": "Web Anywhere", - "id": "webinsight.webAnywhere.linux", - "contexts": { - "OS": [ - { - "id": "linux", - "version": ">=2.6.26" - } - ] - }, - "settingsHandlers": [ - { - "type": "gpii.settingsHandlers.noSettings", - "capabilities": [ - "display.screenReader", - "applications.webinsight\\.webAnywhere\\.linux.id", - "display.screenReader.applications.webinsight\\.webAnywhere\\.linux.name" - ] - } - ], - "lifecycleManager" : { - "start": [ - { - "type": "gpii.launch.exec", - "command": "google-chrome http://webanywhere.cs.washington.edu/beta/?starting_url=http://ec2-107-21-143-113.compute-1.amazonaws.com:443/demos/Mammals.html?token=${{userToken}}" - } - ], - "stop": [ - { - "type": "gpii.launch.exec", - "command": "pkill -2 chrome" - } - ] - } - } -] diff --git a/testData/solutions/linux.json5 b/testData/solutions/linux.json5 new file mode 100644 index 000000000..c658b989f --- /dev/null +++ b/testData/solutions/linux.json5 @@ -0,0 +1,1757 @@ +{ + "org.gnome.desktop.a11y.magnifier": { + "name": "GNOME Shell Magnifier", + "contexts": { + "OS": [ + { + "id": "linux", + "version": ">=2.6.26" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + "settingsHandlers": { + "configuration": { + "type": "gpii.gsettings", + "liveness": "live", + "options": { + "schema": "org.gnome.desktop.a11y.magnifier" + }, + "supportedSettings": { + "mag-factor": { + "schema": { + "title": "Magnification", + "description": "Set up magnification level", + "type": "number", + "default": 1, + "minimum": 1, + "maximum": 5 + } + }, + "show-cross-hairs": { + "schema": { + "title": "Show crosshairs", + "description": "Whether to show crosshairs", + "type": "boolean", + "default": false + } + }, + "focus-tracking": { + "schema": { + "title": "Focus Tracking", + "description": "The mouse pointer is always in view within the magnifier", + "default": 0, + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Centered", "Push", "Proportional"] + } + }, + "caret-tracking": { + "schema": { + "title": "Caret Tracking", + "description": "The mouse pointer is always in view within the magnifier", + "default": 0, + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Centered", "Push", "Proportional"] + } + }, + "lens-mode": { + "schema": { + "title": "Enable lens mode", + "description": "Whether the magnified view should be centered over the location of the system mouse and move with it", + "type": "boolean", + "default": false + } + }, + "mouse-tracking": { + "schema": { + "title": "Mouse Tracking Mode", + "description": "Determines the position of the magnified mouse image within the magnified view and how it reacts to system mouse movement", + "default": "proportional", + "enum": ["none", "centered", "push", "proportional"], + "enumLabels": ["None", "Centered", "Push", "Proportional"] + } + }, + "screen-position": { + "schema": { + "title": "Screen Position", + "description": "The position of the magnified view", + "default": "full-screen", + "enum": ["full-screen", "top-half", "bottom-half", "left-half", "right-half"], + "enumLabels": ["Full screen", "Top half", "Bottom half", "Left half", "Right half"] + } + }, + "scroll-at-edges": { + "schema": { + "title": "Scroll magnified contents beyond the edges of the desktop", + "description": "For centered mouse tracking, when the system pointer is at or near the edge of the screen, the magnified contents continue to scroll such that the screen edge moves into the magnified view", + "type": "boolean", + "default": false + } + } + }, + "capabilitiesTransformations": { + "mag-factor": "http://registry\\.gpii\\.net/common/magnification", + "show-cross-hairs": "http://registry\\.gpii\\.net/common/showCrosshairs", + "transform": { + "type": "fluid.transforms.arrayToSetMembership", + "inputPath": "http://registry\\.gpii\\.net/common/tracking", + "outputPath": "", + "presentValue": "proportional", + "missingValue": "none", + "options": { + "focus": "focus-tracking", + "caret": "caret-tracking", + "mouse": "mouse-tracking" + } + }, + "screen-position": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/magnifierPosition", + "match": { + "FullScreen": "full-screen", + "Lens": "full-screen", + "LeftHalf": "left-half", + "RightHalf": "right-half", + "TopHalf": "top-half", + "BottomHalf": "bottom-half" + }, + "noMatch": { + "outputValue": "full-screen" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/magnification": "mag-factor", + "http://registry\\.gpii\\.net/common/showCrosshairs": "show-cross-hairs", + "transform": [ + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "screen-position", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/magnifierPosition", + "match": { + "full-screen": "FullScreen", + "left-half": "LeftHalf", + "right-half": "RightHalf", + "top-half": "TopHalf", + "bottom-half": "BottomHalf" + } + }, + { + "type": "fluid.transforms.valueMapper", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/tracking", + "defaultInputPath": "mouse-tracking", + "match": { + "centered": "mouse" + } + } + ] + } + } + }, + "launchHandlers": { + "launcher": { + "type": "gpii.gsettings.launch", + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-magnifier-enabled" + } + } + }, + "update": [ + "settings.configuration" + ], + "isInstalled": [ + { + "type": "gpii.packageKit.find", + "name": "gnome-shell" + } + ] + }, + "org.gnome.desktop.interface": { + "name": "GNOME Interface Settings", + "contexts": { + "OS": [ + { + "id": "linux", + "version": ">=2.6.26" + } + ] + }, + "settingsHandlers": { + "configuration": { + "type": "gpii.gsettings", + "liveness": "live", + "supportedSettings": { + "text-scaling-factor": { + "schema": { + "title": "Text scaling factor", + "description": "Factor used to enlarge or reduce text display, without changing font size", + "type": "number", + "default": 1, + "minimum": 0.5, + "maximum": 3 + } + }, + "cursor-size": { + "schema": { + "title": "Cursor size", + "description": "Size of the cursor used as cursor theme", + "type": "number", + "default": 24 + } + }, + "gtk-theme": { + "schema": { + "title": "Gtk+ Theme", + "description": "Basename of the default theme used by gtk+", + "type": "string", + "default": "Adwaita" + } + }, + "icon-theme": { + "schema": { + "title": "Icon Theme", + "description": "Icon theme to use for the panel, nautilus etc", + "type": "string", + "default": "Adwaita" + } + } + }, + "capabilitiesTransformations": { + "text-scaling-factor": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/fontSize", + "factor": 0.08333333333333333 + } + }, + "cursor-size": { + "transform": { + "type": "fluid.transforms.quantize", + "inputPath": "http://registry\\.gpii\\.net/common/cursorSize", + "ranges": [ + { + "upperBound": 0, + "output": -1 + }, + { + "upperBound": 0.333, + "output": 20 + }, + { + "upperBound": 0.666, + "output": 29 + }, + { + "output": 41 + } + ] + } + }, + "gtk-theme": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/common/highContrast/enabled", + "true": "HighContrast", + "false": "Adwaita" + } + }, + "icon-theme": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/common/highContrast/enabled", + "true": "HighContrast", + "false": "gnome" + } + } + }, + "options": { + "schema": "org.gnome.desktop.interface" + }, + "inverseCapabilitiesTransformations": { + "transform": [ + { + "type": "fluid.transforms.binaryOp", + "right": 12, + "operator": "*", + "leftPath": "text-scaling-factor", + "outputPath": "http://registry\\.gpii\\.net/common/fontSize" + }, + { + "type": "fluid.transforms.linearScale", + "inputPath": "cursor-size", + "outputPath": "http://registry\\.gpii\\.net/common/cursorSize", + "factor": 0.01 + }, + { + "type": "fluid.transforms.binaryOp", + "outputPath": "http://registry\\.gpii\\.net/common/highContrast/enabled", + "left": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "gtk-theme", + "operator": "===", + "right": "HighContrast" + } + }, + "right": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "icon-theme", + "operator": "===", + "right": "HighContrast" + } + }, + "operator": "&&" + } + ] + } + } + }, + "isInstalled": [ + { + "type": "gpii.packageKit.find", + "name": "gsettings-desktop-schemas" + } + ] + }, + "org.gnome.nautilus": { + "name": "GNOME Nautilus Settings", + "contexts": { + "OS": [ + { + "id": "linux", + "version": ">=2.6.26" + } + ] + }, + "settingsHandlers": { + "configuration": { + "type": "gpii.gsettings", + "liveness": "live", + "supportedSettings": { + // We need at least one setting with a schema in order for the solution to show in the "codex" at all. + "font": { + "schema": { + "title": "Font", + "description": "Font.", + "type": "string" + } + } + }, + "options": { + "schema": "org.gnome.nautilus.desktop" + } + } + }, + "isInstalled": [ + { + "type": "gpii.packageKit.find", + "name": "nautilus" + } + ] + }, + "org.gnome.desktop.a11y.applications.onscreen-keyboard": { + "name": "GNOME Assistive Technology - Screen Keyboard", + "contexts": { + "OS": [ + { + "id": "linux", + "version": ">=2.6.26" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/onScreenKeyboard/enabled" + ], + "launchHandlers": { + "launching": { + "type": "gpii.gsettings.launch", + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-keyboard-enabled" + } + } + }, + "update": [], + "isInstalled": [ + { + "type": "gpii.packageKit.find", + "name": "gnome-shell" + } + ] + }, + "org.gnome.desktop.a11y.keyboard": { + "name": "GNOME Shell Keyboard Settings", + "contexts": { + "OS": [ + { + "id": "linux", + "version": ">=2.6.26" + } + ] + }, + "settingsHandlers": { + "configuration": { + "type": "gpii.gsettings", + "liveness": "live", + "supportedSettings": { + "stickykeys-enable": {}, + "slowkeys-enable": {}, + // We need at least one setting with a schema in order for the solution to show in the "codex" at all. + "slowkeys-delay": { + "schema": { + "title": "Slowkeys Delay", + "description": "Slowkeys delay.", + "type": "integer" + } + }, + "bouncekeys-enable": {}, + "bouncekeys-delay": {}, + "mousekeys-enable": {}, + "mousekeys-init-delay": {}, + "mousekeys-max-speed": {}, + "mousekeys-accel-time": {} + }, + "capabilitiesTransformations": { + "stickykeys-enable": "http://registry\\.gpii\\.net/common/stickyKeys", + "slowkeys-enable": "http://registry\\.gpii\\.net/common/slowKeys/enabled", + "slowkeys-delay": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/slowKeysInterval", + "factor": 1000 + } + }, + "bouncekeys-enable": "http://registry\\.gpii\\.net/common/debounce/enabled", + "mousekeys-enable": "http://registry\\.gpii\\.net/common/mouseEmulation/enabled", + "mousekeys-init-delay": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/initDelay", + "factor": 1000 + } + }, + "mousekeys-max-speed": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/cursorSpeed", + "factor": 1000 + } + } + }, + "options": { + "schema": "org.gnome.desktop.a11y.keyboard" + } + } + }, + "isInstalled": [ + { + "type": "gpii.packageKit.find", + "name": "gsettings-desktop-schemas" + } + ] + }, + "org.gnome.desktop.wm.preferences": { + "name": "GNOME desktop Window Manager preferences", + "contexts": { + "OS": [ + { + "id": "linux", + "version": ">=2.6.26" + } + ] + }, + "settingsHandlers": { + "configuration": { + "type": "gpii.gsettings", + "liveness": "live", + "options": { + "schema": "org.gnome.desktop.wm.preferences" + } + } + }, + "isInstalled": [ + { + "type": "gpii.packageKit.find", + "name": "gnome-shell" + } + ] + }, + "org.gnome.shell.overrides": { + "name": "GNOME Shell overrides", + "contexts": { + "OS": [ + { + "id": "linux", + "version": ">=2.6.26" + } + ] + }, + "settingsHandlers": { + "configuration": { + "type": "gpii.gsettings", + "liveness": "live", + "options": { + "schema": "org.gnome.shell.overrides" + } + } + }, + "isInstalled": [ + { + "type": "gpii.packageKit.find", + "name": "gnome-shell" + } + ] + }, + "org.gnome.orca": { + "name": "ORCA Screen Reader", + "contexts": { + "OS": [ + { + "id": "linux", + "version": ">=2.6.26" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled" + ], + "settingsHandlers": { + "configuration": { + "type": "gpii.orca", + "liveness": "manualRestart", + "options": { + "user": "${{gpiiKey}}" + }, + "supportedSettings": { + "enableTutorialMessages": { + "schema": { + "title": "Enable Tutorial Messages", + "description": "Enable tutorial messages", + "enum": [0, 1], + "enumLabels": ["off", "on"], + "default": 0 + } + }, + "enableEchoByCharacter": { + "schema": { + "title": "Enable Echo by Character", + "description": "Enable echo by character", + "enum": [0, 1], + "enumLabels": ["off", "on"], + "default": 0 + } + }, + "enableEchoByWord": { + "schema": { + "title": "Enable Echo by Word", + "description": "Enable echo by word", + "enum": [0, 1], + "enumLabels": ["off", "on"], + "default": 0 + } + }, + "enableBraille": { + "schema": { + "title": "Enable Braille", + "description": "Enable braille", + "enum": [0, 1], + "enumLabels": ["off", "on"], + "default": 0 + } + }, + "enableSpeech": { + "schema": { + "title": "Enable Speech", + "description": "Enable speech", + "enum": [0, 1], + "enumLabels": ["off", "on"], + "default": 0 + } + }, + "sayAllStyle": { + "schema": { + "title": "Enable Say all Style", + "description": "Enable say all style", + "enum": [0, 1], + "enumLabels": ["off", "on"], + "default": 0 + } + }, + "voices.default.rate": { + "schema": { + "title": "Voice Rate", + "description": "Config the voice rate", + "type": "number", + "default": 100 + } + }, + "voices.default.average-pitch": { + "schema": { + "title": "Voice Average Pitch", + "description": "Config the voice average pitch", + "type": "number", + "default": 10 + } + }, + "voices.default.gain": { + "schema": { + "title": "Voice Gain", + "description": "Config the voice gain", + "type": "number", + "default": 10 + } + }, + "voices.default.family": { + "schema": { + "title": "Voice Family", + "description": "Config the voice family to be used", + "default": { + "locale": "en", + "name": "english" + } + } + }, + "verbalizePunctuationStyle": { + "schema": { + "title": "Verbalize Punctuation Style", + "description": "Config the verbalize punctuation style", + "enum": [0, 1, 2, 3], + "enumLabels": ["none", "some", "most", "all"], + "default": 0 + } + } + }, + "capabilitiesTransformations": { + "enableTutorialMessages": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/common/speakTutorialMessages", + "true": 1, + "false": 0 + } + }, + "enableEchoByCharacter": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/common/keyEcho", + "true": 1, + "false": 0 + } + }, + "enableEchoByWord": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/common/wordEcho", + "true": 1, + "false": 0 + } + }, + "enableBraille": "http://registry\\.gpii\\.net/common/screenReaderBrailleOutput", + "enableSpeech": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled", + "true": 1, + "false": 0 + } + }, + "sayAllStyle": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/readingUnit", + "match": { + "line": 0, + "sentence": 1 + }, + "noMatch": { + "outputValue": 1 + } + } + }, + "voices\\.default\\.rate": { + "transform": { + "type": "fluid.transforms.binaryOp", + "right": 50, + "operator": "+", + "left": { + "transform": { + "type": "fluid.transforms.binaryOp", + "operator": "/", + "left": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/speechRate", + "operator": "-", + "right": 170 + } + }, + "right": { + "transform": { + "type": "fluid.transforms.condition", + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/speechRate", + "operator": "<", + "right": 170 + } + }, + "true": 1.8, + "false": 4.4 + } + } + } + } + } + }, + "voices\\.default\\.average-pitch": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 10 + } + }, + "voices\\.default\\.gain": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volumeTTS", + "factor": 10 + } + }, + "voices\\.default\\.family": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "noMatch": { + "outputValue": { + "locale": "en", + "name": "english" + } + }, + "match": { + "en": { + "outputValue": { + "locale": "en", + "name": "english" + } + }, + "en-GB": { + "outputValue": { + "locale": "en", + "name": "english" + } + }, + "en-US": { + "outputValue": { + "locale": "en", + "name": "english-us" + } + }, + "en-scotland": { + "outputValue": { + "locale": "en", + "name": "en-scottish" + } + }, + "en-BZ": { + "outputValue": { + "locale": "en", + "name": "en-westindies" + } + }, + "en-BS": { + "outputValue": { + "locale": "en", + "name": "en-westindies" + } + }, + "en-AG": { + "outputValue": { + "locale": "en", + "name": "en-westindies" + } + }, + "en-AI": { + "outputValue": { + "locale": "en", + "name": "en-westindies" + } + }, + "af": { + "outputValue": { + "locale": "af", + "name": "afrikaans" + } + }, + "bg": { + "outputValue": { + "locale": "bg", + "name": "bulgarian-test" + } + }, + "bs": { + "outputValue": { + "locale": "bs", + "name": "bosnian" + } + }, + "ca": { + "outputValue": { + "locale": "ca", + "name": "catalan" + } + }, + "cs": { + "outputValue": { + "locale": "cs", + "name": "czech" + } + }, + "cy": { + "outputValue": { + "locale": "cy", + "name": "welsh-test" + } + }, + "da": { + "outputValue": { + "locale": "da", + "name": "danish" + } + }, + "de": { + "outputValue": { + "locale": "de", + "name": "german" + } + }, + "el": { + "outputValue": { + "locale": "el", + "name": "greek" + } + }, + "grc": { + "outputValue": { + "locale": "grc", + "name": "greek-ancient" + } + }, + "eo": { + "outputValue": { + "locale": "eo", + "name": "esperanto" + } + }, + "es": { + "outputValue": { + "locale": "es", + "name": "spanish" + } + }, + "es-419": { + "outputValue": { + "locale": "es", + "name": "spanish-latin-american" + } + }, + "et": { + "outputValue": { + "locale": "et", + "name": "estonian" + } + }, + "fi": { + "outputValue": { + "locale": "fi", + "name": "finnish" + } + }, + "fr": { + "outputValue": { + "locale": "fr", + "name": "french" + } + }, + "fr-BE": { + "outputValue": { + "locale": "(Belgium)", + "name": "french" + } + }, + "hi": { + "outputValue": { + "locale": "hi", + "name": "hindi" + } + }, + "hr": { + "outputValue": { + "locale": "hr", + "name": "croatian" + } + }, + "hu": { + "outputValue": { + "locale": "hu", + "name": "hungarian" + } + }, + "hy": { + "outputValue": { + "locale": "hy", + "name": "armenian" + } + }, + "hy-arevmda": { + "outputValue": { + "locale": "hy", + "name": "armenian-west" + } + }, + "id": { + "outputValue": { + "locale": "id", + "name": "indonesian-test" + } + }, + "is": { + "outputValue": { + "locale": "is", + "name": "icelandic-test" + } + }, + "it": { + "outputValue": { + "locale": "it", + "name": "italian" + } + }, + "jbo": { + "outputValue": { + "locale": "jbo", + "name": "lojban" + } + }, + "ka": { + "outputValue": { + "locale": "ka", + "name": "georgian-test" + } + }, + "kn": { + "outputValue": { + "locale": "kn", + "name": "kannada" + } + }, + "ku": { + "outputValue": { + "locale": "ku", + "name": "kurdish" + } + }, + "la": { + "outputValue": { + "locale": "la", + "name": "latin" + } + }, + "lv": { + "outputValue": { + "locale": "lv", + "name": "latvian" + } + }, + "mk": { + "outputValue": { + "locale": "mk", + "name": "macedonian-test" + } + }, + "ml": { + "outputValue": { + "locale": "ml", + "name": "malayalam" + } + }, + "nl": { + "outputValue": { + "locale": "nl", + "name": "dutch-test" + } + }, + "no": { + "outputValue": { + "locale": "no", + "name": "norwegian" + } + }, + "pap": { + "outputValue": { + "locale": "pap", + "name": "papiamento-test" + } + }, + "pl": { + "outputValue": { + "locale": "pl", + "name": "polish" + } + }, + "pt-BR": { + "outputValue": { + "locale": "pt", + "name": "brazil" + } + }, + "pt-PT": { + "outputValue": { + "locale": "pt", + "name": "portugal" + } + }, + "ro": { + "outputValue": { + "locale": "ro", + "name": "romanian" + } + }, + "ru": { + "outputValue": { + "locale": "ru", + "name": "russian_test" + } + }, + "sk": { + "outputValue": { + "locale": "sk", + "name": "slovak" + } + }, + "sq": { + "outputValue": { + "locale": "sq", + "name": "albanian" + } + }, + "sr": { + "outputValue": { + "locale": "sr", + "name": "serbian" + } + }, + "sv": { + "outputValue": { + "locale": "sv", + "name": "swedish" + } + }, + "sw": { + "outputValue": { + "locale": "sw", + "name": "swahili-test" + } + }, + "ta": { + "outputValue": { + "locale": "ta", + "name": "tamil" + } + }, + "tr": { + "outputValue": { + "locale": "tr", + "name": "turkish" + } + }, + "vi": { + "outputValue": { + "locale": "vi", + "name": "vietnam" + } + }, + "zh-cmn": { + "outputValue": { + "locale": "zh", + "name": "Mandarin" + } + }, + "cmn": { + "outputValue": { + "locale": "zh", + "name": "Mandarin" + } + }, + "zh-yue": { + "outputValue": { + "locale": "zh", + "name": "cantonese" + } + }, + "yue": { + "outputValue": { + "locale": "zh", + "name": "cantonese" + } + } + } + } + }, + "verbalizePunctuationStyle": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "match": { + "none": 3, + "some": 2, + "most": 1, + "all": 0 + }, + "noMatch": { + "outputValue": 1 + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/screenReaderBrailleOutput": "enableBraille", + "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled": "enableSpeech", + "transform": [ + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "sayAllStyle", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/readingUnit", + "match": { + "0": "line", + "1": "sentence" + }, + "noMatch": { + "outputValue": "sentence" + } + }, + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "verbalizePunctuationStyle", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "defaultInputValue": "1", + "match": { + "3": "none", + "2": "some", + "1": "most", + "0": "all" + } + }, + { + "type": "fluid.transforms.binaryOp", + "outputPath": "http://registry\\.gpii\\.net/common/speechRate", + "right": 170, + "operator": "+", + "left": { + "transform": { + "type": "fluid.transforms.binaryOp", + "operator": "*", + "left": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "voices\\.default\\.rate", + "operator": "-", + "right": 50 + } + }, + "right": { + "transform": { + "type": "fluid.transforms.condition", + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "voices\\.default\\.rate", + "operator": "<", + "right": 50 + } + }, + "true": 1.8, + "false": 4.4 + } + } + } + } + }, + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "voices\\.default\\.family.name", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "noMatch": { + "outputValue": "en" + }, + "match": { + "english": "en", + "english-us": "en-US", + "en-scottish": "en-scotland", + "en-westindies": "en-BZ", + "afrikaans": "af", + "bulgarian-test": "bg", + "bosnian": "bs", + "catalan": "ca", + "czech": "cs", + "welsh-test": "cy", + "danish": "da", + "german": "de", + "greek": "el", + "greek-ancient": "grc", + "esperanto": "eo", + "spanish": "es", + "spanish-latin-american": "es-419", + "estonian": "et", + "finnish": "fi", + "french": "fr", + "hindi": "hi", + "croatian": "hr", + "hungarian": "hu", + "armenian": "hy", + "armenian-west": "hy-arevmda", + "indonesian-test": "id", + "icelandic-test": "is", + "italian": "it", + "lojban": "jbo", + "georgian-test": "ka", + "kannada": "kn", + "kurdish": "ku", + "latin": "la", + "latvian": "lv", + "macedonian-test": "mk", + "malayalam": "ml", + "dutch-test": "nl", + "norwegian": "no", + "papiamento-test": "pap", + "polish": "pl", + "brazil": "pt-BR", + "portugal": "pt-PT", + "romanian": "ro", + "russian_test": "ru", + "slovak": "sk", + "albanian": "sq", + "serbian": "sr", + "swedish": "sv", + "swahili-test": "sw", + "tamil": "ta", + "turkish": "tr", + "vietnam": "vi", + "Mandarin": "zh-cmn", + "cantonese": "zh-yue" + } + } + ] + } + } + }, + "launchHandlers": { + "launcher": { + "type": "gpii.gsettings.launch", + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-reader-enabled" + } + } + }, + "isInstalled": [ + { + "type": "gpii.packageKit.find", + "name": "orca" + } + ] + }, + "org.alsa-project": { + "name": "ALSA System Volume", + "contexts": { + "OS": [ + { + "id": "linux", + "version": ">=2.6.26" + } + ] + }, + "settingsHandlers": { + "configuration": { + "type": "gpii.alsa", + "liveness": "live", + "supportedSettings": { + "masterVolume": { + "schema": { + "title": "Master volume", + "description": "Master volume.", + "type": "number" + } + } + }, + "capabilitiesTransformations": { + "masterVolume": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volume", + "factor": 100 + } + } + } + } + }, + "isInstalled": [ + { + "type": "gpii.packageKit.find", + "name": "alsa" + }, + { + "type": "gpii.packageKit.find", + "name": "alsa-lib" + } + ] + }, + "trace.easyOne.communicator.linux": { + "name": "EasyOne Communicator Linux", + "contexts": { + "OS": [ + { + "id": "linux", + "version": ">=2.6.26" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/simplifiedUi/enabled" + ], + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "setTrue": { + "type": "gpii.launch.exec", + "command": "google-chrome http://easyone.gpii.net/user/${{gpiiKey}}" + }, + "setFalse": { + "type": "gpii.launch.exec", + "command": "pkill -2 chrome" + }, + "getState": [ + { + "type": "gpii.processReporter.neverRunning" + } + ] + } + } + }, + "isInstalled": [ + { + "type": "gpii.packageKit.find", + "name": "google-chrome-beta" + }, + { + "type": "gpii.packageKit.find", + "name": "google-chrome-stable" + }, + { + "type": "gpii.packageKit.find", + "name": "google-chrome-unstable" + } + ] + }, + "trace.easyOne.sudan.linux": { + "name": "EasyOne Communicator Sudan", + "contexts": { + "OS": [ + { + "id": "linux", + "version": ">=2.6.26" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/simplifiedUi/enabled" + ], + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.noSettings", + "liveness": "live" + } + }, + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "setTrue": { + "type": "gpii.launch.exec", + "command": "google-chrome http://easyone.gpii.net/sudan" + }, + "setFalse": { + "type": "gpii.launch.exec", + "command": "pkill -2 chrome" + }, + "getState": [ + { + "type": "gpii.processReporter.neverRunning" + } + ] + } + } + }, + "isInstalled": [ + { + "type": "gpii.packageKit.find", + "name": "google-chrome-beta" + }, + { + "type": "gpii.packageKit.find", + "name": "google-chrome-stable" + }, + { + "type": "gpii.packageKit.find", + "name": "google-chrome-unstable" + } + ] + }, + "webinsight.webAnywhere.linux": { + "name": "Web Anywhere", + "contexts": { + "OS": [ + { + "id": "linux", + "version": ">=2.6.26" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled" + ], + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "setTrue": { + "type": "gpii.launch.exec", + "command": "google-chrome http://webanywhere.cs.washington.edu/beta/?starting_url=http%3A%2F%2Fcloud4all.info" + }, + "setFalse": { + "type": "gpii.launch.exec", + "command": "pkill -2 chrome" + }, + "getState": [ + { + "type": "gpii.processReporter.neverRunning" + } + ] + } + } + }, + "isInstalled": [ + { + "type": "gpii.packageKit.find", + "name": "google-chrome-beta" + }, + { + "type": "gpii.packageKit.find", + "name": "google-chrome-stable" + }, + { + "type": "gpii.packageKit.find", + "name": "google-chrome-unstable" + } + ] + }, + "org.freedesktop.xrandr": { + "name": "Xrandr", + "contexts": { + "OS": [ + { + "id": "linux", + "version": ">=2.6.26" + } + ] + }, + "settingsHandlers": { + "configuration": { + "type": "gpii.xrandr", + "liveness": "liveRestart", + "supportedSettings": { + "screen-resolution": { + schema: { + "title": "Screen resolution", + "description": "Screen resolution", + "type": "array", + "items": { + "type": "object", + "properties": { + "height": { + "type": "integer", + "minimum": 0 + }, + "width": { + "type": "integer", + "minimum": 0 + } + } + } + } + } + } + } + } + }, + "net.gpii.uioPlus": { + "name": "UIO+", + "contexts": { + "OS": [ + { + "id": "linux", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.webSockets", + "liveness": "live", + "options": { + "path": "net.gpii.uioPlus" + }, + "supportedSettings": { + "characterSpace": { + "schema": { + "title": "Character Space", + "description": "Adjust the space between characters.", + "type": "number", + "default": 0, + "minimum": -0.1, + "maximum": 1, + "multipleOf": 0.1 + } + }, + "contrastTheme": { + "schema": { + "title": "Contrast Theme", + "description": "Change text and background colors.", + "default": "default", + "enum": [ + "default", + "bw", + "wb", + "by", + "yb", + "gd", + "gw", + "bbr" + ], + "enumLabels": [ + "Default Theme", + "Black Foreground on a White Background", + "White Foreground on a Black Background", + "Black Foreground on a Yellow Background", + "Yellow Foreground on a Black Background", + "Light Gray Foreground on a Dark Gray Background", + "Gray Foreground on a White Background", + "Black Foreground on a Brown Background" + ] + } + }, + "fontSize": { + "schema": { + "title": "Font Size", + "description": "Adjust text size.", + "type": "number", + "default": 1, + "minimum": 0.25, + "maximum": 5 + } + }, + "inputsLargerEnabled": { + "schema": { + "title": "Enhance Inputs", + "description": "Emphasize links, buttons, menus, textfields, and other inputs.", + "type": "boolean", + "default": false + } + }, + "lineSpace": { + "schema": { + "title": "Line Space", + "description": "Adjusts the spacing between lines of text.", + "type": "number", + "default": 1, + "minimum": 0.7, + "maximum": 3, + "multipleOf": 0.1 + } + }, + "selectionTheme": { + "schema": { + "title": "Selection Theme", + "description": "Change the highlight colour for text selections.", + "default": "default", + "enum": [ + "default", + "yellow", + "green", + "pink" + ], + "enumLabels": [ + "Default Selection Highlight", + "Yellow Selection Highlight", + "Green Selection Highlight", + "Pink Selection Highlight" + ] + } + }, + "selfVoicingEnabled": { + "schema": { + "title": "Self Voicing", + "description": "Let the device read site content aloud.", + "type": "boolean", + "default": false + } + }, + "simplifiedUiEnabled": { + "schema": { + "title": "Simplified UI", + "description": "Only display the main content.", + "type": "boolean", + "default": false + } + }, + "syllabificationEnabled": { + "schema": { + "title": "Syllabification", + "description": "Display words broken down into their syllables.", + "type": "boolean", + "default": false + } + }, + "tableOfContentsEnabled": { + "schema": { + "title": "Table of Contents", + "description": "Create a table of contents.", + "type": "boolean", + "default": false + } + }, + "wordSpace": { + "schema": { + "type": "number", + "default": 0, + "minimum": -0.3, + "maximum": 3, + "multipleOf": 0.1 + } + } + }, + "capabilitiesTransformations": { + "characterSpace": { + "transform": { + "type": "fluid.transforms.round", + "scale": 1, + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "offset": -1, + "inputPath": "http://registry\\.gpii\\.net/common/characterSpace" + } + } + } + }, + "contrastTheme": { + "transform": { + // Use the same condition for applying a high contrast theme to windows: + // (highContrast.enabled || highContrastTheme) && (highContrastTheme != "regular-contrast") + "type": "fluid.transforms.condition", + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/highContrast/enabled", + "left": false, + "operator": "||", + "rightPath": "http://registry\\.gpii\\.net/common/highContrastTheme", + "right": false + } + }, + "operator": "&&", + "right": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": "", + "leftPath": "http://registry\\.gpii\\.net/common/highContrastTheme", + "operator": "!==", + "right": "regular-contrast" + } + } + } + }, + "true": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/highContrastTheme", + "defaultOutputValue": "default", + "match": { + "black-white": "bw", + "white-black": "wb", + "black-yellow": "by", + "yellow-black": "yb", + "grey-black": "lgdg", + "grey-white": "gw", + "black-brown": "bbr" + } + } + }, + "false": "default" + } + }, + "fontSize": { + "transform": { + "type": "fluid.transforms.round", + "scale": 1, + "input": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/fontSize", + "right": 12, + "operator": "/" + } + } + } + }, + "inputsLargerEnabled": "http://registry\\.gpii\\.net/common/inputsLarger/enabled", + "lineSpace": "http://registry\\.gpii\\.net/common/lineSpace", + "selectionTheme": "http://registry\\.gpii\\.net/common/highlightColor", + "selfVoicingEnabled": "http://registry\\.gpii\\.net/common/selfVoicing/enabled", + "simplifiedUiEnabled": "http://registry\\.gpii\\.net/common/simplifiedUi/enabled", + "syllabificationEnabled": "http://registry\\.gpii\\.net/common/syllabification/enabled", + "tableOfContentsEnabled": "http://registry\\.gpii\\.net/common/tableOfContents", + "wordSpace": { + "transform": { + "type": "fluid.transforms.round", + "scale": 1, + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "offset": 1, + "inputPath": "http://registry\\.gpii\\.net/common/wordSpace" + } + } + } + } + }, + "inverseCapabilitiesTransformations": {} + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + } +} diff --git a/testData/solutions/solutionsDescription/com_freedomscientific_jaws.md b/testData/solutions/solutionsDescription/com_freedomscientific_jaws.md new file mode 100644 index 000000000..d55850a24 --- /dev/null +++ b/testData/solutions/solutionsDescription/com_freedomscientific_jaws.md @@ -0,0 +1,17 @@ +# JAWS + +## Details + +* __Name__: JAWS +* __Id__: com.freedomscientific.jaws +* __Platform__: Windows + +## Description +__JAWS__ is the most widely used commercial screenreader in Windows. + +You'll find more information at: + + * [JAWS website](http://www.freedomscientific.com/Products/Blindness/JAWS) + +## Integration +JAWS is integrated to support the most basic screenreader settings. This includes capabilities transformations to common terms and acceptance tests \ No newline at end of file diff --git a/testData/solutions/solutionsDescription/com_microsoft_office.md b/testData/solutions/solutionsDescription/com_microsoft_office.md new file mode 100644 index 000000000..99f0f7772 --- /dev/null +++ b/testData/solutions/solutionsDescription/com_microsoft_office.md @@ -0,0 +1,63 @@ +# Microsoft Office + +## Details + +* __Name__: Microsoft Office +* __Id__: com.microsoft.office +* __Platform__: Windows +* __Contact__: Steve Grundell + +## Description + +This solution provides the ability to use customised layout of the Ribbon, Quick Access Toolbar, and Status bar in the +following Microsoft Office 2016 Applictions: + +* Word +* Excel +* PowerPoint +* OneNote +* Access +* Outlook + +The settings are provided in `http://registry.gpii.net/applications/com.microsoft.office`, which contains the following +items: + +* `tabletMode` - Larger buttons, applies to all Office applications. +* `word-ribbon` - The ribbon and quick-access toolbar customisations (XML) +* `word-status` - The statusbar values to display. +* `excel-ribbon` +* `excel-status` +* `onenote-ribbon` +* `powerpoint-ribbon` +* `powerpoint-status` +* `publisher-ribbon` +* `publisher-status` +* `access-ribbon` +* `access-status` +* `outlook-ribbon` +* `outlook-status` + +The `ribbon` values are the JSON representations of the XML based `.officeUI` files, which found by Office in +`%LOCALAPPDATA%\Microsoft\Office`. This contains customisations for the ribbon and quick-access toolbar. + +To manually create a preference set, either take the relevent `.officeUI` file, +or export the customised interface (in *Options* -> *Customise Ribbon*) to a `.exportedUI` file, and convert the XML +to JSON via the `xml-mapping` library. eg: + +```javascript +console.log(JSON.stringify(require("xml-mapping").tojson(require("fs").readFileSync(0, "utf8")), null, 4)); +``` + +The status bar customisations are each a collection of boolean values, indicating the visibility of a field in the UI. + +## Testing + +The user `otis` performs various customisations of each Office application. + +## Limitations + +* Unable to make running instances of the applications reload the new settings. If more than one window is open, then +the new settings *sometime* reload when switching between the window. +* Only tested with Office 2016. + * The ribbon *may* work in other versions. + * The status bar will *not* work in other versions (the registry location contains the version number) diff --git a/testData/solutions/solutionsDescription/com_microsoft_windows_language.md b/testData/solutions/solutionsDescription/com_microsoft_windows_language.md new file mode 100644 index 000000000..fe9ebdea0 --- /dev/null +++ b/testData/solutions/solutionsDescription/com_microsoft_windows_language.md @@ -0,0 +1,28 @@ +# Windows Display Language + +## Details + +* __Name__: Windows Display Language +* __Id__: com.microsoft.windows.language +* __Platform__: Windows +* __Contact__: Steve Grundell + +## Description + +Sets the display language of the operating system, by changing some values in the Windows Registry. The new setting +is only applied to new processes; only Windows Explorer is restarted by this solution. + +The language identifier is specified in `http://registry.gpii.net/common/language`. + + +## Testing + +The following test users make use of the language setting: + +* Tom: English (US), en-US +* Catalina: Spanish (Spain), es-ES +* Telegu: Telegu (India), te-IN. + +## Limitations + +* The appropriate language pack must already be installed. diff --git a/testData/solutions/solutionsDescription/com_microsoft_windows_mouseSettings.md b/testData/solutions/solutionsDescription/com_microsoft_windows_mouseSettings.md new file mode 100644 index 000000000..7e79efe42 --- /dev/null +++ b/testData/solutions/solutionsDescription/com_microsoft_windows_mouseSettings.md @@ -0,0 +1,36 @@ +# Windows Mouse Settings + +## Details + +* __Name__: Windows Mouse Settings +* __Id__: com.microsoft.windows.mouseSettings +* __Platform__: Windows 10 +* __Contact__: JJ + +## Description + +This solution exposes Windows 10 mouse settings, including: + ++ SwapMouseButtons: Swap mouse left/right buttons. ++ ScrollLines: Changes the number of lines to scroll when moving the scrollwheel. ++ ScrollRouting: Changes the way mouse scrolling is passed to applications. ++ EnableCursorShadow: Enables or disables mouse cursor shadow. ++ ScrollChars: Changes the number of chars that are scrolled horizontally. ++ WindowsTrackingEnabled: Windows receive forcus when mouse hovers over them. ++ AutoWinArrangement: Enable/Disables the automatic Windows arrangment when dragging windows to border with mouse. ++ DoubleClickTime: Changes the amount of time that can elapse between a first click and a second click for the OS to consider the mouse action a double-click. ++ PointerSpeed: Selects a pointer speed. ++ EnhancePrecision: Enable/Disables enhanced pointer precision. ++ SnapToDefaultButton: Automatically move pointer to the default button in a dialogue. ++ HidePointer: Enable/Disables hiding the pointer while typing. ++ MouseSonar: Enable/Disable showing mouse location when pressing the CTRL key. ++ DoubleClickHeight: Changes the height of the rectangle in which a second click should fall for OS to consider the mouse action a double-click. ++ DoubleClickWidth: Changes the width of the rectangle in which a second click should fall for OS to consider the mouse action a double-click. + +## Integration + +Only Windows 10 support this feature. + +## Testing + +For testing all the included features, the preference set [mike](../../preferences/mike.md) can be used. diff --git a/testData/solutions/solutionsDescription/com_microsoft_windows_nightScreen.md b/testData/solutions/solutionsDescription/com_microsoft_windows_nightScreen.md new file mode 100644 index 000000000..5c1d03dbb --- /dev/null +++ b/testData/solutions/solutionsDescription/com_microsoft_windows_nightScreen.md @@ -0,0 +1,41 @@ +# Windows Night Screen + +## Details + +* __Name__: Windows Night Screen +* __Id__: com.microsoft.windows.nightScreen +* __Platform__: Windows 10 +* __Contact__: Steve Grundell + +## Description + +This solution enables the activation of the night-light setting, which reduces the blue light emitted from the screen. +This produces warmer colours which make looking at the screen more comfortable at night. + + +See also: + +* [GPII-2717](https://issues.gpii.net/browse/GPII-2717) + +## Integration + +Only Windows 10 support this feature. + + +## Testing + +The [nyx](../../preferences/nyx.md) user preference set has the following setting: + + +```JSON + "http://registry.gpii.net/common/nightScreen": true +``` + +When activated, blue light from the screen will be reduced, causing white to look yellow. + +Some displays (such as a virtual machine) may not support this setting. In these cases, looking in the "Night Light" +section of System Settings the "Turn on/off" button will display the appropriate text. + +## Limitations + +* This solution is yet to support the strength of the blue light reduction, or time range when should apply. diff --git a/testData/solutions/solutionsDescription/com_microsoft_windows_screenDPI.md b/testData/solutions/solutionsDescription/com_microsoft_windows_screenDPI.md new file mode 100644 index 000000000..e175b5365 --- /dev/null +++ b/testData/solutions/solutionsDescription/com_microsoft_windows_screenDPI.md @@ -0,0 +1,45 @@ +# Windows Screen Scaling + +## Details + +* __Name__: Windows Screen Scaling +* __Id__: com.microsoft.windows.screenDPI +* __Platform__: Windows 10 +* __Contact__: Steve Grundell + +## Description + +This solution provides the ability to change the DPI setting of the primary display. The DPI (dots per inch) provides a way of taking pixel size and viewing distance into account, but it also provides a way of changing the size of things on the display without magnification or altering the resolution. In Windows, this setting described as "Change the size of text, apps, and other items". + +The setting values are a percentage of 96 dpi (dots per inch). 100% is the default scale for the majority of screens, and the maximum depends on the current resolution, up to 500% (for example, 1024x768 supports up to 125%). Setting a value beyond what the current resolution supports will cause the effective DPI to be capped at that maximum value. + +See also: + +* [Writing DPI-Aware Desktop and Win32 Applications](https://msdn.microsoft.com/library/dn469266) +* [GPII-2226](https://issues.gpii.net/browse/GPII-2226) +* Initial research: [GPII-1716](https://issues.gpii.net/browse/GPII-1716#comment-21615) + +## Integration + +Currently, only Windows 10 is supported. Windows 8/8.1 will be supported later, and Windows 7 would a logout/in so there are no current plans to support it. + +Not every application is "DPI-aware", and do not react to the DPI changing. For such applications, the content of the window will be scaled automatically until restarted. + + +## Testing + +```JSON +"http://registry.gpii.net/applications/com.microsoft.windows.screenDPI": { + "screen-dpi": 1.25 +} +``` + +* A resolution of at least 1024x768 is required, in order to support values over 100%. +* Windows only handles values in steps of 25% (or 50% for when over 250%); a value of 1.7 will be rounded to 175%. +* The displayed DPI will always be capped at the maximum supported by the current resolution. + +## Limitations + +* There appear to be no documented APIs for changing the DPI, so this implementation may break in future updates to Windows. +* Only the primary display is affected. +* The DPI setting should be a setting that's specific to the physical display, yet this same value could be applied on different types of displays. diff --git a/testData/solutions/solutionsDescription/com_texthelp_readWriteGold.md b/testData/solutions/solutionsDescription/com_texthelp_readWriteGold.md new file mode 100644 index 000000000..5ed8d32a9 --- /dev/null +++ b/testData/solutions/solutionsDescription/com_texthelp_readWriteGold.md @@ -0,0 +1,49 @@ +# TextHelp Read&Write Gold + +## Details + +* __Name__: Read&Write Gold +* __Id__: com.texthelp.readWriteGold +* __Platform__: Win32 +* __Contact__: Steve Githens , Javier Hernández + +## Description + +Read&Write literacy software makes the web, documents and files more accessible - any time, any place, and on any platform or device. It's great for people with dyslexia and other learning difficulties, or anyone whose first language isn't English. + +You'll find more information at: + +* [Read&Write website](https://www.texthelp.com/en-gb/products/read-and-write-family) + +## Integration +The current implementation of Read&Write allows the GPII to: + +* Configure the application through the GPII's built-in XML settings handler +* Determine whether Read&Write is installed in the system +* Start and stop the application + +Actually it doesn't include any transformation so this implementation only works with application specific settings. + +## Testing +When using a preference set that makes use of Read&Write, it should be launched and configured properly by taking into account the settings included in that preference set. +Actually there are two preference sets included into universal (rwg1 and rwg2) with settings for Read&Write and that you can use to test this implementation. + +Also, the GPII has [acceptance tests](https://github.com/GPII/universal/blob/master/tests/platform/windows/windows-readwrite-testSpec.txt) for Read&Write. + +### rwg1 + +preference set: [preferences/rwg1.json](https://github.com/GPII/universal/blob/master/testData/preferences/rwg1.json) + +* When logging in, Read&Write should be started automatically and among many other adaptations: + * The toolbar must use the "Fun" icon set and they should be shown in "large" mode + * The toolbar must show the "Writing Features" including buttons such as "Spell check", "Prediction", etc. +* When logging out, Read&Write should be stopped and default settings will be restored. + +### rwg2 + +preference set: [preferences/rwg2.json](https://github.com/GPII/universal/blob/master/testData/preferences/rwg2.json) + +* When logging in, Read&Write should be started automatically and among many other adaptations: + * The toolbar must use the "Professional" icon set and they should be shown in "large" mode + * The toolbar must show the "Reading Features" including buttons such as "Dictionary" and the "Play/Pause/Rewind/Forward". +* When logging out, Read&Write should be stopped and default settings will be restored. diff --git a/testData/solutions/solutionsDescription/net_gpii_uioPlus.md b/testData/solutions/solutionsDescription/net_gpii_uioPlus.md new file mode 100644 index 000000000..b1d8143ad --- /dev/null +++ b/testData/solutions/solutionsDescription/net_gpii_uioPlus.md @@ -0,0 +1,32 @@ +# UIO Plus + +## Details + +* __Name__: UIO+ +* __Id__: net.gpii.uioPlus +* __Platform__: Tested on MS Windows and GNU/Linux (it should also work on Mac OSX) +* __Contact__: Justin Obara + +## Description + +UIO+ is a browser extension; currently only supported in Chrome. This extension establishes a connection with GPII to get the active preferences. These preferences are used, by the extension, to adapt the web content within the browser. + +Useful links: + + * [Chrome extensions developer documentation](https://developer.chrome.com/extensions) + * [Source code at github.com](https://github.com/GPII/gpii-chrome-extension) + +## Installation + +Install the latest version from the [Google Chrome Web Store](https://chrome.google.com/webstore/detail/ui-options-plus-uio%20/okenndailhmikjjfcnmolpaefecbpaek). + + +## Testing + +For manual testing, please follow the "Installation" steps above. After the browser extension is installed, you can click the blue gear extension icon in the browser's chrome to open the UIO+ panel. Changing the settings here should affect the content of the page. Any pages that are opened before the extension is loaded/installed will need to be reloaded before changes will properly apply. + +Another option for manual testing is to login to the GPII with a preference set containing preferences supported by the extension. A profile with a set of Needs & Preferences has been provided in order to properly perform the tests. + +### uioPlusCommon + +preference set located at universal's testData/preferences/uioPlusCommon.json diff --git a/testData/solutions/solutionsDescription/org_freedesktop_xrandr.md b/testData/solutions/solutionsDescription/org_freedesktop_xrandr.md new file mode 100644 index 000000000..8daca1d82 --- /dev/null +++ b/testData/solutions/solutionsDescription/org_freedesktop_xrandr.md @@ -0,0 +1,34 @@ +# X.Org XRandR + +## Details + +* __Name__: X.Org XRandR +* __Id__: org.freedesktop.xrandr +* __Platform__: GNU/Linux +* __Contact__: Joseph Scheuhammer + +## Description +__XRandR__ ("resize and rotate") is an extension to the X11 protocol that provides the ability to resize, rotate, and/or reflect the root window of a screen. +XRandR is free software developed under the umbrella of the X.org foundation, and maintained by the freedesktop.org. + +You'll find more information at: + + * [XRandR at X.org's wiki](https://www.x.org/wiki/Projects/XRandR/) + * [Source code at freedesktop's git repository](https://cgit.freedesktop.org/xorg/app/xrandr/) + +## Integration +XRandR is used by the GPII personalization framework as a display settings handler to change the screen resolution on GNOME/Linux desktops. + +To ensure that the solution is well integrated, there are Acceptance Tests (_tests/platform/linux/linux-xrandr-testSpec.js_) as well as tests of XRandR at a settings handler-level (_gpii/node_modules/xrandr/test/xrandrSettingsHandlerTests.js_). The latter are located in the linux repository. + +## Testing +When using an preference set that makes use of a display settings handler to change the screen resolution, XRandR should launch and re-configure the display resolution using the settings included in that preference set. + +### os_gnome_display + +The expected behaviour of using the preference set located at universal's _testData/preferences/acceptanceTests/os_gnome_display.json_ is: + +* When logging in, XRandR should be launched to change the screen resolution as specified in the above json file. In this particular case, it should change to 800 pixels wide by 600 pixels high. + +* When logging out, XRandR should launch again to restore the screen resolution to its original width and height. + diff --git a/testData/solutions/solutionsDescription/org_gnome_orca.md b/testData/solutions/solutionsDescription/org_gnome_orca.md new file mode 100644 index 000000000..929fb0ce6 --- /dev/null +++ b/testData/solutions/solutionsDescription/org_gnome_orca.md @@ -0,0 +1,63 @@ +# GNOME Orca + +## Details + +* __Name__: GNOME Orca +* __Id__: org.gnome.orca +* __Platform__: GNU/Linux +* __Contact__: Javier Hernández + +## Description +__GNOME Orca__ is the default screen reader of the most popular GNU/Linux desktops. +Orca is free software and is developed under the umbrella of the GNOME project. +For that reason, __Orca__ is part of the GNOME Desktop, so it can be found in every version of the GNOME Desktop. + +You'll find more information at: + + * [Orca at GNOME's wiki](https://wiki.gnome.org/Projects/Orca) + * [Orca's user documentation](https://help.gnome.org/users/orca/stable/) + * [GNOME's universal access user documentation](https://help.gnome.org/users/gnome-help/stable/a11y.html.en) + * [Source code at git.gnome.org](https://git.gnome.org/browse/orca) + +## Integration +The Orca screen reader is fully supported by the GPII personalization framework. +Why "fully supported"? The GPII can deal with almost all of the Orca's settings by using its own [settings handler](https://github.com/GPII/linux/tree/master/gpii/node_modules/orca). +The GPII launches Orca by using the GNOME's configuration system (gsettings). + +To ensure that the solution is well integrated, in the linux repo, there are Acceptance Tests (_tests/acceptanceTests/AcceptanceTests_orca.js_), and also, the GPII includes its own tests for Orca at settings handler-level (_gpii/node_modules/orca/test/orcaSettingsHandlerTests.js_) to ensure that Orca's user-settings.conf file can be updated properly. + +Information about value ranges and mappings to common terms can be found [here](https://docs.google.com/spreadsheet/ccc?key=0AppduB_JZh5EdDRYT1pmOTc5eUpNbkpMckhacUVxWXc&usp=sharing) under _LinuxGNOME_ sheet. + +## Testing +When using a preference set that makes use of a screen reader, Orca should be launched and configured properly by taking into account the settings included in that preference set. + +### screenreader_orca + +By using this preference set (located at universal's _testData/preferences/acceptanceTests/screenreader_orca.json_), this is the expected behaviour. + +* When logging in, Orca should be started automatically and you should listen to the screen reader. + +* A new profile should be added to Orca's user-settings.conf file. It should be called as the preference set (_screenreader_orca_), and it should be set as the default used profile. + +
                            +"startingProfile": [
                            +    "screenreader_orca",
                            +    "screenreader_orca"
                            +]
                            +
                            + +* The settings such as the speech rate or the output language should be configured accordingly inside the newly created profile _screenreader_orca_. In this particular case, it should contain: + +
                            +"voices.default.rate": 100
                            +
                            +__AND__ +
                            +"voices.default.family": {
                            +    "locale": "en",
                            +    "name": "en-westindies"
                            +}
                            +
                            + +* When logging out, Orca should be stopped and default settings will be restored to its original values. + diff --git a/testData/solutions/solutionsDescription/se_omnitor_ecmobile.md b/testData/solutions/solutionsDescription/se_omnitor_ecmobile.md new file mode 100644 index 000000000..dd724ff86 --- /dev/null +++ b/testData/solutions/solutionsDescription/se_omnitor_ecmobile.md @@ -0,0 +1,56 @@ +# eCmobile and eCtouch + +## Details + +* __Name__: eCmobile/eCtouch +* __Id__: se.omnitor.ecmobile +* __Platform__: Android (4.2+) +* __Contact__ (developer): Christoffer Friberg (christoffer.friberg@omnitor.se) +* __Contact__ (project lead): Christer Ulfsparre (christer.ulfsparre@omnitor.se) + +## Description +__eCmobile__ and __eCtouch__ are two Android applications for real-time communication using simultaneous video, audio and text. They use [SIP (Session Initiation Protocol)](https://www.ietf.org/rfc/rfc3261.txt) to negotiate and control calls and are fully compatible with other standards compliant SIP clients. +Both applications are closed-source and proprietary. They are mostly sold via various municipal or county aid offices to aid users either in the workplace or in their private life. +The two applications are very similar in operation and implementation, which is why they're treated as a single solution in the GPII. The difference between the two lies in the targeted range of devices. eCtouch is tailored towards tablet devices (7 inches and larger), whereas eCmobile is meant for use in smartphones. Currently we primarily use the Samsung Galaxy Note 10.1 (2014 edition) tablet and the Samsung Galaxy S5 smartphone when selling pre-installed units to end-users. + +You'll find more information at [our website](http://www.omnitor.com), primarily in Swedish. +Installation instructions can be found in [this](https://docs.google.com/document/d/1McMtIfCVSSHe9acMLgIMpqFr-psYBuYRzePzobafIW4) document. + +## Integration +eCtouch and eCmobile are partially integrated with the GPII, limited primarily by the Android operating system. On a non-rooted device, a regular application (i.e. the GPII app) may not be able to shut down another application (eCtouch/eCmobile). This means that the GPII cannot fully control the life cycle of the applications, so upon logging out from the GPII, although the home screen is shown, the application can still be running in background. + + +Information about value ranges and mappings to common terms can be found [here](https://docs.google.com/spreadsheet/ccc?key=0AppduB_JZh5EdDRYT1pmOTc5eUpNbkpMckhacUVxWXc&usp=sharing#gid=27). If the link doesn't take you to the "eCtouch/eCmobile" page, select it in the bottom row. + +## Testing +When using a N&P set that makes use of a the font size or high contrast theme settings, eCmobile or eCtouch should be launched and configured properly by taking into account the settings included in that N&P set. There are two example preference sets included in the universal repository's test data, see below. + +The applications read their settings from an XML file on the device's internal storage (usually the /sdcard/ directory). The file is called output.xml and is only read when the applications start. Click [here](https://docs.google.com/document/d/1_58l11wld2SPIH0II1BLBPUU8LjLhj5m_64xIddb_ZM) for more information about this file and how it's used. + +### omnitor1 +[link](https://github.com/GPII/universal/blob/master/testData/preferences/omnitor1.json) + +This preference set uses the default settings, same as if the app was launched without the GPII. + +The XML file will contain: + + + + 24 + 70 + none + + +### omnitor2 +[link](https://github.com/GPII/universal/blob/master/testData/preferences/omnitor2.json) + +This preference set uses the yellow-black high contrast theme and the maximum possible font size. Note that we apply a scaling factor of 2 to the font size setting, and that the high contrast setting is transformed to fit our format. + +The XML file will contain: + + + + 50 + 70 + yellow-black + diff --git a/testData/solutions/win32.json b/testData/solutions/win32.json deleted file mode 100644 index d91c43070..000000000 --- a/testData/solutions/win32.json +++ /dev/null @@ -1,1379 +0,0 @@ -[ - { - "name": "Windows Built-in Screen Magnifier", - "id": "com.microsoft.windows.magnifier", - "contexts": { - "OS": [ - { - "id": "win32", - "version": ">=5.0" - } - ] - }, - "settingsHandlers": [ - { - "type": "gpii.windows.registrySettingsHandler.set", - "options": { - "hKey": "HKEY_CURRENT_USER", - "path": "Software\\Microsoft\\ScreenMagnifier" - }, - "capabilities": [ - "applications.com\\.microsoft\\.windows\\.magnifier.id", - "display.screenEnhancement.screenMagnification.applications.com\\.microsoft\\.windows\\.magnifier.name" - ], - "capabilitiesTransformations": { - "Magnification.dataType": { - "transform": { - "type": "fluid.transforms.literalValue", - "value": "REG_DWORD" - } - }, - "Invert": { - "value": "display.screenEnhancement.invertImages", - "dataType": { - "literalValue": "REG_DWORD" - } - }, - "transform": [{ - "type": "fluid.transforms.arrayToSetMembership", - "inputPath": "display.screenEnhancement.tracking", - "presentValue": { - "value": 1, - "dataType": "REG_DWORD" - }, - "missingValue": { - "value": 0, - "dataType": "REG_DWORD" - }, - "options": { - "focus": "FollowFocus", - "caret": "FollowCaret", - "mouse": "FollowMouse" - } - }, { - "type": "fluid.transforms.round", - "input": { - "transform": { - "type": "fluid.transforms.linearScale", - "valuePath": "display.screenEnhancement.magnification", - "factor": 100 - } - }, - "outputPath": "Magnification.value" - }], - "MagnificationMode": { - "transform": { - "type": "fluid.transforms.valueMapper", - "inputPath": "display.screenEnhancement.-provisional-magnifierPosition", - "defaultOutputPath": "value", - "options": { - "FullScreen": { - "outputValue": 2 - }, - "Lens": { - "outputValue": 3 - }, - "LeftHalf": { - "outputValue": 1 - }, - "RightHalf": { - "outputValue": 1 - }, - "TopHalf": { - "outputValue": 1 - }, - "BottomHalf": { - "outputValue": 1 - }, - "Custom": { - "outputValue": 2 - } - } - }, - "dataType": { - "transform": { - "type": "fluid.transforms.literalValue", - "value": "REG_DWORD" - } - } - } - } - } - ], - "lifecycleManager": { - "start": [ - "setSettings", - { - "type": "gpii.launch.exec", - "command": "${{environment}.SystemRoot}\\System32\\Magnify.exe" - } - ], - "stop": [ { - "type": "gpii.launch.exec", - "command": "${{environment}.SystemRoot}\\System32\\taskkill.exe /f /im Magnify.exe" - }, - "restoreSettings" - ] - } - }, - - { - "name": "UI Options", - "id": "fluid.uiOptions.windows", - "contexts": { - "OS": [ - { - "id": "win32", - "version": ">=5.0" - } - ] - }, - "settingsHandlers": [ - { - "type": "gpii.settingsHandlers.noSettings", - "capabilities": [ - "display.applications.fluid\\.uiOptions\\.windows.name", - "applications.fluid\\.uiOptions\\.windows.id", - "display.screenEnhancement.fontSize", - "display.screenEnhancement.foregroundColor", - "display.screenEnhancement.backgroundColor", - "display.screenEnhancement.fontFace" - ] - } - ], - "lifecycleManager": { - "start": [ - { - "type": "gpii.launch.exec", - "command": "\"${{environment}.PROGRAMFILES(x86)}\\Mozilla Firefox\\firefox.exe\" http://ec2-107-21-143-113.compute-1.amazonaws.com:443/demos/Mammals.html?token=${{userToken}}" - } - ], - "stop": [ - { - "type": "gpii.launch.exec", - "command": "${{environment}.SystemRoot}\\System32\\taskkill.exe /im firefox.exe" - } - ] - } - }, - - { - "name": "Windows Built-in Onscreen Keyboard", - "id": "com.microsoft.windows.onscreenKeyboard", - "contexts": { - "OS": [ - { - "id": "win32", - "version": ">=5.0" - } - ] - }, - "settingsHandlers": [ - { - "type": "gpii.settingsHandlers.noSettings", - "capabilities": [ - "control.onscreenKeyboard", - "applications.com\\.microsoft\\.windows\\.onscreenKeyboard.id", - "control.onscreenKeyboard.applications.com\\.microsoft\\.windows\\.onscreenKeyboard.name" - ] - } - ], - "lifecycleManager": { - "start": [ - { - "type": "gpii.launch.exec", - "command": "${{environment}.SystemRoot}\\System32\\osk.exe" - } - ], - "stop": [ - { - "type": "gpii.launch.exec", - "command": "${{environment}.SystemRoot}\\System32\\taskkill.exe /im osk.exe" - } - ] - } - }, - - { - "name": "NVDA Screen Reader", - "id": "org.nvda-project", - "contexts": { - "OS": [ - { - "id": "win32", - "version": ">=5.0" - } - ] - }, - "settingsHandlers": [ - { - "type": "gpii.settingsHandlers.INISettingsHandler.set", - "options": { - "path": "${{environment}.APPDATA}\\nvda\\nvda.ini", - "allowNumberSignComments": true, - "allowSubSections": true - }, - "capabilities": [ - "applications.org\\.nvda-project.id", - "display.screenReader.applications.org\\.nvda-project.name" - ], - "capabilitiesTransformations": { - "presentation\\.reportHelpBalloons": "display.screenReader.-provisional-speakTutorialMessages", - "keyboard\\.speakTypedCharacters": "display.screenReader.-provisional-keyEcho", - "keyboard\\.speakTypedWords": "display.screenReader.-provisional-wordEcho", - "speech\\.espeak\\.sayCapForCapitals": "display.screenReader.-provisional-announceCapitals", - "virtualBuffers\\.autoSayAllOnPageLoad": { - "transform": { - "type": "fluid.transforms.valueMapper", - "inputPath": "display.textReadingHighlight.readingUnit", - "defaultInputValue": "sentence", - "options": { - "all": { - "outputValue": true - }, - "sentence": { - "outputValue": false - } - } - } - }, - "transform": [ - { - "type": "fluid.transforms.arrayToSetMembership", - "inputPath": "display.screenEnhancement.trackingTTS", - "presentValue": true, - "missingValue": false, - "options": { - "focus": "reviewCursor\\.followFocus", - "caret": "reviewCursor\\.followCaret", - "mouse": "reviewCursor\\.followMouse" - } - }, - { - "type": "fluid.transforms.valueMapper", - "inputPath": "display.screenReader.-provisional-screenReaderTTSEnabled", - "options": { - "false": { - "outputValue": { - "transform": [ - { - "type": "fluid.transforms.literalValue", - "value": "silence", - "outputPath": "speech\\.synth" - }, - { - "type": "fluid.transforms.literalValue", - "value": "Microsoft Sound Mapper", - "outputPath": "speech\\.outputDevice" - } - ] - } - } - } - } - ], - "speech\\.espeak\\.rate": { - "transform": { - "type": "fluid.transforms.binaryOp", - "right": 3.10, - "operator": "/", - "left": { - "transform": { - "type": "fluid.transforms.binaryOp", - "right": 80, - "operator": "-", - "left": { - "transform": { - "type": "fluid.transforms.condition", - "truePath": "display.screenReader.speechRate", - "false": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenReader.speechRate", - "operator": "/", - "right": 3 - } - }, - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenReader.speechRate", - "operator": "<=", - "right": 390 - } - } - } - } - } - } - } - }, - "speech\\.espeak\\.rateBoost": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenReader.speechRate", - "operator": ">", - "right": 390 - } - }, - "speech\\.symbolLevel": { - "transform": { - "type": "fluid.transforms.valueMapper", - "inputPath": "display.screenReader.-provisional-punctuationVerbosity", - "options": { - "none": { - "outputValue": "0" - }, - "some": { - "outputValue": "100" - }, - "most": { - "outputValue": "200" - }, - "all": { - "outputValue": "300" - } - } - } - }, - "speech\\.espeak\\.voice": { - "transform": { - "type": "fluid.transforms.valueMapper", - "inputPath": "display.screenReader.-provisional-auditoryOutLanguage", - "options": { - "en": { - "outputValue": "en\\en" - }, - "en-GB": { - "outputValue": "en\\en" - }, - "en-US": { - "outputValue": "en\\en-us" - }, - "en-scotland": { - "outputValue": "en\\en-sc" - }, - "en-BZ": { - "outputValue": "en\\en-wi" - }, - "en-BS": { - "outputValue": "en\\en-wi" - }, - "en-AG": { - "outputValue": "en\\en-wi" - }, - "en-AI": { - "outputValue": "en\\en-wi" - }, - "af": { - "outputValue": "af" - }, - "bg": { - "outputValue": "bg" - }, - "bs": { - "outputValue": "bs" - }, - "ca": { - "outputValue": "ca" - }, - "cs": { - "outputValue": "cs" - }, - "cy": { - "outputValue": "cy" - }, - "da": { - "outputValue": "da" - }, - "de": { - "outputValue": "de" - }, - "el": { - "outputValue": "el" - }, - "grc": { - "outputValue": "test\\grc" - }, - "eo": { - "outputValue": "eo" - }, - "es": { - "outputValue": "es" - }, - "es-ES": { - "outputValue": "es" - }, - "es-419": { - "outputValue": "es-la" - }, - "et": { - "outputValue": "et" - }, - "fi": { - "outputValue": "fi" - }, - "fr": { - "outputValue": "fr" - }, - "fr-BE": { - "outputValue": "fr-be" - }, - "hi": { - "outputValue": "hi" - }, - "hr": { - "outputValue": "hr" - }, - "hu": { - "outputValue": "hu" - }, - "hy": { - "outputValue": "hy" - }, - "hy-arevmda": { - "outputValue": "hy-west" - }, - "id": { - "outputValue": "id" - }, - "is": { - "outputValue": "is" - }, - "it": { - "outputValue": "it" - }, - "jbo": { - "outputValue": "test\\jbo" - }, - "ka": { - "outputValue": "ka" - }, - "kn": { - "outputValue": "kn" - }, - "ku": { - "outputValue": "ku" - }, - "la": { - "outputValue": "la" - }, - "lv": { - "outputValue": "lv" - }, - "mk": { - "outputValue": "mk" - }, - "ml": { - "outputValue": "ml" - }, - "nci": { - "outputValue": "test\\nci" - }, - "nl": { - "outputValue": "nl" - }, - "no": { - "outputValue": "no" - }, - "pap": { - "outputValue": "test\\pap" - }, - "pl": { - "outputValue": "pl" - }, - "pt-BR": { - "outputValue": "pt" - }, - "pt-PT": { - "outputValue": "pt-pt" - }, - "ro": { - "outputValue": "ro" - }, - "ru": { - "outputValue": "ru" - }, - "sk": { - "outputValue": "sk" - }, - "sq": { - "outputValue": "sq" - }, - "sr": { - "outputValue": "sr" - }, - "sv": { - "outputValue": "sv" - }, - "sw": { - "outputValue": "sw" - }, - "ta": { - "outputValue": "ta" - }, - "tr": { - "outputValue": "tr" - }, - "vi": { - "outputValue": "vi" - }, - "zh-cmn": { - "outputValue": "zh" - }, - "cmn": { - "outputValue": "zh" - }, - "zh-yue": { - "outputValue": "zh-yue" - }, - "zh-yue": { - "outputValue": "yue" - } - } - } - } - } - } - ], - "lifecycleManager": { - "start": [ - "setSettings", - { - "type": "gpii.launch.exec", - "command": "\"${{environment}.PROGRAMFILES}\\NVDA\\nvda.exe\"" - } - ], - "stop": [ - { - "type": "gpii.launch.exec", - "command": "${{environment}.SystemRoot}\\System32\\taskkill.exe /f /im nvda_service.exe" - },{ - "type": "gpii.launch.exec", - "command": "${{environment}.SystemRoot}\\System32\\taskkill.exe /f /im nvda.exe" - }, - "restoreSettings" - ] - } - }, - - { - "name": "EasyOne Communicator Windows", - "id": "trace.easyOne.communicator.windows", - "contexts": { - "OS": [ - { - "id": "win32", - "version": ">=5.0" - } - ] - }, - - "settingsHandlers": [ - { - "type": "gpii.settingsHandlers.noSettings", - "capabilities": [ - "content.simplification", - "applications.trace\\.easyOne\\.communicator\\.windows.id", - "content.simplification.applications.trace\\.easyOne\\.communicator\\.windows.name" - ] - } - ], - "lifecycleManager" : { - "start": [ - { - "type": "gpii.launch.exec", - "command": "\"${{environment}.PROGRAMFILES(x86)}\\Mozilla Firefox\\firefox.exe\" http://easy1234.org/user/${{userToken}}" - } - ], - "stop": [ - { - "type": "gpii.launch.exec", - "command": "${{environment}.SystemRoot}\\System32\\taskkill.exe /im firefox.exe" - } - ] - } - }, - - { - "name": "EasyOne Communicator Sudan", - "id": "trace.easyOne.sudan.windows", - "contexts": { - "OS": [ - { - "id": "win32", - "version": ">=5.0" - } - ] - }, - "settingsHandlers": [ - { - "type": "gpii.settingsHandlers.noSettings", - "capabilities": [ - "content.pictorialSimplification", - "applications.trace\\.easyOne\\.sudan\\.windows.id", - "content.pictorialSimplification.applications.trace\\.easyOne\\.sudan\\.windows.name" - ] - } - ], - "lifecycleManager" : { - "start": [ - { - "type": "gpii.launch.exec", - "command": "\"${{environment}.PROGRAMFILES(x86)}\\Mozilla Firefox\\firefox.exe\" http://easy1234.org/sudan" - } - ], - "stop": [ - { - "type": "gpii.launch.exec", - "command": "${{environment}.SystemRoot}\\System32\\taskkill.exe /im firefox.exe" - } - ] - } - }, - - { - "name": "Web Anywhere", - "id": "webinsight.webAnywhere.windows", - "contexts": { - "OS": [ - { - "id": "win32", - "version": ">=5.0" - } - ] - }, - "settingsHandlers": [ - { - "type": "gpii.settingsHandlers.noSettings", - "capabilities": [ - "display.screenReader", - "applications.webinsight\\.webAnywhere\\.windows.id", - "display.screenReader.applications.webinsight\\.webAnywhere\\.windows.name" - ] - } - ], - "lifecycleManager" : { - "start": [ - { - "type": "gpii.launch.exec", - "command": "\"${{environment}.PROGRAMFILES(x86)}\\Mozilla Firefox\\firefox.exe\" \"http://webanywhere.cs.washington.edu/beta/?starting_url=http://ec2-107-21-143-113.compute-1.amazonaws.com:443/demos/Mammals.html?token=${{userToken}}\"" - } - ], - "stop": [ - { - "type": "gpii.launch.exec", - "command": "${{environment}.SystemRoot}\\System32\\taskkill.exe /im firefox.exe" - } - ] - } - }, - - { - "name": "maavis", - "id": "net.opendirective.maavis", - "contexts": { - "OS": [ - { - "id": "win32", - "version": ">=5.0" - } - ] - }, - - "settingsHandlers": [{ - "type": "gpii.settingsHandlers.JSONSettingsHandler.set", - "options": { "path": "${{environment}.MAAVIS_HOME}\\MaavisMedia\\Users\\Default\\userconfig.json" }, - "capabilities": [ "applications.net\\.opendirective\\.maavis.id" ] - }], - - "lifecycleManager": { - "start": [ - "setSettings", - { - "type": "gpii.launch.exec", - "command": "${{environment}.ComSpec} /c \"cd ${{environment}.MAAVIS_HOME} && MaavisPortable.cmd\"" - } - ], - "stop": [ - { - "type": "gpii.launch.exec", - "command": "${{environment}.SystemRoot}\\System32\\taskkill.exe /f /im firefox.exe" - }, - "restoreSettings" - ] - } - }, - - { - "name": "Windows High Contrast", - "id": "com.microsoft.windows.highContrast", - "contexts": { - "OS": [ - { - "id": "win32", - "version": ">=5.0" - } - ] - }, - "settingsHandlers": [ - { - "type": "gpii.windows.spiSettingsHandler.set", - "options": { - "getAction": "SPI_GETHIGHCONTRAST", - "setAction": "SPI_SETHIGHCONTRAST", - "uiParam": "struct_size", - "pvParam": { - "type": "struct", - "name": "HIGHCONTRAST" - } - }, - "capabilities": [ - "display.screenEnhancement.highContrast", - "applications.com\\.microsoft\\.windows\\.highContrast.id" - ], - "capabilitiesTransformations": { - "HighContrastOn": { - "transform": { - "type": "fluid.transforms.value", - "inputPath": "display.screenEnhancement.highContrast", - "outputPath": "value" - }, - "path": { - "transform": { - "type": "fluid.transforms.literalValue", - "value": "pvParam.dwFlags.HCF_HIGHCONTRASTON" - } - } - } - } - } - ], - "lifecycleManager": { - "start": [ - "setSettings" - ], - "stop": [ - "restoreSettings" - ] - } - }, - - { - "name": "Windows Non-client Metrics", - "id": "com.microsoft.windows.nonClientMetrics", - "contexts": { - "OS": [ - { - "id": "win32", - "version": ">=5.0" - } - ] - }, - "settingsHandlers": [ - { - "type": "gpii.windows.spiSettingsHandler.set", - "options": { - "getAction": "SPI_GETNONCLIENTMETRICS", - "setAction": "SPI_SETNONCLIENTMETRICS", - "uiParam": "struct_size", - "pvParam": { - "type": "struct", - "name": "NONCLIENTMETRICS" - } - }, - "capabilities": [ - "applications.com\\.microsoft\\.windows\\.nonClientMetrics.id", - "display.screenEnhancement.fontSize" - ], - "capabilitiesTransformations": { - "CaptionFontHeight": { - "transform": { - "type": "fluid.transforms.linearScale", - "valuePath": "display.screenEnhancement.fontSize", - "outputPath": "value", - "factor": -1 - }, - "path": { - "literalValue": "pvParam.lfCaptionFont.lfHeight" - } - }, - "CaptionHeight": { - "literalValue": { - "value": 0, - "path": "pvParam.iCaptionHeight" - } - }, - "MenuFontHeight": { - "transform": { - "type": "fluid.transforms.linearScale", - "valuePath": "display.screenEnhancement.fontSize", - "outputPath": "value", - "factor": -1 - }, - "path": { - "literalValue": "pvParam.lfCaptionFont.lfHeight" - - } - }, - "MenuHeight": { - "value": { - "value": 0, - "path": "pvParam.iMenuHeight" - } - }, - "SmallCaptionFontHeight": { - "transform": { - "type": "fluid.transforms.linearScale", - "valuePath": "display.screenEnhancement.fontSize", - "outputPath": "value", - "factor": -1 - }, - "path": { - "literalValue": "pvParam.lfCaptionFont.lfHeight" - } - }, - "SmallCaptionHeight": { - "literalValue": { - "value": 0, - "path": "pvParam.iSmCaptionHeight" - } - }, - "StatusFontHeight": { - "transform": { - "type": "fluid.transforms.linearScale", - "valuePath": "display.screenEnhancement.fontSize", - "outputPath": "value", - "factor": -1 - }, - "path": { - "literalValue": "pvParam.lfCaptionFont.lfHeight" - } - }, - "MessageFontHeight": { - "transform": { - "type": "fluid.transforms.linearScale", - "valuePath": "display.screenEnhancement.fontSize", - "outputPath": "value", - "factor": -1 - }, - "path": { - "literalValue": "pvParam.lfCaptionFont.lfHeight" - } - } - } - } - ], - "lifecycleManager": { - "start": [ - "setSettings" - ], - "stop": [ - "restoreSettings" - ] - } - }, -{ - "name": "Windows Mouse Trailing", - "id": "com.microsoft.windows.mouseTrailing", - "contexts": { - "OS": [ - { - "id": "win32", - "version": ">=5.0" - } - ] - }, - "settingsHandlers": [ - { - "type": "gpii.windows.spiSettingsHandler.set", - "options": { - "getAction": "SPI_GETMOUSETRAILS", - "setAction": "SPI_SETMOUSETRAILS", - "uiParam": 0, - "pvParam": { - "type": "BOOL" - } - }, - "capabilities": [ - "display.screenEnhancement.mouseTrailing", - "applications.com\\.microsoft\\.windows\\.mouseTrailing.id" - ], - "capabilitiesTransformations": { - "MouseTrails": { - "transform": { - "type": "fluid.transforms.value", - "inputPath": "display.screenEnhancement.mouseTrailing", - "outputPath": "value" - }, - "path": { - "transform": { - "type": "fluid.transforms.literalValue", - "value": { - "get": "pvParam", - "set": "uiParam" - } - } - } - } - } - } - ], - "lifecycleManager": { - "start": [ - "setSettings" - ], - "stop": [ - "restoreSettings" - ] - } - }, - - { - "name": "Windows Cursors", - "id": "com.microsoft.windows.cursors", - "contexts": { - "OS": [ - { - "id": "win32", - "version": ">=5.0" - } - ] - }, - "settingsHandlers": [ - { - "type": "gpii.windows.registrySettingsHandler.set", - "options": { - "hKey": "HKEY_CURRENT_USER", - "path": "Control Panel\\Cursors" - }, - "capabilities": [ - "display.screenEnhancement.cursorSize", - "applications.com\\.microsoft\\.windows\\.cursors.id" - ], - "capabilitiesTransformations": { - "transform": [ - { - "type": "fluid.transforms.condition", - "outputPath": "Arrow.value", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.333 - } - }, - "true": "%SystemRoot%\\cursors\\aero_arrow.cur", - "false": { - "transform": { - "type": "fluid.transforms.condition", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.666 - } - }, - "true": "%SystemRoot%\\cursors\\aero_arrow_l.cur", - "false": "%SystemRoot%\\cursors\\aero_arrow_xl.cur" - } - } - }, { - "type": "fluid.transforms.literalValue", - "value": "REG_SZ", - "outputPath": "Arrow.dataType" - }, { - "type": "fluid.transforms.condition", - "outputPath": "Hand.value", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.333 - } - }, - "true": "%SystemRoot%\\cursors\\aero_link.cur", - "false": { - "transform": { - "type": "fluid.transforms.condition", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.666 - } - }, - "true": "%SystemRoot%\\cursors\\aero_link_l.cur", - "false": "%SystemRoot%\\cursors\\aero_link_xl.cur" - } - } - }, { - "type": "fluid.transforms.literalValue", - "value": "REG_SZ", - "outputPath": "Hand.dataType" - }, { - "type": "fluid.transforms.condition", - "outputPath": "Help.value", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.333 - } - }, - "true": "%SystemRoot%\\cursors\\aero_helpsel.cur", - "false": { - "transform": { - "type": "fluid.transforms.condition", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.666 - } - }, - "true": "%SystemRoot%\\cursors\\aero_helpsel_l.cur", - "false": "%SystemRoot%\\cursors\\aero_helpsel_xl.cur" - } - } - }, { - "type": "fluid.transforms.literalValue", - "value": "REG_SZ", - "outputPath": "Help.dataType" - }, { - "type": "fluid.transforms.condition", - "outputPath": "AppStarting.value", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.333 - } - }, - "true": "%SystemRoot%\\cursors\\aero_working.ani", - "false": { - "transform": { - "type": "fluid.transforms.condition", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.666 - } - }, - "true": "%SystemRoot%\\cursors\\aero_working_l.ani", - "false": "%SystemRoot%\\cursors\\aero_working_xl.ani" - } - } - }, { - "type": "fluid.transforms.literalValue", - "value": "REG_SZ", - "outputPath": "AppStarting.dataType" - }, { - "type": "fluid.transforms.condition", - "outputPath": "No.value", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.333 - } - }, - "true": "%SystemRoot%\\cursors\\aero_unavail.cur", - "false": { - "transform": { - "type": "fluid.transforms.condition", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.666 - } - }, - "true": "%SystemRoot%\\cursors\\aero_unavail_l.cur", - "false": "%SystemRoot%\\cursors\\aero_unavail_xl.cur" - } - } - }, { - "type": "fluid.transforms.literalValue", - "value": "REG_SZ", - "outputPath": "No.dataType" - }, { - "type": "fluid.transforms.condition", - "outputPath": "NWPen.value", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.333 - } - }, - "true": "%SystemRoot%\\cursors\\aero_pen.cur", - "false": { - "transform": { - "type": "fluid.transforms.condition", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.666 - } - }, - "true": "%SystemRoot%\\cursors\\aero_pen_l.cur", - "false": "%SystemRoot%\\cursors\\aero_pen_xl.cur" - } - } - }, { - "type": "fluid.transforms.literalValue", - "value": "REG_SZ", - "outputPath": "NWPen.dataType" - }, { - "type": "fluid.transforms.condition", - "outputPath": "SizeAll.value", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.333 - } - }, - "true": "%SystemRoot%\\cursors\\aero_move.cur", - "false": { - "transform": { - "type": "fluid.transforms.condition", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.666 - } - }, - "true": "%SystemRoot%\\cursors\\aero_move_l.cur", - "false": "%SystemRoot%\\cursors\\aero_move_xl.cur" - } - } - }, { - "type": "fluid.transforms.literalValue", - "value": "REG_SZ", - "outputPath": "SizeAll.dataType" - }, { - "type": "fluid.transforms.condition", - "outputPath": "SizeNESW.value", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.333 - } - }, - "true": "%SystemRoot%\\cursors\\aero_nesw.cur", - "false": { - "transform": { - "type": "fluid.transforms.condition", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.666 - } - }, - "true": "%SystemRoot%\\cursors\\aero_nesw_l.cur", - "false": "%SystemRoot%\\cursors\\aero_nesw_xl.cur" - } - } - }, { - "type": "fluid.transforms.literalValue", - "value": "REG_SZ", - "outputPath": "SizeNESW.dataType" - }, { - "type": "fluid.transforms.condition", - "outputPath": "SizeNS.value", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.333 - } - }, - "true": "%SystemRoot%\\cursors\\aero_ns.cur", - "false": { - "transform": { - "type": "fluid.transforms.condition", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.666 - } - }, - "true": "%SystemRoot%\\cursors\\aero_ns_l.cur", - "false": "%SystemRoot%\\cursors\\aero_ns_xl.cur" - } - } - }, { - "type": "fluid.transforms.literalValue", - "value": "REG_SZ", - "outputPath": "SizeNS.dataType" - }, { - "type": "fluid.transforms.condition", - "outputPath": "SizeNWSE.value", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.333 - } - }, - "true": "%SystemRoot%\\cursors\\aero_nwse.cur", - "false": { - "transform": { - "type": "fluid.transforms.condition", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.666 - } - }, - "true": "%SystemRoot%\\cursors\\aero_nwse_l.cur", - "false": "%SystemRoot%\\cursors\\aero_nwse_xl.cur" - } - } - }, { - "type": "fluid.transforms.literalValue", - "value": "REG_SZ", - "outputPath": "SizeNWSE.dataType" - }, { - "type": "fluid.transforms.condition", - "outputPath": "SizeWE.value", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.333 - } - }, - "true": "%SystemRoot%\\cursors\\aero_ew.cur", - "false": { - "transform": { - "type": "fluid.transforms.condition", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.666 - } - }, - "true": "%SystemRoot%\\cursors\\aero_ew_l.cur", - "false": "%SystemRoot%\\cursors\\aero_ew_xl.cur" - } - } - }, { - "type": "fluid.transforms.literalValue", - "value": "REG_SZ", - "outputPath": "SizeWE.dataType" - }, { - "type": "fluid.transforms.condition", - "outputPath": "UpArrow.value", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.333 - } - }, - "true": "%SystemRoot%\\cursors\\aero_up.cur", - "false": { - "transform": { - "type": "fluid.transforms.condition", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.666 - } - }, - "true": "%SystemRoot%\\cursors\\aero_up_l.cur", - "false": "%SystemRoot%\\cursors\\aero_up_xl.cur" - } - } - }, { - "type": "fluid.transforms.literalValue", - "value": "REG_SZ", - "outputPath": "UpArrow.dataType" - }, { - "type": "fluid.transforms.condition", - "outputPath": "Wait.value", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.333 - } - }, - "true": "%SystemRoot%\\cursors\\aero_busy.ani", - "false": { - "transform": { - "type": "fluid.transforms.condition", - "condition": { - "transform": { - "type": "fluid.transforms.binaryOp", - "leftPath": "display.screenEnhancement.cursorSize", - "operator": "<", - "right": 0.666 - } - }, - "true": "%SystemRoot%\\cursors\\aero_busy_l.ani", - "false": "%SystemRoot%\\cursors\\aero_busy_xl.ani" - } - } - }, { - "type": "fluid.transforms.literalValue", - "value": "REG_SZ", - "outputPath": "Wait.dataType" - } - ] - } - } - ], - "lifecycleManager": { - "start": [ - "setSettings", - { - "type": "gpii.windows.spiSettingsHandler.updateCursors" - } - ], - "stop": [ - "restoreSettings", - { - "type": "gpii.windows.spiSettingsHandler.updateCursors" - } - ] - } - } -] \ No newline at end of file diff --git a/testData/solutions/win32.json5 b/testData/solutions/win32.json5 new file mode 100644 index 000000000..7d14e5e50 --- /dev/null +++ b/testData/solutions/win32.json5 @@ -0,0 +1,36205 @@ +{ + "com.aisquared.zoomtext": { + "name": "ZoomText", + "contexts": { + "OS": [ + { + "id": "win32" + } + ] + }, + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.INISettingsHandler", + "liveness": "manualRestart", + "options": { + "filename": "${{environment}.AppData}\\Freedom Scientific\\ZoomText\\2019\\Config\\zten-US.zxc", + "encoding": "utf16le" + }, + "supportedSettings": { + // [Preferences] + // TODO In the future (especially with the LSR), these types of repeated on/off enum values will + // be inherited rather than defined repeatedly. + "Preferences.SmartInvert": { + "schema": { + "title": "Use Smart Invert", + "description": "Enable/Disable smart invert when using color schemas.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Off", + "On" + ], + "default": 1 + } + }, + "Preferences.PromptOnExit": { + "schema": { + "title": "Prompt On Exit", + "description": "Enable/Disable confirmation prompt when exiting ZoomText.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Off", + "On" + ], + "default": 1 + } + }, + // [PointerScheme] + "PointerScheme.PtrEnhEnable": { + "schema": { + "title": "Use Pointer color schema", + "description": "Enable/Disable the use of a pointer color schema.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Normal", + "Schema" + ], + "default": 1 + } + }, + "PointerScheme.PtrEnhIndex": { + "schema": { + "title": "Pointer color schema", + "description": "Specifies the pointer color schema to use.", + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "enumLabels": [ + "Yellow with Full Crosshairs", + "Red with Circle", + "Giant Green", + "Control Key Crosshairs", + "Circle when Moving", + "Large Yellow" + ], + "default": 6 + } + }, + // [PointerScheme] (Custom settings) + "PointerScheme.PtrEnhSmooth": { + "schema": { + "title": "Pointer smooth", + "description": "Use ZoomText smooth pointer scheme.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "PointerScheme.PtrEnhSize": { + "schema": { + "title": "Pointer size", + "description": "Change pointer size.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "Standard", + "Large", + "ExtraLarge" + ], + "default": 1 + } + }, + "PointerScheme.PtrEnhPointerColor": { + "schema": { + "title": "Pointer color", + "description": "Change pointer color.", + "enum": [ + 255, + 65535, + 65280, + 16776960, + 16711680, + 16711935 + ], + "enumLabels": [ + "Red", + "Yellow", + "Green", + "Cyan", + "Blue", + "Magenta" + ], + "default": 65535 + } + }, + "PointerScheme.PtrEnhEffect": { + "schema": { + "title": "Pointer locator type", + "description": "Change the type of locator for the pointer.", + "enum": [ + 0, + 1, + 2, + 4, + 5, + 3 + ], + "enumLabels": [ + "None", + "Circle", + "Sonar", + "Short Crosshairs", + "Long Crosshairs", + "Full Crosshairs" + ], + "default": 0 + } + }, + "PointerScheme.PtrEnhThickness": { + "schema": { + "title": "Pointer locator thickness", + "description": "Change the thickness of the pointer locator.", + "enum": [ + 1, + 2, + 3 + ], + "enumLabels": [ + "Thin", + "Medium", + "Thick" + ], + "default": 2 + } + }, + "PointerScheme.PtrEnhColor": { + "schema": { + "title": "Pointer locator color", + "description": "Selects a color for the pointer locator.", + "enum": [ + 255, + 65535, + 65280, + 16776960, + 16711680, + 16711935 + ], + "enumLabels": [ + "Red", + "Yellow", + "Green", + "Cyan", + "Blue", + "Magenta" + ], + "default": 65535 + } + }, + "PointerScheme.PtrEnhTransparency": { + "schema": { + "title": "Pointer locator transparency", + "description": "Specifies the pointer locator transparency to use. From 'Clear' to 'Solid'.", + "type": "integer", + "minimum": 31, + "maximum": 255, + "default": 143 + } + }, + "PointerScheme.PtrEnhApply": { + "schema": { + "title": "Pointer locator display mode", + "description": "Changes when to display the pointer locator.", + "enum": [ + 1, + 3, + 2, + 4 + ], + "enumLabels": [ + "Always", + "When pointer is stationary", + "When pointer is moving", + "When modifier key is pressed" + ], + "default": 1 + } + }, + // Bitmask Setting + // GPII-830 Commenting out for now due to missing default value. + // "PointerScheme.PtrEnhModifier": { + // // TODO: Unknown + // "schema": { + // "title": "Pointer Scheme pointer modifiers", + // "description": "Bitmask holding 'pointer behavior modifiers'.", + // "type": "integer" + // }, + // "label": "bitmask" + // }, + // [CursorScheme] + "CursorScheme.CursorEnhEnable": { + "schema": { + "title": "Use Cursor color schema", + "description": "Enable/Disable the use of a cursor color schema.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Normal", + "Schema" + ], + "default": 1 + } + }, + "CursorScheme.CursorEnhIndex": { + "schema": { + "title": "Cursor color schema", + "description": "Specifies the cursor color schema to use.", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "enumLabels": [ + "Custom", + "Blue Wedge", + "Green Circle", + "Red Frame", + "Thin Magenta Frame" + ], + "default": 1 + } + }, + // [CursorScheme] (Custom settings) + "CursorScheme.CursorEnhType": { + "schema": { + "title": "Cursor locator type", + "description": "Selects the type of cursor locator.", + "enum": [ + 1, + 3, + 8 + ], + "enumLabels": [ + "Wedges", + "Circle", + "Frame" + ], + "default": 1 + } + }, + "CursorScheme.CursorEnhSize": { + "schema": { + "title": "Cursor locator size", + "description": "Selects the size of the cursor locator.", + "enum": [ + 1, + 2, + 4 + ], + "enumLabels": [ + "Thin", + "Medium", + "Thick" + ], + "default": 2 + } + }, + "CursorScheme.CursorEnhColor": { + "schema": { + "title": "Cursor locator color", + "description": "Selects the color to use for the cursor locator.", + "enum": [ + 255, + 65535, + 65280, + 16776960, + 16711680, + 16711935 + ], + "enumLabels": [ + "Red", + "Yellow", + "Green", + "Cyan", + "Blue", + "Magenta" + ], + "default": 16711680 + } + }, + "CursorScheme.CursorEnhTransparency": { + "schema": { + "title": "Cursor locator transparency", + "description": "Specifies the cursor locator transparency to use. From 'Clear' to 'Solid'.", + "type": "integer", + "minimum": 31, + "maximum": 255, + "default": 143 + } + }, + "CursorScheme.PtrEnhApply": { + "schema": { + "title": "Cursor locator display mode", + "description": "Changes when to display the cursor locator.", + "enum": [ + 1, + 3, + 2, + 4 + ], + "enumLabels": [ + "Always", + "When pointer is stationary", + "When pointer is moving", + "When modifier key is pressed" + ], + "default": 1 + } + }, + // Bitmask Setting + // GPII-830 Commenting out for now due to missing default value. + // "CursorScheme.PtrEnhModifier": { + // // TODO: Unknown + // "schema": { + // "title": "Cursor Scheme pointer modifiers", + // "description": "Bitmask holding 'pointer behavior modifiers'.", + // "type": "integer" + // }, + // "label": "bitmask" + // }, + // [ColorScheme] + "ColorScheme.ColorSchEnable": { + "schema": { + "title": "Use Color schema", + "description": "Enable/Disable the use of a color schema.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Normal", + "Scheme" + ], + "default": 1 + } + }, + "ColorScheme.ColorSchIndex": { + "schema": { + "title": "Color schema", + "description": "Specifies the color schema to use.", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6 + ], + "enumLabels": [ + "Custom", + "Invert Brightness", + "Invert Colors", + "Yellow on Black", + "Blue Dye", + "Black and White", + "White and Black" + ], + "default": 1 + } + }, + // [ColorScheme] Custom settings + "ColorScheme.ColorSchBrightness": { + "schema": { + "title": "Color schema brightness", + "description": "Specifies the brightness of the screen to use.", + "type": "integer", + "minimum": 40, + "maximum": 160, + "default": 100 + } + }, + "ColorScheme.ColorSchContrastScalar": { + "schema": { + "title": "Color schema contrast", + "description": "Specifies the contrast of the screen to use.", + "type": "integer", + "minimum": 10, + "maximum": 190, + "default": 100 + } + }, + // GPII-830 Commenting out for now due to missing default value. + // "ColorScheme.ColorSchType": { + // "schema": { + // "title": "Color effect", + // "description": "Selects a effect to be used in screen colors.", + // "enum": [ + // 0, + // 2048, + // 16, + // 1, + // 2, + // 4 + // ], + // "enumLabels": [ + // "Invert Brightness", + // "Invert Colors", + // "Dyed", + // "Two-Color", + // "Replace Color" + // ] + // } + // }, + // GPII-830 Commenting out for now due to missing default value. + // "ColorScheme.ColorSchDye": { + // "schema": { + // "title": "Color hue map", + // "description": "Selects a color hue in which all screen colors are going to be mapped.", + // "enum": [ + // 16740194, + // 65535, + // 65280, + // 16776960, + // 6451199, + // 16744703 + // ], + // "enumLabels": [ + // "Light Red", + // "Yellow", + // "Green", + // "Cyan", + // "Light Blue", + // "Light Magenta" + // ] + // } + // }, + // GPII-830 Commenting out for now due to missing default value. + // "ColorScheme.ColorSchTwoColorOne": { + // "schema": { + // "title": "Two-Color effect foreground color", + // "description": "Color to be selected as foreground color in the two-color effect.", + // "enum": [ + // 32768, + // 8388736, + // 8388608, + // 128, + // 4934475, + // 0 + // ], + // "enumLabels": [ + // "Dark Green", + // "Dark Magenta", + // "Dark Blue", + // "Dark Red", + // "Dark Gray", + // "Black" + // ] + // } + // }, + // GPII-830 Commenting out for now due to missing default value. + // "ColorScheme.ColorSchTwoColorTwo": { + // "schema": { + // "title": "Two-Color effect background color", + // "description": "Color to be selected as background color in the two-color effect.", + // "enum": [ + // 65535, + // 8454016, + // 16776960, + // 16762566, + // 16762623, + // 11842740, + // 16777215 + // ], + // "enumLabels": [ + // "Yellow", + // "Ligth Green", + // "Cyan", + // "Slate Blue", + // "Rose", + // "Ligth Gray", + // "White" + // ] + // } + // }, + "ColorScheme.ColorSchReplaceColor": { + "schema": { + "title": "Color to be replaced", + "description": "Color that is going to be replaced.", + "enum": [ + 255, + 65535, + 65280, + 16776960, + 16711935, + 49344752 + ], + "enumLabels": [ + "Default", + "Red", + "Yellow", + "Green", + "Cyan", + "Magenta", + "Dialogbox Gray" + ], + "default": 49344752 + } + }, + "ColorScheme.ColorSchWithColor": { + "schema": { + "title": "Color to be replaced", + "description": "Color that is going to be replaced.", + "enum": [ + 255, + 65535, + 65280, + 16776960, + 16711680, + 16711935 + ], + "enumLabels": [ + "Red", + "Yellow", + "Green", + "Cyan", + "Blue", + "White" + ], + "default": 16711935 + } + }, + "ColorScheme.ColorSchReplaceLevel": { + "schema": { + "title": "Two-Color effect background color", + "description": "Color to be selected as background color in the two-color effect.", + "type": "integer", + "minimum": 0, + "maximum": 255, + "default": 87 + } + }, + // [FocusScheme] + "FocusScheme.FocusENHEnable": { + "schema": { + "title": "Use Focus color schema", + "description": "Enable/Disable the use of a 'Focus color schema'.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Normal", + "Scheme" + ], + "default": 1 + } + }, + "FocusScheme.FocusENHIndex": { + "schema": { + "title": "Focus color schema", + "description": "Specifies the Focus color schema to use.", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "enumLabels": [ + "Custom", + "Red Rectangle", + "Thick Green Underline", + "Yellow Block", + "Thin Red Rectangle" + ], + "default": 1 + } + }, + // [FocusScheme] (Custom settings) + "FocusScheme.FocusENHShape": { + "schema": { + "title": "Focus locator type", + "description": "Specifies the Focus locator type to use.", + "enum": [ + 1, + 3, + 2 + ], + "enumLabels": [ + "Block", + "Underline", + "Frame" + ], + "default": 2 + } + }, + "FocusScheme.FocusENHColor": { + "schema": { + "title": "Focus locator color", + "description": "Specifies the locator color.", + "enum": [ + -2147483648, + 0, + 255, + 65535, + 65280, + 16776960, + 16711680, + 16711935, + 16777215 + ], + "enumLabels": [ + "Invert", + "Black", + "Red", + "Yellow", + "Green", + "Cyan", + "Blue", + "Magenta", + "White" + ], + "default": 255 + } + }, + "FocusScheme.FocusENHThickness": { + "schema": { + "title": "Focus locator thickness", + "description": "Specifies the thickness of the focus locator.", + "enum": [ + 1, + 5, + 9 + ], + "enumLabels": [ + "Thin", + "Medium", + "Thick" + ], + "default": 5 + } + }, + "FocusScheme.FocusENHTransparency": { + "schema": { + "title": "Focus locator transparency", + "description": "Specifies the transparency of the focus locator. Range goes from 'Clear' to 'Solid'.", + "type": "integer", + "minimum": 31, + "maximum": 255, + "default": 143 + } + }, + "FocusScheme.FocusENHPadding": { + "schema": { + "title": "Focus locator margin", + "description": "Specifies the margin for the focus locator. Range goes from 'Narrow' to 'Wide'.", + "type": "integer", + "minimum": 1, + "maximum": 9, + "default": 1 + } + }, + "FocusScheme.FocusENHMode": { + "schema": { + "title": "Focus locator display mode", + "description": "Specifies how and when the focus locator is displayed.", + "enum": [ + 2, + 1, + 3 + ], + "enumLabels": [ + "Continuosly", + "Briefly", + "When the modifier key is pressed" + ], + "default": 2 + } + }, + // Bitmask setting + // GPII-830 Commenting out for now due to missing default value. + // "FocusScheme.FocusENHModifier": { + // // TODO: Unknown + // "schema": { + // "title": "Focus Modifiers", + // "description": "Bitmask holding 'focus behavior modifiers'.", + // "type": "integer" + // }, + // "label": "bitmask" + // }, + // [EchoTyping] + "EchoTyping.Mode": { + "schema": { + "title": "Keyboard Echo", + "description": "Echoing mode for keyboard.", + "enum": [ + 2, + 32769, + 32770, + 32771 + ], + "enumLabels": [ + "No Echo", + "Keys", + "Words", + "Keys and Words" + ], + "default": 32770 + } + }, + // [EchoMouse] + "EchoMouse.HoverMode": { + "schema": { + "title": "Mouse Echo Hover mode", + "description": "Automatically moves the pointer to menu items and dialog controls when they receive focus.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Instant mode", + "Hover mode" + ], + "default": 1 + } + }, + "EchoMouse.Enabled": { + "schema": { + "title": "Mouse Echo", + "description": "Enable/Disables mouse echo.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Off", + "On" + ], + "default": 1 + } + }, + "EchoMouse.LineMode": { + "schema": { + "title": "Word echo mode", + "description": "Changes the behavior when mouse passes over a word.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Echo the word only", + "Echo all the words in the line or object" + ], + "default": 1 + } + }, + // [Verbosity] + "Verbosity.CurrentLevel": { + "schema": { + "title": "Verbosity Level", + "description": "Adjust the amount of information spoken about program controls when they become active or highlighted.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "High", + "Medium", + "Low" + ], + "default": 2 + } + }, + // [ZtSpeech] + // ================================================================== + "ZtSpeech.SapiProductName": { + "schema": { + "title": "Current Synthesizer", + "description": "Name of the current synthesizer to be used.", + "enum": [ + "VocalizerExpressive-English", // Language: 9 + "Microsoft Zira Desktop - English (United States)-English", // Language: 1033 + "Microsoft David Desktop - English (United States)-English", // Language: 1033 + "msmobile-English", // Language: 9 + "VocalizerExpressive-German", // Language: 7 + "VocalizerExpressive-Spanish", // Language: 10 + "VocalizerExpressive-French", // Language: 12 + "VocalizerExpressive-Dutch", // Language: 19 + "VocalizerExpressive-Arabic", // Language: 1 + "VocalizerExpressive-Portuguese", // Language: 22 + "VocalizerExpressive-Czech", // Language: 5 + "VocalizerExpressive-Danish", // Language: 6 + "VocalizerExpressive-Finnish", // Language: 11 + "VocalizerExpressive-Hebrew", // Language: 13 + "VocalizerExpressive-Hungarian", // Language: 13 + "VocalizerExpressive-Italian", // Language: 16 + "VocalizerExpressive-Korean", // Language: 18 + "VocalizerExpressive-Norwegian", // Language: 20 + "VocalizerExpressive-Polish", // Language: 21 + "VocalizerExpressive-Romanian", // Language: 24 + "VocalizerExpressive-Slovak" // Language: 27 + ], + "enumLabels": [ + "VocalizerExpressive-English", + "Microsoft Zira Desktop - English (United States)-English", + "Microsoft David Desktop - English (United States)-English", + "msmobile-English", + "VocalizerExpressive-German", + "VocalizerExpressive-French", + "VocalizerExpressive-Dutch", + "VocalizerExpressive-Arabic", + "VocalizerExpressive-Portuguese", + "VocalizerExpressive-Czech", + "VocalizerExpressive-Danish", + "VocalizerExpressive-Hebrew", + "VocalizerExpressive-Hungarian", + "VocalizerExpressive-Italian", + "VocalizerExpressive-Korean", + "VocalizerExpressive-Norwegian", + "VocalizerExpressive-Polish", + "VocalizerExpressive-Romanian", + "VocalizerExpressive-Slovak" + ], + "default": "VocalizerExpressive-English" + } + }, + "ZtSpeech.Language": { + "schema": { + "title": "Language id", + "description": "Language code identifier.", + "enum": [ + 9, + 7, + 10, + 12, + 1033, + 19, + 1, + 22, + 5, + 6, + 11, + 13, + 14, + 16, + 18, + 20, + 24, + 27 + ], + "enumLabels": [ + "msmobile-English", + "German", + "Spanish", + "French", + "English", + "Dutch", + "Arabic", + "Portuguese", + "Czech", + "Danish", + "Finnish", + "Hebrew", + "Hungarian", + "Italian", + "Korean", + "Norwegian", + "Romanian", + "Slovak" + ], + "default": 9 + } + }, + // VOICES SECTION + // ================================================================== + + // [Microsoft David Desktop - English (United States)-English Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "Microsoft David Desktop - English (United States)-English Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - Default. + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "Microsoft David Desktop - English (United States)-English Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "Microsoft David Desktop - English (United States)-English Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "Microsoft David Desktop - English (United States)-English Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "Microsoft David Desktop - English (United States)-English Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "default": 0 + } + }, + "Microsoft David Desktop - English (United States)-English Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 50, + "maximum": 400, + "default": 100 + } + }, + "Microsoft David Desktop - English (United States)-English Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 100 + } + }, + "Microsoft David Desktop - English (United States)-English Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "Microsoft David Desktop - English (United States)-English Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "Microsoft David Desktop - English (United States)-English Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [Microsoft David Desktop - English (United States)-English Speech - DocReader] + // GPII-830 Commenting out for now due to missing default value. + // "Microsoft David Desktop - English (United States)-English Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - Default. + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "Microsoft David Desktop - English (United States)-English Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "Microsoft David Desktop - English (United States)-English Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "Microsoft David Desktop - English (United States)-English Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "Microsoft David Desktop - English (United States)-English Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "default": 0 + } + }, + "Microsoft David Desktop - English (United States)-English Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 50, + "maximum": 400, + "default": 100 + } + }, + "Microsoft David Desktop - English (United States)-English Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 100 + } + }, + "Microsoft David Desktop - English (United States)-English Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "Microsoft David Desktop - English (United States)-English Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "Microsoft David Desktop - English (United States)-English Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [Microsoft Zira Desktop - English (United States)-English Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "Microsoft Zira Desktop - English (United States)-English Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - Default. + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "Microsoft Zira Desktop - English (United States)-English Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "Microsoft Zira Desktop - English (United States)-English Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "Microsoft Zira Desktop - English (United States)-English Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "Microsoft Zira Desktop - English (United States)-English Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "default": 0 + } + }, + "Microsoft Zira Desktop - English (United States)-English Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 50, + "maximum": 400, + "default": 100 + } + }, + "Microsoft Zira Desktop - English (United States)-English Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 100 + } + }, + "Microsoft Zira Desktop - English (United States)-English Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "Microsoft Zira Desktop - English (United States)-English Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "Microsoft Zira Desktop - English (United States)-English Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [Microsoft Zira Desktop - English (United States)-English Speech - DocReader] + // GPII-830 Commenting out for now due to missing default value. + // "Microsoft Zira Desktop - English (United States)-English Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - Default. + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "Microsoft Zira Desktop - English (United States)-English Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "Microsoft Zira Desktop - English (United States)-English Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "Microsoft Zira Desktop - English (United States)-English Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "Microsoft Zira Desktop - English (United States)-English Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "default": 0 + } + }, + "Microsoft Zira Desktop - English (United States)-English Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 50, + "maximum": 400, + "default": 100 + } + }, + "Microsoft Zira Desktop - English (United States)-English Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 100 + } + }, + "Microsoft Zira Desktop - English (United States)-English Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "Microsoft Zira Desktop - English (United States)-English Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "Microsoft Zira Desktop - English (United States)-English Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [msmobile-English Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "msmobile-English Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Microsoft David, United States" + // // - "Microsoft Mark, United States" + // // - "Microsoft Zira, United States" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "msmobile-English Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "msmobile-English Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "msmobile-English Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "msmobile-English Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "Microsoft David, United States", + "Microsoft Mark, United States", + "Microsoft Zira, United States" + ], + "default": 0 + } + }, + "msmobile-English Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 20, + "default": 10 + } + }, + "msmobile-English Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 100 + } + }, + "msmobile-English Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "msmobile-English Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "msmobile-English Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [msmobile-English Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "msmobile-English Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Microsoft David, United States" + // // - "Microsoft Mark, United States" + // // - "Microsoft Zira, United States" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "msmobile-English Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "msmobile-English Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "msmobile-English Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "msmobile-English Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "Microsoft David, United States", + "Microsoft Mark, United States", + "Microsoft Zira, United States" + ], + "default": 0 + } + }, + "msmobile-English Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 20, + "default": 10 + } + }, + "msmobile-English Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 100 + } + }, + "msmobile-English Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "msmobile-English Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "msmobile-English Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [VocalizerExpressive-English Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-English Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Tom Compact, United States", + // // - "Zoe Compact, United States", + // // - "Malcolm Compact, British" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-English Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-English Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-English Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-English Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "Tom Compact, United States", + "Zoe Compact, United States", + "Malcolm Compact, British" + ], + "default": 0 + } + }, + "VocalizerExpressive-English Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-English Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-English Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-English Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-English Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-English Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-English Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Tom Compact, United States", + // // - "Zoe Compact, United States", + // // - "Malcolm Compact, British" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-English Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-English Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-English Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-English Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "Tom Compact, United States", + "Zoe Compact, United States", + "Malcolm Compact, British" + ], + "default": 0 + } + }, + "VocalizerExpressive-English Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-English Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-English Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-English Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-English Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [VocalizerExpressive-German Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-German Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Anna Compact", + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-German Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-German Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-German Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-German Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Anna Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-German Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-German Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-German Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-German Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-German Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-German Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-German Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Anna Compact", + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-German Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-German Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-German Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-German Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Anna Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-German Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-German Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-German Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-German Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-German Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [VocalizerExpressive-French Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-French Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Audrey Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-French Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-French Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-French Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-French Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Audrey Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-French Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-French Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-French Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-French Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-French Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-French Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-French Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Anna Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-French Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-French Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-French Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-French Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Audrey Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-French Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-French Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-French Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-French Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-French Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [VocalizerExpressive-Spanish Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Spanish Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Jorge Compact, Castilian" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Spanish Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Spanish Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Spanish Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Spanish Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Jorge Compact, Castilian" + ], + "default": 0 + } + }, + "VocalizerExpressive-Spanish Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Spanish Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Spanish Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Spanish Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Spanish Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-Spanish Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Spanish Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Jorge Compact, Castilian" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Spanish Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Spanish Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Spanish Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Spanish Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Jorge Compact, Castilian" + ], + "default": 0 + } + }, + "VocalizerExpressive-Spanish Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Spanish Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Spanish Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Spanish Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Spanish Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [VocalizerExpressive-Dutch Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Dutch Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Claire Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Dutch Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Dutch Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Dutch Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Dutch Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Claire Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Dutch Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Dutch Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Dutch Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Dutch Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Dutch Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-Dutch Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Dutch Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Claire Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Dutch Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Dutch Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Dutch Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Dutch Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Claire Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Dutch Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Dutch Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Dutch Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Dutch Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Dutch Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [VocalizerExpressive-Arabic Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Arabic Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Tarik Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Arabic Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Arabic Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Arabic Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Arabic Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Tarik Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Arabic Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Arabic Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Arabic Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Arabic Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Arabic Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-Arabic Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Arabic Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Tarik Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Arabic Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Arabic Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Arabic Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Arabic Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Tarik Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Arabic Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Arabic Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Arabic Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Arabic Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Arabic Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [VocalizerExpressive-Portuguese Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Portuguese Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Felipe Compact, Brazilian" + // // - "Catarina Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Portuguese Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Portuguese Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Portuguese Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Portuguese Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Felipe Compact, Brazilian", + "Catarina Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Portuguese Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Portuguese Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Portuguese Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Portuguese Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Portuguese Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-Portuguese Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Portuguese Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Felipe Compact, Brazilian" + // // - "Catarina Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Portuguese Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Portuguese Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Portuguese Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Portuguese Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Felipe Compact, Brazilian", + "Catarina Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Portuguese Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Portuguese Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Portuguese Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Portuguese Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Portuguese Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [VocalizerExpressive-Czech Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Czech Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Zuzana Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Czech Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Czech Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Czech Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Czech Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Zuzana Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Czech Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Czech Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Czech Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Czech Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Czech Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-Czech Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Czech Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Zuzana Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Czech Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Czech Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Czech Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Czech Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Zuzana Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Czech Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Czech Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Czech Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Czech Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Czech Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [VocalizerExpressive-Danish Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Danish Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Sara Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Danish Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Danish Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Danish Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Danish Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Sara Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Danish Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Danish Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Danish Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Danish Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Danish Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-Danish Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Danish Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Sara Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Danish Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Danish Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Danish Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Danish Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Sara Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Danish Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Danish Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Danish Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Danish Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Danish Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [VocalizerExpressive-Finnish Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Finnish Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Satu Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Finnish Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Finnish Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Finnish Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Finnish Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Satu Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Finnish Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Finnish Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Finnish Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Finnish Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Finnish Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-Finnish Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Finnish Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Satu Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Finnish Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Finnish Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Finnish Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Finnish Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Satu Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Finnish Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Finnish Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Finnish Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Finnish Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Finnish Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [VocalizerExpressive-Hebrew Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Hebrew Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Carmit Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Hebrew Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Hebrew Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Hebrew Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Hebrew Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Carmit Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Hebrew Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Hebrew Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Hebrew Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Hebrew Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Hebrew Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-Hebrew Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Hebrew Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Carmit Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Hebrew Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Hebrew Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Hebrew Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Hebrew Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Carmit Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Hebrew Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Hebrew Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Hebrew Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Hebrew Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Hebrew Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [VocalizerExpressive-Hungarian Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Hungarian Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Mariska Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Hungarian Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Hungarian Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Hungarian Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Hungarian Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Mariska Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Hungarian Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Hungarian Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Hungarian Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Hungarian Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Hungarian Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-Hungarian Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Hungarian Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Mariska Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Hungarian Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Hungarian Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Hungarian Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Hungarian Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Mariska Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Hungarian Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Hungarian Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Hungarian Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Hungarian Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Hungarian Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [VocalizerExpressive-Italian Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Italian Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Alice Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Italian Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Italian Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Italian Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Italian Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Alice Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Italian Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Italian Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Italian Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Italian Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Italian Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-Italian Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Italian Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Alice Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Italian Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Italian Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Italian Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Italian Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Alice Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Italian Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Italian Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Italian Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Italian Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Italian Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [VocalizerExpressive-Korean Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Korean Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Sora Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Korean Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Korean Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Korean Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Korean Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Sora Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Korean Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Korean Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Korean Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Korean Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Korean Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-Korean Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Korean Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Sora Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Korean Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Korean Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Korean Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Korean Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Sora Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Korean Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Korean Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Korean Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Korean Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Korean Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [VocalizerExpressive-Norwegian Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Norwegian Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Henrik Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Norwegian Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Norwegian Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Norwegian Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Norwegian Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Henrik Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Norwegian Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Norwegian Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Norwegian Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Norwegian Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Norwegian Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-Norwegian Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Norwegian Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Henrik Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Norwegian Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Norwegian Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Norwegian Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Norwegian Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Henrik Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Norwegian Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Norwegian Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Norwegian Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Norwegian Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Norwegian Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [VocalizerExpressive-Polish Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Polish Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Krzysztof Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Polish Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Polish Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Polish Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Polish Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Krzysztof Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Polish Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Polish Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Polish Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Polish Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Polish Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-Polish Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Polish Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Krzysztof Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Polish Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Polish Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Polish Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Polish Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Krzysztof Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Polish Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Polish Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Polish Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Polish Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Polish Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [VocalizerExpressive-Romanian Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Romanian Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Ioana Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Romanian Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Romanian Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Romanian Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Romanian Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Ioana Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Romanian Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Romanian Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Romanian Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Romanian Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Romanian Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-Romanian Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Romanian Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Ioana Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Romanian Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Romanian Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Romanian Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Romanian Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Ioana Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Romanian Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Romanian Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Romanian Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Romanian Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Romanian Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [VocalizerExpressive-Swedish Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Swedish Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Oskar Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Swedish Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Swedish Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Swedish Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Swedish Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Oskar Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Swedish Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Swedish Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Swedish Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Swedish Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Swedish Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-Swedish Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Swedish Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Oskar Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Swedish Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Swedish Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Swedish Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Swedish Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Oskar Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Swedish Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Swedish Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Swedish Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Swedish Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Swedish Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + + // [VocalizerExpressive-Slovak Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Slovak Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Laura Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Slovak Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Slovak Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Slovak Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Slovak Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Laura Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Slovak Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Slovak Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Slovak Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Slovak Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Slovak Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-Slovak Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Slovak Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Laura Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Slovak Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Slovak Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Slovak Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Slovak Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Laura Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Slovak Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Slovak Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Slovak Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Slovak Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Slovak Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [VocalizerExpressive-Turkish Speech - Screen Reader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Turkish Speech - Screen Reader.Speaker": { + // // NOTE: Possible known values: + // // - "Yelda Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Turkish Speech - Screen Reader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Turkish Speech - Screen Reader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Turkish Speech - Screen Reader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Turkish Speech - Screen Reader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Yelda Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Turkish Speech - Screen Reader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Turkish Speech - Screen Reader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Turkish Speech - Screen Reader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Turkish Speech - Screen Reader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Turkish Speech - Screen Reader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // [VocalizerExpressive-Turkish Speech - DocReader] + // ================================================================== + // GPII-830 Commenting out for now due to missing default value. + // "VocalizerExpressive-Turkish Speech - DocReader.Speaker": { + // // NOTE: Possible known values: + // // - "Yelda Compact" + // "schema": { + // "title": "Voice Name", + // "description": "Specifies the voice name to use.", + // "type": "string" + // } + // }, + "VocalizerExpressive-Turkish Speech - DocReader.Valid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Turkish Speech - DocReader.SettingsValid": { + "schema": { + "title": "Valid", + "description": "Unknown, always 1 when setting is selected.", + "default": 1 + } + }, + "VocalizerExpressive-Turkish Speech - DocReader.Muted": { + "schema": { + "title": "Muted", + "description": "Mutes audio output for this synthesizer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "On", + "Muted" + ], + "default": 0 + } + }, + "VocalizerExpressive-Turkish Speech - DocReader.Voice": { + "schema": { + "title": "Voice Name", + "description": "Specifies the voice name to use.", + "enum": [ + 0 + ], + "enumLabels": [ + "Yelda Compact" + ], + "default": 0 + } + }, + "VocalizerExpressive-Turkish Speech - DocReader.Rate": { + "schema": { + "title": "Speech Rate (WPM)", + "description": "Specifies speech rate for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 400, + "default": 100 + } + }, + "VocalizerExpressive-Turkish Speech - DocReader.Volume": { + "schema": { + "title": "Voice volume", + "description": "Specifies the voice volume for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "VocalizerExpressive-Turkish Speech - DocReader.Pitch": { + "schema": { + "title": "Voice Pitch", + "description": "Specifies the voice pitch for the current synthesizer.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 33 + } + }, + "VocalizerExpressive-Turkish Speech - DocReader.CapPitchDelta": { + "schema": { + "title": "Pitch when reading a capital", + "description": "Specifies a percentage change in pitch while reading capital letters.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "VocalizerExpressive-Turkish Speech - DocReader.LinkPitchDelta": { + "schema": { + "title": "Pitch when reading a link", + "description": "Specifies a percentage change in pitch while reading links.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + + // [Tracking] + // GPII-830 Commenting out for now due to missing default value. + // "Tracking.AlignFlags": { + // // TODO: Unknown + // "schema": { + // "title": "Align Flags", + // "description": "Bitmask holding 'align flags'.", + // "type": "integer" + // }, + // "label": "bitmask" + // }, + "Tracking.RouteMouse": { + "schema": { + "title": "Route pointer into view when it's moved", + "description": "Automatically moves the pointer to the center of the magnified view, whenever the pointer moves while located outside the view.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Off", + "On" + ], + "default": 1 + } + }, + "Tracking.MouseToFocus": { + "schema": { + "title": "Route pointer over the active control", + "description": "Automatically moves the pointer to menu items and dialog controls when they receive focus.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Off", + "On" + ], + "default": 1 + } + }, + // [MagEngine] + // + // NOTE: UI values to bitmask: + // - "Hold Shift key to move only horizontally or vertically": 0 + // - "Hold Ctrl key to stay within the active window": 1 + // GPII-830 Commenting out for now due to lack of a minimum or maximum value. + // "MagEngine.MouseConstraint": { + // "schema": { + // "title": "Pointer Constraint", + // "description": "Specifies desired pointer movement constratins.", + // "type": "integer", + // "default": 0 + // }, + // "label": "bitmask" + // }, + // [Smooth Panning] + "Smooth Panning.Enabled": { + "schema": { + "title": "Smooth Panning", + "description": "Enable/Disables smooth panning feature.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Off", + "On" + ], + "default": 1 + } + }, + "Smooth Panning.Panning Speed": { + "schema": { + "title": "Smooth Panning", + "description": "Specifies the desired panning speed.", + "type": "integer", + "minimum": 1, + "maximum": 9, + "default": 1 + } + }, + // [SmoothPanning] + "SmoothPanning.EnableSmoothPanInAppReader": { + "schema": { + "title": "Enable", + "description": "Specifies the desired panning speed.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Off", + "On" + ], + "default": 0 + } + }, + // [BACKGROUND] + // GPII-830 Commenting out for now due to missing default value. + // "BACKGROUND.WindowFlags": { + // // TODO: Unknown + // "schema": { + // "title": "Window Flags", + // "description": "Bitmask holding 'window flags'.", + // "type": "integer" + // }, + // "label": "bitmask" + // }, + // [PRIMARY] + "PRIMARY.magPower": { + "schema": { + "title": "Zoom Level", + "description": "Adjust the zoom level.", + "enum": [ + 100, + 120, + 140, + 160, + 180, + 200, + 225, + 250, + 275, + 300, + 350, + 400, + 500, + 600, + 700, + 800, + 900, + 1000, + 1400, + 1500, + 1600, + 1700, + 1800, + 1900, + 2000, + 2100, + 2200, + 2300, + 2400, + 2500, + 2600, + 2700, + 2800, + 2900, + 3000, + 3100, + 3200, + 3300, + 3400, + 3500, + 3600, + 3700, + 3800, + 3900, + 4000, + 4100, + 4200, + 4300, + 4400, + 4500, + 4600, + 4700, + 4800, + 4900, + 5000, + 5100, + 5200, + 5300, + 5400, + 5500, + 5600, + 5700, + 5800, + 5900, + 6000 + ], + "enumLabels": [ + "100", + "120", + "140", + "160", + "180", + "200", + "225", + "250", + "275", + "300", + "350", + "400", + "500", + "600", + "700", + "800", + "900", + "1000", + "1400", + "1500", + "1600", + "1700", + "1800", + "1900", + "2000", + "2100", + "2200", + "2300", + "2400", + "2500", + "2600", + "2700", + "2800", + "2900", + "3000", + "3100", + "3200", + "3300", + "3400", + "3500", + "3600", + "3700", + "3800", + "3900", + "4000", + "4100", + "4200", + "4300", + "4400", + "4500", + "4600", + "4700", + "4800", + "4900", + "5000", + "5100", + "5200", + "5300", + "5400", + "5500", + "5600", + "5700", + "5800", + "5900", + "6000" + ], + "default": 200 + } + }, + "PRIMARY.wndType": { + "schema": { + "title": "Zoom Window Type", + "description": "Specifies desired zoom window type.", + "enum": [ + 0, + 1, + 2, + 5, + // TODO: Non portable + // ========================================================= + 3, + 4 + // ========================================================= + ], + "enumLabels": [ + "Full", + "Overlay", + "Lens", + "Line", + // TODO: Non portable + "Docked Top | Docked Bottom", + "Docked Left | Docked Right" + // ========================================================= + ], + "default": 0 + } + }, + // GPII-830 Commenting out for now due to missing default value. + // "PRIMARY.WindowFlags": { + // // TODO: Unknown + // "schema": { + // "title": "Window Flags", + // "description": "Bitmask holding 'Window Flags'.", + // "type": "integer" + // }, + // "label": "bitmask" + // }, + // [STATIC 1] + "STATIC 1.magPower": { + "schema": { + "title": "Zoom Level", + "description": "Adjust the zoom level.", + "enum": [ + 100, + 120, + 140, + 160, + 180, + 200, + 225, + 250, + 275, + 300, + 350, + 400, + 500, + 600, + 700, + 800, + 900, + 1000, + 1400, + 1500, + 1600, + 1700, + 1800, + 1900, + 2000, + 2100, + 2200, + 2300, + 2400, + 2500, + 2600, + 2700, + 2800, + 2900, + 3000, + 3100, + 3200, + 3300, + 3400, + 3500, + 3600, + 3700, + 3800, + 3900, + 4000, + 4100, + 4200, + 4300, + 4400, + 4500, + 4600, + 4700, + 4800, + 4900, + 5000, + 5100, + 5200, + 5300, + 5400, + 5500, + 5600, + 5700, + 5800, + 5900, + 6000 + ], + "enumLabels": [ + "100", + "120", + "140", + "160", + "180", + "200", + "225", + "250", + "275", + "300", + "350", + "400", + "500", + "600", + "700", + "800", + "900", + "1000", + "1400", + "1500", + "1600", + "1700", + "1800", + "1900", + "2000", + "2100", + "2200", + "2300", + "2400", + "2500", + "2600", + "2700", + "2800", + "2900", + "3000", + "3100", + "3200", + "3300", + "3400", + "3500", + "3600", + "3700", + "3800", + "3900", + "4000", + "4100", + "4200", + "4300", + "4400", + "4500", + "4600", + "4700", + "4800", + "4900", + "5000", + "5100", + "5200", + "5300", + "5400", + "5500", + "5600", + "5700", + "5800", + "5900", + "6000" + ], + "default": 200 + } + }, + // Available: "Tracking.AlignFlags: ["Centered within the zoom window", "Centered within the zoom window", "***"]" + "PRIMARY.hMarginSmooth": { + "schema": { + "title": "Mouse horizontal edge margin", + "description": "Specifies the edge margin of mouse within the zoom window.", + "type": "integer", + "minimum": 0, + "maximum": 50, + "default": 15 + } + }, + // Available: "Tracking.AlignFlags: ["Centered within the zoom window", "Centered within the zoom window", "***"]" + "PRIMARY.vMarginSmooth": { + "schema": { + "title": "Mouse vertical edge margin", + "description": "Specifies the edge margin of mouse within the zoom window.", + "type": "integer", + "minimum": 0, + "maximum": 50, + "default": 15 + } + }, + // Available: "Tracking.AlignFlags: ["Centered within the zoom window", "Centered within the zoom window", "***"]" + "PRIMARY.hMarginCaretSmooth": { + "schema": { + "title": "Text cursor horizontal edge margin", + "description": "Specifies the edge margin of cursor within the zoom window.", + "type": "integer", + "minimum": 0, + "maximum": 50, + "default": 15 + } + }, + // Available: "Tracking.AlignFlags: ["Centered within the zoom window", "Centered within the zoom window", "***"]" + "PRIMARY.vMarginCaretSmooth": { + "schema": { + "title": "Text cursor edge margin", + "description": "Specifies the edge margin of text cursor within the zoom window.", + "type": "integer", + "minimum": 0, + "maximum": 50, + "default": 15 + } + } + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/magnification" + ], + "capabilitiesTransformations": { + "Preferences\\.PromptOnExit": { + "literalValue": 0 + }, + "PRIMARY\\.magPower": { + transform: { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/magnification", + "factor": 100 + } + }, + "STATIC 1\\.magPower": { + transform: { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/magnification", + "factor": 100 + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/magnification": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "PRIMARY\\.magPower", + "factor": 0.01 + } + } + } + } + }, + "configure": [ + "settings.configuration" + ], + "restore": [ + "settings.configuration" + ], + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "AiSquared.ZoomText.UI.exe" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\ZoomText2019.exe\\}\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "Zt.exe", + "options": { + "closeWindow": true + } + } + ] + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.registryKeyExists", + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Freedom Scientific\\ZoomText\\2019", + "subPath": "installedDesktop", + "dataType": "REG_SZ" + } + ] + }, + "com.freedomscientific.jaws": { + "name": "JAWS", + "contexts": { + "OS": [ + { + "id": "win32" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled" + // "http://registry\\.gpii\\.net/applications/com\\.freedomscientific\\.jaws" + ], + "settingsHandlers": { + "configuration1": { + "type": "gpii.settingsHandlers.INISettingsHandler", + "liveness": "manualRestart", + "options": { + // TODO: Create a base grade for JAWS and version-specific variations. Make JAWS 2019 the default. Restart version-detection discussions and "capability" discussions. + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF" + }, + "supportedSettings": { + // ------------------- Basic Settings --------------------- + + // ------------- Basic Settings - Tutor Messages ---------- + "OutputModes.TUTOR": { + "schema": { + "title": "Tutor messages", + "description": "Select which kind of menu and control help to announce", + "enum": [ + "0|0|0|Tutor", + "1|1|1|Tutor", + "2|2|2|Tutor" + ], + "enumLabels": [ + "Turn off menu and control help", + "Announce menu and control help", + "Announce custom messages only" + ], + "default": "1|1|1|Tutor" + } + }, + + // ------------- Basic Settings - Access Key -------------- + + "OutputModes.ACCESS_KEY": { + "schema": { + "title": "Access Key", + "description": "Select which access keys to speak", + "enum": [ + "0|0|0|Access Key", + "1|1|1|Access Key", + "2|2|2|Access Key", + "3|3|3|Access Key" + ], + "enumLabels": [ + "Off", + "Speak all", + "Speak menus only", + "Speak dialogs only" + ], + "default": "1|1|1|Access Key" + } + }, + + // -------------------------------------------------------- + + "Options.confirmWhenExitingJAWS": { + "schema": { + "title": "Confirm when Exiting JAWS", + "description": "Whether or not to present a confirmation menu when exiting JAWS.", + "enum": [0, 1], + "enumLabels": ["off", "on"], + "default": 1 + } + }, + "Options.AutomaticNotificationOfUpdates": { + "schema": { + "title": "Automatic Updates Notifications", + "description": "Whether or not to present automatic notifications about JAWS updates.", + "enum": [0, 1], + "enumLabels": ["off", "on"], + "default": 1 + } + }, + "Options.ViewMode": { + "schema": { + "title": "View Mode", + "description": "Select whether to run JAWS from the system tray or not.", + "enum": [0, 1], + "enumLabels": ["off", "on"], + "default": 0 + } + }, + "Options.bVirtViewer": { + "schema": { + "title": "Virtual Viewer", + "description": "Enable or disable showing the 'Virtual Viewer' on the screen.", + "enum": [0, 1], + "enumLabels": ["off", "on"], + "default": 0 + } + }, + "Options.KeyboardType": { + "schema": { + "title": "Keyboard Type", + "description": "Set the keyboard type being used.", + "enum": [ + "Desktop", + "Laptop", + "Kinesis" + ], + "enumLabels": [ + "Desktop", + "Laptop", + "Kinesis" + ], + "default": "Desktop" + } + }, + + // -------------- Basic Settings: Advanced ---------------- + + // NOTE: Placed in configuration3 due to different INI file. + + // ---- Basic Settings: Windows Settings Parameters ------- + + "Options.DisableShadowMouse": { + "schema": { + "title": "Pointer Shadow", + "description": "Disable Pointer Shadow.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Options.DisablePersonalizedMenus": { + "schema": { + "title": "Personalized Menus", + "description": "Disable Personalized Menus.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + // GPII-4226 Leaving out for now, needs a non-empty title + // "Options.DisableHideAltKey": { + // "schema": { + // "title": "", + // "description": "Disable hide keyboard navigation indicators until I use the ALT Key.", + // "enum": [ + // 0, + // 1 + // ], + // "enumLabels": [ + // "off", + // "on" + // ], + // "default": 0 + // } + // }, + // GPII-4226 Leaving out for now, needs a non-empty title + // "Options.IgnoreColorsSpecifiedOnWebPages": { + // "schema": { + // "title": "", + // "description": "Ignore colors specified on web pages.", + // "enum": [ + // 0, + // 1 + // ], + // "enumLabels": [ + // "off", + // "on" + // ], + // "default": 0 + // } + // }, + "Options.DisableLanguageBar": { + "schema": { + "title": "Language Bar", + "description": "Disable Language Bar.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + + // ----------------------- User --------------------------- + + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Options.ProgressBarUpdateInterval": { + // "schema": { + // "title": "Progress Bar Update Interval", + // "description": "How often to announce progress bar updates, in milliseconds. Set to 0 to disable update announcements, and 1 to announce all.", + // "type": "integer", + // "default": 5000, + // "minimum": 0 + // } + // }, + "Options.TypingEcho": { + "schema": { + "title": "Typing echo", + "description": "How to announce typed words and/or characters.", + "default": 1, + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "Off", + "Characters", + "Words", + "Characters and Words" + ] + } + }, + "Options.ScreenEcho": { + "schema": { + "title": "Screen Echo", + "description": "Specifies how much text is read when information on the screen changes.", + "default": 1, + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "Echo No Text", + "Echo Highlighted Text", + "Echo All Text" + ] + } + }, + + "Options.TypingInterrupt": { + "schema": { + "title": "Typing Interrupt", + "description": "Whether to stop speaking when the user types.", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + // TODO: Doesn't seem to work. + "Options.InsertKeyMode": { + "schema": { + "title": "Insert Key Mode", + "description": "The operating mode of the insert key.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "JAWS", + "Sticky" + ] + } + }, + // NOTE: "Options.bVirtViewer" Duplicated from Basic Section + "Options.KeyRepeat": { + "schema": { + "title": "Key Repeat", + "description": "Whether or not to allow repeated keys. Repeated keys are allowed by default.", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.LowerOtherAppsVolumeWhileJAWSIsRunning": { + "schema": { + "title": "Lower Audio Volume of Programs while JAWS Speaks", + "description": "Whether to lower the volume of programs other than JAWS when speaking.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + + // ----------------- WEB / HTML / PDF ----------------------- + + // ----------- WEB / HTML / PDF - Reading ------------------- + + "VirtualCursorVerbosity.VirtualCursorVerbosityLevel": { + "schema": { + "title": "Web Verbosity Level", + "description": "Verbosity level used for reading Web pages, HTML files and PDF files.", + "enum": [ + 2, + 1, + 0 + ], + "enumLabels": [ + "High", + "Medium", + "Low" + ], + "default": 0 + } + }, + + // --- WEB / HTML / PDF - Reading - Configure Web Verbosity Levels --- + + // TODO: The following settings have a string format + // of type "0|0|0" where each of the number positions + // states if the future is active for the different leves + // of verbosity. + // + // - High: "1|0|0" + // - Medium: "0|1|0" + // - Low: "0|0|1" + + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.BlockQuotation": { + // "schema": { + // "title": "Announce Block Quotation", + // "description": "Announce Start and End of Block Quotations.", + // "type": "string", + // "default": "1|1|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.Feed": { + // "schema": { + // "title": "Announce Feed", + // "description": "Announce Start and End of Feeds.", + // "type": "string", + // "default": "1|1|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.Figure": { + // "schema": { + // "title": "Announce Figure", + // "description": "Announce Start and End of Figures.", + // "type": "string", + // "default": "1|1|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.FlashObject": { + // "schema": { + // "title": "Announce Flash Object", + // "description": "Announce Start and End of Flash Objects.", + // "type": "string", + // "default": "1|0|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.Frame": { + // "schema": { + // "title": "Announce Frame", + // "description": "Announce Start and End of Frame.", + // "type": "string", + // "default": "1|0|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.GroupBox": { + // "schema": { + // "title": "Announce Group Box", + // "description": "Announce Start and End of Group Box.", + // "type": "string", + // "default": "1|1|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.List": { + // "schema": { + // "title": "Announce List", + // "description": "Announce Start and End of List.", + // "type": "string", + // "default": "1|1|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.Log": { + // "schema": { + // "title": "Announce Log", + // "description": "Announce Start and End of Log.", + // "type": "string", + // "default": "1|1|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.Note": { + // "schema": { + // "title": "Announce Note", + // "description": "Announce Start and End of Note.", + // "type": "string", + // "default": "1|1|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.ApplicationRegion": { + // "schema": { + // "title": "Announce Application Region", + // "description": "Announce Start and End of Application Region.", + // "type": "string", + // "default": "0|0|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.ArticleRegion": { + // "schema": { + // "title": "Announce Article Region", + // "description": "Announce Start and End of Article Region.", + // "type": "string", + // "default": "1|1|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.BannerRegion": { + // "schema": { + // "title": "Announce Banner Region", + // "description": "Announce Start and End of Banner Region.", + // "type": "string", + // "default": "1|0|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.ComplementaryRegion": { + // "schema": { + // "title": "Announce Complementary Region", + // "description": "Announce Start and End of Complementary Region.", + // "type": "string", + // "default": "1|0|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.ContentInfoRegion": { + // "schema": { + // "title": "Announce Content Info Region", + // "description": "Announce Start and End of Content Info.", + // "type": "string", + // "default": "1|0|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.DocumentRegion": { + // "schema": { + // "title": "Announce Article Region", + // "description": "Announce Start and End of Document Region.", + // "type": "string", + // "default": "0|0|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.FormRegion": { + // "schema": { + // "title": "Announce Form Regions", + // "description": "Announce Start and End of Form Regions.", + // "type": "string", + // "default": "1|0|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.MainRegion": { + // "schema": { + // "title": "Announce Main Region", + // "description": "Announce Start and End of Main Region.", + // "type": "string", + // "default": "1|1|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.NavigationRegion": { + // "schema": { + // "title": "Navigation Region", + // "description": "Announce Start and End of Navigation Regions.", + // "type": "string", + // "default": "1|0|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.Region": { + // "schema": { + // "title": "Announce Region", + // "description": "Announce Start and End of 'Regions'.", + // "type": "string", + // "default": "1|0|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.SearchRegion": { + // "schema": { + // "title": "Announce Search Regions", + // "description": "Announce Start and End of Search Regions.", + // "type": "string", + // "default": "1|0|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.TableOrGrid": { + // "schema": { + // "title": "Announce Table or Grids", + // "description": "Announce Start and End of \"Tables or Grids\".", + // "type": "string", + // "default": "1|1|1" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.TabPanel": { + // "schema": { + // "title": "Announce Tab Panels", + // "description": "Announce Start and End of Tab Panels.", + // "type": "string", + // "default": "1|1|0" + // } + // }, + // GPII-4226 Leaving out for now, should be converted to an enum + // "VirtualCursorVerbosity.Toolbar": { + // "schema": { + // "title": "Announce Toolbars", + // "description": "Announce Start and End of Toolbars.", + // "type": "string", + // "default": "1|1|0" + // } + // }, + + // --- WEB / HTML / PDF - Reading - Custom Placemarker Page Summary --- + + // GPII-4226 Leaving out for now, needs a non-empty title + // "options.CustomPageSummary": { + // "schema": { + // "title": "", + // "description": "Announce Start and End of Toolbars.", + // "enum": [ + // 1, + // 2, + // 0 + // ], + // "enumLabels": [ + // "Speak Placemarkers On Page Load", + // "List Placemarkers in Results Viewer On Page Load", + // "Do nothing" + // ], + // "default": 0 + // } + // }, + + // ---------------------------------------------------------- + + "HTML.SayAllOnDocumentLoad": { + "schema": { + "title": "Read Web Pages Automatically When Loaded", + "description": "Whether to automatically read web pages when they're first loaded.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "HTML.SkipPastRepeatedText": { + "schema": { + "title": "Skip Past Repeated Text", + "description": "Whether to position the Virtual Cursor on the first line which is different than the previous page whenever a new page is displayed.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + + // NOTE-TODO: "Enable Flexible Web" Setting is stored in SQLite file + // located in "enu/FlexibleWeb.db". In order to support this file + // a new settingHandler is necessary. + + "HTML.ExpandAbbreviations": { + "schema": { + "title": "Expand Abbreviations", + "description": "Whether or not to expand abbreviations (using the abbr tag).", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "HTML.ExpandAcronyms": { + "schema": { + "title": "Expand Acronyms", + "description": "Whether or not to expand acronyms (using the acronym tag).", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "HTML.AccessKeys": { + "schema": { + "title": "Speak Access Keys Within Web Page", + "description": "Whether to announce HTML element access keys when reading a web page.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + + // ---------------- WEB / HTML / PDF - Navigation ------------- + + "HTML.SmartNavigation": { + "schema": { + "title": "Smart Navigation", + "description": "Whether to enable \"smart navigation\" when reading web pages.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "Off", + "Controls", + "Controls And Tables" + ], + "default": 0 + } + }, + + // --- WEB / HTML / PDF - Navigation - Document Presentation Mode --- + + "HTML.DocumentPresentationMode": { + "schema": { + "title": "Document Presentation Mode", + "description": "The presentation mode used for document reading.", + "enum": [0, 1], + "enumLabels": ["Simple Layout", "Screen Layout"], + "default": 0 + } + }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "HTML.DocPresModeMaxLineLength": { + // // NOTE: Doesn't appear to have relevant min-max values. + // "schema": { + // "title": "Maximum Line Lenght", + // "description": "Stablish a maximum line length.", + // "type": "integer", + // "default": 500 + // } + // }, + + // ----------------------------------------------------------- + + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "HTML.TextBlockLength": { + // "schema": { + // "title": "Text Block Length", + // "description": "The number of consecutive characters that must appear in a web page for JAWS to treat this as a block of text.", + // "type": "integer", + // "default": 25 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "HTML.LinesPerPage": { + // "schema": { + // "title": "Lines per Page", + // "description": "The number of consecutive lines to be considered as a page for JAWS.", + // "type": "integer", + // "default": 25 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "HTML.MaxLineLength": { + // "schema": { + // "title": "Max Line Length", + // "description": "The maximum number of characters which can appear on a line of a paragraph.", + // "type": "integer", + // "default": 150 + // } + // }, + "HTML.ScreenFollowsVCursor": { + "schema": { + "title": "Screen Follows Virtual Cursor", + "description": "Whether the screen should automatically scroll to display the line on which the Virtual Cursor is positioned.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "HTML.WrapNavigation": { + "schema": { + "title": "Wrap Navigation", + "description": "Whether or not to allow navigation keystrokes such as tab and shift tab to wrap to the top or bottom of the document.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + + // --------- WEB / HTML / PDF - Images Graphics ------------ + + "HTML.IncludeGraphics": { + "schema": { + "title": "Include Graphics", + "description": "Whether/when to include graphics in web pages.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "No Images", + "Labeled Images", + "All Images" + ], + "default": 1 + } + }, + "HTML.GraphicRenderingOption": { + "schema": { + "title": "Graphic Rendering Option", + "description": "Select what to speak for an image.", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "enumLabels": [ + "Title", + "Alt Text", + "Tooltip", + "Longest of Above", + "The first attribute found in the order list" + ], + "default": 1 + } + }, + "HTML.CustomGraphicAttrs": { + "schema": { + "title": "Custom Graphic Attrs", + "description": "Specify the order in which attributes of an image should be searched.", + "type": "string", + "default": "title|alt|src" + } + }, + "HTML.GraphicalLinkLastResort": { + "schema": { + "title": "Graphical Link Last Resort", + "description": "What to announce when a graphical link contains no title or alt text for its enclosing image and the enclosing anchor has no title.", + "enum": [ 0, 1], + "enumLabels": ["Speak Image's URL", "Speak Anchor's URL."], + "default": 0 + } + }, + + // ---------------- WEB / HTML / PDF - Links ---------------- + + "HTML.FilterConsecutiveDuplicateLinks": { + "schema": { + "title": "Filter Consecutive Duplicate Links", + "description": "Whether to skip (not announce) consecutive duplicate links.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "HTML.IdentifyLinkType": { + "schema": { + "title": "Identify Link Type", + "description": "Whether to distinguish the various types of links e.g. \"FTP link\", \"mailto link\" versus simply announcing \"link\" for all types of links.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "HTML.IdentifySamePageLinks": { + "schema": { + "title": "Identify Same Page Links", + "description": "Whether to identify links that point to other places on the current page by saying \"same page link\".", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + // GPII-4226 Leaving out for now, needs a non-empty title + // "HTML.LinkText": { + // "schema": { + // "title": "", + // "description": "Select what to speak for an image.", + // "enum": [ + // 0, + // 1, + // 2, + // 3, + // 4 + // ], + // "enumLabels": [ + // "Title", + // "Screen Text", + // "Tool Tip", + // "Longest of Above", + // "The first attribute found in the order list" + // ], + // "default": 1 + // } + // }, + // GPII-4226 Leaving out for now, should be converted to a user facing array of strings along with + // a transform to convert it to the appropriate format for the settings handler. + // "HTML.CustomTextLinkAttrs": { + // "schema": { + // "title": "Custom Text Link Attributes", + // "description": "Specify the order in which attributes of text links should be searched.", + // "type": "string", + // "default": "text|title|alt|href" + // } + // }, + "HTML.IncludeGraphicLinks": { + "schema": { + "title": "Include Image Links", + "description": "Specify how to speak image links.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "All Image Links", + "Labelled Image Links", + "No Image Links" + ], + "default": 1 + } + }, + "HTML.IncludeImageMapLinks": { + "schema": { + "title": "Identify Image Map Links", + "description": "Specify how to speak \"Image Map Links\".", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "All Image Links", + "Labelled Image Links", + "No Image Links" + ], + "default": 1 + } + }, + + // ----------- WEB / HTML / PDF - Headers and Frames ------------- + + "HTML.HeadingNavigation": { + "schema": { + "title": "Heading Navigation using 1-6", + "description": "Specify how heading navigation should work when using keys 1-6.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "Restrict to Section", + "Speak Ancestor Headings when Jumping to a New Section", + "Play Sound when Jumping to a New Section" + ], + "default": 1 + } + }, + "HTML.HeadingIndication": { + "schema": { + "title": "Indicate Headings", + "description": "Specify which headings should be announced.", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "enumLabels": [ + "No Headings", + "Headings Only", + "Headings with Level", + "Play Sound", + "Speak Heading with Heading Voice" + ], + "default": 2 + } + }, + "HTML.IgnoreInlineFrames": { + "schema": { + "title": "Ignore Inline Frames", + "description": "Whether to ignore inline frames, such as those used for advertising.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + + // ----------- WEB / HTML / PDF - Tables ------------- + + "HTML.IndicateColSpan": { + "schema": { + "title": "Indicate ColSpan", + "description": "Whether to announce column spans when reading table data in web pages.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + + // ----------- TODO: Unkwnonw ------------- + + "HTML.TableDetection": { + "schema": { + "title": "Table Detection", + "description": "Whether to announce all tables, or only data tables.", + "enum": [0, 1], + "enumLabels": ["Indicate all tables", "Only indicate data tables"], + "default": 1 + } + }, + + // ----------------------------------------- + + "OSM.TableDetection": { + "schema": { + "title": "Table Presentation Information", + "description": "Which tables to read as tables (data tables, or all tables).", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "data tables only", + "all tables" + ], + "default": 0 + } + }, + + // ----- WEB / HTML / PDF - Miscellaneous ---- + + "Options.EnableEdge": { + "schema": { + "title": "Enable Edge", + "description": "Whether JAWS should provide popular JAWS browser features in Edge such as Virtual Cursor, Navigation Quick Keys, List of Objects, and many more.", + "enum": [0, 1], + "enumLabels": ["off", "on"], + "default": 1 + } + }, + "HTML.EmbeddedActiveXSupport": { + "schema": { + "title": "Embedded ActiveX Support", + "description": "Whether or not to support embedded ActiveX controls such as Macromedia Flash Movies. If enabled, only objects defined in JActiveX.ini are affected by this setting.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "HTML.FormFieldPromptOptions": { + "schema": { + "title": "Form Field Prompt Options", + "description": "Which attributes to prefer when describing form fields.", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "enumLabels": [ + "Label Tag", + "Title", + "Alt Text", + "Longest of the above", + "Both Label and Title if different", + "Both Label and Alt if different" + ], + "default": 0 + } + }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "HTML.PageRefreshFilter": { + // // NOTE-TODO: Even if the setting is an integer the values + // // that is possible to set through the interface are three: + // // - Refresh As Needed: 0 + // // - Refresh Every 60 Seconds: 60000 + // // - Suppress All Refreshes: -1 + // "schema": { + // "title": "Page Refresh Filter", + // "description": "How often, in milliseconds, to allow all refreshes. Set to 0 to allow all refreshes without any delay.", + // "type": "integer", + // "default": 0 + // } + // }, + "Options.VirtualDocumentLinkActivationMethod": { + "schema": { + "title": "Link Activation", + "description": "How to activate a link when the enter key is pressed. Either simulates a mouse click or passes along the enter keypress.", + "enum": [0,1], + "enumLabels": ["Enter simulates mouse click.", "Enter sends Enter key."], + "default": 0 + } + }, + "HTML.ButtonTextOptions": { + "schema": { + "title": "Button Text Options", + "description": "Indicate which elements from a Button speak.", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "enumLabels": [ + "Title", + "Screen Text", + "Alt", + "Value", + "Longest of Above", + "The first attribute found in the order list" + ], + "default": 1 + } + }, + // GPII-4226 This should be converted to the intra-application transform style (aka "alias"). + // "HTML.CustomButtonAttrs": { + // "schema": { + // "title": "Custom Button Attributes", + // "description": "Specify the order in which attributes of Buttons should be searched.", + // "type": "string", + // "default": "text|title|alt|href" + // } + // }, + "HTML.IndicateElementAttributes": { + "schema": { + "title": "Indicate Element Attributes", + "description": "Whether to announce any HTML attributes defined in the \"HTML Attributes Behavior\" map of the current scheme.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.AllowWebAppReservedKeystrokes": { + "schema": { + "title": "Allow Webapp-reserved Keystrokes", + "description": "When both JAWS and a website such as Facebook support a key shortcut, whether to handle the key in JAWS (the default) or pass the key to the web app.", + "enum": [0, 1], + "enumLabels": ["off", "on"], + "default": 1 + } + }, + + // --------------------- Forms Mode ------------------------- + + // ---------- Forms Mode: Select Forms Mode ----------------- + + "FormsMode.AutoFormsMode": { + // TODO: The value of the "Auto" choice is a guess, should + // be verified + "schema": { + "title": "Forms Mode", + "description": "Selects the form mode for input information in HTML.", + "enum": [ + 1, + 2, + 0 + ], + "enumLabels": [ + "Auto", + "SemiAuto", + "Manual" + ], + "default": 1 + } + }, + "FormsMode.AutoFormsModeThreshold": { + "schema": { + "title": "Navigation Quick Key Delay", + "description": "Selects the delay to be used with the quick navigation key.", + "enum": [ + 500, + 1000, + 1500, + 2000, + 3000, + 4000, + 5000, + 0 + ], + "enumLabels": [ + ".5 Seconds", + "1 Seconds", + "1.5 Seconds", + "2 Seconds", + "3 Seconds", + "4 Seconds", + "5 Seconds", + "Never" + ], + "default": 0 + } + }, + "FormsMode.FormsModeAutoOff": { + "schema": { + "title": "Forms Mode Auto Off", + "description": "Disable Forms Mode when a New page loads.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "FormsMode.IndicateFormsModeWithSounds": { + "schema": { + "title": "Forms Sound Indication", + "description": "Enables forms indication using sounds.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "FormsMode.EnterFormsModeSound": { + "schema": { + "title": "Entering Forms Sound", + "description": "Sets the sound to be played when entering a form.", + "type": "string", + "default": "Boink2.wav" + } + }, + "FormsMode.ExitFormsModeSound=": { + "schema": { + "title": "Exiting Forms Sound", + "description": "Sets the sound to be played when exiting a form.", + "type": "string", + "default": "Boink1.wav" + } + }, + + // ------------------ Text Processing ----------------------- + + // ------------- Text Processing - General ------------------ + + "Options.Filter": { + // NOTE-TODO: This setting has a weird relationship with the + // setting "Repetitions", in the UI, the last element for the + // dropdown list "Filter Repeated Characters", is + // "Count Repeated Characters". When this item is selected, the value + // of this setting is not changed, but the value of "Repetitions" + // setting is set to one. This is could imply that this setting is + // invalidated by the use of the setting "Repetitions". + "schema": { + "title": "Filter Repeated Characters", + "description": "Determines how repeated characters are handled.", + "enum": [ + 3, + 4, + 5, + 6, + 0 + ], + "enumLabels": [ + "Say First 3 Repeated Characters", + "Say First 4 Repeated Characters", + "Say First 5 Repeated Characters", + "Say First 6 Repeated Characters", + "Say All Repeated Characters" + ], + "default": 3 + } + }, + "Options.Repetitions": { + "schema": { + "title": "Repetitions", + "description": "Whether or not to count repeated characters.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.IndicateCaps": { + "schema": { + "title": "Indicate Caps", + "description": "When to indicate the presence of capital letters.", + "default": 0, + "enum": [ + 0, + 1, + 2, + 3 + ], + // TODO: Once this is moved into the LSR, these will all be message keys. + "enumLabels": [ + "No Indication", + "Read By Char, Spell", + "Read By Word, Char, Spell", + "Read By Line, Word, Char, Spell" + ] + } + }, + "Options.IndicateSelected": { + "schema": { + "title": "List Item", + "description": "Use this list to determine how JAWS describes list box items. Select \"Say None\" to silence reading of list box descriptions. When you select \"Say Selected\", JAWS only tells you when list box items are selected. When you select the \"Say Not Selected\" item, JAWS only tells you when list box items are not selected. This is the default setting. If you select \"Say Both\", JAWS tells you when list box items are selected, and when they are not selected.", + "default": 2, + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "None", + "Selected", + "Not Selected", + "Both" + ] + } + }, + "Options.SpellAlphanumericData": { + "schema": { + "title": "Spell Alphanumeric Data", + "description": "How to read alphanumeric data.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "Off", + "Spell", + "Spell Phonetically" + ], + "default": 0 + } + }, + + // --- Text Processing - Indicate Newlines When ------------ + + // GPII-4226 Leaving out for now due to lack of a default value. + // "Options.IndicateNewlinesAndParagraphs": { + // "schema": { + // "title": "Indicate Newlines and Paragraphs", + // "description": "Whether/how to indicate the presence of new lines and paragraphs.", + // "type": "integer" + // }, + // "label": "bitmask" + // }, + + // --- Text Processing - Phonetic Announcement ------------ + + "Options.SpellPhonetic": { + "schema": { + "title": "Spell Phonetic Always", + "description": "How to announce word spellings.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.PhoneticCharAfterPause": { + "schema": { + "title": "Announce Phonetic Character after Pause", + "description": "Whether pausing on character will cause JAWS to speak it phonetically while navigating text by character.", + "enum": [0, 5, 10, 15], + "enumLabels": [ + "Never", + "After a half second pause", + "After a one second pause", + "After a one and a half second pause" + ], + "default": 15 + } + }, + + // ------------------------------------------------------- + + "Options.SmartWordReading": { + "schema": { + "title": "Smart Word Reading", + "description": "Whether or not to enable \"smart reading\" when using the \"Say Word\" command.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.MixedCase": { + "schema": { + "title": "Mixed Case processing", + "description": "Whether to announce words with embedded capital letters (such as \"MixedCase\") as separate words.", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.Dictionary": { + "schema": { + "title": "Dictionary", + "description": "Whether words, phrases, abbreviations, or symbols should be processed through a global or application specific dictionary to determine proper pronunciation. This is enabled by default.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.LanguageDetection": { + "schema": { + "title": "Language Detection", + "description": "Whether to enable automatic Language detection.", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.GeneralizeDialect": { + "schema": { + "title": "Generalize Dialect", + "description": "Whether to switch languages when encountering content with the same underlying base language.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.DetectKeyboardInputLanguage": { + "schema": { + "title": "Detect Keyboard Languages", + "description": "Whether to detect the language used by the keyboard.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "RichEdit and Edit Control Options.UseOSM": { + "schema": { + "title": "Enhanced Edit Support", + "description": "Whether to activate or deactivate enhanced edit support.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.SaySelectedFirst": { + "schema": { + "title": "Say Selected First", + "description": "Whether to announce the selected text first.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.SayWindowTypeFirst": { + "schema": { + "title": "Say Window Type First", + "description": "Whether to announce the type of window before announcing the window title/text.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.SayStateFirst": { + "schema": { + "title": "Say State First", + "description": "Whether to announce the window state before title/text.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.Indentation": { + "schema": { + "title": "Indentation", + "description": "Whether or not to announce indentation.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + + // --- Text Processing - Number And Date Processing -------- + + "Options.SayNumericDates": { + "schema": { + "title": "Say Numeric Dates", + "description": "How to announce numeric dates. With no translation, dates are read as numbers. With some translation, dd-mm-yy values are read as text. With extended translation, both dd-mm-yy and dd-mm values are read as text.", + "default": 0, + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "No Translation", + "Some Translation", + "Extended Translation" + ] + } + }, + "Options.Numbers": { + "schema": { + "title": "Numbers", + "description": "How to announce numbers.", + "default": 0, + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "Controlled By Synthesizer", + "Single Digits", + "Pairs", + "Full Numbers" + ] + } + }, + "Options.SingleDigitThreshold": { + "schema": { + "title": "Single Digit Threshold", + "description": "The number of digits a number must contain before it is read as single digits (like a phone number). Defaults to five.", + "enum": [ + 0, + 5, + 6, + 7, + 8 + ], + "enumLabels": [ + "Controlled By Synthesizer", + "5 or more digits", + "6 or more digits", + "7 or more digits", + "8 or more digits" + ], + "default": 5 + } + }, + "Options.SpeakNumbersSepByDashesAsDigits": { + "schema": { + "title": "Numbers Containing Dashes as Digits", + "description": "Whether to read numbers that contain dashes (such as phone numbers) as a series of digits.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.SayDollars": { + "schema": { + "title": "Say Dollars", + "description": "Whether to announce currency symbols.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + + // -------------- Text Processing - Proofing ---------------- + + // ----- TODO: Check relationship of this two settings ------ + + "RichEdit and Edit Control Options.CountableSelectionContextItems": { + "schema": { + "title": "Speak Spelling Error Count", + "description": "Wether to speak spelling error count.", + "enum": [ + 0, + 2, + 4, + 6 + ], + "enumLabels": [ + "Off", + "Speak Spelling Error Count", + "Speak Grammar /Proofing Error Count", + "Speak Spelling Error Count and Speak Grammar /Proofing Error Count" + ], + "default": 0 + } + }, + "RichEdit and Edit Control Options.SelectionContextFlags": { + "schema": { + "title": "Context Selection Flags", + "description": "Select the context flags for detection of spelling/grammar errors.", + "enum": [ + 0, + 1, + 3 + ], + "enumLabels": [ + "Off", + "Say Misspelled", + "Grammar and Advanced Proofing" + ], + "default": 0 + } + }, + + // -------------- --------------------------- ---------------- + + // GPII-4226 Leaving out for now due to lack of a default value. + // "Options.SpellingAttributeOverride": { + // // NOTE-TODO: Unknown setting format, but when it's selected + // // in the menu "Detect Spelling While Navigating" the + // // value is set to "|messageVoice|BuzzerShort.wav|messageVoice|" + // "schema": { + // "title": "Spelling Attribute Override", + // "description": "Overrides the behavior when a spelling mistake is found. Complete functionality is unknown.", + // "type": "string" + // } + // }, + "Options.IndicateMistypedWord": { + "schema": { + "title": "Indicate Mistyped Words", + "description": "Whether to indicate a mistyped word.", + "enum": [0, 1], + "enumLabels": ["off", "on"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Options.GrammarAttributeOverride": { + // // NOTE-TODO: The format of the settign is unknown, but + // // when the setting is activated is set to: "1|messageVoice||messageVoice|" + // "schema": { + // "title": "Grammar and Advanced Proofing", + // "description": "Enable or disable 'Grammar and Advanced Proofing' feature.", + // "type": "string" + // } + // }, + "Options.IgnoreSpellingAndGrammarErrorsDuringSayAll": { + "schema": { + "title": "Apply options during SayAll", + "description": "Apply spelling options for errors during SayAll.", + "enum": [0, 1], + "enumLabels": ["off", "on"], + "default": 0 + } + }, + + // -------- Text Processing - Proofing - Braille ------------ + + "OSM.UnderlineProofreadingErrors": { + "schema": { + "title": "Underline Spelling and Grammar errors", + "description": "Underline Speeling Errors and/or Underline Grammatical and advanced proofing errors.", + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "Off", + "Underline Spelling Errors", + "Underline Grammatical and advanced proofing errors", + "Underline Spelling Errors and Underline Grammatical and advanced proofing errors" + ], + "default": 1 + } + }, + + // ------------ Text Processing - Proofing ----------------- + + // -------------------- Speech Verbosity ------------------- + + "Options.Verbosity": { + "schema": { + "title": "Verbosity", + "description": "How verbose announcements should be.", + "default": 0, + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "Beginner, Highest", + "Intermediate", + "Advanced, Lowest" + ] + } + }, + + // ---- Speech Verbosity: Configure Verbosity Levels ------- + + // NOTE: This settings have the following format: + // ========================================================= + + // - "1|2|2|Help Balloon" + // + // The first three values changes if the item is going + // to be spoken, if the different 3 verbosity levels, + // being first "Beginner", the second one "Itermediate" and + // the third one "Advanced". If the element isn't going to + // be spoken the value is 0, if it's "Short" is "2" and if + // it's Long it's "1". The last element is a setting identifier + // extracted for the following list: + // + // - "Application Start Message" + // - "Control Description" + // - "Control Group Name" + // - "Control Name" + // - "ControlType" + // - "Dialog Name" + // - "Dialog Text" + // - "Document Name" + // - "Error message" + // - "Help Balloon" + // - "Help Message" + // - "Item Number" + // - "Item State" + // - "JAWS Message" + // - "Mouse Echo" + // - "Position Information" + // - "Screen Message" + // - "Select" + // - "Selected Item" + // - "Smart Help Messages" + // - "Status Information" + // - "Toasts" + // - "ToolTip" + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.CONTROL_GROUP_NAME": { + // "schema": { + // "title": "Control Group Name Verbosity", + // "description": "How verbose \"control group names\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.APPLICATION_START_MESSAGE": { + // "schema": { + // "title": "Application Start Message Verbosity", + // "description": "How verbose \"application start message\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.CONTROL_DESCRIPTION": { + // "schema": { + // "title": "Control Description Verbosity", + // "description": "How verbose \"Control Description\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.CONTROL_NAME": { + // "schema": { + // "title": "Control Name Verbosity", + // "description": "How verbose \"Control Name\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.CONTROLTYPE": { + // "schema": { + // "title": "Control Type Verbosity", + // "description": "How verbose \"Control Type\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.DIALOG_NAME": { + // "schema": { + // "title": "Dialog Name Verbosity", + // "description": "How verbose \"Dialog Name\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.DIALOG_TEXT": { + // "schema": { + // "title": "Dialog Text Verbosity", + // "description": "How verbose \"Dialog Text\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.DOCUMENT_NAME": { + // "schema": { + // "title": "Document Name Verbosity", + // "description": "How verbose \"Document Name\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.ERROR_MESSAGE": { + // "schema": { + // "title": "Error Message Verbosity", + // "description": "How verbose \"Error Message\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.HELP_BALLOON": { + // "schema": { + // "title": "Help Balloon Verbosity", + // "description": "How verbose \"Help Balloon\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.HELP_MESSAGE": { + // "schema": { + // "title": "Help Message Verbosity", + // "description": "How verbose \"Help Message\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.ITEM_NUMBER": { + // "schema": { + // "title": "Item Number Verbosity", + // "description": "How verbose \"Item Number\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.ITEM_STATE": { + // "schema": { + // "title": "Item State Verbosity", + // "description": "How verbose \"Item State\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.JAWS_MESSAGE": { + // "schema": { + // "title": "JAWS Message Verbosity", + // "description": "How verbose \"JAWS Message\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.MOUSE_ECHO": { + // "schema": { + // "title": "Mouse Echo Verbosity", + // "description": "How verbose \"Mouse Echo\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.POSITION_INFORMATION": { + // "schema": { + // "title": "Position Information Verbosity", + // "description": "How verbose \"Position Information\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.SCREEN_MESSAGE": { + // "schema": { + // "title": "Screen Message Verbosity", + // "description": "How verbose \"Screen Message\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.SELECT": { + // "schema": { + // "title": "Selection Verbosity", + // "description": "How verbose \"Selection\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.SELECTED_ITEM": { + // "schema": { + // "title": "Selected Item Verbosity", + // "description": "How verbose \"Selected Item\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.SMART_HELP_MESSAGES": { + // "schema": { + // "title": "Smart Help Messages Verbosity", + // "description": "How verbose \"Smart Help Messages\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.STATUS_INFORMATION": { + // "schema": { + // "title": "Status Information Verbosity", + // "description": "How verbose \"Status Information\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.TOASTS": { + // "schema": { + // "title": "Toasts Verbosity", + // "description": "How verbose \"Toasts\" announcements should be.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "OutputModes.TOOLTIP": { + // "schema": { + // "title": "Tooltip Verbosity", + // "description": "How verbose \"Tooltip\" announcements should be.", + // "type": "string" + // } + // }, + + // ========================================================= + + // -------------------- Speech Options ------------------ + + "Options.SpeechMode": { + "schema": { + "title": "Speech Mode", + "description": "The \"speech mode\" to use, either \"Full\" or \"Less\".", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Full speech", + "Less speech" + ], + "default": 0 + } + }, + "Options.LessSpeechMode": { + "schema": { + "title": "Less Speech Options", + "description": "How to behave when in \"Less Speech Mode\".", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Speech On Demand", + "Mute Speech" + ], + "default": 0 + } + }, + + // TODO: Setting should remain disabled until GPII-4300 is solved. + // ============================================================== + + // "Options.AllowSpeechOnDemandWhenMuted": { + // "schema": { + // "title": "Allow Reading Commands", + // "description": "Whether to allow speech on demand when otherwise muted.", + // "enum": [ + // 0, + // 1 + // ], + // "enumLabels": [ + // "off", + // "on" + // ], + // "default": 1 + // } + // }, + // "Options.AllowMouseEchoWhenMuted": { + // "schema": { + // "title": "Allow Mouse Echo", + // "description": "Whether to echo mouse movement when otherwise muted.", + // "enum": [ + // 0, + // 1 + // ], + // "enumLabels": [ + // "off", + // "on" + // ], + // "default": 0 + // } + // }, + // "Options.AllowTypingEchoWhenMuted": { + // "schema": { + // "title": "Allow Typing Echo", + // "description": "Whether to echo typed keys when otherwise muted.", + // "enum": [ + // 0, + // 1 + // ], + // "enumLabels": [ + // "off", + // "on" + // ], + // "default": 0 + // } + // }, + + // ============================================================== + + // ------------- Speech and Sounds Schemes ---------------- + + "Options.Scheme": { + "schema": { + "title": "Scheme", + "description": "Whether or not to intercept CreateDIBSection Note that this value is only read once per JAWS session at startup thus changing this value while JAWS is running has no effect.", + "enum": [ + "Attributes and Colors", + "Classic", + "Classic (Attributes and Font info)", + "Classic (Attributes)", + "Classic (Attributes, Font and Color)", + "Classic With Quotes", + "Colors", + "Indent (tab is 0.125 inches Script Manager)", + "Indent (Tab is Eight Spaces)", + "Indent (Tab is Four Spaces)", + "Indent (Tab is Two Spaces)", + "Kindle", + "ProofReading (Attributes and Font Info)", + "ProofReading (Attributes)", + "ProofReading (Attributes, Font Info and color)", + "SayAll Text Only", + "SayAll Text With Sounds", + "Visual Studio .Net", + "Visual Studio .Net (Colors)", + "Visual Studio .Net (Indent)", + "Web RentACrowd", + "Word Classic" + ], + "enumLabels": [ + "Attributes and Colors", + "Classic", + "Classic (Attributes and Font info)", + "Classic (Attributes)", + "Classic (Attributes, Font and Color)", + "Classic With Quotes", + "Colors", + "Indent (tab is 0.125 inches Script Manager)", + "Indent (Tab is Eight Spaces)", + "Indent (Tab is Four Spaces)", + "Indent (Tab is Two Spaces)", + "Kindle", + "ProofReading (Attributes and Font Info)", + "ProofReading (Attributes)", + "ProofReading (Attributes, Font Info and color)", + "SayAll Text Only", + "SayAll Text With Sounds", + "Visual Studio .Net", + "Visual Studio .Net (Colors)", + "Visual Studio .Net (Indent)", + "Web RentACrowd", + "Word Classic" + ], + "default": "Classic" + } + }, + + // ----- Speech and Sounds Schemes: Modified schemes ----- + + // TODO: Scheme modification are a very complete feature with + // at least 100 extra settings, and stored in different configuration + // files, a more complete solution is going to be required for + // this settings. + + // ------------------- Say All ----------------------- + + // --------------- Say All Reads ----------------------- + + "Options.SayAllMode": { + // NOTE: In the user interface this settings work in combination with + // LinePauses in a single combo box. + "schema": { + "title": "\"Say All\" Mode", + "description": "Sets the amount of text which \"Say All\" sends to the synthesizer as a single unit.", + "default": 0, + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "Line at a time", + "Setence at a time", + "Paragraph at a time" + ] + } + }, + "Options.LinePauses": { + "schema": { + "title": "Line Pauses", + "description": "Whether to pause at end of a line.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + + // --------------- Say All Scheme ----------------------- + + // GPII-4226 Leaving out for now due to lack of a default value. + // "Options.SayAllScheme": { + // // NOTE-TODO: Setting can be a selection from multiple schemas available in the + // // UI. + // "schema": { + // "title": "\"Say All\" Scheme", + // "description": "Which \"Say All\" scheme to use.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Options.SayAllSchemes": { + // // NOTE-TODO: This setting range can't be known ahead of time because it should + // // be parsed, since it's a text of the following form: + // // - "Classic|Text Only|Text With Sounds" + // // created using a combination of the user selected schemes to be placed in the + // // menu. + // "schema": { + // "title": "\"Say All\" Schemes", + // "description": "Which \"Say All\" schemes to display in the \"Select Scheme\" menu.", + // "type": "string" + // } + // }, + "Options.SayBlankLineCount": { + "schema": { + "title": "Say Blank Line Count", + "description": "In \"Say All\" mode, whether to announce the number of blank lines.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.SayAllIndicateCaps": { + // The same as http://registry.gpii.net/common/announceCapitals. We should just reuse it and override with custom values + "schema": { + "title": "\"Say All\" Announces Capitals", + "description": "In \"Say All\" mode, announce an initial capital letter or capitalized word.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "on", + "off" + ] + } + }, + "Options.SayAllIgnoreShiftKeys": { + "schema": { + "title": "\"Say All\" Ignores Shift Keys", + "description": "Allows sticky keys to be used, i.e.: ignores shift keys during SayAll. User can use left/right arrow to do same functions.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "on", + "off" + ], + "default": 1 + } + }, + + // ------------- Graphics and Symbols ---------------- + + "Options.SpeakANSIChars": { + "schema": { + "title": "Announce Special Symbols", + "description": "Select where to announce special symbols.", + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "Never", + "TypeEcho, SayChar, SpellWord", + "Also SayWord", + "Also SayAll" + ], + "default": 0 + } + }, + "Options.SpeakCharacterValueInHex": { + "schema": { + "title": "Speak Character Value In Hex", + "description": "Whether to announce the hex value of a character when numpad 5 is pressed three times quickly.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Options.SpeakCharacterValueAsMultibyteSequence": { + "schema": { + "title": "Speak Character Value As Sequence of Multibyte Values", + "description": "Whether to read unicode characters as a series of multi-byte values.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + + // ----- Graphics and Symbols: Graphic Verbosity ----- + + "OSM.IncludeGraphics": { + "schema": { + "title": "Include Graphics", + "description": "Which graphics to announce.", + "enum": [ 0, 1, 2 ], + "enumLabels": ["Ignore all graphics", "Announce labeled graphics only", "Announce all graphics"], + "default": 2 + } + }, + + // -- Graphics and Symbols: Graphic Recognition Dimensions ----- + + "OSM.GraphicMinY": { + "schema": { + "title": "Minimum Recognition Height", + "description": "Set the minimum recognition height.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 10 + } + }, + "OSM.GraphicMinX": { + "schema": { + "title": "Minimum Recognition width", + "description": "Set the minimum recognition width.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 10 + } + }, + "OSM.GraphicMaxY": { + "schema": { + "title": "Maximum Recognition width", + "description": "Set the maximum recognition width.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 64 + } + }, + "OSM.GraphicMaxX": { + "schema": { + "title": "Maximum Recognition width", + "description": "Set the maximum recognition width.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 64 + } + }, + + // ----------------- Braille General --------------------------- + + // ---------- Braille General - Language ----------------------- + + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Braille.TranslatorMode": { + // "schema": { + // // NOTE: This setting is a number assigned from a combination of + // // the three menus "Language", "Output" and "Input". + // // When the Input and Output have another value than + // // "Computer Braille", the combination of the three values is + // // used to generate an integer that represents the current translation + // // mode. + // "title": "Language", + // "description": "Language to use for the translation.", + // "type": "integer", + // "default": 19 + // } + // }, + "Braille.Grade2Translation": { + "schema": { + "title": "Translate to language", + "description": "Output language to be translated into.", + "type": "integer", + "minimum": 0, + "maximum": 1, + "default": 0 + } + }, + "Braille.ContractedBrailleInput": { + "schema": { + "title": "Translate to braille", + "description": "Input language to be translated.", + "type": "integer", + "minimum": 0, + "maximum": 1, + "default": 0 + } + }, + "Braille.Grade2ExpandCurrentWord": { + "schema": { + "title": "Expand current word", + "description": "Show current word in Computer Braille.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Braille.Grade2SuppressCapitalSigns": { + "schema": { + "title": "Suppress Capital Signs", + "description": "Supress capital signs in braille.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + + // --------- Braille General - Panning -------------- + + "Braille.AllDotsBrailleCursor": { + "schema": { + "title": "All Dots Braille Cursor", + "description": "Whether to display the braille cursor as all dots raised instead of showing it using only dots seven and eight.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Braille.AutoPanMode": { + "schema": { + "title": "AutoPan Mode", + "description": "The \"Auto Pan\" algorithm to use.", + "enum": [ + 0, + 2, + 255 + ], + "enumLabels": [ + "Off", + "Match Manual Panning", + "Automatic" + ], + "default": 255 + } + }, + "Braille.BraillePanMode": { + "schema": { + "title": "Braille Pan Mode", + "description": "The pan mode algorithm to use.", + "enum": [ + 0, + 1, + 2, + 255 + ], + "enumLabels": [ + "Best Fit", + "Fixed Increment", + "Maximize Text", + "Automatic" + ], + "default": 255 + } + }, + "Braille.AutoAdvanceInterval": { + "schema": { + "title": "Auto Advance Maximum Interval", + "description": "The maximum advance interval to use.", + "enum": [ + 500, + 1000, + 1500, + 2000, + 2500, + 3000, + 3500, + 4000, + 4500, + 5000, + 5500, + 6000, + 6500, + 7000, + 7500, + 8000, + 8500, + 9000, + 9500, + 10000, + 10500, + 11000, + 11500, + 12000, + 12500, + 13000, + 13500, + 14000, + 14500, + 15000, + 15500, + 16000, + 16500, + 17000, + 17500, + 18000, + 18500, + 19000, + 19500, + 20000 + ], + "enumLabels": [ + "500 Milliseconds", + "1000 Milliseconds", + "1500 Milliseconds", + "2000 Milliseconds", + "2500 Milliseconds", + "3000 Milliseconds", + "3500 Milliseconds", + "4000 Milliseconds", + "4500 Milliseconds", + "5000 Milliseconds", + "5500 Milliseconds", + "6000 Milliseconds", + "6500 Milliseconds", + "7000 Milliseconds", + "7500 Milliseconds", + "8000 Milliseconds", + "8500 Milliseconds", + "9000 Milliseconds", + "9500 Milliseconds", + "10000 Milliseconds", + "10500 Milliseconds", + "11000 Milliseconds", + "11500 Milliseconds", + "12000 Milliseconds", + "12500 Milliseconds", + "13000 Milliseconds", + "13500 Milliseconds", + "14000 Milliseconds", + "14500 Milliseconds", + "15000 Milliseconds", + "15500 Milliseconds", + "16000 Milliseconds", + "16500 Milliseconds", + "17000 Milliseconds", + "17500 Milliseconds", + "18000 Milliseconds", + "18500 Milliseconds", + "19000 Milliseconds", + "19500 Milliseconds", + "20000 Milliseconds" + ], + "default": 5000 + } + }, + "Braille.UseOSM": { + "schema": { + "title": "Braille Presentation and Panning", + "description": "Select presentation and panning mode.", + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "Always use DOM if available", + "Use DOM except in edit controls.", + "Use DOM except when away from focus", + "Always use OSM (screen presentation)" + ], + "default": 1 + } + }, + "Braille.ReversePanningButtons": { + "schema": { + "title": "Reverse Panning Buttons", + "description": "Whether or not panning buttons are reversed.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + + // --------- Braille General - Mode -------------- + + "Braille.BrailleAutoDetectBluetooth": { + "schema": { + "title": "Autodetect Bluetooth Braille Display", + "description": "Whether to automatically detect bluetooth displays.", + "enum": [0,1], + "enumLabels": ["off", "on"], + "default": 0 + } + }, + "Braille.BrailleAutoRouteToCursor": { + "schema": { + "title": "Braille Cursor Active Cursor", + "description": "Braille cursor follows Active Cursor.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Braille.BrailleMode": { + "schema": { + "title": "Braille Mode", + "description": "Use this option to control the format of the information sent to the braille display. When Line mode is selected, JAWS sends the line of text at the current cursor position to the braille display. When Structured mode is selected, JAWS sends information to the display that is relevant to the current cursor position. The information sent includes things such as control type, dialog name, or number of items in a list. When Speech Output mode is selected, JAWS sends the same text to the display that it sends to the synthesizer.", + "default": 1, + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "Line", + "Structured", + "Speech Output" + ] + } + }, + "Braille.BrailleMoveActiveCursor": { + "schema": { + "title": "Braille display cursor", + "description": "Active cursor follows Braille display.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Braille.BrailleSleepMode": { + "schema": { + "title": "Enable Braille Sleep Mode", + "description": "Whether to turn off the braille driver, typically used to disable braille for a specific application.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Braille.EightDotBraille": { + "schema": { + "title": "Braille Dots", + "description": "The number of braille dots (six or eight) to use.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "6 dot braille", + "8 dot braille" + ], + "default": 1 + } + }, + "Braille.WordWrap": { + "schema": { + "title": "Enable Word Wrap", + "description": "Whether or not to allow words to be split in order to maximize the use of the braille display. Ignored if panning is set to fixed increment.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + + // ----------------- Braille Marking ----------------- + + // ------ Braille Marking - Braille Marking ---------- + + // TODO: Temporarly bitmasks settings will be labeled as "bitmask" + "Braille.BrailleShowMarking": { + "schema": { + "title": "Braille Marking", + "description": "Show Marking may be any combination of the following ored together brlMarkNothing= 0, brlMarkText = 1, brlMarkBold = 2, brlMarkItalic = 4, brlMarkUnderline = 8, brlMarkStrikeout = 16, brlMarkGraphic = 32, brlMarkHighlight = 64, brlMarkColor= 128, // see brlOptColorsToMark string option brlMarkExtended=256, // things like spelling and grammatical errors in MSWord etc, script defined marking brlMarkDoubleStrikeout=512, brlMarkSuperscript=1024, brlMarkSubscript=2048, brlMarkShadow=4096, brlMarkOutline=8192, brlMarkEmboss=16384, brlMarkEngrave=32768, Note when Extended marking is on, JAWS calls the PointNeedsMarking function passing in the coordinates of each cell to determine whether text at that location requires marking. This is useful when the attribute you want to mark is not actually known to JAWS such as the red and green zigzags in MSWord indicating spelling or grammatical errors.", + "type": "integer", + "minimum": 0, + "maximum": 65536, + "default": 64 + }, + "label": "bitmask" + }, + // ---- Braille Marking - Attribute Rotation Rate ----- + + "Braille.AttributeRotationDelay": { + "schema": { + "title": "Attribute Rotation Delay", + "description": "When in Attribute Mode and characters have multiple attributes, determines how long each attribute should be shown for. This value is in Milliseconds.", + "enum": [ + 500, + 1000, + 1500, + 2000 + ], + "enumLabels": [ + "500", + "1000", + "1500", + "2000" + ], + "default": 1000 + } + }, + + // ---- Braille Marking - Dot Pattern Values ----- + + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Braille.BrailleHighlightDots": { + // "schema": { + // "title": "Dot Pattern Values - Highlight", + // "description": "Dot pattern to be used for highlighted text.", + // "type": "integer", + // "default": 125 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Braille.BrailleBoldDots": { + // "schema": { + // "title": "Dot Pattern Values - Bold", + // "description": "Dot pattern to be used for bold text.", + // "type": "integer", + // "default": 12 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Braille.BrailleExtMarkingDots": { + // "schema": { + // "title": "Dot Pattern Values - Markings", + // "description": "Dot pattern to be used for marked text.", + // "type": "integer", + // "default": 15 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Braille.BrailleItalicDots": { + // "schema": { + // "title": "Dot Pattern Values - Italic", + // "description": "Dot pattern to be used for italic text.", + // "type": "integer", + // "default": 24 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Braille.BrailleNormalTextDots": { + // "schema": { + // "title": "Dot Pattern Values - Normal Text", + // "description": "Dot pattern to be used for normal text.", + // "type": "integer", + // "default": 1345 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Braille.BrailleStrikeoutDots": { + // "schema": { + // "title": "Dot Pattern Values - Strikeout", + // "description": "Dot pattern to be used for strikeout text.", + // "type": "integer", + // "default": 234 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Braille.BrailleUnderlineDots": { + // "schema": { + // "title": "Dot Pattern Values - Underline", + // "description": "Dot pattern to be used for underline text.", + // "type": "integer", + // "default": 136 + // } + // }, + + "Braille.FilterControlCharacters": { + "schema": { + "title": "Filter Control Characters", + "description": "Whether or not to filter certain control characters (currently newlines, linefeeds and tabs).", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Braille.GeneralizeBullets": { + "schema": { + "title": "Generalize Bullets", + "description": "Whether to treat all bullets the same.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + + // ---- Braille Cursor ----- + + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Braille.BraillePCCursorDots": { + // "schema": { + // "title": "PC Cursor Dot Pattern", + // "description": "The dot pattern to use for the PC cursor.", + // "type": "integer", + // "default": 78 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Braille.BraillePCCursorBlinkRate": { + // "schema": { + // "title": "PC Cursor blink rate", + // "description": "Stablish the braille cursor blinking rate.", + // "type": "integer", + // // NOTE: The actual menu has 3 entries, with different values reserved + // // for each one: + // // - "Braille Cursor is Always Up": 0 + // // - "Braille Cursor is Always Down": -1 + // // - "Braille Cursor is Blinking": Integer + // "default": 500 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Braille.BrailleJAWSCursorDots": { + // "schema": { + // "title": "JAWS Cursor Dot Pattern", + // "description": "The dot pattern to use for the JAWS cursor.", + // "type": "integer", + // "default": 78 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Braille.BrailleJAWSCursorBlinkRate": { + // "schema": { + // "title": "JAWS Cursor blink rate", + // "description": "Stablish the JAWS braille cursor blinking rate.", + // "type": "integer", + // // NOTE: The actual menu has 3 entries, with different values reserved + // // for each one: + // // - "Braille Cursor is Always Up": 0 + // // - "Braille Cursor is Always Down": -1 + // // - "Braille Cursor is Blinking": Integer + // "default": 500 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Braille.BrailleInvisibleCursorDots": { + // "schema": { + // "title": "Invisible Cursor Dot Pattern", + // "description": "The dot pattern to use for the \"Invisible Cursor\".", + // "type": "integer", + // "default": 78 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Braille.BrailleInvisibleCursorBlinkRate": { + // "schema": { + // "title": "JAWS Cursor blink rate", + // "description": "Stablish the JAWS braille cursor blinking rate.", + // "type": "integer", + // // NOTE: The actual menu has 3 entries, with different values reserved + // // for each one: + // // - "Braille Cursor is Always Up": 0 + // // - "Braille Cursor is Always Down": -1 + // // - "Braille Cursor is Blinking": Integer + // "default": 500 + // } + // }, + + // ---- Braille UNKNOWN ----- + + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Braille.BrailleCursorBlinkRate": { + // "schema": { + // "title": "Braille Cursor Blink Rate", + // "description": "Speed, in milliseconds, at which the cursor on the braille display should blink.", + // "type": "integer", + // "default": 500 + // } + // }, + + "Braille.BrailleKeyInterruptSpeech": { + "schema": { + "title": "Braille Key Interrupt Speech", + "description": "Whether to Interrupt speech if a keystroke in the Braille display is pressed.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + + // ---- Braille Flash Messages ----- + + "Braille.BrailleMessages": { + "schema": { + "title": "Enable Flash Messages", + "description": "Whether or not to show Flash Messages on a braille display.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Braille.MessageTime": { + // "schema": { + // "title": "Flash Message Timeout", + // "description": "How long in milliseconds a flash message is going to be displayed in the Braille display.", + // "type": "integer", + // "default": 5000 + // } + // }, + + // ---- Braille Flash Messages - Verbosity Levels ----- + + // GPII-4226 Leaving out for now due to lack of a default value. + // "Braille.BrailleVerbosity": { + // "schema": { + // "title": "Braille Verbosity", + // "description": "Sets braille verbosity for Flash messages.", + // "enum": [ + // 0, + // 1, + // 2 + // ], + // "enumLabels": [ + // "Beginner, Highest", + // "Intermediate", + // "Advanced, Lowest" + // ] + // } + // }, + + // ---- Braille - Flash Messages - Verbosity Levels - Items to brailed ----- + + "Braille OutputModes.APP_START": { + "schema": { + "title": "App start messages", + "description": "Enable messaging application starts with different levels of verbosity.", + "type": "string", + // TODO: The string format is of type: + // - Beginnner: "1|0|0|app" + // - Intermediate: "0|2|0|app" + // - Advanced: "0|0|2|app" + // The breakdown of the format is: + // - The first three bar separated numbers stands for the level selection. + // Being 1 used for long message length, and 2 for short message length. + // - The final string is for the braille text that identifies the message. + "default": "Unknown" + } + }, + "Braille OutputModes.ERROR": { + "schema": { + "title": "Error messages", + "description": "Enable messaging errors with different levels of verbosity.", + "type": "string", + // TODO: The string format is of type: + // - Beginnner: "1|0|0|err" + // - Intermediate: "0|2|0|err" + // - Advanced: "0|0|2|err" + // The breakdown of the format is: + // - The first three bar separated numbers stands for the level selection. + // Being 1 used for long message length, and 2 for short message length. + // - The final string is for the braille text that identifies the message. + "default": "Unknown" + } + }, + "Braille OutputModes.HELP": { + "schema": { + "title": "Help messages", + "description": "Enable help messages with different levels of verbosity.", + "type": "string", + // TODO: The string format is of type: + // - Beginnner: "1|0|0|hlp" + // - Intermediate: "0|2|0|hlp" + // - Advanced: "0|0|2|hlp" + // The breakdown of the format is: + // - The first three bar separated numbers stands for the level selection. + // Being 1 used for long message length, and 2 for short message length. + // - The final string is for the braille text that identifies the message. + "default": "Unknown" + } + }, + "Braille OutputModes.HELP_BALLOON": { + "schema": { + "title": "Error messages", + "description": "Enable messaging errors with different levels of verbosity.", + "type": "string", + // TODO: The string format is of type: + // - Beginnner: "1|0|0|bln" + // - Intermediate: "0|2|0|bln" + // - Advanced: "0|0|2|bln" + // The breakdown of the format is: + // - The first three bar separated numbers stands for the level selection. + // Being 1 used for long message length, and 2 for short message length. + // - The final string is for the braille text that identifies the message. + "default": "Unknown" + } + }, + "Braille OutputModes.JAWS_MESSAGE": { + "schema": { + "title": "JAWS messages", + "description": "Enable JAWS messages with different levels of verbosity.", + "type": "string", + // TODO: The string format is of type: + // - Beginnner: "1|0|0|jm" + // - Intermediate: "0|2|0|jm" + // - Advanced: "0|0|2|jm" + // The breakdown of the format is: + // - The first three bar separated numbers stands for the level selection. + // Being 1 used for long message length, and 2 for short message length. + // - The final string is for the braille text that identifies the message. + "default": "Unknown" + } + }, + "Braille OutputModes.MouseEcho": { + "schema": { + "title": "Mouse echo messages", + "description": "Enable mouse echo messages with different levels of verbosity.", + "type": "string", + // TODO: The string format is of type: + // - Beginnner: "1|0|0|me" + // - Intermediate: "0|2|0|me" + // - Advanced: "0|0|2|me" + // The breakdown of the format is: + // - The first three bar separated numbers stands for the level selection. + // Being 1 used for long message length, and 2 for short message length. + // - The final string is for the braille text that identifies the message. + "default": "Unknown" + } + }, + "Braille OutputModes.STATUS": { + "schema": { + "title": "Status messages", + "description": "Enable status messages with different levels of verbosity.", + "type": "string", + // TODO: The string format is of type: + // - Beginnner: "1|0|0|sts" + // - Intermediate: "0|2|0|sts" + // - Advanced: "0|0|2|sts" + // The breakdown of the format is: + // - The first three bar separated numbers stands for the level selection. + // Being 1 used for long message length, and 2 for short message length. + // - The final string is for the braille text that identifies the message. + "default": "Unknown" + } + }, + "Braille OutputModes.SMART_HELP": { + "schema": { + "title": "Smart help messages", + "description": "Enable start smart help messages with different levels of verbosity.", + "type": "string", + // TODO: The string format is of type: + // - Beginnner: "1|0|0|shlp" + // - Intermediate: "0|2|0|shlp" + // - Advanced: "0|0|2|shlp" + // The breakdown of the format is: + // - The first three bar separated numbers stands for the level selection. + // Being 1 used for long message length, and 2 for short message length. + // - The final string is for the braille text that identifies the message. + "default": "Unknown" + } + }, + "Braille OutputModes.TOASTS": { + "schema": { + "title": "Toasts messages", + "description": "Enable messaging Toasts with different levels of verbosity.", + "type": "string", + // TODO: The string format is of type: + // - Beginnner: "1|0|0|sts" + // - Intermediate: "0|2|0|sts" + // - Advanced: "0|0|2|sts" + // The breakdown of the format is: + // - The first three bar separated numbers stands for the level selection. + // Being 1 used for long message length, and 2 for short message length. + // - The final string is for the braille text that identifies the message. + "default": "Unknown" + } + }, + "Braille OutputModes.TOOL_TIP": { + "schema": { + "title": "Tool Tips messages", + "description": "Enable messaging Tool Tips with different levels of verbosity.", + "type": "string", + // TODO: The string format is of type: + // - Beginnner: "1|0|0|tip" + // - Intermediate: "0|2|0|tip" + // - Advanced: "0|0|2|tip" + // The breakdown of the format is: + // - The first three bar separated numbers stands for the level selection. + // Being 1 used for long message length, and 2 for short message length. + // - The final string is for the braille text that identifies the message. + "default": "Unknown" + } + }, + "Braille OutputModes.TUTOR": { + "schema": { + "title": "Tutor messages", + "description": "Enable messaging Tutors with different levels of verbosity.", + "type": "string", + // TODO: The string format is of type: + // - Beginnner: "1|0|0|ttr" + // - Intermediate: "0|2|0|ttr" + // - Advanced: "0|0|2|ttr" + // The breakdown of the format is: + // - The first three bar separated numbers stands for the level selection. + // Being 1 used for long message length, and 2 for short message length. + // - The final string is for the braille text that identifies the message. + "default": "Unknown" + } + }, + "Braille OutputModes.USER_REQUESTED_INFORMATION": { + "schema": { + "title": "User requested information messages", + "description": "Enable messaging user requested information with different levels of verbosity.", + "type": "string", + // TODO: The string format is of type: + // - Beginnner: "1|0|0|usr" + // - Intermediate: "0|2|0|usr" + // - Advanced: "0|0|2|usr" + // The breakdown of the format is: + // - The first three bar separated numbers stands for the level selection. + // Being 1 used for long message length, and 2 for short message length. + // - The final string is for the braille text that identifies the message. + "default": "Unknown" + } + }, + + // ---- Braille - Structured Mode ----- + + // ---- Braille - Structured Mode - Include Optional Components ------ + + "Braille.UseStatusCellsForControlType": { + // NOTE: This setting work in combination with + // StructuredModeInclusionFlags. + // + // "UseStatusCellsForControlType" acts like switch to consider outputing the + // optional components in the "Status Cells" of the braille display. + // "StructuredModeInclusionFlags" is an intenger in which all the flags for the extra elements + // to be included. + "schema": { + "title": "Status cell for control type", + "description": ".", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Braille.StructuredModeInclusionFlags": { + // "schema": { + // "title": "Structure Mode Inclusion Flags", + // "description": "Store in a bitmask the elements to be included in 'Braille Structure Mode'.", + // "type": "integer", + // "default": 8191 + // }, + // "label": "bitmask" + // }, + + // ------------------------------------------------------------------ + + "Braille.StructuredModeAlignToSegment": { + "schema": { + "title": "Align To Structured Segment", + "description": "Align output to structured segment.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Braille.StructuredModeReverseOrder": { + "schema": { + "title": "Reverse Order of Structured Data", + "description": "Whether to reverse the order of the structured data to show the focused control's info first, followed by its grouping information, and then the dialog box information. By default, items are read in the opposite order.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + + // ---------------------- Braille - Advanced ------------------------ + + "Braille.PositionOfStatusCells": { + // NOTE: When this setting is used with a valu of 2 ("None"), the setting + // "UseHowManyStatusCells" is set to 0. + "schema": { + "title": "Placement of Status Cells", + "description": "Selects the placement of the status cells in the braille display.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "Left", + "Right", + "None" + ], + "default": 0 + } + }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Braille.UseHowManyStatusCells": { + // "schema": { + // "title": "Use How Many Status Cells", + // "description": "How many cells to use for status information if the display defines none of its own..", + // "type": "integer", + // "default": 4 + // } + // }, + "Braille.DotFirmness": { + // NOTE-TODO: This setting appears to have a bug, + // when the slide is moved isn't deleted from the configuration file, but + // it keeps getting added to the INI file. + "schema": { + "title": "Dot firmness", + "description": "Selects the firmness of the dots in the braille display.", + "type": "integer", + "minimum": 0, + "maximum": 4, + "default": 4 + } + }, + "Braille.LiblouisVersion": { + "schema": { + "title": "Grade 2 Translator", + "description": "Selects the translator to be used.", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "enumLabels": [ + "Liblouis 3.9.0", + "Liblouis 3.7.0", + "Liblouis 3.5.0", + "Liblouis 3.3.0", + "Liblouis 2.6.3" + ], + "default": 4 + } + }, + + // ---------------------- Braille - ???????? ------------------------ + + "Braille.MessagePrefixes": { + "schema": { + "title": "Enable Flash Message Prefixes", + "description": "If a display has no Status Cells, whether or not to prepend the Message Prefix to the Flash Message.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Braille.MessageStatusText": { + "schema": { + "title": "Message Status Text", + "description": "The text to show in the status area of the display when a Flash message is being shown if the display has no status cells, the text will be prepended to the Flash message.", + "type": "string", + "default": "msg" + } + }, + "Braille.UseScreenModelForBrailleInRichEdits": { + "schema": { + "title": "Use Screen Model for Braille in Rich Edits", + "description": "Whether to use the screen model when providing field prompts and text in Braille with rich edits.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + + // ------------------------------------------------------------------ + + // ---------------------- Caret And Cursor -------------------------- + + "Options.SayCursorShapeChange": { + "schema": { + "title": "Say Cursor Shape Change", + "description": "Whether to announce a change in the shape of the cursor.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Options.CaretBlinkRate": { + // "schema": { + // "title": "Caret Blink Rate", + // "description": "How fast the caret blinks, in milliseconds.", + // "type": "integer", + // "default": 53, + // "minimum": 1 + // } + // }, + + // ---------- Caret And Cursor: Vertical Caret Dimensions ---------- + + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "OSM.MouseClickFudgeX": { + // "schema": { + // "title": "Mouse Click Fudge X", + // "description": "Number of pixels to the right of the current JAWS cursor at which a mouse click should be simulated", + // "type": "integer", + // "default": 1 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "OSM.MouseClickFudgeY": { + // "schema": { + // "title": "Mouse Click Fudge Y", + // "description": "Number of pixels below the current JAWS cursor at which a mouse click should be simulated", + // "type": "integer", + // "default": -2 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "OSM.VertCaretMinY": { + // "schema": { + // "title": "Vertical Caret Minimum Y Dimension", + // "description": "Minimum number pixels of height for the 'Vertical Caret'", + // "type": "integer", + // "default": 4 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "OSM.VertCaretMinX": { + // "schema": { + // "title": "Vertical Caret Minimum X Dimension", + // "description": "Minimum number pixels of width for the 'Vertical Caret'", + // "type": "integer", + // "default": 1 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "OSM.VertCaretMaxY": { + // "schema": { + // "title": "Vertical Caret Maximum Y Dimension", + // "description": "Maximum number pixels of height for the 'Vertical Caret'", + // "type": "integer", + // "default": 110 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "OSM.VertCaretMaxX": { + // "schema": { + // "title": "Vertical Caret Maximum X Dimension", + // "description": "Maximum number pixels of width for the 'Vertical Caret'", + // "type": "integer", + // "default": 4 + // } + // }, + + // ------- Caret And Cursor: Horizontal Caret Dimensions ---------- + + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "OSM.HorizCaretMinY": { + // "schema": { + // "title": "Horizontal Caret Minimum Y Dimension", + // "description": "Minimum number pixels of height for the 'Horizontal Caret'", + // "type": "integer", + // "default": 1 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "OSM.HorizCaretMinX": { + // "schema": { + // "title": "Horizontal Caret Minimum X Dimension", + // "description": "Minimum number pixels of width for the 'Horizontal Caret'", + // "type": "integer", + // "default": 4 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "OSM.HorizCaretMaxY": { + // "schema": { + // "title": "Horizontal Caret Maximum Y Dimension", + // "description": "Maximum number pixels of height for the 'Horizontal Caret'", + // "type": "integer", + // "default": 4 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "OSM.HorizCaretMaxX": { + // "schema": { + // "title": "Horizontal Caret Maximum X Dimension", + // "description": "Maximum number pixels of width for the 'Horizontal Caret'", + // "type": "integer", + // "default": 14 + // } + // }, + + // ------------------- Synthesizer ------------------- + + "Options.SimultaneousSynthAndWave": { + "schema": { + "title": "Simultaneous Synth and Wave", + "description": "Whether to enable smoother playing of WAV files when using the DEC Talk Access 32 software synthesizer. This option is only available in Default.jcf. Changes to this setting take effect the next time you start JAWS. Disabled by default.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Options.SynthesizerResetFrequency": { + // "schema": { + // "title": "Synthesizer Reset Frequency", + // "description": "How often to reset communication with the synthesizer, in milliseconds.", + // "type": "integer", + // "default": 2000, + // "minimum": 0 + // } + // }, + + // ---------------- Punctuation ------------------ + + // TODO: Research how "Customize Voice Aliases" works + + // TODO: "Punctuation Level" not present in this config file. + + // ----------------------------------------------- + + // ---------------- Voice Aliases ---------------- + + // TODO: Research how "Customize Voice Aliases" works + + // ------------------------------------------------ + + // ---------------- Windows Classes --------------- + + // TODO: Research how "Customize Window Classes" works + + // ------------------------------------------------ + + // ------------------ Keyboard -------------------- + + // ------------- Keyboard - General --------------- + + "Options.QuickKeyNavigationMode": { + "schema": { + "title": "Quick Key Navigation Mode", + "description": "Navigation Quick Keys let you move through Web pages with easy to remember commands, such as T for table, F for form field, N for non link text, and V for visited link. These commands are only available when the Virtual Cursor is active. Use these radio buttons to set Navigation Quick Keys off, on, or on only during Say All reading. The default is on.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "Off", + "On", + "On during \"Say All\"" + ], + "default": 1 + } + }, + "Options.JAWSInsertKey": { + "schema": { + "title": "JAWS Insert Key", + "description": "Whether/which insert keys can be used as a JAWS Insert.", + "default": 3, + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "None", + "Numpad Insert", + "Extended Insert", + "Use both Numpad and Extended Inserted" + ] + } + }, + "Options.InitialNumlockState": { + "schema": { + "title": "Initial State of NumLock Key", + "description": "The initial state of the NumLock key.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "Turn off at startup", + "Turn on at startup", + "Do not modify" + ], + "default": 0 + } + }, + "Options.UseExtendedKeys": { + "schema": { + "title": "Use Extended Keys", + "description": "Whether to distinguish between extended and number pad keys.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Treat Extended and Numpad keys the same", + "Differentiate between Extended and Numpad keys" + ] + } + }, + "Options.GeneralizeModifiers": { + "schema": { + "title": "Generalize Modifiers", + "description": "Differentiate Between Left and Right Paired Keys.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Treat Extended and Numpad keys the same", + "Differentiate between Extended and Numpad keys" + ] + } + }, + "Options.OnScreenKeyboard": { + "schema": { + "title": "Allow On Screen Keyboards", + "description": "Whether to allow on screen keyboards.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + + // ----------- Keyboard - Manage Key Labels ------- + + // NOTE: The format of the setting is of the kind: "KeyLabels.Add": "1|+", + // where the first value represents if the setting label + // is muted or not, and the second the keylabel used for + // describing it. + + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Add": { + // "schema": { + // "title": "Manage Add Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Apps": { + // "schema": { + // "title": "Manage Apps Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Attn": { + // "schema": { + // "title": "Manage Attn Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Back": { + // "schema": { + // "title": "Manage Back Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Cancel": { + // "schema": { + // "title": "Manage Cancel Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Capital": { + // "schema": { + // "title": "Manage Capital Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Clear": { + // "schema": { + // "title": "Manage Clear Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Control": { + // "schema": { + // "title": "Manage Control Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.CrSel": { + // "schema": { + // "title": "Manage CrSel Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Decimal": { + // "schema": { + // "title": "Manage Decimal Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Delete": { + // "schema": { + // "title": "Manage Delete Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Divide": { + // "schema": { + // "title": "Manage Divide Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Down": { + // "schema": { + // "title": "Manage Down Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.End": { + // "schema": { + // "title": "Manage End Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.ErEOF": { + // "schema": { + // "title": "Manage ErEOF Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Escape": { + // "schema": { + // "title": "Manage Escape Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Execute": { + // "schema": { + // "title": "Manage Execute Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.ExSel": { + // "schema": { + // "title": "Manage ExSel Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F1": { + // "schema": { + // "title": "Manage F1 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F10": { + // "schema": { + // "title": "Manage F10 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F11": { + // "schema": { + // "title": "Manage F11 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F12": { + // "schema": { + // "title": "Manage F12 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F13": { + // "schema": { + // "title": "Manage F13 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F14": { + // "schema": { + // "title": "Manage F14 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F15": { + // "schema": { + // "title": "Manage F15 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F16": { + // "schema": { + // "title": "Manage F16 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F17": { + // "schema": { + // "title": "Manage F17 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F18": { + // "schema": { + // "title": "Manage F18 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F19": { + // "schema": { + // "title": "Manage F19 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F2": { + // "schema": { + // "title": "Manage F2 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F20": { + // "schema": { + // "title": "Manage F20 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F21": { + // "schema": { + // "title": "Manage F21 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F22": { + // "schema": { + // "title": "Manage F22 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F23": { + // "schema": { + // "title": "Manage F23 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F3": { + // "schema": { + // "title": "Manage F3 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F4": { + // "schema": { + // "title": "Manage F4 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F5": { + // "schema": { + // "title": "Manage F5 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F6": { + // "schema": { + // "title": "Manage F6 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F7": { + // "schema": { + // "title": "Manage F7 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F8": { + // "schema": { + // "title": "Manage F8 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.F9": { + // "schema": { + // "title": "Manage F9 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Help": { + // "schema": { + // "title": "Manage Help Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Home": { + // "schema": { + // "title": "Manage Home Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Insert": { + // "schema": { + // "title": "Manage Insert Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Left": { + // "schema": { + // "title": "Manage Left Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Letters": { + // "schema": { + // "title": "Manage Letters Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.LWin": { + // "schema": { + // "title": "Manage LWin Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Menu": { + // "schema": { + // "title": "Manage Menu Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.ModifiedPrintable": { + // "schema": { + // "title": "Manage ModifiedPrintable Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Multiply": { + // "schema": { + // "title": "Manage Multiply Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Next": { + // "schema": { + // "title": "Manage Next Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Numbers": { + // "schema": { + // "title": "Manage Numbers Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.NumLock": { + // "schema": { + // "title": "Manage NumLock Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.NumPad0": { + // "schema": { + // "title": "Manage NumPad0 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.NumPad1": { + // "schema": { + // "title": "Manage NumPad1 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.NumPad2": { + // "schema": { + // "title": "Manage NumPad2 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.NumPad3": { + // "schema": { + // "title": "Manage NumPad3 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.NumPad4": { + // "schema": { + // "title": "Manage NumPad4 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.NumPad5": { + // "schema": { + // "title": "Manage NumPad5 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.NumPad6": { + // "schema": { + // "title": "Manage NumPad6 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.NumPad7": { + // "schema": { + // "title": "Manage NumPad7 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.NumPad8": { + // "schema": { + // "title": "Manage NumPad8 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.OEM_clear": { + // "schema": { + // "title": "Manage OEM_clear Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.PA1": { + // "schema": { + // "title": "Manage PA1 Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Pause": { + // "schema": { + // "title": "Manage Pause Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Play": { + // "schema": { + // "title": "Manage Play Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Print": { + // "schema": { + // "title": "Manage Print Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Prior": { + // "schema": { + // "title": "Manage Prior Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Return": { + // "schema": { + // "title": "Manage Return Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Right": { + // "schema": { + // "title": "Manage Right Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.RWin": { + // "schema": { + // "title": "Manage RWin Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Scroll": { + // "schema": { + // "title": "Manage Scroll Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Select": { + // "schema": { + // "title": "Manage Select Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Separator": { + // "schema": { + // "title": "Manage Separator Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Shift": { + // "schema": { + // "title": "Manage Shift Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.SnapShot ": { + // "schema": { + // "title": "Manage SnapShot Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Space": { + // "schema": { + // "title": "Manage Space Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Subtract": { + // "schema": { + // "title": "Manage Subtract Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Symbols": { + // "schema": { + // "title": "Manage Symbols Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Tab": { + // "schema": { + // "title": "Manage Tab Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Up": { + // "schema": { + // "title": "Manage Up Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "KeyLabels.Zoom": { + // "schema": { + // "title": "Manage Zoom Keylabel", + // "description": "Change Keylabel and Muting.", + // "type": "string" + // } + // }, + + // ----------- Keyboard - Touch Keyboard ------- + + "Options.TouchTypingMode": { + "schema": { + "title": "Touch Typing Mode", + "description": "Which keyboard mode to use for the touch keyboard.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Standard Typing", + "Touch typing" + ], + "default": 1 + } + }, + "Options.TouchTypingEcho": { + "schema": { + "title": "Touch Typing Echo", + "description": "Whether/how to echo text entered using the touch keyboard.", + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "None", + "Characters", + "Words", + "Both characters and words" + ], + "default": 3 + } + }, + "Options.TouchKeyboardNotification": { + "schema": { + "title": "Touch Keyboard Notification", + "description": "Whether/how to notify when the touch keyboard appears/disappears.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "By Message", + "By Sound", + "By both Message and Sound" + ], + "default": 2 + } + }, + "Options.TouchKeyboardChildPanelNotification": { + "schema": { + "title": "Touch Alternative Character Panel Popup Notification", + "description": "How to announce the appearance of a panel of alternate keys in the touch keyboard.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "By both Message and Sound", + "By Sound only" + ], + "default": 0 + } + }, + + // -------------- Custom Highligth ------------- + + // -------- TODO: Check how to access ---------- + + // ---------------- Research It ---------------- + + // GPII-4226 Leaving out for now due to lack of a default value. + // "Research It Options.Primary": { + // "schema": { + // "title": "Primary Research Option", + // "description": "Defines the primary research option.", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Research It Exclusions.Exclude1": { + // "schema": { + // "title": "Research Exclusions \"Exclude1\"", + // "description": "Defines a exclusion for \"Research It\".", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Research It Exclusions.Exclude2": { + // "schema": { + // "title": "Research Exclusions \"Exclude2\"", + // "description": "Defines a exclusion for \"Research It\".", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Research It Exclusions.Exclude3": { + // "schema": { + // "title": "Research Exclusions \"Exclude3\"", + // "description": "Defines a exclusion for \"Research It\".", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Research It Exclusions.Exclude4": { + // "schema": { + // "title": "Research Exclusions \"Exclude4\"", + // "description": "Defines a exclusion for \"Research It\".", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Research It Exclusions.Exclude5": { + // "schema": { + // "title": "Research Exclusions \"Exclude5\"", + // "description": "Defines a exclusion for \"Research It\".", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Research It Exclusions.Exclude6": { + // "schema": { + // "title": "Research Exclusions \"Exclude6\"", + // "description": "Defines a exclusion for \"Research It\".", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Research It Exclusions.Exclude7": { + // "schema": { + // "title": "Research Exclusions \"Exclude7\"", + // "description": "Defines a exclusion for \"Research It\".", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Research It Exclusions.Exclude8": { + // "schema": { + // "title": "Research Exclusions \"Exclude8\"", + // "description": "Defines a exclusion for \"Research It\".", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Research It Exclusions.Exclude9": { + // "schema": { + // "title": "Research Exclusions \"Exclude9\"", + // "description": "Defines a exclusion for \"Research It\".", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Research It Exclusions.Exclude10": { + // "schema": { + // "title": "Research Exclusions \"Exclude10\"", + // "description": "Defines a exclusion for \"Research It\".", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Research It Exclusions.Exclude11": { + // "schema": { + // "title": "Research Exclusions \"Exclude11\"", + // "description": "Defines a exclusion for \"Research It\".", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Research It Exclusions.Exclude12": { + // "schema": { + // "title": "Research Exclusions \"Exclude12\"", + // "description": "Defines a exclusion for \"Research It\".", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Research It Exclusions.Exclude13": { + // "schema": { + // "title": "Research Exclusions \"Exclude13\"", + // "description": "Defines a exclusion for \"Research It\".", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Research It Exclusions.Exclude14": { + // "schema": { + // "title": "Research Exclusions \"Exclude14\"", + // "description": "Defines a exclusion for \"Research It\".", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Research It Exclusions.Exclude15": { + // "schema": { + // "title": "Research Exclusions \"Exclude15\"", + // "description": "Defines a exclusion for \"Research It\".", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Research It Exclusions.Exclude16": { + // "schema": { + // "title": "Research Exclusions \"Exclude16\"", + // "description": "Defines a exclusion for \"Research It\".", + // "type": "string" + // } + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Research It Exclusions.Exclude17": { + // "schema": { + // "title": "Research Exclusions \"Exclude17\"", + // "description": "Defines a exclusion for \"Research It\".", + // "type": "string" + // } + // }, + + // ------------------ Text Analyzer ------------------ + + // ---- Text Analyzer - Text Analyzer ---- + + "Options.TextAnalyser": { + "schema": { + "title": "Text Analyser", + "description": "Indicates text analyzer behavior.", + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "Turn Off", + "Indicate with Sound", + "Speak Count", + "Describe Inconsistences" + ], + "default": 0 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Options.TextAnalyserTypeFlags": { + // "schema": { + // "title": "Text Analyser Type", + // "description": "Indicates how the text analyzer should threat different symbols.", + // "type": "integer" + // }, + // "label": "bitmask" + // }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "Options.TextAnalyserSymbolFlags": { + // "schema": { + // "title": "Text Analyser Symbols", + // "description": "Indicates the symbols the text analyzer should check.", + // "type": "integer" + // }, + // "label": "bitmask" + // }, + + // ------------------ Convenient OCR ------------------ + + // NOTE: The language codes used to describe the primary and secondary languages + // are taken from the following list: + // + // -https://docs.microsoft.com/es-es/windows/win32/intl/language-identifier-constants-and-strings + // + + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "OCR.PrimaryRecognitionLanguage": { + // "schema": { + // "title": "Primary OCR Recognition Language", + // "description": "Indicates the primary language for OCR.", + // "type": "integer", + // "default": 1033 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "OCR.SecondaryRecognitionLanguage": { + // "schema": { + // "title": "Secondary OCR Recognition Language", + // "description": "Indicates the secondary language for OCR.", + // "type": "integer", + // "default": 1033 + // } + // }, + + // --------------------- Gestures ----------------------- + + "Touch.TapEventDurationMax": { + "schema": { + "title": "Explore Delay", + "description": "Sets the maximum duration of a user's screen touch that will be considered as a 'tap'", + "type": "integer", + "default": 210, + "minimum": 160, + "maximum": 410 + } + }, + "Touch.TapInterEventDurationMax": { + "schema": { + "title": "Double Tab Speed", + "description": "Sets the time interval between individual taps in a double or triple tap.", + "type": "integer", + "default": 600, + "minimum": 300, + "maximum": 900 + } + }, + "Touch.TapTranslationMax": { + "schema": { + "title": "Tap Radius", + "description": "Sets the size of the finger contact spot on the touch screen.", + "type": "integer", + "default": 20, + "minimum": 10, + "maximum": 60 + } + }, + "Touch.FlickVelocityMin": { + "schema": { + "title": "Flick Velocity", + "description": "Sets the minimum finger velocity on the touch screen above which the movement is recognized as a flick.", + "type": "integer", + "default": 67, + "minimum": 7, + "maximum": 112 + } + }, + + // ------------------- Mouse Echo --------------------- + + "Options.MouseSpeechEnabled": { + "schema": { + "title": "Enable Mouse Echo", + "description": "Whether or not to enable the mouse echo, i.e. reading content under the mouse pointer.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Options.MouseMovementStopsSpeech": { + "schema": { + "title": "Mouse Echo Interrupt", + "description": "Whether to stop speaking when the mouse is moved.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.MouseSpeechInstant": { + "schema": { + "title": "Mouse Echo Instant", + "description": "Enable text reading while the mouse is in motion so you do not have to pause to hear the text under the mouse.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.MouseSpeechDelay": { + "schema": { + "title": "Mouse Echo Delay", + "description": "How long to delay announcing content under the mouse, in milliseconds. Defaults to 0 (no delay).", + "type": "integer", + "default": 400, + "minimum": 200, + "maximum": 2000 + } + }, + "Options.MouseSpeechEchoUnit": { + "schema": { + "title": "Mouse Echo Unit", + "description": "The unit of text to announce when the mouse pauses over text in a control. Defaults to reading the whole line.", + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "Character", + "Word", + "Line", + "Paragraph" + ], + "default": 2 + } + }, + "Options.MouseEchoSpeaksHelpAndDescription": { + "schema": { + "title": "Speak Description of Item", + "description": "Whether to speak the description of an item on mouseover.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Options.MouseEchoSpeaksControlTypeAndState": { + "schema": { + "title": "Speak Control Type and State of Item", + "description": "Whether to speak the type of control and state of item when mousing over a control.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + + // ------------ Visual Tracking ------------ + + "Options.VTUseVirtualPCCursor": { + "schema": { + "title": "Use Virtual PC Cursor", + "description": "Whether to use the virtual PC cursor when reading HTML documents.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.VTUseVirtualRibbon": { + "schema": { + "title": "Use Virtual Ribbon Menu", + "description": "Whether to turn on the virtual ribbon menu.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Options.VTUseTouchCursor": { + "schema": { + "title": "Highlight Touch Cursor", + "description": "Whether or not to highlight selected items when using the touch cursor or touch gestures.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + + // ---- Visual Tracking - Visual Settings ---- + + "Options.VTcolor": { + "schema": { + "title": "Visual Tracking Color", + "description": "The colour to use when highlighting items selected using the touch cursor.", + "enum": [ + 11393254, + 49151, + 255, + 139, + 8388736, + 16711935, + 16711680, + 16753920, + 16776960, + 1293915, + 32768, + 7048739, + 9127187, + 14596231 + ], + "enumLabels": [ + "Light Blue", + "Deep Sky Blue", + "Blue", + "Dark Blue", + "Purple", + "Fuchsia", + "Red", + "Orange", + "Yellow", + "Lime Green", + "Green", + "Olive Drab", + "Saddle Brown", + "Burly Wood" + ], + "default": 1293915 + } + }, + "Options.VTstyle": { + "schema": { + "title": "Visual Tracking Style", + "description": "The style of highlighting to use.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "Box", + "Block", + "Underline" + ], + "default": 1 + } + }, + "Options.VTtransparency": { + "schema": { + "title": "Visual Tracking Transparency", + "description": "How transparent to make the visual highlighting of selected items.", + "type": "integer", + "maximum": 100, + "minimum": 0, + "default": 25 + } + }, + "Options.VTthickness": { + "schema": { + "title": "Visual Tracking Thickness", + "description": "How thick to make the \"box\" and \"underline\" highlighting styles.", + "type": "integer", + "minimum": 1, + "maximum": 10, + "default": 2 + } + }, + "Options.VTspacing": { + "schema": { + "title": "Visual Tracking Spacing", + "description": "The amount of spacing between the highlight box and the highlighted item.", + "type": "integer", + "minimum": 1, + "maximum": 10, + "default": 2 + } + }, + + // ------------ Miscellaneous --------------- + + "OSM.EditPromptSearch": { + "schema": { + "title": "Search for Prompts", + "description": "Determine to what extent JAWS searches for prompts.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "Minimum Search", + "Extended Search", + "Maximum Search" + ], + "default": 2 + } + }, + "OSM.HookingMode": { + "schema": { + "title": "Use Accessibility Driver for Screen Capture", + "description": "Whether to use the legacy accessibility driver.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.VirtualRibbonSupport": { + "schema": { + "title": "Virtual Ribbon Support", + "description": "Whether to use the Virtual PC cursor to read HTML documents. When using the Virtual PC cursor, Web pages and other HTML documents are read as if they were formatted into single columns, with each link or form field on a line of its own. This mode allows you to navigate or select text in a Web page as if you were in a standard text document, while also giving you the ability to fill out forms or activate links.", + "enum": [0, 1], + "enumLabels": ["off", "on"], + "default": 0 + } + }, + "OSM.UseVirtualPCCursor": { + "schema": { + "title": "Virtual PC Cursor", + "description": "Whether to use the virtual PC cursor when reading HTML documents.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.AnnounceMultilineEdit": { + "schema": { + "title": "Announce Multiline Edit Controls", + "description": "Whether to announce multi-line edit controls.", + "enum": [0, 1], + "enumLabels": ["off", "on"], + "default": 0 + } + }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "OSM.TextOutDelay": { + // "schema": { + // "title": "Text Out Delay", + // "description": "The number of milliseconds to wait before announcing newly written text. Setting this to a non-zero value may cause terminal programs to respond in a less choppy fashion.", + // "type": "integer", + // "default": 0 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "OSM.PixelsPerSpace": { + // "schema": { + // "title": "Pixels Per Space", + // "description": "When simulating spaces in blank portions of a line, the number of pixels used to represent each space.", + // "type": "integer", + // "default": 8 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "OSM.PixelsPerTab": { + // "schema": { + // "title": "Pixels Per Tab", + // "description": "The number of pixels per tab, used to estimate the number of tab spaces contained within the blank portion of a line.", + // "type": "integer", + // "default": 10 + // } + // }, + "OSM.TrackFocusRect": { + "schema": { + "title": "Track Focus Rectangle", + "description": "Set to one causes MAGic to track the focus rectangle in standard controls that utilize focus rectangles.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "OSM.LeftMargin": { + // "schema": { + // "title": "Left Margin in Pixels", + // "description": "Ignores the distance specified in pixels from the left border of the window.", + // "type": "integer", + // "default": 0 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "OSM.UnderlineProximity": { + // "schema": { + // "title": "Underline Proximity", + // "description": "How far away in pixels can an underline be before being considered an actual underline.", + // "type": "integer", + // "default": 0 + // } + // }, + "FSCasts.EnableNotifications": { + "schema": { + "title": "FSCast Notifications", + "description": "Whether to have JAWS display a dialog when new podcasts from Freedom Scientific are available.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + + // -------- Miscellaneous - Submit Anonymouse Usage Data ------------ + + // NOTE-TODO: Setting not present in the default config file. + + // ------------------------------------------------------------------- + + // ---------------- Miscellaneous - Error Reporting ----------------- + + // NOTE-TODO: Setting not present in the default config file. + + // ------------------------------------------------------------------- + + // ------------ Graphics and Symbols ------------ + + "OSM.GraphicsMode": { + "schema": { + "title": "Graphics Mode", + "description": "How to handle graphic CRC values.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Store graphic CRC values when the graphics are being drawn to the screen.", + "Compute them from the screen image at the time they are requested to be spoken." + ], + "default": 0 + } + }, + "OSM.IgnoreSetSysModal": { + "schema": { + "title": "Ignore Set Sys Modal", + "description": "Whether to disallow Windows calling the SetSysModalWindow function. In most cases this is desirable, because System Modal Windows cannot be spoken.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "OSM.IncludeLines": { + "schema": { + "title": "Include Lines", + "description": "Whether to include line segments in the Off Screen Model. This is required for Excel and other programs which draw a focus rectangle.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "OSM.SpeakSysModalMessageBox": { + "schema": { + "title": "Speak Sys Modal Message Box", + "description": "Whether to announce System Modal Message Boxes.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "OSM.TrackCommandBars": { + "schema": { + "title": "Track Command Bars", + "description": "Whether to track the menu bar in Office applications.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "OSM.TrimBrailleGraphicLabels": { + "schema": { + "title": "Trim Braille Graphic Labels", + "description": "Whether to trim the size of a braille graphic. Useful to preserve the virtical positioning of braille text.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + + // ------------ GLOBAL ------------ + + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "OSM.PixelsPerBlankLine": { + // "schema": { + // "title": "Pixels Per Blank Line", + // "description": "how many pixels (high) per blank line", + // "type": "integer", + // "default": 13 + // } + // }, + "Voice Profiles.ActiveVoiceProfileName": { + "schema": { + "title": "Active Voice Profile Name", + "description": "The active voice profile.", + "type": "string", + "default": "Eloquence" + } + }, + "HTML.Abbreviations": { + "schema": { + "title": "Abbreviations", + "description": "Whether to expand abbreviations. If this is enabled, abbreviations with a title attribute will have their title read instead of the on screen text.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "HTML.Acronyms": { + "schema": { + "title": "Acronyms", + "description": "If this is enabled, acronyms with a title attribute will have their title read instead of the on screen text.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "HTML.BlockQuoteIndication": { + "schema": { + "title": "Block Quote Indication", + "description": "Whether to indicate HTML block quotations.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "HTML.FormsModeAutoOff": { + "schema": { + "title": "Forms Mode Auto Off", + "description": "Whether or not Forms Mode should be automatically turned off when the current page is updated.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "Leave Forms Mode on", + "Turn Forms Mode off automatically" + ], + "default": 1 + } + }, + "HTML.ListIndication": { + "schema": { + "title": "List Indication", + "description": "Whether to announce lists with the item count and nesting level.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "HTML.TblMaxCellTextLength": { + // "schema": { + // "title": "Table Maximum Cell Text Length", + // "description": "A cell is considered a valid data cell if it contains upto this number of characters.", + // "type": "integer", + // "default": 250 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "HTML.TblMinCellTextLength": { + // "schema": { + // "title": "Table Minimum Cell Text Length", + // "description": "A cell is considered a valid data cell if it contains at least this number of characters.", + // "type": "integer", + // "default": 1 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "HTML.TblMinTextColumns": { + // "schema": { + // "title": "Table Minimum Text Columns", + // "description": "This option specifies the minimum number of columns which must contain text in order for the table to be considered a data table.", + // "type": "integer", + // "default": 2 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "HTML.TblMinTextRows": { + // "schema": { + // "title": "Table Minimum Text Rows", + // "description": "This option specifies the minimum number of rows which must contain text in order for the table to be considered a data table.", + // "type": "integer", + // "default": 2 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "HTML.TblMinValidDataRows": { + // "schema": { + // "title": "Table Minimum Valid Data Rows", + // "description": "Next options used for configuring how data tables are detected This option specifies the minimum number of valid data rows that a table must contain in order for it to be considered a data table.", + // "type": "integer", + // "default": 2 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "HTML.TblValidRowThreshold": { + // "schema": { + // "title": "Table Valid Row Threshold", + // "description": "This option specifies the minimum number of cells that a row must contain in order for it to be considered a valid data row.", + // "type": "integer", + // "default": 2 + // } + // }, + "HTML.UseLegacyIESupport": { + "schema": { + "title": "Use Legacy IE Support", + "description": "Whether or not to use legacy IE support instead of the default FS Dom Server support.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.AllCapsIndicator": { + "schema": { + "title": "AllCapsIndicator", + "description": "The text to read before text that appears in all capital letters.", + "type": "string", + "default": "all cap" + } + }, + // This does not seem safe to update, as we have no idea which displays are available on a given machine. + // "Options.BrailleDisplay": { + // "schema": { + // "title": "Braille Display", + // "description": "The braille display to use.", + // "type": "string", + // "default": "Braille1" + // } + // }, + "Options.CapIndicator": { + "schema": { + "title": "Cap Indicator", + "description": "The text to read before a capital letter.", + "type": "string", + "default": "cap" + } + }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Options.CaretDetect": { + // "schema": { + // "title": "Caret Detect", + // "description": "How many cursor blinks are used to locate the caret.", + // "type": "integer", + // "default": 1, + // "minimum": 1 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Options.CaretDetectTimeOut": { + // "schema": { + // "title": "Caret Detect Timeout", + // "description": "The number of milliseconds before JAWS for Windows will give up looking for the caret.", + // "type": "integer", + // "default": 250, + // "minimum": 0 + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "OSM.CaretTimeOut": { + // "schema": { + // "title": "Caret Time Out", + // "description": "The time in milliseconds to wait before redrawing the caret.", + // "type": "integer", + // "default": 1000 + // } + // }, + "Options.Case": { + "schema": { + "title": "Case", + "description": "Whether to indicate the case of spoken text with a change of inflection.", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + // There's not even anything in the JAWS documentation for this. Disabled for now. + // "Options.DosScreenReader": { + // "schema": { + // "title": "Dos Screen Reader", + // "description": "", + // "type": "", + // "default": 0, + // "enum": [], + // "enumLabels": [] + // } + // }, + "Options.IndicateAttributesInDialogsAndMenus": { + "schema": { + "title": "Indicate Attributes on Dialogs and Menus", + "description": "Whether to announce attributes in dialogs and menus if the scheme allows for it.", + "default": 0, + // TODO: All of these 0,1 examples are a firm use case for an application-specific transform mechanism so that our JSON payloads can use true/false. + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.JAWSPunctuationEnabled": { + "schema": { + "title": "JAWS Punctuation Enabled", + "description": "Which punctuation marks to speak.", + "default": 2, + "enum": [ + 0, + 1, + 2, + 3 + ], + "enumLabels": [ + "None", + "Some", + "Most", + "All" + ] + } + }, + "Options.LeftShiftSkipBack": { + "schema": { + "title": "Left Shift Skip Back", + "description": "How the shift keys should behave in \"Say All\" mode.", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "during SayAll, right shift will skip back and left shift will skip forward", + "during SayAll, left shift will skip back and right shift will skip forward" + ] + } + }, + // Can't find any documentation about this. + // "Options.ListBoxCheckBoxes": { + // "schema": { + // "title": "List Box Check Boxes", + // "description": "0 off, 1 non-ownerdrawn listboxes, 2 also check listboxes with LBS_OWNERDRAWVARIABLE or LBS_OWNERDRAWFIXED window style.", + // "type": "number", + // "default": 0, + // "enum": [ + // 0, + // 1, + // 2 + // ], + // "enumLabels": [ + // "off", + // "non-ownerdrawn listboxes", + // "also check listboxes with LBS_OWNERDRAWVARIABLE or LBS_OWNERDRAWFIXED window style." + // ] + // } + // }, + "Options.MigrationWizardDisplayed": { + "schema": { + "title": "Migration Wizard Displayed", + "description": "Whether or not to display the migration wizard on startup.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + }, + "Options.ProcessToolTipEvent": { + "schema": { + "title": "Process Tooltip Event", + "description": "Whether or not to process tooltip events.", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "on", + "off" + ] + } + }, + "Options.ReadingInterrupt": { + "schema": { + "title": "Reading Interrupt", + "description": "Whether to interrupt speech when a key that is bound to a macro is pressed.", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "Options.SkimReadingIndication": { + "schema": { + "title": "Skim Reading Indication", + "description": "Whether or not to indicate (via a beep) when skimming over text units for which the regular expression returns FALSE.", + "default": 1, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "beep every 20 units skimmed over" + ] + } + }, + "Options.SkipILM": { + "schema": { + "title": "Skip ILM", + "description": "Whether to show the licensing manager on startup when running in 40-minute evaluation mode.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "display the license manager on startup", + "do not display the license manager on startup" + ] + } + }, + // This is not meaningful unless it's coupled with an application for context. Disabled for now. + // "Options.SleepMode": { + // "schema": { + // "title": "SleepMode", + // "description": "Whether to put JAWS to sleep. In the context of a specific application, this preference can be set to disable JAWS within that application.", + // "type": "number", + // "default": 0, + // "enum": [ + // 0, + // 1 + // ], + // "enumLabels": [ + // "off", + // "on" + // ] + // } + // }, + "Options.SpeechHistory": { + "schema": { + "title": "Enable Speech History", + "description": "Whether to read through the 50-item speech history when \"insert+spacebar\" and then \"h\" are pressed.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + // TODO: Verify. Look at the INI file and suggest defaults? + "Options.Synthesizer": { + "schema": { + "title": "Synthesizer", + "description": "The synthesizer to use/ Available synthesizers are listed in JfW.INI", + "enum": ["Synth1"], + "enumLabels": ["Default speech synthesizer"], + "default": "Synth1" + } + }, + "Options.TetherJawsToPC": { + "schema": { + "title": "Tether Jaws to PC", + "description": "Whether or not to tether the JAWS cursor to the PC cursor. If this is set to 'on', whenever the Pc cursor moves, the Jaws cursor will follow it. If this is set to 'off', the JAWS cursor is indifferent to PC cursor movements (this is the default).", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + // Can't find information in the help file or default settings. Disabled for now. + // "Options.TreeViewCheckBoxes": { + // "schema": { + // "title": "Tree View Check Boxes", + // "description": "", + // "type": "number", + // "default": 1, + // "enum": [ + // 0, + // 1 + // ], + // "enumLabels": [ + // "off", + // "on" + // ] + // } + // }, + "Options.UseVirtualInfoInFormsMode": { + "schema": { + "title": "Use Virtual Info in Forms Mode", + "description": "Whether to use information from the virtual HTML area for prompts in Forms mode. Warning: this will not always result in accurate information, particularly on pages where there are errors in the HTML coding which result in forms mode and virtual mode being out of synchronization.", + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + } + // No documentation or onscreen option. Disabled for now. + // "Options.UseWindowsKeyboardHook": { + // "schema": { + // "title": "Use Windows Keyboard Hook", + // "description": "", + // "type": "number", + // "default": 0, + // "enum": [ + // 0, + // 1 + // ], + // "enumLabels": [ + // "off", + // "on" + // ] + // } + // }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Options.VIRTUALMSAAREFRESH": { + // "schema": { + // "title": "Virtual MSAA Refresh", + // "description": "How often (in milliseconds) should an embedded ActiveX control cause the Virtual HTML area to be updated when receiving rapid ValueChange events from MSAA. Set to -1 to disable refreshes.", + // "type": "integer", + // "default": 0, + // "minimum": -1 + // } + // } + }, + "capabilitiesTransformations": { + "Options\\.confirmWhenExitingJAWS": { + "literalValue": 0 + }, + "Options\\.SayAllIndicateCaps": { + "transform": { + "type": "valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/announceCapitals", + "match": [ + { + "inputValue": true, + "outputValue": 1 + }, + { + "inputValue": false, + "outputValue": 0 + } + ], + "noMatch": { + "outputValue": 0 + } + } + }, + "Options\\.TypingEcho": { + // (1=keyEcho, 2=wordEcho, 3=key&wordEcho) + "transform": { + "type": "fluid.transforms.condition", + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/keyEcho", + "rightPath": "http://registry\\.gpii\\.net/common/wordEcho", + "operator": "&&" + } + }, + "true": 3, + "false": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/common/keyEcho", + "true": 1, + "false": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/common/wordEcho", + "true": 2 + } + } + } + } + } + }, + "Options\\.SayAllMode": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/readingUnit", + "match": { + "line": 0, + "sentence": 1, + "paragraph": 2 + } + } + }, + "Braille\\.BrailleMode": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/brailleMode", + "match": { + "line": 0, + "structured": 1, + "speechHistory": 2 + } + } + }, + "Options\\.SayAllIgnoreShiftKeys": { + "transform": { + "type": "valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/stickyKeys", + "match": [ + { + "inputValue": true, + "outputValue": 1 + }, + { + "inputValue": false, + "outputValue": 0 + } + ], + "noMatch": { + "outputValue": 0 + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/announceCapitals": { + "transform": { + "type": "valueMapper", + "defaultInputPath": "Options\\.SayAllIndicateCaps", + "match": [ + { + "inputValue": 0, + "outputValue": false + }, + { + "inputValue": 1, + "outputValue": true + } + ], + "noMatch": { + "outputValue": false + } + } + }, + "http://registry\\.gpii\\.net/common/keyEcho": { + "transform": { + "type": "valueMapper", + "defaultInputPath": "Options\\.TypingEcho", + // (1=keyEcho, 2=wordEcho, 3=key&wordEcho) + "match": [ + { + "inputValue": 1, + "outputValue": true + }, + { + "inputValue": 3, + "outputValue": true + } + ], + "noMatch": { + "outputValue": false + } + } + }, + "http://registry\\.gpii\\.net/common/wordEcho": { + "transform": { + "type": "valueMapper", + "defaultInputPath": "Options\\.TypingEcho", + // (1=keyEcho, 2=wordEcho, 3=key&wordEcho) + "match": [ + { + "inputValue": 2, + "outputValue": true + }, + { + "inputValue": 3, + "outputValue": true + } + ], + "noMatch": { + "outputValue": false + } + } + }, + "http://registry\\.gpii\\.net/common/readingUnit": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "Options\\.SayAllMode", + "match": [ + { + "inputValue": 0, + "outputValue": "line" + }, + { + "inputValue": 1, + "outputValue": "sentence" + }, + { + "inputValue": 2, + "outputValue": "paragraph" + } + ] + } + }, + "http://registry\\.gpii\\.net/common/brailleMode": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "Braille\\.BrailleMode", + "match": [ + { + "inputValue": 0, + "outputValue": "line" + }, + { + "inputValue": 1, + "outputValue": "structured" + }, + { + "inputValue": 2, + "outputValue": "speechHistory" + } + ] + } + } + } + }, + "configuration2": { + "type": "gpii.settingsHandlers.JAWSSettingsHandler", + "liveness": "manualRestart", + "options": { + // TODO: Create a base grade for JAWS and version-specific variations. Make JAWS 2019 the default. Restart version-detection discussions and "capability" discussions. + "defaultSettingsFilePath": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", + "voiceProfilesDirPath": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\VoiceProfiles" + }, + "supportedSettings": { + // -------- Options ---------- + + "Options.PrimarySynthesizer": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Primary synthesizer", + "description": "The synthesizer to use for the voice profile.", + "enum": [ + "eloq", + "MSMobile", + "SAPI 5X", + "SAPI 5X 64" + ], + "enumLabels": [ + "eloq", + "MSMobile", + "SAPI 5X", + "SAPI 5X 64" + ], + "default": "eloq" + } + }, + "Options.PrimaryLanguage": { + "schema": { + "title": "Primary language", + "description": "The primary language used for the voice profile.", + "enum": [ + "DEU", + "ENG", + "ENU", + "ESM", + "ESN", + "FIN", + "FRA", + "FRC", + "ITA", + "PTB" + ], + "enumLabels": [ + "DEU", + "ENG", + "ENU", + "ESM", + "ESN", + "FIN", + "FRA", + "FRC", + "ITA", + "PTB" + ], + "default": "ENU" + } + }, + // GPII-4226 Leaving out for now due to lack of either a minimum or maximum value. + // "Options.Factory": { + // "schema": { + // // NOTE: Setting purpose isn't known and should remain private + // "title": "Factory", + // "description": "Unknown.", + // "type": "integer", + // "default": 0 + // } + // }, + + // ======================================================= + + // -------- German - GLOBAL ---------- + + "DEU-Global.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "None" + } + }, + "DEU-Global.Pitch": { + "schema": { + "title": "Pitch", + "description": "The pitch to use for all types of announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "DEU-Global.Punctuation": { + "schema": { + "title": "Punctuation", + "description": "How much punctuation to announce for all types of announcements.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 2 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "DEU-Global.Rate": { + // "schema": { + // // NOTE: This setting should only be used through the exposed application specific + // // term, this setting itself should not be modified by the user. + // "title": "Rate", + // "description": "Speech rate for all announcements (scale varies by synthesizer).", + // "type": "integer" + // } + // }, + "DEU-Global.SpellRateDelta": { + "schema": { + "title": "The spell rate delta", + "description": "The ammount speech rates decresases when spelling words or lines.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": -25 + } + }, + "DEU-Global.SynthLangString": { + "schema": { + "title": "Synth Language", + "description": "The language to use for all announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "DEU-Global.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "DEU-Global.Volume": { + "schema": { + "title": "Global Volume", + "description": "The volume for all announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "DEU-Global.VoiceName": { + "schema": { + "title": "Global voice name", + "description": "The voice name to use by default.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hedda", + "Microsoft Katja", + "Microsoft Stefan", + // If synth is SAPI 5X 64 + "Microsoft Hedda Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hedda", + "Microsoft Katja", + "Microsoft Stefan", + // If synth is SAPI 5X 64 + "Microsoft Hedda Desktop" + ], + "default": "Reed" + } + }, + + // ---------- ENGLISH UNITED STATES - GLOBAL ---------- + + "ENU-Global.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "None" + } + }, + "ENU-Global.Pitch": { + "schema": { + "title": "Pitch", + "description": "The pitch to use for all types of announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ENU-Global.Punctuation": { + "schema": { + "title": "Punctuation", + "description": "How much punctuation to announce for all types of announcements.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 2 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ENU-Global.Rate": { + // "schema": { + // // NOTE: This setting should only be used through the exposed application specific + // // term, this setting itself should not be modified by the user. + // "title": "Rate", + // "description": "Speech rate for all announcements (scale varies by synthesizer).", + // "type": "integer" + // } + // }, + "ENU-Global.SpellRateDelta": { + "schema": { + "title": "The spell rate delta", + "description": "The ammount speech rates decresases when spelling words or lines.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": -25 + } + }, + "ENU-Global.SynthLangString": { + "schema": { + "title": "Synth Language", + "description": "The language to use for all announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ENU-Global.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ENU-Global.Volume": { + "schema": { + "title": "Global Volume", + "description": "The volume for all announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ENU-Global.VoiceName": { + "schema": { + "title": "Global voice name", + "description": "The voice name to use by default.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "default": "Reed" + } + }, + + // -------- ENGLISH (United Kingdom) - GLOBAL ---------- + + "ENG-Global.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "None" + } + }, + "ENG-Global.Pitch": { + "schema": { + "title": "Pitch", + "description": "The pitch to use for all types of announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ENG-Global.Punctuation": { + "schema": { + "title": "Punctuation", + "description": "How much punctuation to announce for all types of announcements.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 2 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ENG-Global.Rate": { + // "schema": { + // // NOTE: This setting should only be used through the exposed application specific + // // term, this setting itself should not be modified by the user. + // "title": "Rate", + // "description": "Speech rate for all announcements (scale varies by synthesizer).", + // "type": "integer" + // } + // }, + "ENG-Global.SpellRateDelta": { + "schema": { + "title": "The spell rate delta", + "description": "The ammount speech rates decresases when spelling words or lines.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": -25 + } + }, + "ENG-Global.SynthLangString": { + "schema": { + "title": "Synth Language", + "description": "The language to use for all announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ENG-Global.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ENG-Global.Volume": { + "schema": { + "title": "Global Volume", + "description": "The volume for all announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ENG-Global.VoiceName": { + "schema": { + "title": "Global voice name", + "description": "The voice name to use by default.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "default": "Reed" + } + }, + + // -------- SPANISH (Mexico) - GLOBAL ---------- + + "ESM-Global.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "None" + } + }, + "ESM-Global.Pitch": { + "schema": { + "title": "Pitch", + "description": "The pitch to use for all types of announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ESM-Global.Punctuation": { + "schema": { + "title": "Punctuation", + "description": "How much punctuation to announce for all types of announcements.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 2 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ESM-Global.Rate": { + // "schema": { + // // NOTE: This setting should only be used through the exposed application specific + // // term, this setting itself should not be modified by the user. + // "title": "Rate", + // "description": "Speech rate for all announcements (scale varies by synthesizer).", + // "type": "integer" + // } + // }, + "ESM-Global.SpellRateDelta": { + "schema": { + "title": "The spell rate delta", + "description": "The ammount speech rates decresases when spelling words or lines.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": -25 + } + }, + "ESM-Global.SynthLangString": { + "schema": { + "title": "Synth Language", + "description": "The language to use for all announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ESM-Global.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ESM-Global.Volume": { + "schema": { + "title": "Global Volume", + "description": "The volume for all announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ESM-Global.VoiceName": { + "schema": { + "title": "Global voice name", + "description": "The voice name to use by default.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "default": "Reed" + } + }, + + // -------- SPANISH (Spain) - GLOBAL ---------- + + "ESN-Global.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "None" + } + }, + "ESN-Global.Pitch": { + "schema": { + "title": "Pitch", + "description": "The pitch to use for all types of announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ESN-Global.Punctuation": { + "schema": { + "title": "Punctuation", + "description": "How much punctuation to announce for all types of announcements.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 2 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ESN-Global.Rate": { + // "schema": { + // // NOTE: This setting should only be used through the exposed application specific + // // term, this setting itself should not be modified by the user. + // "title": "Rate", + // "description": "Speech rate for all announcements (scale varies by synthesizer).", + // "type": "integer" + // } + // }, + "ESN-Global.SpellRateDelta": { + "schema": { + "title": "The spell rate delta", + "description": "The ammount speech rates decresases when spelling words or lines.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": -25 + } + }, + "ESN-Global.SynthLangString": { + "schema": { + "title": "Synth Language", + "description": "The language to use for all announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ESN-Global.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ESN-Global.Volume": { + "schema": { + "title": "Global Volume", + "description": "The volume for all announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ESN-Global.VoiceName": { + "schema": { + "title": "Global voice name", + "description": "The voice name to use by default.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // "Microsoft Helena Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // "Microsoft Helena Desktop" + ], + "default": "Reed" + } + }, + + // -------- Finnish - GLOBAL ---------- + + "FIN-Global.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "None" + } + }, + "FIN-Global.Pitch": { + "schema": { + "title": "Pitch", + "description": "The pitch to use for all types of announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "FIN-Global.Punctuation": { + "schema": { + "title": "Punctuation", + "description": "How much punctuation to announce for all types of announcements.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 2 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "FIN-Global.Rate": { + // "schema": { + // // NOTE: This setting should only be used through the exposed application specific + // // term, this setting itself should not be modified by the user. + // "title": "Rate", + // "description": "Speech rate for all announcements (scale varies by synthesizer).", + // "type": "integer" + // } + // }, + "FIN-Global.SpellRateDelta": { + "schema": { + "title": "The spell rate delta", + "description": "The ammount speech rates decresases when spelling words or lines.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": -25 + } + }, + "FIN-Global.SynthLangString": { + "schema": { + "title": "Synth Language", + "description": "The language to use for all announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "FIN-Global.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "FIN-Global.Volume": { + "schema": { + "title": "Global Volume", + "description": "The volume for all announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "FIN-Global.VoiceName": { + "schema": { + "title": "Global voice name", + "description": "The voice name to use by default.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa" + ], + "default": "Reed" + } + }, + + // -------- French (France) - GLOBAL ---------- + + "FRA-Global.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "None" + } + }, + "FRA-Global.Pitch": { + "schema": { + "title": "Pitch", + "description": "The pitch to use for all types of announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "FRA-Global.Punctuation": { + "schema": { + "title": "Punctuation", + "description": "How much punctuation to announce for all types of announcements.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 2 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "FRA-Global.Rate": { + // "schema": { + // // NOTE: This setting should only be used through the exposed application specific + // // term, this setting itself should not be modified by the user. + // "title": "Rate", + // "description": "Speech rate for all announcements (scale varies by synthesizer).", + // "type": "integer" + // } + // }, + "FRA-Global.SpellRateDelta": { + "schema": { + "title": "The spell rate delta", + "description": "The ammount speech rates decresases when spelling words or lines.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": -25 + } + }, + "FRA-Global.SynthLangString": { + "schema": { + "title": "Synth Language", + "description": "The language to use for all announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "FRA-Global.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "FRA-Global.Volume": { + "schema": { + "title": "Global Volume", + "description": "The volume for all announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "FRA-Global.VoiceName": { + "schema": { + "title": "Global voice name", + "description": "The voice name to use by default.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "default": "Reed" + } + }, + + // -------- French (Canada) - GLOBAL ---------- + + "FRC-Global.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "None" + } + }, + "FRC-Global.Pitch": { + "schema": { + "title": "Pitch", + "description": "The pitch to use for all types of announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "FRC-Global.Punctuation": { + "schema": { + "title": "Punctuation", + "description": "How much punctuation to announce for all types of announcements.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 2 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "FRC-Global.Rate": { + // "schema": { + // // NOTE: This setting should only be used through the exposed application specific + // // term, this setting itself should not be modified by the user. + // "title": "Rate", + // "description": "Speech rate for all announcements (scale varies by synthesizer).", + // "type": "integer" + // } + // }, + "FRC-Global.SpellRateDelta": { + "schema": { + "title": "The spell rate delta", + "description": "The ammount speech rates decresases when spelling words or lines.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": -25 + } + }, + "FRC-Global.SynthLangString": { + "schema": { + "title": "Synth Language", + "description": "The language to use for all announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "FRC-Global.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "FRC-Global.Volume": { + "schema": { + "title": "Global Volume", + "description": "The volume for all announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "FRC-Global.VoiceName": { + "schema": { + "title": "Global voice name", + "description": "The voice name to use by default.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "default": "Reed" + } + }, + + // -------- Italian - GLOBAL ---------- + + "ITA-Global.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "JAWS Cursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ITA-Global.Pitch": { + "schema": { + "title": "JAWS Cursor Pitch", + "description": "The pitch to use for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ITA-Global.Punctuation": { + "schema": { + "title": "JAWS Cursor Punctuation", + "description": "How much punctuation to announce for the JAWS cursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ITA-Global.Rate": { + // "schema": { + // "title": "JAWS Cursor Rate", + // "description": "Speech rate for announcements related to the JAWS cursor (scale varies by voice).", + // "type": "integer" + // } + // }, + "ITA-Global.SynthLangString": { + "schema": { + "title": "JAWS Cursor Language", + "description": "The language to use for announcements related to the JAWS cursor.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ITA-Global.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ITA-Global.Volume": { + "schema": { + "title": "JAWS Cursor Volume", + "description": "The volume for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ITA-Global.VoiceName": { + "schema": { + "title": "Global voice name", + "description": "The voice name to use by default.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Cosmino", + "Microsoft Elsa", + // If synth is SAPI + "Microsoft Elsa Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Cosmino", + "Microsoft Elsa", + // If synth is SAPI + "Microsoft Elsa Desktop" + ], + "default": "Reed" + } + }, + + // -------- Brazil - GLOBAL ---------- + + "PTB-Global.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "None" + } + }, + "PTB-Global.Pitch": { + "schema": { + "title": "Pitch", + "description": "The pitch to use for all types of announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "PTB-Global.Punctuation": { + "schema": { + "title": "Punctuation", + "description": "How much punctuation to announce for all types of announcements.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 2 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "PTB-Global.Rate": { + // "schema": { + // // NOTE: This setting should only be used through the exposed application specific + // // term, this setting itself should not be modified by the user. + // "title": "Rate", + // "description": "Speech rate for all announcements (scale varies by synthesizer).", + // "type": "integer" + // } + // }, + "PTB-Global.SpellRateDelta": { + "schema": { + "title": "The spell rate delta", + "description": "The ammount speech rates decresases when spelling words or lines.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": -25 + } + }, + "PTB-Global.SynthLangString": { + "schema": { + "title": "Synth Language", + "description": "The language to use for all announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "PTB-Global.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "PTB-Global.Volume": { + "schema": { + "title": "Global Volume", + "description": "The volume for all announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "PTB-Global.VoiceName": { + "schema": { + "title": "Global voice name", + "description": "The voice name to use by default.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Daniel", + "Microsoft Maria", + // If synth is SAPI 5X 64 + "Microsoft Maria Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Daniel", + "Microsoft Maria", + // If synth is SAPI 5X 64 + "Microsoft Maria Desktop" + ], + "default": "Reed" + } + }, + + // ======================================================= + + // -------- German - JAWCURSOR ---------- + + "DEU-JAWSCursor.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "JAWS Cursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "DEU-JAWSCursor.Pitch": { + "schema": { + "title": "JAWS Cursor Pitch", + "description": "The pitch to use for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "DEU-JAWSCursor.Punctuation": { + "schema": { + "title": "JAWS Cursor Punctuation", + "description": "How much punctuation to announce for the JAWS cursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "DEU-JAWSCursor.Rate": { + // "schema": { + // "title": "JAWS Cursor Rate", + // "description": "Speech rate for announcements related to the JAWS cursor (scale varies by voice).", + // "type": "integer" + // } + // }, + "DEU-JAWSCursor.SynthLangString": { + "schema": { + "title": "JAWS Cursor Language", + "description": "The language to use for announcements related to the JAWS cursor.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "DEU-JAWSCursor.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "DEU-JAWSCursor.Volume": { + "schema": { + "title": "JAWS Cursor Volume", + "description": "The volume for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "DEU-JAWSCursor.VoiceName": { + "schema": { + "title": "Global voice name", + "description": "The voice name to use by default.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hedda", + "Microsoft Katja", + "Microsoft Stefan", + // If synth is SAPI 5X 64 + "Microsoft Hedda Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hedda", + "Microsoft Katja", + "Microsoft Stefan", + // If synth is SAPI 5X 64 + "Microsoft Hedda Desktop" + ], + "default": "Reed" + } + }, + + // -------- ENGLISH (UNITED STATES) - JAWCURSOR ---------- + + "ENU-JAWSCursor.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "JAWS Cursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ENU-JAWSCursor.Pitch": { + "schema": { + "title": "JAWS Cursor Pitch", + "description": "The pitch to use for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ENU-JAWSCursor.Punctuation": { + "schema": { + "title": "JAWS Cursor Punctuation", + "description": "How much punctuation to announce for the JAWS cursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ENU-JAWSCursor.Rate": { + // "schema": { + // "title": "JAWS Cursor Rate", + // "description": "Speech rate for announcements related to the JAWS cursor (scale varies by voice).", + // "type": "integer" + // } + // }, + "ENU-JAWSCursor.SynthLangString": { + "schema": { + "title": "JAWS Cursor Language", + "description": "The language to use for announcements related to the JAWS cursor.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ENU-JAWSCursor.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ENU-JAWSCursor.Volume": { + "schema": { + "title": "JAWS Cursor Volume", + "description": "The volume for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ENU-JAWSCursor.VoiceName": { + "schema": { + "title": "JAWS cursor voice name", + "description": "The voice name to use for JAWS cursor.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "default": "Reed" + } + }, + + // -------- ENGLISH (United Kingdom) - JAWCURSOR ---------- + + "ENG-JAWSCursor.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "JAWS Cursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ENG-JAWSCursor.Pitch": { + "schema": { + "title": "JAWS Cursor Pitch", + "description": "The pitch to use for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ENG-JAWSCursor.Punctuation": { + "schema": { + "title": "JAWS Cursor Punctuation", + "description": "How much punctuation to announce for the JAWS cursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ENG-JAWSCursor.Rate": { + // "schema": { + // "title": "JAWS Cursor Rate", + // "description": "Speech rate for announcements related to the JAWS cursor (scale varies by voice).", + // "type": "integer" + // } + // }, + "ENG-JAWSCursor.SynthLangString": { + "schema": { + "title": "JAWS Cursor Language", + "description": "The language to use for announcements related to the JAWS cursor.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ENG-JAWSCursor.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ENG-JAWSCursor.Volume": { + "schema": { + "title": "JAWS Cursor Volume", + "description": "The volume for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ENG-JAWSCursor.VoiceName": { + "schema": { + "title": "JAWS cursor voice name", + "description": "The voice name to use for JAWS cursor.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "default": "Reed" + } + }, + + // -------- SPANISH (Mexico) - JAWCURSOR ---------- + + "ESM-JAWSCursor.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "JAWS Cursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ESM-JAWSCursor.Pitch": { + "schema": { + "title": "JAWS Cursor Pitch", + "description": "The pitch to use for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ESM-JAWSCursor.Punctuation": { + "schema": { + "title": "JAWS Cursor Punctuation", + "description": "How much punctuation to announce for the JAWS cursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ESM-JAWSCursor.Rate": { + // "schema": { + // "title": "JAWS Cursor Rate", + // "description": "Speech rate for announcements related to the JAWS cursor (scale varies by voice).", + // "type": "integer" + // } + // }, + "ESM-JAWSCursor.SynthLangString": { + "schema": { + "title": "JAWS Cursor Language", + "description": "The language to use for announcements related to the JAWS cursor.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ESM-JAWSCursor.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ESM-JAWSCursor.Volume": { + "schema": { + "title": "JAWS Cursor Volume", + "description": "The volume for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ESM-JAWSCursor.VoiceName": { + "schema": { + "title": "JAWS cursor voice name", + "description": "The voice name to use for JAWS cursor.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "default": "Reed" + } + }, + + // -------- SPANISH (Spain) - JAWCURSOR ---------- + + "ESN-JAWSCursor.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "JAWS Cursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ESN-JAWSCursor.Pitch": { + "schema": { + "title": "JAWS Cursor Pitch", + "description": "The pitch to use for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ESN-JAWSCursor.Punctuation": { + "schema": { + "title": "JAWS Cursor Punctuation", + "description": "How much punctuation to announce for the JAWS cursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ESN-JAWSCursor.Rate": { + // "schema": { + // "title": "JAWS Cursor Rate", + // "description": "Speech rate for announcements related to the JAWS cursor (scale varies by voice).", + // "type": "integer" + // } + // }, + "ESN-JAWSCursor.SynthLangString": { + "schema": { + "title": "JAWS Cursor Language", + "description": "The language to use for announcements related to the JAWS cursor.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ESN-JAWSCursor.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ESN-JAWSCursor.Volume": { + "schema": { + "title": "JAWS Cursor Volume", + "description": "The volume for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ESN-JAWSCursor.VoiceName": { + "schema": { + "title": "JAWS cursor voice name", + "description": "The voice name to use for JAWS cursor.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "default": "Reed" + } + }, + + // -------- Finnish - JAWCURSOR ---------- + + "FIN-JAWSCursor.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "JAWS Cursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "FIN-JAWSCursor.Pitch": { + "schema": { + "title": "JAWS Cursor Pitch", + "description": "The pitch to use for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "FIN-JAWSCursor.Punctuation": { + "schema": { + "title": "JAWS Cursor Punctuation", + "description": "How much punctuation to announce for the JAWS cursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "FIN-JAWSCursor.Rate": { + // "schema": { + // "title": "JAWS Cursor Rate", + // "description": "Speech rate for announcements related to the JAWS cursor (scale varies by voice).", + // "type": "integer" + // } + // }, + "FIN-JAWSCursor.SynthLangString": { + "schema": { + "title": "JAWS Cursor Language", + "description": "The language to use for announcements related to the JAWS cursor.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "FIN-JAWSCursor.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "FIN-JAWSCursor.Volume": { + "schema": { + "title": "JAWS Cursor Volume", + "description": "The volume for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "FIN-JAWSCursor.VoiceName": { + "schema": { + "title": "JAWS cursor voice name", + "description": "The voice name to use for JAWS cursor.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa" + ], + "default": "Reed" + } + }, + + // -------- French (France) - JAWCURSOR ---------- + + "FRA-JAWSCursor.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "JAWS Cursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "FRA-JAWSCursor.Pitch": { + "schema": { + "title": "JAWS Cursor Pitch", + "description": "The pitch to use for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "FRA-JAWSCursor.Punctuation": { + "schema": { + "title": "JAWS Cursor Punctuation", + "description": "How much punctuation to announce for the JAWS cursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "FRA-JAWSCursor.Rate": { + // "schema": { + // "title": "JAWS Cursor Rate", + // "description": "Speech rate for announcements related to the JAWS cursor (scale varies by voice).", + // "type": "integer" + // } + // }, + "FRA-JAWSCursor.SynthLangString": { + "schema": { + "title": "JAWS Cursor Language", + "description": "The language to use for announcements related to the JAWS cursor.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "FRA-JAWSCursor.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "FRA-JAWSCursor.Volume": { + "schema": { + "title": "JAWS Cursor Volume", + "description": "The volume for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "FRA-JAWSCursor.VoiceName": { + "schema": { + "title": "JAWS cursor voice name", + "description": "The voice name to use for JAWS cursor.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "default": "Reed" + } + }, + + // -------- French (Canada) - JAWCURSOR ---------- + + "FRC-JAWSCursor.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "JAWS Cursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "FRC-JAWSCursor.Pitch": { + "schema": { + "title": "JAWS Cursor Pitch", + "description": "The pitch to use for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "FRC-JAWSCursor.Punctuation": { + "schema": { + "title": "JAWS Cursor Punctuation", + "description": "How much punctuation to announce for the JAWS cursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "FRC-JAWSCursor.Rate": { + // "schema": { + // "title": "JAWS Cursor Rate", + // "description": "Speech rate for announcements related to the JAWS cursor (scale varies by voice).", + // "type": "integer" + // } + // }, + "FRC-JAWSCursor.SynthLangString": { + "schema": { + "title": "JAWS Cursor Language", + "description": "The language to use for announcements related to the JAWS cursor.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "FRC-JAWSCursor.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "FRC-JAWSCursor.Volume": { + "schema": { + "title": "JAWS Cursor Volume", + "description": "The volume for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "FRC-JAWSCursor.VoiceName": { + "schema": { + "title": "JAWS cursor voice name", + "description": "The voice name to use for JAWS cursor.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "default": "Reed" + } + }, + + // -------- Italian - JAWCURSOR ---------- + + "ITA-JAWSCursor.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "JAWS Cursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ITA-JAWSCursor.Pitch": { + "schema": { + "title": "JAWS Cursor Pitch", + "description": "The pitch to use for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ITA-JAWSCursor.Punctuation": { + "schema": { + "title": "JAWS Cursor Punctuation", + "description": "How much punctuation to announce for the JAWS cursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ITA-JAWSCursor.Rate": { + // "schema": { + // "title": "JAWS Cursor Rate", + // "description": "Speech rate for announcements related to the JAWS cursor (scale varies by voice).", + // "type": "integer" + // } + // }, + "ITA-JAWSCursor.SynthLangString": { + "schema": { + "title": "JAWS Cursor Language", + "description": "The language to use for announcements related to the JAWS cursor.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ITA-JAWSCursor.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ITA-JAWSCursor.Volume": { + "schema": { + "title": "JAWS Cursor Volume", + "description": "The volume for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ITA-JAWSCursor.VoiceName": { + "schema": { + "title": "JAWS cursor voice name", + "description": "The voice name to use for JAWS cursor.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Cosmino", + "Microsoft Elsa", + // If synth is SAPI + "Microsoft Elsa Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Cosmino", + "Microsoft Elsa", + // If synth is SAPI + "Microsoft Elsa Desktop" + ], + "default": "Reed" + } + }, + + // -------- Brazil - JAWCURSOR ---------- + + "PTB-JAWSCursor.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "JAWS Cursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "PTB-JAWSCursor.Pitch": { + "schema": { + "title": "JAWS Cursor Pitch", + "description": "The pitch to use for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "PTB-JAWSCursor.Punctuation": { + "schema": { + "title": "JAWS Cursor Punctuation", + "description": "How much punctuation to announce for the JAWS cursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "PTB-JAWSCursor.Rate": { + // "schema": { + // "title": "JAWS Cursor Rate", + // "description": "Speech rate for announcements related to the JAWS cursor (scale varies by voice).", + // "type": "integer" + // } + // }, + "PTB-JAWSCursor.SynthLangString": { + "schema": { + "title": "JAWS Cursor Language", + "description": "The language to use for announcements related to the JAWS cursor.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "PTB-JAWSCursor.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "PTB-JAWSCursor.Volume": { + "schema": { + "title": "JAWS Cursor Volume", + "description": "The volume for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "PTB-JAWSCursor.VoiceName": { + "schema": { + "title": "JAWS cursor voice name", + "description": "The voice name to use for JAWS cursor.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Daniel", + "Microsoft Maria", + // If synth is SAPI 5X 64 + "Microsoft Maria Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Daniel", + "Microsoft Maria", + // If synth is SAPI 5X 64 + "Microsoft Maria Desktop" + ], + "default": "Reed" + } + }, + + // ======================================================= + + // -------- German - KEYBOARD ---------- + + "DEU-Keyboard.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Keyboard parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "DEU-Keyboard.Pitch": { + "schema": { + "title": "Keyboard pitch", + "description": "The pitch to use for announcements related to the keyboard.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "DEU-Keyboard.Punctuation": { + "schema": { + "title": "Keyboard punctuation", + "description": "How much punctuation to announce for the keyboard.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "DEU-Keyboard.Rate": { + // "schema": { + // "title": "Keyboard rate", + // "description": "Speech rate for announcements related to the keyboard (scale varies by synth).", + // "type": "integer" + // } + // }, + "DEU-Keyboard.SynthLangString": { + "schema": { + "title": "Keyboard language", + "description": "The language to use for announcements related to the keyboard.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "DEU-Keyboard.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "DEU-Keyboard.Volume": { + "schema": { + "title": "Keyboard volume", + "description": "The volume for announcements related to the keyboard.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "DEU-Keyboard.VoiceName": { + "schema": { + "title": "Keyboard voice name", + "description": "The voice name used for keyboard announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hedda", + "Microsoft Katja", + "Microsoft Stefan", + // If synth is SAPI 5X 64 + "Microsoft Hedda Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hedda", + "Microsoft Katja", + "Microsoft Stefan", + // If synth is SAPI 5X 64 + "Microsoft Hedda Desktop" + ], + "default": "Reed" + } + }, + + // -------- ENGLISH (UNITED STATES) - KEYBOARD ---------- + + "ENU-Keyboard.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Keyboard parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ENU-Keyboard.Pitch": { + "schema": { + "title": "Keyboard pitch", + "description": "The pitch to use for announcements related to the keyboard.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ENU-Keyboard.Punctuation": { + "schema": { + "title": "Keyboard punctuation", + "description": "How much punctuation to announce for the keyboard.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ENU-Keyboard.Rate": { + // "schema": { + // "title": "Keyboard rate", + // "description": "Speech rate for announcements related to the keyboard (scale varies by synth).", + // "type": "integer" + // } + // }, + "ENU-Keyboard.SynthLangString": { + "schema": { + "title": "Keyboard language", + "description": "The language to use for announcements related to the keyboard.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ENU-Keyboard.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ENU-Keyboard.Volume": { + "schema": { + "title": "Keyboard volume", + "description": "The volume for announcements related to the keyboard.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ENU-Keyboard.VoiceName": { + "schema": { + "title": "Keyboard voice name", + "description": "The voice name used for keyboard announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "default": "Reed" + } + }, + + // -------- ENGLISH (United Kingdom) - KEYBOARD ---------- + + "ENG-Keyboard.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Keyboard parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ENG-Keyboard.Pitch": { + "schema": { + "title": "Keyboard pitch", + "description": "The pitch to use for announcements related to the keyboard.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ENG-Keyboard.Punctuation": { + "schema": { + "title": "Keyboard punctuation", + "description": "How much punctuation to announce for the keyboard.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ENG-Keyboard.Rate": { + // "schema": { + // "title": "Keyboard rate", + // "description": "Speech rate for announcements related to the keyboard (scale varies by synth).", + // "type": "integer" + // } + // }, + "ENG-Keyboard.SynthLangString": { + "schema": { + "title": "Keyboard language", + "description": "The language to use for announcements related to the keyboard.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ENG-Keyboard.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ENG-Keyboard.Volume": { + "schema": { + "title": "Keyboard volume", + "description": "The volume for announcements related to the keyboard.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ENG-Keyboard.VoiceName": { + "schema": { + "title": "Keyboard voice name", + "description": "The voice name used for keyboard announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "default": "Reed" + } + }, + + // -------- SPANISH (Mexico) - KEYBOARD ---------- + + "ESM-Keyboard.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Keyboard parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ESM-Keyboard.Pitch": { + "schema": { + "title": "Keyboard pitch", + "description": "The pitch to use for announcements related to the keyboard.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ESM-Keyboard.Punctuation": { + "schema": { + "title": "Keyboard punctuation", + "description": "How much punctuation to announce for the keyboard.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ESM-Keyboard.Rate": { + // "schema": { + // "title": "Keyboard rate", + // "description": "Speech rate for announcements related to the keyboard (scale varies by synth).", + // "type": "integer" + // } + // }, + "ESM-Keyboard.SynthLangString": { + "schema": { + "title": "Keyboard language", + "description": "The language to use for announcements related to the keyboard.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ESM-Keyboard.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ESM-Keyboard.Volume": { + "schema": { + "title": "Keyboard volume", + "description": "The volume for announcements related to the keyboard.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ESM-Keyboard.VoiceName": { + "schema": { + "title": "Keyboard voice name", + "description": "The voice name used for keyboard announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + //- "Microsoft Helena Desktop" + ], + "default": "Reed" + } + }, + + // -------- SPANISH (Spain) - KEYBOARD ---------- + + "ESN-Keyboard.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Keyboard parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ESN-Keyboard.Pitch": { + "schema": { + "title": "Keyboard pitch", + "description": "The pitch to use for announcements related to the keyboard.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ESN-Keyboard.Punctuation": { + "schema": { + "title": "Keyboard punctuation", + "description": "How much punctuation to announce for the keyboard.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ESN-Keyboard.Rate": { + // "schema": { + // "title": "Keyboard rate", + // "description": "Speech rate for announcements related to the keyboard (scale varies by synth).", + // "type": "integer" + // } + // }, + "ESN-Keyboard.SynthLangString": { + "schema": { + "title": "Keyboard language", + "description": "The language to use for announcements related to the keyboard.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ESN-Keyboard.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ESN-Keyboard.Volume": { + "schema": { + "title": "Keyboard volume", + "description": "The volume for announcements related to the keyboard.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ESN-Keyboard.VoiceName": { + "schema": { + "title": "Keyboard voice name", + "description": "The voice name used for keyboard announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "default": "Reed" + } + }, + + // -------- Finnish - KEYBOARD ---------- + + "FIN-Keyboard.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Keyboard parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "FIN-Keyboard.Pitch": { + "schema": { + "title": "Keyboard pitch", + "description": "The pitch to use for announcements related to the keyboard.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "FIN-Keyboard.Punctuation": { + "schema": { + "title": "Keyboard punctuation", + "description": "How much punctuation to announce for the keyboard.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "FIN-Keyboard.Rate": { + // "schema": { + // "title": "Keyboard rate", + // "description": "Speech rate for announcements related to the keyboard (scale varies by synth).", + // "type": "integer" + // } + // }, + "FIN-Keyboard.SynthLangString": { + "schema": { + "title": "Keyboard language", + "description": "The language to use for announcements related to the keyboard.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "FIN-Keyboard.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "FIN-Keyboard.Volume": { + "schema": { + "title": "Keyboard volume", + "description": "The volume for announcements related to the keyboard.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "FIN-Keyboard.VoiceName": { + "schema": { + "title": "Keyboard voice name", + "description": "The voice name used for keyboard announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa" + ], + "default": "Reed" + } + }, + + // -------- French (France) - KEYBOARD ---------- + + "FRA-Keyboard.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Keyboard parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "FRA-Keyboard.Pitch": { + "schema": { + "title": "Keyboard pitch", + "description": "The pitch to use for announcements related to the keyboard.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "FRA-Keyboard.Punctuation": { + "schema": { + "title": "Keyboard punctuation", + "description": "How much punctuation to announce for the keyboard.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "FRA-Keyboard.Rate": { + // "schema": { + // "title": "Keyboard rate", + // "description": "Speech rate for announcements related to the keyboard (scale varies by synth).", + // "type": "integer" + // } + // }, + "FRA-Keyboard.SynthLangString": { + "schema": { + "title": "Keyboard language", + "description": "The language to use for announcements related to the keyboard.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "FRA-Keyboard.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "FRA-Keyboard.Volume": { + "schema": { + "title": "Keyboard volume", + "description": "The volume for announcements related to the keyboard.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "FRA-Keyboard.VoiceName": { + "schema": { + "title": "Keyboard voice name", + "description": "The voice name used for keyboard announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "default": "Reed" + } + }, + + // -------- French (Canada) - KEYBOARD ---------- + + "FRC-Keyboard.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Keyboard parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "FRC-Keyboard.Pitch": { + "schema": { + "title": "Keyboard pitch", + "description": "The pitch to use for announcements related to the keyboard.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "FRC-Keyboard.Punctuation": { + "schema": { + "title": "Keyboard punctuation", + "description": "How much punctuation to announce for the keyboard.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "FRC-Keyboard.Rate": { + // "schema": { + // "title": "Keyboard rate", + // "description": "Speech rate for announcements related to the keyboard (scale varies by synth).", + // "type": "integer" + // } + // }, + "FRC-Keyboard.SynthLangString": { + "schema": { + "title": "Keyboard language", + "description": "The language to use for announcements related to the keyboard.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "FRC-Keyboard.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "FRC-Keyboard.Volume": { + "schema": { + "title": "Keyboard volume", + "description": "The volume for announcements related to the keyboard.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "FRC-Keyboard.VoiceName": { + "schema": { + "title": "Keyboard voice name", + "description": "The voice name used for keyboard announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "default": "Reed" + } + }, + + // -------- Italian - JAWCURSOR ---------- + + "ITA-Keyboard.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "JAWS Cursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ITA-Keyboard.Pitch": { + "schema": { + "title": "JAWS Cursor Pitch", + "description": "The pitch to use for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ITA-Keyboard.Punctuation": { + "schema": { + "title": "JAWS Cursor Punctuation", + "description": "How much punctuation to announce for the JAWS cursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ITA-Keyboard.Rate": { + // "schema": { + // "title": "JAWS Cursor Rate", + // "description": "Speech rate for announcements related to the JAWS cursor (scale varies by voice).", + // "type": "integer" + // } + // }, + "ITA-Keyboard.SynthLangString": { + "schema": { + "title": "JAWS Cursor Language", + "description": "The language to use for announcements related to the JAWS cursor.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ITA-Keyboard.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ITA-Keyboard.Volume": { + "schema": { + "title": "JAWS Cursor Volume", + "description": "The volume for announcements related to the JAWS cursor.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ITA-Keyboard.VoiceName": { + "schema": { + "title": "Keyboard voice name", + "description": "The voice name used for keyboard announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Cosmino", + "Microsoft Elsa", + // If synth is SAPI + "Microsoft Elsa Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Cosmino", + "Microsoft Elsa", + // If synth is SAPI + "Microsoft Elsa Desktop" + ], + "default": "Reed" + } + }, + + // -------- Brazil - KEYBOARD ---------- + + "PTB-Keyboard.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Keyboard parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "PTB-Keyboard.Pitch": { + "schema": { + "title": "Keyboard pitch", + "description": "The pitch to use for announcements related to the keyboard.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "PTB-Keyboard.Punctuation": { + "schema": { + "title": "Keyboard punctuation", + "description": "How much punctuation to announce for the keyboard.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "PTB-Keyboard.Rate": { + // "schema": { + // "title": "Keyboard rate", + // "description": "Speech rate for announcements related to the keyboard (scale varies by synth).", + // "type": "integer" + // } + // }, + "PTB-Keyboard.SynthLangString": { + "schema": { + "title": "Keyboard language", + "description": "The language to use for announcements related to the keyboard.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "PTB-Keyboard.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "PTB-Keyboard.Volume": { + "schema": { + "title": "Keyboard volume", + "description": "The volume for announcements related to the keyboard.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "PTB-Keyboard.VoiceName": { + "schema": { + "title": "Keyboard voice name", + "description": "The voice name used for keyboard announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Daniel", + "Microsoft Maria", + // If synth is SAPI 5X 64 + "Microsoft Maria Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Daniel", + "Microsoft Maria", + // If synth is SAPI 5X 64 + "Microsoft Maria Desktop" + ], + "default": "Reed" + } + }, + + // ======================================================= + + // ======================================================= + + // -------- German - MENU AND DIALOG ---------- + + "DEU-MenuAndDialog.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "MenuAndDialog parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "DEU-MenuAndDialog.Pitch": { + "schema": { + "title": "MenuAndDialog pitch", + "description": "The pitch to use for announcements related to the MenuAndDialog.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "DEU-MenuAndDialog.Punctuation": { + "schema": { + "title": "MenuAndDialog punctuation", + "description": "How much punctuation to announce for the MenuAndDialog.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "DEU-MenuAndDialog.Rate": { + // "schema": { + // "title": "MenuAndDialog rate", + // "description": "Speech rate for announcements related to the MenuAndDialog (scale varies by synth).", + // "type": "integer" + // } + // }, + "DEU-MenuAndDialog.SynthLangString": { + "schema": { + "title": "MenuAndDialog language", + "description": "The language to use for announcements related to the MenuAndDialog.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "DEU-MenuAndDialog.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "DEU-MenuAndDialog.Volume": { + "schema": { + "title": "MenuAndDialog volume", + "description": "The volume for announcements related to the MenuAndDialog.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "DEU-MenuAndDialog.VoiceName": { + "schema": { + "title": "MenuAndDialog voice name", + "description": "The voice name used for MenuAndDialog announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hedda", + "Microsoft Katja", + "Microsoft Stefan", + // If synth is SAPI 5X 64 + "Microsoft Hedda Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hedda", + "Microsoft Katja", + "Microsoft Stefan", + // If synth is SAPI 5X 64 + "Microsoft Hedda Desktop" + ], + "default": "Reed" + } + }, + + // -------- ENGLISH (UNITED STATES) - MENU AND DIALOG ---------- + + "ENU-MenuAndDialog.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "MenuAndDialog parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ENU-MenuAndDialog.Pitch": { + "schema": { + "title": "MenuAndDialog pitch", + "description": "The pitch to use for announcements related to the MenuAndDialog.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ENU-MenuAndDialog.Punctuation": { + "schema": { + "title": "MenuAndDialog punctuation", + "description": "How much punctuation to announce for the MenuAndDialog.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ENU-MenuAndDialog.Rate": { + // "schema": { + // "title": "MenuAndDialog rate", + // "description": "Speech rate for announcements related to the MenuAndDialog (scale varies by synth).", + // "type": "integer" + // } + // }, + "ENU-MenuAndDialog.SynthLangString": { + "schema": { + "title": "MenuAndDialog language", + "description": "The language to use for announcements related to the MenuAndDialog.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ENU-MenuAndDialog.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ENU-MenuAndDialog.Volume": { + "schema": { + "title": "MenuAndDialog volume", + "description": "The volume for announcements related to the MenuAndDialog.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ENU-MenuAndDialog.VoiceName": { + "schema": { + "title": "MenuAndDialog voice name", + "description": "The voice name used for MenuAndDialog announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "default": "Reed" + } + }, + + // -------- ENGLISH (United Kingdom) - MENU AND DIALOG ---------- + + "ENG-MenuAndDialog.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "MenuAndDialog parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ENG-MenuAndDialog.Pitch": { + "schema": { + "title": "MenuAndDialog pitch", + "description": "The pitch to use for announcements related to the MenuAndDialog.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ENG-MenuAndDialog.Punctuation": { + "schema": { + "title": "MenuAndDialog punctuation", + "description": "How much punctuation to announce for the MenuAndDialog.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ENG-MenuAndDialog.Rate": { + // "schema": { + // "title": "MenuAndDialog rate", + // "description": "Speech rate for announcements related to the MenuAndDialog (scale varies by synth).", + // "type": "integer" + // } + // }, + "ENG-MenuAndDialog.SynthLangString": { + "schema": { + "title": "MenuAndDialog language", + "description": "The language to use for announcements related to the MenuAndDialog.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ENG-MenuAndDialog.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ENG-MenuAndDialog.Volume": { + "schema": { + "title": "MenuAndDialog volume", + "description": "The volume for announcements related to the MenuAndDialog.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ENG-MenuAndDialog.VoiceName": { + "schema": { + "title": "MenuAndDialog voice name", + "description": "The voice name used for MenuAndDialog announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "default": "Reed" + } + }, + + // -------- SPANISH (Mexico) - MENU AND DIALOG ---------- + + "ESM-MenuAndDialog.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "MenuAndDialog parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ESM-MenuAndDialog.Pitch": { + "schema": { + "title": "MenuAndDialog pitch", + "description": "The pitch to use for announcements related to the MenuAndDialog.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ESM-MenuAndDialog.Punctuation": { + "schema": { + "title": "MenuAndDialog punctuation", + "description": "How much punctuation to announce for the MenuAndDialog.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ESM-MenuAndDialog.Rate": { + // "schema": { + // "title": "MenuAndDialog rate", + // "description": "Speech rate for announcements related to the MenuAndDialog (scale varies by synth).", + // "type": "integer" + // } + // }, + "ESM-MenuAndDialog.SynthLangString": { + "schema": { + "title": "MenuAndDialog language", + "description": "The language to use for announcements related to the MenuAndDialog.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ESM-MenuAndDialog.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ESM-MenuAndDialog.Volume": { + "schema": { + "title": "MenuAndDialog volume", + "description": "The volume for announcements related to the MenuAndDialog.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ESM-MenuAndDialog.VoiceName": { + "schema": { + "title": "MenuAndDialog voice name", + "description": "The voice name used for MenuAndDialog announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "default": "Reed" + } + }, + + // -------- SPANISH (Spain) - MENU AND DIALOG ---------- + + "ESN-MenuAndDialog.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "MenuAndDialog parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ESN-MenuAndDialog.Pitch": { + "schema": { + "title": "MenuAndDialog pitch", + "description": "The pitch to use for announcements related to the MenuAndDialog.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ESN-MenuAndDialog.Punctuation": { + "schema": { + "title": "MenuAndDialog punctuation", + "description": "How much punctuation to announce for the MenuAndDialog.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ESN-MenuAndDialog.Rate": { + // "schema": { + // "title": "MenuAndDialog rate", + // "description": "Speech rate for announcements related to the MenuAndDialog (scale varies by synth).", + // "type": "integer" + // } + // }, + "ESN-MenuAndDialog.SynthLangString": { + "schema": { + "title": "MenuAndDialog language", + "description": "The language to use for announcements related to the MenuAndDialog.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ESN-MenuAndDialog.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ESN-MenuAndDialog.Volume": { + "schema": { + "title": "MenuAndDialog volume", + "description": "The volume for announcements related to the MenuAndDialog.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ESN-MenuAndDialog.VoiceName": { + "schema": { + "title": "MenuAndDialog voice name", + "description": "The voice name used for MenuAndDialog announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "default": "Reed" + } + }, + + // -------- Finnish - MENU AND DIALOG ---------- + + "FIN-MenuAndDialog.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "MenuAndDialog parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "FIN-MenuAndDialog.Pitch": { + "schema": { + "title": "MenuAndDialog pitch", + "description": "The pitch to use for announcements related to the MenuAndDialog.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "FIN-MenuAndDialog.Punctuation": { + "schema": { + "title": "MenuAndDialog punctuation", + "description": "How much punctuation to announce for the MenuAndDialog.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "FIN-MenuAndDialog.Rate": { + // "schema": { + // "title": "MenuAndDialog rate", + // "description": "Speech rate for announcements related to the MenuAndDialog (scale varies by synth).", + // "type": "integer" + // } + // }, + "FIN-MenuAndDialog.SynthLangString": { + "schema": { + "title": "MenuAndDialog language", + "description": "The language to use for announcements related to the MenuAndDialog.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "FIN-MenuAndDialog.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "FIN-MenuAndDialog.Volume": { + "schema": { + "title": "MenuAndDialog volume", + "description": "The volume for announcements related to the MenuAndDialog.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "FIN-MenuAndDialog.VoiceName": { + "schema": { + "title": "MenuAndDialog voice name", + "description": "The voice name used for MenuAndDialog announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa" + ], + "default": "Reed" + } + }, + + // -------- French (France) - MENU AND DIALOG ---------- + + "FRA-MenuAndDialog.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "MenuAndDialog parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "FRA-MenuAndDialog.Pitch": { + "schema": { + "title": "MenuAndDialog pitch", + "description": "The pitch to use for announcements related to the MenuAndDialog.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "FRA-MenuAndDialog.Punctuation": { + "schema": { + "title": "MenuAndDialog punctuation", + "description": "How much punctuation to announce for the MenuAndDialog.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "FRA-MenuAndDialog.Rate": { + // "schema": { + // "title": "MenuAndDialog rate", + // "description": "Speech rate for announcements related to the MenuAndDialog (scale varies by synth).", + // "type": "integer" + // } + // }, + "FRA-MenuAndDialog.SynthLangString": { + "schema": { + "title": "MenuAndDialog language", + "description": "The language to use for announcements related to the MenuAndDialog.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "FRA-MenuAndDialog.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "FRA-MenuAndDialog.Volume": { + "schema": { + "title": "MenuAndDialog volume", + "description": "The volume for announcements related to the MenuAndDialog.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "FRA-MenuAndDialog.VoiceName": { + "schema": { + "title": "MenuAndDialog voice name", + "description": "The voice name used for MenuAndDialog announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "default": "Reed" + } + }, + + // -------- French (Canada) - MENU AND DIALOG ---------- + + "FRC-MenuAndDialog.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "MenuAndDialog parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "FRC-MenuAndDialog.Pitch": { + "schema": { + "title": "MenuAndDialog pitch", + "description": "The pitch to use for announcements related to the MenuAndDialog.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "FRC-MenuAndDialog.Punctuation": { + "schema": { + "title": "MenuAndDialog punctuation", + "description": "How much punctuation to announce for the MenuAndDialog.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "FRC-MenuAndDialog.Rate": { + // "schema": { + // "title": "MenuAndDialog rate", + // "description": "Speech rate for announcements related to the MenuAndDialog (scale varies by synth).", + // "type": "integer" + // } + // }, + "FRC-MenuAndDialog.SynthLangString": { + "schema": { + "title": "MenuAndDialog language", + "description": "The language to use for announcements related to the MenuAndDialog.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "FRC-MenuAndDialog.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "FRC-MenuAndDialog.Volume": { + "schema": { + "title": "MenuAndDialog volume", + "description": "The volume for announcements related to the MenuAndDialog.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "FRC-MenuAndDialog.VoiceName": { + "schema": { + "title": "MenuAndDialog voice name", + "description": "The voice name used for MenuAndDialog announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "default": "Reed" + } + }, + + // -------- Brazil - MENU AND DIALOG ---------- + + "PTB-MenuAndDialog.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "MenuAndDialog parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "PTB-MenuAndDialog.Pitch": { + "schema": { + "title": "MenuAndDialog pitch", + "description": "The pitch to use for announcements related to the MenuAndDialog.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "PTB-MenuAndDialog.Punctuation": { + "schema": { + "title": "MenuAndDialog punctuation", + "description": "How much punctuation to announce for the MenuAndDialog.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "PTB-MenuAndDialog.Rate": { + // "schema": { + // "title": "MenuAndDialog rate", + // "description": "Speech rate for announcements related to the MenuAndDialog (scale varies by synth).", + // "type": "integer" + // } + // }, + "PTB-MenuAndDialog.SynthLangString": { + "schema": { + "title": "MenuAndDialog language", + "description": "The language to use for announcements related to the MenuAndDialog.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "PTB-MenuAndDialog.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "PTB-MenuAndDialog.Volume": { + "schema": { + "title": "MenuAndDialog volume", + "description": "The volume for announcements related to the MenuAndDialog.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "PTB-MenuAndDialog.VoiceName": { + "schema": { + "title": "MenuAndDialog voice name", + "description": "The voice name used for MenuAndDialog announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Daniel", + "Microsoft Maria", + // If synth is SAPI 5X 64 + "Microsoft Maria Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Daniel", + "Microsoft Maria", + // If synth is SAPI 5X 64 + "Microsoft Maria Desktop" + ], + "default": "Reed" + } + }, + + // ======================================================= + + // ======================================================= + + // -------- German - MESSAGE ---------- + + "DEU-Message.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Message parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "DEU-Message.Pitch": { + "schema": { + "title": "Message pitch", + "description": "The pitch to use for message announcements .", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "DEU-Message.Punctuation": { + "schema": { + "title": "Message punctuation", + "description": "How much punctuation to announce for messages.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "DEU-Message.Rate": { + // "schema": { + // "title": "Message rate", + // "description": "Speech rate for message announcements (scale varies by synth).", + // "type": "integer" + // } + // }, + "DEU-Message.SynthLangString": { + "schema": { + "title": "Message language", + "description": "The language to use for message announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "DEU-Message.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "DEU-Message.Volume": { + "schema": { + "title": "Message volume", + "description": "The volume for message announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "DEU-Message.VoiceName": { + "schema": { + "title": "Message voice name", + "description": "The voice name for message announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hedda", + "Microsoft Katja", + "Microsoft Stefan", + // If synth is SAPI 5X 64 + "Microsoft Hedda Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hedda", + "Microsoft Katja", + "Microsoft Stefan", + // If synth is SAPI 5X 64 + "Microsoft Hedda Desktop" + ], + "default": "Reed" + } + }, + + // -------- ENGLISH (UNITED STATES) - MESSAGE ---------- + + "ENU-Message.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Message parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ENU-Message.Pitch": { + "schema": { + "title": "Message pitch", + "description": "The pitch to use for message announcements .", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ENU-Message.Punctuation": { + "schema": { + "title": "Message punctuation", + "description": "How much punctuation to announce for messages.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ENU-Message.Rate": { + // "schema": { + // "title": "Message rate", + // "description": "Speech rate for message announcements (scale varies by synth).", + // "type": "integer" + // } + // }, + "ENU-Message.SynthLangString": { + "schema": { + "title": "Message language", + "description": "The language to use for message announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ENU-Message.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ENU-Message.Volume": { + "schema": { + "title": "Message volume", + "description": "The volume for message announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ENU-Message.VoiceName": { + "schema": { + "title": "Message voice name", + "description": "The voice name for message announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "default": "Reed" + } + }, + + // -------- ENGLISH (United Kingdom) - MESSAGE ---------- + + "ENG-Message.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Message parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ENG-Message.Pitch": { + "schema": { + "title": "Message pitch", + "description": "The pitch to use for message announcements .", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ENG-Message.Punctuation": { + "schema": { + "title": "Message punctuation", + "description": "How much punctuation to announce for messages.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ENG-Message.Rate": { + // "schema": { + // "title": "Message rate", + // "description": "Speech rate for message announcements (scale varies by synth).", + // "type": "integer" + // } + // }, + "ENG-Message.SynthLangString": { + "schema": { + "title": "Message language", + "description": "The language to use for message announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ENG-Message.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ENG-Message.Volume": { + "schema": { + "title": "Message volume", + "description": "The volume for message announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ENG-Message.VoiceName": { + "schema": { + "title": "Message voice name", + "description": "The voice name for message announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "default": "Reed" + } + }, + + // -------- SPANISH (Mexico) - MESSAGE ---------- + + "ESM-Message.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Message parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ESM-Message.Pitch": { + "schema": { + "title": "Message pitch", + "description": "The pitch to use for message announcements .", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ESM-Message.Punctuation": { + "schema": { + "title": "Message punctuation", + "description": "How much punctuation to announce for messages.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ESM-Message.Rate": { + // "schema": { + // "title": "Message rate", + // "description": "Speech rate for message announcements (scale varies by synth).", + // "type": "integer" + // } + // }, + "ESM-Message.SynthLangString": { + "schema": { + "title": "Message language", + "description": "The language to use for message announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ESM-Message.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ESM-Message.Volume": { + "schema": { + "title": "Message volume", + "description": "The volume for message announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ESM-Message.VoiceName": { + "schema": { + "title": "Message voice name", + "description": "The voice name for message announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "default": "Reed" + } + }, + + // -------- SPANISH (Spain) - MESSAGE ---------- + + "ESN-Message.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Message parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ESN-Message.Pitch": { + "schema": { + "title": "Message pitch", + "description": "The pitch to use for message announcements .", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ESN-Message.Punctuation": { + "schema": { + "title": "Message punctuation", + "description": "How much punctuation to announce for messages.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ESN-Message.Rate": { + // "schema": { + // "title": "Message rate", + // "description": "Speech rate for message announcements (scale varies by synth).", + // "type": "integer" + // } + // }, + "ESN-Message.SynthLangString": { + "schema": { + "title": "Message language", + "description": "The language to use for message announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ESN-Message.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ESN-Message.Volume": { + "schema": { + "title": "Message volume", + "description": "The volume for message announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ESN-Message.VoiceName": { + "schema": { + "title": "Message voice name", + "description": "The voice name for message announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "default": "Reed" + } + }, + + // -------- Finnish - MESSAGE ---------- + + "FIN-Message.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Message parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "FIN-Message.Pitch": { + "schema": { + "title": "Message pitch", + "description": "The pitch to use for message announcements .", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "FIN-Message.Punctuation": { + "schema": { + "title": "Message punctuation", + "description": "How much punctuation to announce for messages.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "FIN-Message.Rate": { + // "schema": { + // "title": "Message rate", + // "description": "Speech rate for message announcements (scale varies by synth).", + // "type": "integer" + // } + // }, + "FIN-Message.SynthLangString": { + "schema": { + "title": "Message language", + "description": "The language to use for message announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "FIN-Message.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "FIN-Message.Volume": { + "schema": { + "title": "Message volume", + "description": "The volume for message announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "FIN-Message.VoiceName": { + "schema": { + "title": "Message voice name", + "description": "The voice name for message announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa" + ], + "default": "Reed" + } + }, + + // -------- French (France) - MESSAGE ---------- + + "FRA-Message.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Message parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "FRA-Message.Pitch": { + "schema": { + "title": "Message pitch", + "description": "The pitch to use for message announcements .", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "FRA-Message.Punctuation": { + "schema": { + "title": "Message punctuation", + "description": "How much punctuation to announce for messages.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "FRA-Message.Rate": { + // "schema": { + // "title": "Message rate", + // "description": "Speech rate for message announcements (scale varies by synth).", + // "type": "integer" + // } + // }, + "FRA-Message.SynthLangString": { + "schema": { + "title": "Message language", + "description": "The language to use for message announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "FRA-Message.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "FRA-Message.Volume": { + "schema": { + "title": "Message volume", + "description": "The volume for message announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "FRA-Message.VoiceName": { + "schema": { + "title": "Message voice name", + "description": "The voice name for message announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "default": "Reed" + } + }, + + // -------- French (Canada) - MESSAGE ---------- + + "FRC-Message.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Message parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "FRC-Message.Pitch": { + "schema": { + "title": "Message pitch", + "description": "The pitch to use for message announcements .", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "FRC-Message.Punctuation": { + "schema": { + "title": "Message punctuation", + "description": "How much punctuation to announce for messages.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "FRC-Message.Rate": { + // "schema": { + // "title": "Message rate", + // "description": "Speech rate for message announcements (scale varies by synth).", + // "type": "integer" + // } + // }, + "FRC-Message.SynthLangString": { + "schema": { + "title": "Message language", + "description": "The language to use for message announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "FRC-Message.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "FRC-Message.Volume": { + "schema": { + "title": "Message volume", + "description": "The volume for message announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "FRC-Message.VoiceName": { + "schema": { + "title": "Message voice name", + "description": "The voice name for message announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "default": "Reed" + } + }, + + // -------- Brazil - MESSAGE ---------- + + "PTB-Message.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "Message parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "PTB-Message.Pitch": { + "schema": { + "title": "Message pitch", + "description": "The pitch to use for message announcements .", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "PTB-Message.Punctuation": { + "schema": { + "title": "Message punctuation", + "description": "How much punctuation to announce for messages.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "PTB-Message.Rate": { + // "schema": { + // "title": "Message rate", + // "description": "Speech rate for message announcements (scale varies by synth).", + // "type": "integer" + // } + // }, + "PTB-Message.SynthLangString": { + "schema": { + "title": "Message language", + "description": "The language to use for message announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "PTB-Message.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "PTB-Message.Volume": { + "schema": { + "title": "Message volume", + "description": "The volume for message announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "PTB-Message.VoiceName": { + "schema": { + "title": "Message voice name", + "description": "The voice name for message announcements.", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Daniel", + "Microsoft Maria", + // If synth is SAPI 5X 64 + "Microsoft Maria Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Daniel", + "Microsoft Maria", + // If synth is SAPI 5X 64 + "Microsoft Maria Desktop" + ], + "default": "Reed" + } + }, + + // ======================================================= + + // ======================================================= + + // -------- German - MESSAGE ---------- + + "DEU-PCCursor.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "PCCursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "DEU-PCCursor.Pitch": { + "schema": { + "title": "PCCursor pitch", + "description": "The pitch to use for PCCursor announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "DEU-PCCursor.Punctuation": { + "schema": { + "title": "PCCursor punctuation", + "description": "How much punctuation to announce for PCCursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "DEU-PCCursor.Rate": { + // "schema": { + // "title": "PCCursor rate", + // "description": "Speech rate for PCCursor announcements (scale varies by synth).", + // "type": "integer" + // } + // }, + "DEU-PCCursor.SynthLangString": { + "schema": { + "title": "PCCursor language", + "description": "The language to use for PCCursor announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "DEU-PCCursor.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "DEU-PCCursor.Volume": { + "schema": { + "title": "PCCursor volume", + "description": "The volume for PCCursor announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "DEU-PCCursor.VoiceName": { + "schema": { + "title": "PCCursor voice name", + "description": "The voice for message announcements..", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hedda", + "Microsoft Katja", + "Microsoft Stefan", + // If synth is SAPI 5X 64 + "Microsoft Hedda Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hedda", + "Microsoft Katja", + "Microsoft Stefan", + // If synth is SAPI 5X 64 + "Microsoft Hedda Desktop" + ], + "default": "Reed" + } + }, + + // -------- ENGLISH (UNITED STATES) - MESSAGE ---------- + + "ENU-PCCursor.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "PCCursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ENU-PCCursor.Pitch": { + "schema": { + "title": "PCCursor pitch", + "description": "The pitch to use for PCCursor announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ENU-PCCursor.Punctuation": { + "schema": { + "title": "PCCursor punctuation", + "description": "How much punctuation to announce for PCCursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ENU-PCCursor.Rate": { + // "schema": { + // "title": "PCCursor rate", + // "description": "Speech rate for PCCursor announcements (scale varies by synth).", + // "type": "integer" + // } + // }, + "ENU-PCCursor.SynthLangString": { + "schema": { + "title": "PCCursor language", + "description": "The language to use for PCCursor announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ENU-PCCursor.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ENU-PCCursor.Volume": { + "schema": { + "title": "PCCursor volume", + "description": "The volume for PCCursor announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ENU-PCCursor.VoiceName": { + "schema": { + "title": "PCCursor voice name", + "description": "The voice for message announcements..", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "default": "Reed" + } + }, + + // -------- ENGLISH (United Kingdom) - MESSAGE ---------- + + "ENG-PCCursor.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "PCCursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ENG-PCCursor.Pitch": { + "schema": { + "title": "PCCursor pitch", + "description": "The pitch to use for PCCursor announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ENG-PCCursor.Punctuation": { + "schema": { + "title": "PCCursor punctuation", + "description": "How much punctuation to announce for PCCursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ENG-PCCursor.Rate": { + // "schema": { + // "title": "PCCursor rate", + // "description": "Speech rate for PCCursor announcements (scale varies by synth).", + // "type": "integer" + // } + // }, + "ENG-PCCursor.SynthLangString": { + "schema": { + "title": "PCCursor language", + "description": "The language to use for PCCursor announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ENG-PCCursor.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ENG-PCCursor.Volume": { + "schema": { + "title": "PCCursor volume", + "description": "The volume for PCCursor announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ENG-PCCursor.VoiceName": { + "schema": { + "title": "PCCursor voice name", + "description": "The voice for message announcements..", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // If synth is SAPI 5X 64 + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "default": "Reed" + } + }, + + // -------- SPANISH (Mexico) - MESSAGE ---------- + + "ESM-PCCursor.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "PCCursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ESM-PCCursor.Pitch": { + "schema": { + "title": "PCCursor pitch", + "description": "The pitch to use for PCCursor announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ESM-PCCursor.Punctuation": { + "schema": { + "title": "PCCursor punctuation", + "description": "How much punctuation to announce for PCCursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ESM-PCCursor.Rate": { + // "schema": { + // "title": "PCCursor rate", + // "description": "Speech rate for PCCursor announcements (scale varies by synth).", + // "type": "integer" + // } + // }, + "ESM-PCCursor.SynthLangString": { + "schema": { + "title": "PCCursor language", + "description": "The language to use for PCCursor announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ESM-PCCursor.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ESM-PCCursor.Volume": { + "schema": { + "title": "PCCursor volume", + "description": "The volume for PCCursor announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ESM-PCCursor.VoiceName": { + "schema": { + "title": "PCCursor voice name", + "description": "The voice for message announcements..", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "default": "Reed" + } + }, + + // -------- SPANISH (Spain) - MESSAGE ---------- + + "ESN-PCCursor.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "PCCursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "ESN-PCCursor.Pitch": { + "schema": { + "title": "PCCursor pitch", + "description": "The pitch to use for PCCursor announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "ESN-PCCursor.Punctuation": { + "schema": { + "title": "PCCursor punctuation", + "description": "How much punctuation to announce for PCCursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "ESN-PCCursor.Rate": { + // "schema": { + // "title": "PCCursor rate", + // "description": "Speech rate for PCCursor announcements (scale varies by synth).", + // "type": "integer" + // } + // }, + "ESN-PCCursor.SynthLangString": { + "schema": { + "title": "PCCursor language", + "description": "The language to use for PCCursor announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "ESN-PCCursor.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "ESN-PCCursor.Volume": { + "schema": { + "title": "PCCursor volume", + "description": "The volume for PCCursor announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "ESN-PCCursor.VoiceName": { + "schema": { + "title": "PCCursor voice name", + "description": "The voice for message announcements..", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + // Spain + "Microsoft David", + "Microsoft Mark", + "Microsoft Zira", + // Mexican + "Microsoft Raul", + "Microsoft Sabina", + // If synth is SAPI 5X + // Spain + "Microsoft Helena Desktop", + // Mexican + "Microsoft Sabina Desktop" + // If synth is SAPI 5X 64 + // - "Microsoft Helena Desktop" + ], + "default": "Reed" + } + }, + + // -------- Finnish - MESSAGE ---------- + + "FIN-PCCursor.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "PCCursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "FIN-PCCursor.Pitch": { + "schema": { + "title": "PCCursor pitch", + "description": "The pitch to use for PCCursor announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "FIN-PCCursor.Punctuation": { + "schema": { + "title": "PCCursor punctuation", + "description": "How much punctuation to announce for PCCursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "FIN-PCCursor.Rate": { + // "schema": { + // "title": "PCCursor rate", + // "description": "Speech rate for PCCursor announcements (scale varies by synth).", + // "type": "integer" + // } + // }, + "FIN-PCCursor.SynthLangString": { + "schema": { + "title": "PCCursor language", + "description": "The language to use for PCCursor announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "FIN-PCCursor.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "FIN-PCCursor.Volume": { + "schema": { + "title": "PCCursor volume", + "description": "The volume for PCCursor announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "FIN-PCCursor.VoiceName": { + "schema": { + "title": "PCCursor voice name", + "description": "The voice for message announcements..", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa" + ], + "default": "Reed" + } + }, + + // -------- French (France) - MESSAGE ---------- + + "FRA-PCCursor.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "PCCursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "FRA-PCCursor.Pitch": { + "schema": { + "title": "PCCursor pitch", + "description": "The pitch to use for PCCursor announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "FRA-PCCursor.Punctuation": { + "schema": { + "title": "PCCursor punctuation", + "description": "How much punctuation to announce for PCCursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "FRA-PCCursor.Rate": { + // "schema": { + // "title": "PCCursor rate", + // "description": "Speech rate for PCCursor announcements (scale varies by synth).", + // "type": "integer" + // } + // }, + "FRA-PCCursor.SynthLangString": { + "schema": { + "title": "PCCursor language", + "description": "The language to use for PCCursor announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "FRA-PCCursor.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "FRA-PCCursor.Volume": { + "schema": { + "title": "PCCursor volume", + "description": "The volume for PCCursor announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "FRA-PCCursor.VoiceName": { + "schema": { + "title": "PCCursor voice name", + "description": "The voice for message announcements..", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "default": "Reed" + } + }, + + // -------- French (Canada) - MESSAGE ---------- + + "FRC-PCCursor.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "PCCursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "FRC-PCCursor.Pitch": { + "schema": { + "title": "PCCursor pitch", + "description": "The pitch to use for PCCursor announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "FRC-PCCursor.Punctuation": { + "schema": { + "title": "PCCursor punctuation", + "description": "How much punctuation to announce for PCCursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "FRC-PCCursor.Rate": { + // "schema": { + // "title": "PCCursor rate", + // "description": "Speech rate for PCCursor announcements (scale varies by synth).", + // "type": "integer" + // } + // }, + "FRC-PCCursor.SynthLangString": { + "schema": { + "title": "PCCursor language", + "description": "The language to use for PCCursor announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "FRC-PCCursor.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "FRC-PCCursor.Volume": { + "schema": { + "title": "PCCursor volume", + "description": "The volume for PCCursor announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "FRC-PCCursor.VoiceName": { + "schema": { + "title": "PCCursor voice name", + "description": "The voice for message announcements..", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Hortense", + "Microsoft Julie", + "Microsoft Paul", + // If synth is SAPI 5X 64 + "Microsoft Hortense Desktop" + ], + "default": "Reed" + } + }, + + // -------- Brazil - MESSAGE ---------- + + "PTB-PCCursor.Parent": { + "schema": { + // NOTE: This setting should remain private and not be exposed + "title": "PCCursor parent config", + "description": "The parent config from which to inherit.", + "type": "string", + "default": "Global" + } + }, + "PTB-PCCursor.Pitch": { + "schema": { + "title": "PCCursor pitch", + "description": "The pitch to use for PCCursor announcements.", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 65 + } + }, + "PTB-PCCursor.Punctuation": { + "schema": { + "title": "PCCursor punctuation", + "description": "How much punctuation to announce for PCCursor.", + "enum": [0, 1, 2, 3], + "enumLabels": ["None", "Some", "Most", "All"], + "default": 1 + } + }, + // GPII-4226 Leaving out for now due to lack of a default value. + // "PTB-PCCursor.Rate": { + // "schema": { + // "title": "PCCursor rate", + // "description": "Speech rate for PCCursor announcements (scale varies by synth).", + // "type": "integer" + // } + // }, + "PTB-PCCursor.SynthLangString": { + "schema": { + "title": "PCCursor language", + "description": "The language to use for PCCursor announcements.", + "enum": [ + "0x409", + "0x407", + "0x40b", + "0x40c", + "0x410", + "0x416", + "0x809", + "0x80a", + "0xc0a", + "0xc0c" + ], + "enumLabels": [ + "English - US", + "German", + "Finish", + "French - France", + "Italian", + "Portuguese - Brazil", + "English - United Kingdom", + "Spanish - Mexico", + "Spanish - Spain", + "French - Canada" + ], + "default": "0x409" + } + }, + "PTB-PCCursor.UppercasePitchDelta": { + "schema": { + "title": "Uppercase pitch delta", + "description": "Pitch variation as you type or arrow accross capital letters.", + "type": "integer", + "minimum": -99, + "maximum": 999, + "default": 50 + } + }, + "PTB-PCCursor.Volume": { + "schema": { + "title": "PCCursor volume", + "description": "The volume for PCCursor announcements.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 100 + } + }, + "PTB-PCCursor.VoiceName": { + "schema": { + "title": "PCCursor voice name", + "description": "The voice for message announcements..", + "enum": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Daniel", + "Microsoft Maria", + // If synth is SAPI 5X 64 + "Microsoft Maria Desktop" + ], + "enumLabels": [ + // If synth is eloq + "Reed", + "Shelly", + "Bobby", + "Rocko", + "Glen", + "Sandy", + "Grandma", + "Grandpa", + // If synth is mmobile + "Microsoft Daniel", + "Microsoft Maria", + // If synth is SAPI 5X 64 + "Microsoft Maria Desktop" + ], + "default": "Reed" + } + }, + + // ======================================================= + + // ------------ SPANISH (Spain) - VOICE ALIASES -------------- + + "ESN-Voice Aliases.PCCursorVoice": { + "schema": { + "title": "PC Cursor Voice", + "description": "The voice to be used for the PC cursor.", + "type": "string", + "default": "Reed|0|0" + } + }, + "ESN-Voice Aliases.MessageVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Glen|0|0" + } + }, + "ESN-Voice Aliases.HeaderVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Reed|-20%|0" + } + }, + "ESN-Voice Aliases.JAWSCursorVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice used for JAWS cursor.", + "type": "string", + "default": "Reed|-10%|0" + } + }, + "ESN-Voice Aliases.NormalVoice": { + "schema": { + "title": "Normal voice", + "description": "The voice used for regular text.", + "type": "string", + "default": "*|0|0" + } + }, + "ESN-Voice Aliases.BoldVoice": { + "schema": { + "title": "Bold voice", + "description": "The voice used for bold text.", + "type": "string", + "default": "Rocko" + } + }, + "ESN-Voice Aliases.UnderlineVoice": { + "schema": { + "title": "Underline voice", + "description": "The underline used for underline text.", + "type": "string", + "default": "Shelly" + } + }, + "ESN-Voice Aliases.ItalicVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for italic text.", + "type": "string", + "default": "Grandpa" + } + }, + "ESN-Voice Aliases.StrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy" + } + }, + "ESN-Voice Aliases.DoubleStrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy|0|10%" + } + }, + "ESN-Voice Aliases.GraphicVoice": { + "schema": { + "title": "Graphic voice", + "description": "The voice used for graphics.", + "type": "string", + "default": "Grandma" + } + }, + "ESN-Voice Aliases.HighlightVoice": { + "schema": { + "title": "Highlight voice", + "description": "The voice used for highlighted text.", + "type": "string", + "default": "Bobby" + } + }, + "ESN-Voice Aliases.HugeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-20%|0" + } + }, + "ESN-Voice Aliases.LargeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-10%|0" + } + }, + "ESN-Voice Aliases.SmallTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|10%|0" + } + }, + "ESN-Voice Aliases.TinyTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|20%|0" + } + }, + "ESN-Voice Aliases.SuperscriptVoice": { + "schema": { + "title": "Superscript voice", + "description": "The voice used for superscript text.", + "type": "string", + "default": "*|40%|0" + } + }, + "ESN-Voice Aliases.SubscriptVoice": { + "schema": { + "title": "Subscript voice", + "description": "The voice used for subscript text.", + "type": "string", + "default": "*|-40%|0" + } + }, + "ESN-Voice Aliases.ShadowVoice": { + "schema": { + "title": "Shadow voice", + "description": "The voice used for shadow text.", + "type": "string", + "default": "*|0|0" + } + }, + "ESN-Voice Aliases.EmbossVoice": { + "schema": { + "title": "Emboss voice", + "description": "The voice used for emboss text.", + "type": "string", + "default": "*|0|0" + } + }, + "ESN-Voice Aliases.EngraveVoice": { + "schema": { + "title": "Engrave voice", + "description": "The voice used for engraved text.", + "type": "string", + "default": "*|0|0" + } + }, + "ESN-Voice Aliases.OutlineVoice": { + "schema": { + "title": "Outline voice", + "description": "The voice used for outlined text.", + "type": "string", + "default": "*|0|0" + } + }, + "ESN-Voice Aliases.HeadingLevel1Voice": { + "schema": { + "title": "HeadingLevel1 voice", + "description": "The voice used for heading 1 text.", + "type": "string", + "default": "Rocko|5%|0" + } + }, + "ESN-Voice Aliases.HeadingLevel2Voice": { + "schema": { + "title": "HeadingLevel2 voice", + "description": "The voice used for heading 2 text.", + "type": "string", + "default": "Rocko|10%|0" + } + }, + "ESN-Voice Aliases.HeadingLevel3Voice": { + "schema": { + "title": "HeadingLevel3 voice", + "description": "The voice used for heading 3 text.", + "type": "string", + "default": "Rocko|15%|0" + } + }, + "ESN-Voice Aliases.HeadingLevel4Voice": { + "schema": { + "title": "HeadingLevel4 voice", + "description": "The voice used for heading 4 text.", + "type": "string", + "default": "Rocko|20%|0" + } + }, + "ESN-Voice Aliases.HeadingLevel5Voice": { + "schema": { + "title": "HeadingLevel5 voice", + "description": "The voice used for heading 5 text.", + "type": "string", + "default": "Rocko|25%|0" + } + }, + "ESN-Voice Aliases.HeadingLevel6Voice": { + "schema": { + "title": "HeadingLevel6 voice", + "description": "The voice used for heading 6 text.", + "type": "string", + "default": "Rocko|30%|0" + } + }, + "ESN-Voice Aliases.LinkVoice": { + "schema": { + "title": "Link voice", + "description": "The voice used for link text.", + "type": "string", + "default": "Shelly" + } + }, + "ESN-Voice Aliases.SpellingVoice": { + "schema": { + "title": "Spelling voice", + "description": "The voice used for spelling text.", + "type": "string", + "default": "*|0|-25%" + } + }, + "ESN-Voice Aliases.QuotationVoice": { + "schema": { + "title": "Quotation voice", + "description": "The voice used for quoted text.", + "type": "string", + "default": "Glen|10%|0" + } + }, + "ESN-Voice Aliases.SingleCapVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "ESN-Voice Aliases.AllCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for all caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "ESN-Voice Aliases.SmallCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for small caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "ESN-Voice Aliases.OCRVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "Rocko|-5%|-5%" + } + }, + + // ------------ SPANISH (Mexico) - VOICE ALIASES -------------- + + "ESM-Voice Aliases.PCCursorVoice": { + "schema": { + "title": "PC Cursor Voice", + "description": "The voice to be used for the PC cursor.", + "type": "string", + "default": "Reed|0|0" + } + }, + "ESM-Voice Aliases.MessageVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Glen|0|0" + } + }, + "ESM-Voice Aliases.HeaderVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Reed|-20%|0" + } + }, + "ESM-Voice Aliases.JAWSCursorVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice used for JAWS cursor.", + "type": "string", + "default": "Reed|-10%|0" + } + }, + "ESM-Voice Aliases.NormalVoice": { + "schema": { + "title": "Normal voice", + "description": "The voice used for regular text.", + "type": "string", + "default": "*|0|0" + } + }, + "ESM-Voice Aliases.BoldVoice": { + "schema": { + "title": "Bold voice", + "description": "The voice used for bold text.", + "type": "string", + "default": "Rocko" + } + }, + "ESM-Voice Aliases.UnderlineVoice": { + "schema": { + "title": "Underline voice", + "description": "The underline used for underline text.", + "type": "string", + "default": "Shelly" + } + }, + "ESM-Voice Aliases.ItalicVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for italic text.", + "type": "string", + "default": "Grandpa" + } + }, + "ESM-Voice Aliases.StrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy" + } + }, + "ESM-Voice Aliases.DoubleStrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy|0|10%" + } + }, + "ESM-Voice Aliases.GraphicVoice": { + "schema": { + "title": "Graphic voice", + "description": "The voice used for graphics.", + "type": "string", + "default": "Grandma" + } + }, + "ESM-Voice Aliases.HighlightVoice": { + "schema": { + "title": "Highlight voice", + "description": "The voice used for highlighted text.", + "type": "string", + "default": "Bobby" + } + }, + "ESM-Voice Aliases.HugeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-20%|0" + } + }, + "ESM-Voice Aliases.LargeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-10%|0" + } + }, + "ESM-Voice Aliases.SmallTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|10%|0" + } + }, + "ESM-Voice Aliases.TinyTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|20%|0" + } + }, + "ESM-Voice Aliases.SuperscriptVoice": { + "schema": { + "title": "Superscript voice", + "description": "The voice used for superscript text.", + "type": "string", + "default": "*|40%|0" + } + }, + "ESM-Voice Aliases.SubscriptVoice": { + "schema": { + "title": "Subscript voice", + "description": "The voice used for subscript text.", + "type": "string", + "default": "*|-40%|0" + } + }, + "ESM-Voice Aliases.ShadowVoice": { + "schema": { + "title": "Shadow voice", + "description": "The voice used for shadow text.", + "type": "string", + "default": "*|0|0" + } + }, + "ESM-Voice Aliases.EmbossVoice": { + "schema": { + "title": "Emboss voice", + "description": "The voice used for emboss text.", + "type": "string", + "default": "*|0|0" + } + }, + "ESM-Voice Aliases.EngraveVoice": { + "schema": { + "title": "Engrave voice", + "description": "The voice used for engraved text.", + "type": "string", + "default": "*|0|0" + } + }, + "ESM-Voice Aliases.OutlineVoice": { + "schema": { + "title": "Outline voice", + "description": "The voice used for outlined text.", + "type": "string", + "default": "*|0|0" + } + }, + "ESM-Voice Aliases.HeadingLevel1Voice": { + "schema": { + "title": "HeadingLevel1 voice", + "description": "The voice used for heading 1 text.", + "type": "string", + "default": "Rocko|5%|0" + } + }, + "ESM-Voice Aliases.HeadingLevel2Voice": { + "schema": { + "title": "HeadingLevel2 voice", + "description": "The voice used for heading 2 text.", + "type": "string", + "default": "Rocko|10%|0" + } + }, + "ESM-Voice Aliases.HeadingLevel3Voice": { + "schema": { + "title": "HeadingLevel3 voice", + "description": "The voice used for heading 3 text.", + "type": "string", + "default": "Rocko|15%|0" + } + }, + "ESM-Voice Aliases.HeadingLevel4Voice": { + "schema": { + "title": "HeadingLevel4 voice", + "description": "The voice used for heading 4 text.", + "type": "string", + "default": "Rocko|20%|0" + } + }, + "ESM-Voice Aliases.HeadingLevel5Voice": { + "schema": { + "title": "HeadingLevel5 voice", + "description": "The voice used for heading 5 text.", + "type": "string", + "default": "Rocko|25%|0" + } + }, + "ESM-Voice Aliases.HeadingLevel6Voice": { + "schema": { + "title": "HeadingLevel6 voice", + "description": "The voice used for heading 6 text.", + "type": "string", + "default": "Rocko|30%|0" + } + }, + "ESM-Voice Aliases.LinkVoice": { + "schema": { + "title": "Link voice", + "description": "The voice used for link text.", + "type": "string", + "default": "Shelly" + } + }, + "ESM-Voice Aliases.SpellingVoice": { + "schema": { + "title": "Spelling voice", + "description": "The voice used for spelling text.", + "type": "string", + "default": "*|0|-25%" + } + }, + "ESM-Voice Aliases.QuotationVoice": { + "schema": { + "title": "Quotation voice", + "description": "The voice used for quoted text.", + "type": "string", + "default": "Glen|10%|0" + } + }, + "ESM-Voice Aliases.SingleCapVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "ESM-Voice Aliases.AllCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for all caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "ESM-Voice Aliases.SmallCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for small caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "ESM-Voice Aliases.OCRVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "Rocko|-5%|-5%" + } + }, + + // ------------ ENGLISH US - VOICE ALIASES -------------- + + "ENU-Voice Aliases.PCCursorVoice": { + "schema": { + "title": "PC Cursor Voice", + "description": "The voice to be used for the PC cursor.", + "type": "string", + "default": "Reed|0|0" + } + }, + "ENU-Voice Aliases.MessageVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Glen|0|0" + } + }, + "ENU-Voice Aliases.HeaderVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Reed|-20%|0" + } + }, + "ENU-Voice Aliases.JAWSCursorVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice used for JAWS cursor.", + "type": "string", + "default": "Reed|-10%|0" + } + }, + "ENU-Voice Aliases.NormalVoice": { + "schema": { + "title": "Normal voice", + "description": "The voice used for regular text.", + "type": "string", + "default": "*|0|0" + } + }, + "ENU-Voice Aliases.BoldVoice": { + "schema": { + "title": "Bold voice", + "description": "The voice used for bold text.", + "type": "string", + "default": "Rocko" + } + }, + "ENU-Voice Aliases.UnderlineVoice": { + "schema": { + "title": "Underline voice", + "description": "The underline used for underline text.", + "type": "string", + "default": "Shelly" + } + }, + "ENU-Voice Aliases.ItalicVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for italic text.", + "type": "string", + "default": "Grandpa" + } + }, + "ENU-Voice Aliases.StrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy" + } + }, + "ENU-Voice Aliases.DoubleStrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy|0|10%" + } + }, + "ENU-Voice Aliases.GraphicVoice": { + "schema": { + "title": "Graphic voice", + "description": "The voice used for graphics.", + "type": "string", + "default": "Grandma" + } + }, + "ENU-Voice Aliases.HighlightVoice": { + "schema": { + "title": "Highlight voice", + "description": "The voice used for highlighted text.", + "type": "string", + "default": "Bobby" + } + }, + "ENU-Voice Aliases.HugeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-20%|0" + } + }, + "ENU-Voice Aliases.LargeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-10%|0" + } + }, + "ENU-Voice Aliases.SmallTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|10%|0" + } + }, + "ENU-Voice Aliases.TinyTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|20%|0" + } + }, + "ENU-Voice Aliases.SuperscriptVoice": { + "schema": { + "title": "Superscript voice", + "description": "The voice used for superscript text.", + "type": "string", + "default": "*|40%|0" + } + }, + "ENU-Voice Aliases.SubscriptVoice": { + "schema": { + "title": "Subscript voice", + "description": "The voice used for subscript text.", + "type": "string", + "default": "*|-40%|0" + } + }, + "ENU-Voice Aliases.ShadowVoice": { + "schema": { + "title": "Shadow voice", + "description": "The voice used for shadow text.", + "type": "string", + "default": "*|0|0" + } + }, + "ENU-Voice Aliases.EmbossVoice": { + "schema": { + "title": "Emboss voice", + "description": "The voice used for emboss text.", + "type": "string", + "default": "*|0|0" + } + }, + "ENU-Voice Aliases.EngraveVoice": { + "schema": { + "title": "Engrave voice", + "description": "The voice used for engraved text.", + "type": "string", + "default": "*|0|0" + } + }, + "ENU-Voice Aliases.OutlineVoice": { + "schema": { + "title": "Outline voice", + "description": "The voice used for outlined text.", + "type": "string", + "default": "*|0|0" + } + }, + "ENU-Voice Aliases.HeadingLevel1Voice": { + "schema": { + "title": "HeadingLevel1 voice", + "description": "The voice used for heading 1 text.", + "type": "string", + "default": "Rocko|5%|0" + } + }, + "ENU-Voice Aliases.HeadingLevel2Voice": { + "schema": { + "title": "HeadingLevel2 voice", + "description": "The voice used for heading 2 text.", + "type": "string", + "default": "Rocko|10%|0" + } + }, + "ENU-Voice Aliases.HeadingLevel3Voice": { + "schema": { + "title": "HeadingLevel3 voice", + "description": "The voice used for heading 3 text.", + "type": "string", + "default": "Rocko|15%|0" + } + }, + "ENU-Voice Aliases.HeadingLevel4Voice": { + "schema": { + "title": "HeadingLevel4 voice", + "description": "The voice used for heading 4 text.", + "type": "string", + "default": "Rocko|20%|0" + } + }, + "ENU-Voice Aliases.HeadingLevel5Voice": { + "schema": { + "title": "HeadingLevel5 voice", + "description": "The voice used for heading 5 text.", + "type": "string", + "default": "Rocko|25%|0" + } + }, + "ENU-Voice Aliases.HeadingLevel6Voice": { + "schema": { + "title": "HeadingLevel6 voice", + "description": "The voice used for heading 6 text.", + "type": "string", + "default": "Rocko|30%|0" + } + }, + "ENU-Voice Aliases.LinkVoice": { + "schema": { + "title": "Link voice", + "description": "The voice used for link text.", + "type": "string", + "default": "Shelly" + } + }, + "ENU-Voice Aliases.SpellingVoice": { + "schema": { + "title": "Spelling voice", + "description": "The voice used for spelling text.", + "type": "string", + "default": "*|0|-25%" + } + }, + "ENU-Voice Aliases.QuotationVoice": { + "schema": { + "title": "Quotation voice", + "description": "The voice used for quoted text.", + "type": "string", + "default": "Glen|10%|0" + } + }, + "ENU-Voice Aliases.SingleCapVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "ENU-Voice Aliases.AllCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for all caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "ENU-Voice Aliases.SmallCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for small caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "ENU-Voice Aliases.OCRVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "Rocko|-5%|-5%" + } + }, + + // ------------ ENGLISH (United Kingdom) - VOICE ALIASES -------------- + + "ENG-Voice Aliases.PCCursorVoice": { + "schema": { + "title": "PC Cursor Voice", + "description": "The voice to be used for the PC cursor.", + "type": "string", + "default": "Reed|0|0" + } + }, + "ENG-Voice Aliases.MessageVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Glen|0|0" + } + }, + "ENG-Voice Aliases.HeaderVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Reed|-20%|0" + } + }, + "ENG-Voice Aliases.JAWSCursorVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice used for JAWS cursor.", + "type": "string", + "default": "Reed|-10%|0" + } + }, + "ENG-Voice Aliases.NormalVoice": { + "schema": { + "title": "Normal voice", + "description": "The voice used for regular text.", + "type": "string", + "default": "*|0|0" + } + }, + "ENG-Voice Aliases.BoldVoice": { + "schema": { + "title": "Bold voice", + "description": "The voice used for bold text.", + "type": "string", + "default": "Rocko" + } + }, + "ENG-Voice Aliases.UnderlineVoice": { + "schema": { + "title": "Underline voice", + "description": "The underline used for underline text.", + "type": "string", + "default": "Shelly" + } + }, + "ENG-Voice Aliases.ItalicVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for italic text.", + "type": "string", + "default": "Grandpa" + } + }, + "ENG-Voice Aliases.StrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy" + } + }, + "ENG-Voice Aliases.DoubleStrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy|0|10%" + } + }, + "ENG-Voice Aliases.GraphicVoice": { + "schema": { + "title": "Graphic voice", + "description": "The voice used for graphics.", + "type": "string", + "default": "Grandma" + } + }, + "ENG-Voice Aliases.HighlightVoice": { + "schema": { + "title": "Highlight voice", + "description": "The voice used for highlighted text.", + "type": "string", + "default": "Bobby" + } + }, + "ENG-Voice Aliases.HugeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-20%|0" + } + }, + "ENG-Voice Aliases.LargeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-10%|0" + } + }, + "ENG-Voice Aliases.SmallTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|10%|0" + } + }, + "ENG-Voice Aliases.TinyTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|20%|0" + } + }, + "ENG-Voice Aliases.SuperscriptVoice": { + "schema": { + "title": "Superscript voice", + "description": "The voice used for superscript text.", + "type": "string", + "default": "*|40%|0" + } + }, + "ENG-Voice Aliases.SubscriptVoice": { + "schema": { + "title": "Subscript voice", + "description": "The voice used for subscript text.", + "type": "string", + "default": "*|-40%|0" + } + }, + "ENG-Voice Aliases.ShadowVoice": { + "schema": { + "title": "Shadow voice", + "description": "The voice used for shadow text.", + "type": "string", + "default": "*|0|0" + } + }, + "ENG-Voice Aliases.EmbossVoice": { + "schema": { + "title": "Emboss voice", + "description": "The voice used for emboss text.", + "type": "string", + "default": "*|0|0" + } + }, + "ENG-Voice Aliases.EngraveVoice": { + "schema": { + "title": "Engrave voice", + "description": "The voice used for engraved text.", + "type": "string", + "default": "*|0|0" + } + }, + "ENG-Voice Aliases.OutlineVoice": { + "schema": { + "title": "Outline voice", + "description": "The voice used for outlined text.", + "type": "string", + "default": "*|0|0" + } + }, + "ENG-Voice Aliases.HeadingLevel1Voice": { + "schema": { + "title": "HeadingLevel1 voice", + "description": "The voice used for heading 1 text.", + "type": "string", + "default": "Rocko|5%|0" + } + }, + "ENG-Voice Aliases.HeadingLevel2Voice": { + "schema": { + "title": "HeadingLevel2 voice", + "description": "The voice used for heading 2 text.", + "type": "string", + "default": "Rocko|10%|0" + } + }, + "ENG-Voice Aliases.HeadingLevel3Voice": { + "schema": { + "title": "HeadingLevel3 voice", + "description": "The voice used for heading 3 text.", + "type": "string", + "default": "Rocko|15%|0" + } + }, + "ENG-Voice Aliases.HeadingLevel4Voice": { + "schema": { + "title": "HeadingLevel4 voice", + "description": "The voice used for heading 4 text.", + "type": "string", + "default": "Rocko|20%|0" + } + }, + "ENG-Voice Aliases.HeadingLevel5Voice": { + "schema": { + "title": "HeadingLevel5 voice", + "description": "The voice used for heading 5 text.", + "type": "string", + "default": "Rocko|25%|0" + } + }, + "ENG-Voice Aliases.HeadingLevel6Voice": { + "schema": { + "title": "HeadingLevel6 voice", + "description": "The voice used for heading 6 text.", + "type": "string", + "default": "Rocko|30%|0" + } + }, + "ENG-Voice Aliases.LinkVoice": { + "schema": { + "title": "Link voice", + "description": "The voice used for link text.", + "type": "string", + "default": "Shelly" + } + }, + "ENG-Voice Aliases.SpellingVoice": { + "schema": { + "title": "Spelling voice", + "description": "The voice used for spelling text.", + "type": "string", + "default": "*|0|-25%" + } + }, + "ENG-Voice Aliases.QuotationVoice": { + "schema": { + "title": "Quotation voice", + "description": "The voice used for quoted text.", + "type": "string", + "default": "Glen|10%|0" + } + }, + "ENG-Voice Aliases.SingleCapVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "ENG-Voice Aliases.AllCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for all caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "ENG-Voice Aliases.SmallCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for small caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "ENG-Voice Aliases.OCRVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "Rocko|-5%|-5%" + } + }, + + // ------------ Finish - VOICE ALIASES -------------- + + "FIN-Voice Aliases.PCCursorVoice": { + "schema": { + "title": "PC Cursor Voice", + "description": "The voice to be used for the PC cursor.", + "type": "string", + "default": "Reed|0|0" + } + }, + "FIN-Voice Aliases.MessageVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Glen|0|0" + } + }, + "FIN-Voice Aliases.HeaderVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Reed|-20%|0" + } + }, + "FIN-Voice Aliases.JAWSCursorVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice used for JAWS cursor.", + "type": "string", + "default": "Reed|-10%|0" + } + }, + "FIN-Voice Aliases.NormalVoice": { + "schema": { + "title": "Normal voice", + "description": "The voice used for regular text.", + "type": "string", + "default": "*|0|0" + } + }, + "FIN-Voice Aliases.BoldVoice": { + "schema": { + "title": "Bold voice", + "description": "The voice used for bold text.", + "type": "string", + "default": "Rocko" + } + }, + "FIN-Voice Aliases.UnderlineVoice": { + "schema": { + "title": "Underline voice", + "description": "The underline used for underline text.", + "type": "string", + "default": "Shelly" + } + }, + "FIN-Voice Aliases.ItalicVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for italic text.", + "type": "string", + "default": "Grandpa" + } + }, + "FIN-Voice Aliases.StrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy" + } + }, + "FIN-Voice Aliases.DoubleStrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy|0|10%" + } + }, + "FIN-Voice Aliases.GraphicVoice": { + "schema": { + "title": "Graphic voice", + "description": "The voice used for graphics.", + "type": "string", + "default": "Grandma" + } + }, + "FIN-Voice Aliases.HighlightVoice": { + "schema": { + "title": "Highlight voice", + "description": "The voice used for highlighted text.", + "type": "string", + "default": "Bobby" + } + }, + "FIN-Voice Aliases.HugeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-20%|0" + } + }, + "FIN-Voice Aliases.LargeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-10%|0" + } + }, + "FIN-Voice Aliases.SmallTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|10%|0" + } + }, + "FIN-Voice Aliases.TinyTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|20%|0" + } + }, + "FIN-Voice Aliases.SuperscriptVoice": { + "schema": { + "title": "Superscript voice", + "description": "The voice used for superscript text.", + "type": "string", + "default": "*|40%|0" + } + }, + "FIN-Voice Aliases.SubscriptVoice": { + "schema": { + "title": "Subscript voice", + "description": "The voice used for subscript text.", + "type": "string", + "default": "*|-40%|0" + } + }, + "FIN-Voice Aliases.ShadowVoice": { + "schema": { + "title": "Shadow voice", + "description": "The voice used for shadow text.", + "type": "string", + "default": "*|0|0" + } + }, + "FIN-Voice Aliases.EmbossVoice": { + "schema": { + "title": "Emboss voice", + "description": "The voice used for emboss text.", + "type": "string", + "default": "*|0|0" + } + }, + "FIN-Voice Aliases.EngraveVoice": { + "schema": { + "title": "Engrave voice", + "description": "The voice used for engraved text.", + "type": "string", + "default": "*|0|0" + } + }, + "FIN-Voice Aliases.OutlineVoice": { + "schema": { + "title": "Outline voice", + "description": "The voice used for outlined text.", + "type": "string", + "default": "*|0|0" + } + }, + "FIN-Voice Aliases.HeadingLevel1Voice": { + "schema": { + "title": "HeadingLevel1 voice", + "description": "The voice used for heading 1 text.", + "type": "string", + "default": "Rocko|5%|0" + } + }, + "FIN-Voice Aliases.HeadingLevel2Voice": { + "schema": { + "title": "HeadingLevel2 voice", + "description": "The voice used for heading 2 text.", + "type": "string", + "default": "Rocko|10%|0" + } + }, + "FIN-Voice Aliases.HeadingLevel3Voice": { + "schema": { + "title": "HeadingLevel3 voice", + "description": "The voice used for heading 3 text.", + "type": "string", + "default": "Rocko|15%|0" + } + }, + "FIN-Voice Aliases.HeadingLevel4Voice": { + "schema": { + "title": "HeadingLevel4 voice", + "description": "The voice used for heading 4 text.", + "type": "string", + "default": "Rocko|20%|0" + } + }, + "FIN-Voice Aliases.HeadingLevel5Voice": { + "schema": { + "title": "HeadingLevel5 voice", + "description": "The voice used for heading 5 text.", + "type": "string", + "default": "Rocko|25%|0" + } + }, + "FIN-Voice Aliases.HeadingLevel6Voice": { + "schema": { + "title": "HeadingLevel6 voice", + "description": "The voice used for heading 6 text.", + "type": "string", + "default": "Rocko|30%|0" + } + }, + "FIN-Voice Aliases.LinkVoice": { + "schema": { + "title": "Link voice", + "description": "The voice used for link text.", + "type": "string", + "default": "Shelly" + } + }, + "FIN-Voice Aliases.SpellingVoice": { + "schema": { + "title": "Spelling voice", + "description": "The voice used for spelling text.", + "type": "string", + "default": "*|0|-25%" + } + }, + "FIN-Voice Aliases.QuotationVoice": { + "schema": { + "title": "Quotation voice", + "description": "The voice used for quoted text.", + "type": "string", + "default": "Glen|10%|0" + } + }, + "FIN-Voice Aliases.SingleCapVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "FIN-Voice Aliases.AllCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for all caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "FIN-Voice Aliases.SmallCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for small caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "FIN-Voice Aliases.OCRVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "Rocko|-5%|-5%" + } + }, + + // ------------ French (France) - VOICE ALIASES -------------- + + "FRA-Voice Aliases.PCCursorVoice": { + "schema": { + "title": "PC Cursor Voice", + "description": "The voice to be used for the PC cursor.", + "type": "string", + "default": "Reed|0|0" + } + }, + "FRA-Voice Aliases.MessageVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Glen|0|0" + } + }, + "FRA-Voice Aliases.HeaderVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Reed|-20%|0" + } + }, + "FRA-Voice Aliases.JAWSCursorVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice used for JAWS cursor.", + "type": "string", + "default": "Reed|-10%|0" + } + }, + "FRA-Voice Aliases.NormalVoice": { + "schema": { + "title": "Normal voice", + "description": "The voice used for regular text.", + "type": "string", + "default": "*|0|0" + } + }, + "FRA-Voice Aliases.BoldVoice": { + "schema": { + "title": "Bold voice", + "description": "The voice used for bold text.", + "type": "string", + "default": "Rocko" + } + }, + "FRA-Voice Aliases.UnderlineVoice": { + "schema": { + "title": "Underline voice", + "description": "The underline used for underline text.", + "type": "string", + "default": "Shelly" + } + }, + "FRA-Voice Aliases.ItalicVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for italic text.", + "type": "string", + "default": "Grandpa" + } + }, + "FRA-Voice Aliases.StrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy" + } + }, + "FRA-Voice Aliases.DoubleStrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy|0|10%" + } + }, + "FRA-Voice Aliases.GraphicVoice": { + "schema": { + "title": "Graphic voice", + "description": "The voice used for graphics.", + "type": "string", + "default": "Grandma" + } + }, + "FRA-Voice Aliases.HighlightVoice": { + "schema": { + "title": "Highlight voice", + "description": "The voice used for highlighted text.", + "type": "string", + "default": "Bobby" + } + }, + "FRA-Voice Aliases.HugeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-20%|0" + } + }, + "FRA-Voice Aliases.LargeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-10%|0" + } + }, + "FRA-Voice Aliases.SmallTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|10%|0" + } + }, + "FRA-Voice Aliases.TinyTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|20%|0" + } + }, + "FRA-Voice Aliases.SuperscriptVoice": { + "schema": { + "title": "Superscript voice", + "description": "The voice used for superscript text.", + "type": "string", + "default": "*|40%|0" + } + }, + "FRA-Voice Aliases.SubscriptVoice": { + "schema": { + "title": "Subscript voice", + "description": "The voice used for subscript text.", + "type": "string", + "default": "*|-40%|0" + } + }, + "FRA-Voice Aliases.ShadowVoice": { + "schema": { + "title": "Shadow voice", + "description": "The voice used for shadow text.", + "type": "string", + "default": "*|0|0" + } + }, + "FRA-Voice Aliases.EmbossVoice": { + "schema": { + "title": "Emboss voice", + "description": "The voice used for emboss text.", + "type": "string", + "default": "*|0|0" + } + }, + "FRA-Voice Aliases.EngraveVoice": { + "schema": { + "title": "Engrave voice", + "description": "The voice used for engraved text.", + "type": "string", + "default": "*|0|0" + } + }, + "FRA-Voice Aliases.OutlineVoice": { + "schema": { + "title": "Outline voice", + "description": "The voice used for outlined text.", + "type": "string", + "default": "*|0|0" + } + }, + "FRA-Voice Aliases.HeadingLevel1Voice": { + "schema": { + "title": "HeadingLevel1 voice", + "description": "The voice used for heading 1 text.", + "type": "string", + "default": "Rocko|5%|0" + } + }, + "FRA-Voice Aliases.HeadingLevel2Voice": { + "schema": { + "title": "HeadingLevel2 voice", + "description": "The voice used for heading 2 text.", + "type": "string", + "default": "Rocko|10%|0" + } + }, + "FRA-Voice Aliases.HeadingLevel3Voice": { + "schema": { + "title": "HeadingLevel3 voice", + "description": "The voice used for heading 3 text.", + "type": "string", + "default": "Rocko|15%|0" + } + }, + "FRA-Voice Aliases.HeadingLevel4Voice": { + "schema": { + "title": "HeadingLevel4 voice", + "description": "The voice used for heading 4 text.", + "type": "string", + "default": "Rocko|20%|0" + } + }, + "FRA-Voice Aliases.HeadingLevel5Voice": { + "schema": { + "title": "HeadingLevel5 voice", + "description": "The voice used for heading 5 text.", + "type": "string", + "default": "Rocko|25%|0" + } + }, + "FRA-Voice Aliases.HeadingLevel6Voice": { + "schema": { + "title": "HeadingLevel6 voice", + "description": "The voice used for heading 6 text.", + "type": "string", + "default": "Rocko|30%|0" + } + }, + "FRA-Voice Aliases.LinkVoice": { + "schema": { + "title": "Link voice", + "description": "The voice used for link text.", + "type": "string", + "default": "Shelly" + } + }, + "FRA-Voice Aliases.SpellingVoice": { + "schema": { + "title": "Spelling voice", + "description": "The voice used for spelling text.", + "type": "string", + "default": "*|0|-25%" + } + }, + "FRA-Voice Aliases.QuotationVoice": { + "schema": { + "title": "Quotation voice", + "description": "The voice used for quoted text.", + "type": "string", + "default": "Glen|10%|0" + } + }, + "FRA-Voice Aliases.SingleCapVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "FRA-Voice Aliases.AllCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for all caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "FRA-Voice Aliases.SmallCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for small caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "FRA-Voice Aliases.OCRVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "Rocko|-5%|-5%" + } + }, + + // ------------ French (Canada) - VOICE ALIASES -------------- + + "FRC-Voice Aliases.PCCursorVoice": { + "schema": { + "title": "PC Cursor Voice", + "description": "The voice to be used for the PC cursor.", + "type": "string", + "default": "Reed|0|0" + } + }, + "FRC-Voice Aliases.MessageVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Glen|0|0" + } + }, + "FRC-Voice Aliases.HeaderVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Reed|-20%|0" + } + }, + "FRC-Voice Aliases.JAWSCursorVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice used for JAWS cursor.", + "type": "string", + "default": "Reed|-10%|0" + } + }, + "FRC-Voice Aliases.NormalVoice": { + "schema": { + "title": "Normal voice", + "description": "The voice used for regular text.", + "type": "string", + "default": "*|0|0" + } + }, + "FRC-Voice Aliases.BoldVoice": { + "schema": { + "title": "Bold voice", + "description": "The voice used for bold text.", + "type": "string", + "default": "Rocko" + } + }, + "FRC-Voice Aliases.UnderlineVoice": { + "schema": { + "title": "Underline voice", + "description": "The underline used for underline text.", + "type": "string", + "default": "Shelly" + } + }, + "FRC-Voice Aliases.ItalicVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for italic text.", + "type": "string", + "default": "Grandpa" + } + }, + "FRC-Voice Aliases.StrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy" + } + }, + "FRC-Voice Aliases.DoubleStrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy|0|10%" + } + }, + "FRC-Voice Aliases.GraphicVoice": { + "schema": { + "title": "Graphic voice", + "description": "The voice used for graphics.", + "type": "string", + "default": "Grandma" + } + }, + "FRC-Voice Aliases.HighlightVoice": { + "schema": { + "title": "Highlight voice", + "description": "The voice used for highlighted text.", + "type": "string", + "default": "Bobby" + } + }, + "FRC-Voice Aliases.HugeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-20%|0" + } + }, + "FRC-Voice Aliases.LargeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-10%|0" + } + }, + "FRC-Voice Aliases.SmallTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|10%|0" + } + }, + "FRC-Voice Aliases.TinyTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|20%|0" + } + }, + "FRC-Voice Aliases.SuperscriptVoice": { + "schema": { + "title": "Superscript voice", + "description": "The voice used for superscript text.", + "type": "string", + "default": "*|40%|0" + } + }, + "FRC-Voice Aliases.SubscriptVoice": { + "schema": { + "title": "Subscript voice", + "description": "The voice used for subscript text.", + "type": "string", + "default": "*|-40%|0" + } + }, + "FRC-Voice Aliases.ShadowVoice": { + "schema": { + "title": "Shadow voice", + "description": "The voice used for shadow text.", + "type": "string", + "default": "*|0|0" + } + }, + "FRC-Voice Aliases.EmbossVoice": { + "schema": { + "title": "Emboss voice", + "description": "The voice used for emboss text.", + "type": "string", + "default": "*|0|0" + } + }, + "FRC-Voice Aliases.EngraveVoice": { + "schema": { + "title": "Engrave voice", + "description": "The voice used for engraved text.", + "type": "string", + "default": "*|0|0" + } + }, + "FRC-Voice Aliases.OutlineVoice": { + "schema": { + "title": "Outline voice", + "description": "The voice used for outlined text.", + "type": "string", + "default": "*|0|0" + } + }, + "FRC-Voice Aliases.HeadingLevel1Voice": { + "schema": { + "title": "HeadingLevel1 voice", + "description": "The voice used for heading 1 text.", + "type": "string", + "default": "Rocko|5%|0" + } + }, + "FRC-Voice Aliases.HeadingLevel2Voice": { + "schema": { + "title": "HeadingLevel2 voice", + "description": "The voice used for heading 2 text.", + "type": "string", + "default": "Rocko|10%|0" + } + }, + "FRC-Voice Aliases.HeadingLevel3Voice": { + "schema": { + "title": "HeadingLevel3 voice", + "description": "The voice used for heading 3 text.", + "type": "string", + "default": "Rocko|15%|0" + } + }, + "FRC-Voice Aliases.HeadingLevel4Voice": { + "schema": { + "title": "HeadingLevel4 voice", + "description": "The voice used for heading 4 text.", + "type": "string", + "default": "Rocko|20%|0" + } + }, + "FRC-Voice Aliases.HeadingLevel5Voice": { + "schema": { + "title": "HeadingLevel5 voice", + "description": "The voice used for heading 5 text.", + "type": "string", + "default": "Rocko|25%|0" + } + }, + "FRC-Voice Aliases.HeadingLevel6Voice": { + "schema": { + "title": "HeadingLevel6 voice", + "description": "The voice used for heading 6 text.", + "type": "string", + "default": "Rocko|30%|0" + } + }, + "FRC-Voice Aliases.LinkVoice": { + "schema": { + "title": "Link voice", + "description": "The voice used for link text.", + "type": "string", + "default": "Shelly" + } + }, + "FRC-Voice Aliases.SpellingVoice": { + "schema": { + "title": "Spelling voice", + "description": "The voice used for spelling text.", + "type": "string", + "default": "*|0|-25%" + } + }, + "FRC-Voice Aliases.QuotationVoice": { + "schema": { + "title": "Quotation voice", + "description": "The voice used for quoted text.", + "type": "string", + "default": "Glen|10%|0" + } + }, + "FRC-Voice Aliases.SingleCapVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "FRC-Voice Aliases.AllCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for all caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "FRC-Voice Aliases.SmallCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for small caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "FRC-Voice Aliases.OCRVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "Rocko|-5%|-5%" + } + }, + + // ------------ German - VOICE ALIASES -------------- + + "DEU-Voice Aliases.PCCursorVoice": { + "schema": { + "title": "PC Cursor Voice", + "description": "The voice to be used for the PC cursor.", + "type": "string", + "default": "Reed|0|0" + } + }, + "DEU-Voice Aliases.MessageVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Glen|0|0" + } + }, + "DEU-Voice Aliases.HeaderVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Reed|-20%|0" + } + }, + "DEU-Voice Aliases.JAWSCursorVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice used for JAWS cursor.", + "type": "string", + "default": "Reed|-10%|0" + } + }, + "DEU-Voice Aliases.NormalVoice": { + "schema": { + "title": "Normal voice", + "description": "The voice used for regular text.", + "type": "string", + "default": "*|0|0" + } + }, + "DEU-Voice Aliases.BoldVoice": { + "schema": { + "title": "Bold voice", + "description": "The voice used for bold text.", + "type": "string", + "default": "Rocko" + } + }, + "DEU-Voice Aliases.UnderlineVoice": { + "schema": { + "title": "Underline voice", + "description": "The underline used for underline text.", + "type": "string", + "default": "Shelly" + } + }, + "DEU-Voice Aliases.ItalicVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for italic text.", + "type": "string", + "default": "Grandpa" + } + }, + "DEU-Voice Aliases.StrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy" + } + }, + "DEU-Voice Aliases.DoubleStrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy|0|10%" + } + }, + "DEU-Voice Aliases.GraphicVoice": { + "schema": { + "title": "Graphic voice", + "description": "The voice used for graphics.", + "type": "string", + "default": "Grandma" + } + }, + "DEU-Voice Aliases.HighlightVoice": { + "schema": { + "title": "Highlight voice", + "description": "The voice used for highlighted text.", + "type": "string", + "default": "Bobby" + } + }, + "DEU-Voice Aliases.HugeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-20%|0" + } + }, + "DEU-Voice Aliases.LargeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-10%|0" + } + }, + "DEU-Voice Aliases.SmallTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|10%|0" + } + }, + "DEU-Voice Aliases.TinyTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|20%|0" + } + }, + "DEU-Voice Aliases.SuperscriptVoice": { + "schema": { + "title": "Superscript voice", + "description": "The voice used for superscript text.", + "type": "string", + "default": "*|40%|0" + } + }, + "DEU-Voice Aliases.SubscriptVoice": { + "schema": { + "title": "Subscript voice", + "description": "The voice used for subscript text.", + "type": "string", + "default": "*|-40%|0" + } + }, + "DEU-Voice Aliases.ShadowVoice": { + "schema": { + "title": "Shadow voice", + "description": "The voice used for shadow text.", + "type": "string", + "default": "*|0|0" + } + }, + "DEU-Voice Aliases.EmbossVoice": { + "schema": { + "title": "Emboss voice", + "description": "The voice used for emboss text.", + "type": "string", + "default": "*|0|0" + } + }, + "DEU-Voice Aliases.EngraveVoice": { + "schema": { + "title": "Engrave voice", + "description": "The voice used for engraved text.", + "type": "string", + "default": "*|0|0" + } + }, + "DEU-Voice Aliases.OutlineVoice": { + "schema": { + "title": "Outline voice", + "description": "The voice used for outlined text.", + "type": "string", + "default": "*|0|0" + } + }, + "DEU-Voice Aliases.HeadingLevel1Voice": { + "schema": { + "title": "HeadingLevel1 voice", + "description": "The voice used for heading 1 text.", + "type": "string", + "default": "Rocko|5%|0" + } + }, + "DEU-Voice Aliases.HeadingLevel2Voice": { + "schema": { + "title": "HeadingLevel2 voice", + "description": "The voice used for heading 2 text.", + "type": "string", + "default": "Rocko|10%|0" + } + }, + "DEU-Voice Aliases.HeadingLevel3Voice": { + "schema": { + "title": "HeadingLevel3 voice", + "description": "The voice used for heading 3 text.", + "type": "string", + "default": "Rocko|15%|0" + } + }, + "DEU-Voice Aliases.HeadingLevel4Voice": { + "schema": { + "title": "HeadingLevel4 voice", + "description": "The voice used for heading 4 text.", + "type": "string", + "default": "Rocko|20%|0" + } + }, + "DEU-Voice Aliases.HeadingLevel5Voice": { + "schema": { + "title": "HeadingLevel5 voice", + "description": "The voice used for heading 5 text.", + "type": "string", + "default": "Rocko|25%|0" + } + }, + "DEU-Voice Aliases.HeadingLevel6Voice": { + "schema": { + "title": "HeadingLevel6 voice", + "description": "The voice used for heading 6 text.", + "type": "string", + "default": "Rocko|30%|0" + } + }, + "DEU-Voice Aliases.LinkVoice": { + "schema": { + "title": "Link voice", + "description": "The voice used for link text.", + "type": "string", + "default": "Shelly" + } + }, + "DEU-Voice Aliases.SpellingVoice": { + "schema": { + "title": "Spelling voice", + "description": "The voice used for spelling text.", + "type": "string", + "default": "*|0|-25%" + } + }, + "DEU-Voice Aliases.QuotationVoice": { + "schema": { + "title": "Quotation voice", + "description": "The voice used for quoted text.", + "type": "string", + "default": "Glen|10%|0" + } + }, + "DEU-Voice Aliases.SingleCapVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "DEU-Voice Aliases.AllCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for all caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "DEU-Voice Aliases.SmallCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for small caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "DEU-Voice Aliases.OCRVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "Rocko|-5%|-5%" + } + }, + + // ------------ Italian - VOICE ALIASES -------------- + + "ITA-Voice Aliases.PCCursorVoice": { + "schema": { + "title": "PC Cursor Voice", + "description": "The voice to be used for the PC cursor.", + "type": "string", + "default": "Reed|0|0" + } + }, + "ITA-Voice Aliases.MessageVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Glen|0|0" + } + }, + "ITA-Voice Aliases.HeaderVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Reed|-20%|0" + } + }, + "ITA-Voice Aliases.JAWSCursorVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice used for JAWS cursor.", + "type": "string", + "default": "Reed|-10%|0" + } + }, + "ITA-Voice Aliases.NormalVoice": { + "schema": { + "title": "Normal voice", + "description": "The voice used for regular text.", + "type": "string", + "default": "*|0|0" + } + }, + "ITA-Voice Aliases.BoldVoice": { + "schema": { + "title": "Bold voice", + "description": "The voice used for bold text.", + "type": "string", + "default": "Rocko" + } + }, + "ITA-Voice Aliases.UnderlineVoice": { + "schema": { + "title": "Underline voice", + "description": "The underline used for underline text.", + "type": "string", + "default": "Shelly" + } + }, + "ITA-Voice Aliases.ItalicVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for italic text.", + "type": "string", + "default": "Grandpa" + } + }, + "ITA-Voice Aliases.StrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy" + } + }, + "ITA-Voice Aliases.DoubleStrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy|0|10%" + } + }, + "ITA-Voice Aliases.GraphicVoice": { + "schema": { + "title": "Graphic voice", + "description": "The voice used for graphics.", + "type": "string", + "default": "Grandma" + } + }, + "ITA-Voice Aliases.HighlightVoice": { + "schema": { + "title": "Highlight voice", + "description": "The voice used for highlighted text.", + "type": "string", + "default": "Bobby" + } + }, + "ITA-Voice Aliases.HugeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-20%|0" + } + }, + "ITA-Voice Aliases.LargeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-10%|0" + } + }, + "ITA-Voice Aliases.SmallTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|10%|0" + } + }, + "ITA-Voice Aliases.TinyTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|20%|0" + } + }, + "ITA-Voice Aliases.SuperscriptVoice": { + "schema": { + "title": "Superscript voice", + "description": "The voice used for superscript text.", + "type": "string", + "default": "*|40%|0" + } + }, + "ITA-Voice Aliases.SubscriptVoice": { + "schema": { + "title": "Subscript voice", + "description": "The voice used for subscript text.", + "type": "string", + "default": "*|-40%|0" + } + }, + "ITA-Voice Aliases.ShadowVoice": { + "schema": { + "title": "Shadow voice", + "description": "The voice used for shadow text.", + "type": "string", + "default": "*|0|0" + } + }, + "ITA-Voice Aliases.EmbossVoice": { + "schema": { + "title": "Emboss voice", + "description": "The voice used for emboss text.", + "type": "string", + "default": "*|0|0" + } + }, + "ITA-Voice Aliases.EngraveVoice": { + "schema": { + "title": "Engrave voice", + "description": "The voice used for engraved text.", + "type": "string", + "default": "*|0|0" + } + }, + "ITA-Voice Aliases.OutlineVoice": { + "schema": { + "title": "Outline voice", + "description": "The voice used for outlined text.", + "type": "string", + "default": "*|0|0" + } + }, + "ITA-Voice Aliases.HeadingLevel1Voice": { + "schema": { + "title": "HeadingLevel1 voice", + "description": "The voice used for heading 1 text.", + "type": "string", + "default": "Rocko|5%|0" + } + }, + "ITA-Voice Aliases.HeadingLevel2Voice": { + "schema": { + "title": "HeadingLevel2 voice", + "description": "The voice used for heading 2 text.", + "type": "string", + "default": "Rocko|10%|0" + } + }, + "ITA-Voice Aliases.HeadingLevel3Voice": { + "schema": { + "title": "HeadingLevel3 voice", + "description": "The voice used for heading 3 text.", + "type": "string", + "default": "Rocko|15%|0" + } + }, + "ITA-Voice Aliases.HeadingLevel4Voice": { + "schema": { + "title": "HeadingLevel4 voice", + "description": "The voice used for heading 4 text.", + "type": "string", + "default": "Rocko|20%|0" + } + }, + "ITA-Voice Aliases.HeadingLevel5Voice": { + "schema": { + "title": "HeadingLevel5 voice", + "description": "The voice used for heading 5 text.", + "type": "string", + "default": "Rocko|25%|0" + } + }, + "ITA-Voice Aliases.HeadingLevel6Voice": { + "schema": { + "title": "HeadingLevel6 voice", + "description": "The voice used for heading 6 text.", + "type": "string", + "default": "Rocko|30%|0" + } + }, + "ITA-Voice Aliases.LinkVoice": { + "schema": { + "title": "Link voice", + "description": "The voice used for link text.", + "type": "string", + "default": "Shelly" + } + }, + "ITA-Voice Aliases.SpellingVoice": { + "schema": { + "title": "Spelling voice", + "description": "The voice used for spelling text.", + "type": "string", + "default": "*|0|-25%" + } + }, + "ITA-Voice Aliases.QuotationVoice": { + "schema": { + "title": "Quotation voice", + "description": "The voice used for quoted text.", + "type": "string", + "default": "Glen|10%|0" + } + }, + "ITA-Voice Aliases.SingleCapVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "ITA-Voice Aliases.AllCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for all caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "ITA-Voice Aliases.SmallCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for small caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "ITA-Voice Aliases.OCRVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "Rocko|-5%|-5%" + } + }, + + // ------------ Portuguese (Brazil) - VOICE ALIASES -------------- + + "PTB-Voice Aliases.PCCursorVoice": { + "schema": { + "title": "PC Cursor Voice", + "description": "The voice to be used for the PC cursor.", + "type": "string", + "default": "Reed|0|0" + } + }, + "PTB-Voice Aliases.MessageVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Glen|0|0" + } + }, + "PTB-Voice Aliases.HeaderVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice to be used for messages.", + "type": "string", + "default": "Reed|-20%|0" + } + }, + "PTB-Voice Aliases.JAWSCursorVoice": { + "schema": { + "title": "PC Message Voice", + "description": "The voice used for JAWS cursor.", + "type": "string", + "default": "Reed|-10%|0" + } + }, + "PTB-Voice Aliases.NormalVoice": { + "schema": { + "title": "Normal voice", + "description": "The voice used for regular text.", + "type": "string", + "default": "*|0|0" + } + }, + "PTB-Voice Aliases.BoldVoice": { + "schema": { + "title": "Bold voice", + "description": "The voice used for bold text.", + "type": "string", + "default": "Rocko" + } + }, + "PTB-Voice Aliases.UnderlineVoice": { + "schema": { + "title": "Underline voice", + "description": "The underline used for underline text.", + "type": "string", + "default": "Shelly" + } + }, + "PTB-Voice Aliases.ItalicVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for italic text.", + "type": "string", + "default": "Grandpa" + } + }, + "PTB-Voice Aliases.StrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy" + } + }, + "PTB-Voice Aliases.DoubleStrikeoutVoice": { + "schema": { + "title": "Italic voice", + "description": "The voice used for strike-out text.", + "type": "string", + "default": "Sandy|0|10%" + } + }, + "PTB-Voice Aliases.GraphicVoice": { + "schema": { + "title": "Graphic voice", + "description": "The voice used for graphics.", + "type": "string", + "default": "Grandma" + } + }, + "PTB-Voice Aliases.HighlightVoice": { + "schema": { + "title": "Highlight voice", + "description": "The voice used for highlighted text.", + "type": "string", + "default": "Bobby" + } + }, + "PTB-Voice Aliases.HugeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-20%|0" + } + }, + "PTB-Voice Aliases.LargeTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|-10%|0" + } + }, + "PTB-Voice Aliases.SmallTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|10%|0" + } + }, + "PTB-Voice Aliases.TinyTextVoice": { + "schema": { + "title": "Huge Text voice", + "description": "The voice used for huge text.", + "type": "string", + "default": "*|20%|0" + } + }, + "PTB-Voice Aliases.SuperscriptVoice": { + "schema": { + "title": "Superscript voice", + "description": "The voice used for superscript text.", + "type": "string", + "default": "*|40%|0" + } + }, + "PTB-Voice Aliases.SubscriptVoice": { + "schema": { + "title": "Subscript voice", + "description": "The voice used for subscript text.", + "type": "string", + "default": "*|-40%|0" + } + }, + "PTB-Voice Aliases.ShadowVoice": { + "schema": { + "title": "Shadow voice", + "description": "The voice used for shadow text.", + "type": "string", + "default": "*|0|0" + } + }, + "PTB-Voice Aliases.EmbossVoice": { + "schema": { + "title": "Emboss voice", + "description": "The voice used for emboss text.", + "type": "string", + "default": "*|0|0" + } + }, + "PTB-Voice Aliases.EngraveVoice": { + "schema": { + "title": "Engrave voice", + "description": "The voice used for engraved text.", + "type": "string", + "default": "*|0|0" + } + }, + "PTB-Voice Aliases.OutlineVoice": { + "schema": { + "title": "Outline voice", + "description": "The voice used for outlined text.", + "type": "string", + "default": "*|0|0" + } + }, + "PTB-Voice Aliases.HeadingLevel1Voice": { + "schema": { + "title": "HeadingLevel1 voice", + "description": "The voice used for heading 1 text.", + "type": "string", + "default": "Rocko|5%|0" + } + }, + "PTB-Voice Aliases.HeadingLevel2Voice": { + "schema": { + "title": "HeadingLevel2 voice", + "description": "The voice used for heading 2 text.", + "type": "string", + "default": "Rocko|10%|0" + } + }, + "PTB-Voice Aliases.HeadingLevel3Voice": { + "schema": { + "title": "HeadingLevel3 voice", + "description": "The voice used for heading 3 text.", + "type": "string", + "default": "Rocko|15%|0" + } + }, + "PTB-Voice Aliases.HeadingLevel4Voice": { + "schema": { + "title": "HeadingLevel4 voice", + "description": "The voice used for heading 4 text.", + "type": "string", + "default": "Rocko|20%|0" + } + }, + "PTB-Voice Aliases.HeadingLevel5Voice": { + "schema": { + "title": "HeadingLevel5 voice", + "description": "The voice used for heading 5 text.", + "type": "string", + "default": "Rocko|25%|0" + } + }, + "PTB-Voice Aliases.HeadingLevel6Voice": { + "schema": { + "title": "HeadingLevel6 voice", + "description": "The voice used for heading 6 text.", + "type": "string", + "default": "Rocko|30%|0" + } + }, + "PTB-Voice Aliases.LinkVoice": { + "schema": { + "title": "Link voice", + "description": "The voice used for link text.", + "type": "string", + "default": "Shelly" + } + }, + "PTB-Voice Aliases.SpellingVoice": { + "schema": { + "title": "Spelling voice", + "description": "The voice used for spelling text.", + "type": "string", + "default": "*|0|-25%" + } + }, + "PTB-Voice Aliases.QuotationVoice": { + "schema": { + "title": "Quotation voice", + "description": "The voice used for quoted text.", + "type": "string", + "default": "Glen|10%|0" + } + }, + "PTB-Voice Aliases.SingleCapVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "PTB-Voice Aliases.AllCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for all caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "PTB-Voice Aliases.SmallCapsVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for small caps.", + "type": "string", + "default": "*|50%|0" + } + }, + "PTB-Voice Aliases.OCRVoice": { + "schema": { + "title": "Single Cap voice", + "description": "The voice used for single caps.", + "type": "string", + "default": "Rocko|-5%|-5%" + } + } + }, + "capabilitiesTransformations": { + // TODO-NOTE: This transforms should be dependent in the current "Options.PrimaryLanguage" + // setting, and change just the target ones. + + // ---------------------------- speechRate -------------------------------- + + /* + "ENU-Global\\.Rate": "http://registry\\.gpii\\.net/common/speechRate", + "ENU-JAWSCursor\\.Rate": "http://registry\\.gpii\\.net/common/speechRate", + "ENU-Keyboard\\.Rate": "http://registry\\.gpii\\.net/common/speechRate", + "ENU-MenuAndDialog\\.Rate": "http://registry\\.gpii\\.net/common/speechRate", + "ENU-Message\\.Rate": "http://registry\\.gpii\\.net/common/speechRate", + "ENU-PCCursor\\.Rate": "http://registry\\.gpii\\.net/common/speechRate", + */ + + // ------------------------------- Pitch -------------------------------- + + /* + "ENU-Global\\.Pitch": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 99, + "offset": 1 + } + } + } + }, + "ENU-JAWSCursor\\.Pitch": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 99, + "offset": 1 + } + } + } + }, + "ENU-Keyboard\\.Pitch": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 99, + "offset": 1 + } + } + } + }, + "ENU-MenuAndDialog\\.Pitch": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 99, + "offset": 1 + } + } + } + }, + "ENU-Message\\.Pitch": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 99, + "offset": 1 + } + } + } + }, + "ENU-PCCursor\\.Pitch": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 99, + "offset": 1 + } + } + } + }, + */ + + // ------------------------------- Volume -------------------------------- + + /* + "ENU-Global\\.Volume": { + "transform": { + "type": "fluid.transforms.binaryOp", + "operator": "+", + "left": 10, + "right": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volumeTTS", + "factor": 90 + } + } + } + }, + "ENU-JAWSCursor\\.Volume": { + "transform": { + "type": "fluid.transforms.binaryOp", + "operator": "+", + "left": 10, + "right": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volumeTTS", + "factor": 90 + } + } + } + }, + "ENU-Keyboard\\.Volume": { + "transform": { + "type": "fluid.transforms.binaryOp", + "operator": "+", + "left": 10, + "right": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volumeTTS", + "factor": 90 + } + } + } + }, + "ENU-MenuAndDialog\\.Volume": { + "transform": { + "type": "fluid.transforms.binaryOp", + "operator": "+", + "left": 10, + "right": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volumeTTS", + "factor": 90 + } + } + } + }, + "ENU-Message\\.Volume": { + "transform": { + "type": "fluid.transforms.binaryOp", + "operator": "+", + "left": 10, + "right": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volumeTTS", + "factor": 90 + } + } + } + }, + "ENU-PCCursor\\.Volume": { + "transform": { + "type": "fluid.transforms.binaryOp", + "operator": "+", + "left": 10, + "right": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volumeTTS", + "factor": 90 + } + } + } + }, + */ + + // ---------------------------- Punctuation -------------------------------- + + /* + "ENU-Global\\.Punctuation": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "match": { + "none": 0, + "some": 1, + "most": 2, + "all": 3 + } + } + }, + "ENU-JAWSCursor\\.Punctuation": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "match": { + "none": 0, + "some": 1, + "most": 2, + "all": 3 + } + } + }, + "ENU-Keyboard\\.Punctuation": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "match": { + "none": 0, + "some": 1, + "most": 2, + "all": 3 + } + } + }, + "ENU-MenuAndDialog\\.Punctuation": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "match": { + "none": 0, + "some": 1, + "most": 2, + "all": 3 + } + } + }, + "ENU-Message\\.Punctuation": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "match": { + "none": 0, + "some": 1, + "most": 2, + "all": 3 + } + } + }, + "ENU-PCCursor\\.Punctuation": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "match": { + "none": 0, + "some": 1, + "most": 2, + "all": 3 + } + } + }, + */ + + // -------------------------- SynthLangString -------------------------------- + + /* + // TODO: This should be shared between all variations within this solution + "ENU-Global\\.SynthLangString": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "match": { + "en-US": "American English", + "en-GB": "British English", + "fr": "French", + "fr-CA": "French", + "it": "Italian", + "pt-BR": "Brazilian Portuguese", + "de": "German", + "es-US": "Latin American Spanish", + "es-ES": "Castilian Spanish", + "fi": "Finnish" + } + } + }, + // TODO: This should be shared between all variations within this solution + "ENU-JAWSCursor\\.SynthLangString": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "match": { + "en-US": "American English", + "en-GB": "British English", + "fr": "French", + "fr-CA": "French", + "it": "Italian", + "pt-BR": "Brazilian Portuguese", + "de": "German", + "es-US": "Latin American Spanish", + "es-ES": "Castilian Spanish", + "fi": "Finnish" + } + } + }, + // TODO: This should be shared between all variations within this solution + "ENU-Keyboard\\.SynthLangString": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "match": { + "en-US": "American English", + "en-GB": "British English", + "fr": "French", + "fr-CA": "French", + "it": "Italian", + "pt-BR": "Brazilian Portuguese", + "de": "German", + "es-US": "Latin American Spanish", + "es-ES": "Castilian Spanish", + "fi": "Finnish" + } + } + }, + // TODO: This should be shared between all variations within this solution + "ENU-MenuAndDialog\\.SynthLangString": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "match": { + "en-US": "American English", + "en-GB": "British English", + "fr": "French", + "fr-CA": "French", + "it": "Italian", + "pt-BR": "Brazilian Portuguese", + "de": "German", + "es-US": "Latin American Spanish", + "es-ES": "Castilian Spanish", + "fi": "Finnish" + } + } + }, + // TODO: This should be shared between all variations within this solution + "ENU-Message\\.SynthLangString": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "match": { + "en-US": "American English", + "en-GB": "British English", + "fr": "French", + "fr-CA": "French", + "it": "Italian", + "pt-BR": "Brazilian Portuguese", + "de": "German", + "es-US": "Latin American Spanish", + "es-ES": "Castilian Spanish", + "fi": "Finnish" + } + } + }, + // TODO: This should be shared between all variations within this solution + "ENU-PCCursor\\.SynthLangString": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "match": { + "en-US": "American English", + "en-GB": "British English", + "fr": "French", + "fr-CA": "French", + "it": "Italian", + "pt-BR": "Brazilian Portuguese", + "de": "German", + "es-US": "Latin American Spanish", + "es-ES": "Castilian Spanish", + "fi": "Finnish" + } + } + } + */ + }, + "inverseCapabilitiesTransformations": { + // ---------------------------- speechRate -------------------------------- + + /* + "http://registry\\.gpii\\.net/common/speechRate": "ENU-Global\\.Rate", + */ + + // ------------------------------- Pitch -------------------------------- + + /* + "http://registry\\.gpii\\.net/common/pitch": { + "transform": { + "type": "fluid.transforms.round", + "scale": 1, + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "ENU-Global\\.Pitch", + "factor": 0.010101, + "offset": -0.010101 + } + } + } + }, + */ + + // ------------------------------- Volume -------------------------------- + + /* + "http://registry\\.gpii\\.net/common/volumeTTS": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "ENU-Global\\.Volume", + "factor": 0.01 + } + }, + */ + + // ---------------------------- Punctuation -------------------------------- + + /* + "http://registry\\.gpii\\.net/common/punctuationVerbosity": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "ENU-Global\\.Punctuation", + "match": [ + { + "inputValue": 0, + "outputValue": "none" + }, + { + "inputValue": 1, + "outputValue": "some" + }, + { + "inputValue": 2, + "outputValue": "most" + }, + { + "inputValue": 3, + "outputValue": "all" + } + ] + } + }, + */ + + // -------------------------- SynthLangString -------------------------------- + + /* + // TODO: Review the "common" language settings WRT various solution language settings. + "http://registry\\.gpii\\.net/common/auditoryOutLanguage": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "ENU-Global\\.SynthLangString", + "match": { + "American English": "en-US", + "British English": "en-GB", + "French": "fr", + "Italian": "it", + "Brazilian Portuguese": "pt-BR", + "German": "de", + "Latin American Spanish": "es-US", + "Castilian Spanish": "es-ES", + "Finnish": "fi" + } + } + } + */ + } + }, + "configuration3": { + "type": "gpii.settingsHandlers.INISettingsHandler", + "liveness": "manualRestart", + "options": { + "filename": "${{environment}.PROGRAMDATA}\\Freedom Scientific\\JAWS\\2019\\SETTINGS\\Init\\JFW.INI" + }, + "supportedSettings": { + // ----------------------- ------------------------- + // TODO: Find settings "Enable Remote Access Client" and "Enable Remote Access Server" + "Remote Access.UseVirtualSpeech": { + "schema": { + "title": "Virtual Speech", + "description": "Enable or disables virtual speech on startup.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Remote Access.UseVirtualBraille": { + "schema": { + "title": "Virtual Braille", + "description": "Enable or disables virtual braille on startup.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Remote Access.UsePACMateAuthorization": { + "schema": { + "title": "Use PAC Mate authorization", + "description": "Enable or disables PAC Mate authorization.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "Options.Run Jaws Without Speech": { + "schema": { + "title": "Run JAWS without speech", + "description": "Disable JAWS speech at application startup.", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + } + } + }, + "capabilitiesTransformations": { + }, + "inverseCapabilitiesTransformations": { + } + } + }, + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "jfw.exe" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\JAWS2019.exe\\}\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "jfw.exe", + "options": { + "closeWindow": true + } + } + ] + } + } + }, + "configure": [ + "settings.configuration1", + "settings.configuration2", + "settings.configuration3" + ], + "restore": [ + "settings.configuration2", + "settings.configuration1", + "settings.configuration3" + ], + "isInstalled": [ + { + "type": "gpii.deviceReporter.registryKeyExists", + "hKey": "HKEY_LOCAL_MACHINE", + "path": "64:SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\JAWS2019.exe", + "subPath": "", + "dataType": "REG_SZ" + } + ] + }, + "com.freedomscientific.magic": { + "name": "MAGic", + "contexts": { + "OS": [ + { + "id": "win32" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + "settingsHandlers": { + "configuration1": { + "type": "gpii.settingsHandlers.INISettingsHandler", + "liveness": "manualRestart", + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\MAGIC\\14.0\\Settings\\enu\\DEFAULT.MCF" + }, + "supportedSettings": { + "mag.caretbutton": { + schema: { + title: "Enable Mouse Scheme", + description: "Whether to enable the mouse pointer scheme.", + enum: [0,1], + enumLabels: ["Off", "On"], + default: 0 + } + }, + "mag.colorbutton": { + schema: { + title: "Enable Color Scheme", + description: "Whether to enable the color scheme.", + enum: [0,1], + enumLabels: ["Off", "On"], + default: 0 + } + }, + "mag.cursorbutton": { + schema: { + title: "Enable Cursor Scheme", + description: "Whether to enable the text cursor scheme.", + enum: [0,1], + enumLabels: ["Off", "On"], + default: 0 + } + }, + "mag.detectKeyboardInputLanguage": { + schema: { + title: "Detect Keyboard Language", + description: "Whether or not text echo should use the language of the keyboard itself rather than the language defined for the voice profile.", + enum: [0,1], + enumLabels: ["Off", "On"], + default: 1 + } + }, + "mag.DisabledScreenSaver": { + schema: { + title: "Disable Screen Saver", + description: "Whether or not to disable the screen saver while MAGic is running", + enum: [0,1], + enumLabels: ["Off", "On"], + default: 1 + } + }, + "mag.dynlensbordercolor": { + schema: { + title: "Dynamic Lens Border Color", + description: "The border color to use for the 'dynamic lens' view.", + enum: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + enumLabels: [ + "Black", + "White", + "Medium Gray", + "Light Gray", + "Dark Red", + "Red", + "Dark Yellow", + "Yellow", + "Dark Green", + "Green", + "Dark Cyan", + "Cyan", + "Dark Blue", + "Blue", + "Dark Magenta", + "Magenta" + ], + default: 5 + } + }, + "mag.dynlensborderwidth": { + schema: { + title: "Dynamic Lens Border Width", + description: "The border width in the 'dynamic lens' view.", + type: "integer", + minimum: 2, + maximum: 12, + default: 8 + } + }, + "mag.enablebilingual": { + schema: { + title: "Enable Bilingual Mode", + description: "Whether or not to enable 'bilingual mode' in MAGic. If this setting is enabled, MAGic will change its language when a voice profile is selected that uses a different language.", + enum: [0,1], + enumLabels: ["Off", "On"], + default: 0 + } + }, + "mag.enablemagicfrmsystray": { + schema: { + title: "Enable MAGic from System Tray", + description: "Whether or not to allow enabling and disabling of MAGic features from the system tray.", + enum: [0,1], + enumLabels: ["Off", "On"], + default: 0 + } + }, + "mag.Focus Enhancement Enabled": { + schema: { + title: "Focus Enhancement Enabled", + description: "Whether or not to enable focus enhancement.", + enum: [0,1], + enumLabels: ["Off", "On"], + default: 0 + } + }, + "mag.IndicateCaps": { + schema: { + title: "Indicate Caps During", + description: "When to indicate capital letters in echoing typed text.", + enum: [0,1,2], + enumLabels: [ + "Say Character and Spelling", + "Say Word,Character and Spelling", + "Say Line, Word, Character and Spelling" + ], + default: 0 + } + }, + "mag.IndicateCapsBy": { + schema: { + title: "Indicate Capitalization By", + description: "How to indicate capital letters in echoing typed text.", + enum: [0,1,2], + enumLabels: [ + "No Indication", + "Pitch Increment", + "Say 'Cap'" + ], + default: 1 + } + }, + // Custom focus color settings left in as a placeholder but not implemented. To support them we'd + // need to understand the color value better and create a picker for potentially millions of colors. + //"mag.Focus Enhancement Color": {} // 10485760 =Dark Blue + //"mag.Focus Enhancement Use Custom Color": {} // 1 = on + "mag.lensbordercolor": { + schema: { + title: "Lens Border Color", + description: "The border color to use for the 'lens' view.", + enum: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + enumLabels: [ + "Black", + "White", + "Medium Gray", + "Light Gray", + "Dark Red", + "Red", + "Dark Yellow", + "Yellow", + "Dark Green", + "Green", + "Dark Cyan", + "Cyan", + "Dark Blue", + "Blue", + "Dark Magenta", + "Magenta" + ], + default: 5 + } + }, + "mag.lensborderwidth": { + schema: { + title: "Lens Border Width", + description: "The width of the border in the 'lens' view.", + type: "integer", + minimum: 2, + maximum: 12, + default: 8 + } + }, + "mag.LowerOtherAppsVolumeWhileMagicIsRunning": { + schema: { + title: "Lower Other Apps' Volume", + description: "Whether or not the volume of other apps should be lowered when MAGic is running.", + enum: [0,1], + enumLabels: ["Off", "On"], + default: 0 + } + }, + "mag.overlaybordercolor": { + schema: { + title: "Lens Border Color", + description: "The border color to use for the 'lens' view.", + enum: [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15 + ], + enumLabels: [ + "Black", + "White", + "Medium Gray", + "Light Gray", + "Dark Red", + "Red", + "Dark Yellow", + "Yellow", + "Dark Green", + "Green", + "Dark Cyan", + "Cyan", + "Dark Blue", + "Blue", + "Dark Magenta", + "Magenta" + ], + default: 5 + } + }, + "mag.overlayborderwidth": { + schema: { + title: "Overlay Border Width", + description: "The width of the frame border in the 'split' view.", + type: "integer", + minimum: 2, + maximum: 12, + default: 8 + } + }, + "mag.promptmagictosave": { + schema: { + title: "Prompt to Save", + description: "Whether or not MAGic should prompt to save settings changes on shutdown.", + enum: [0,1], + enumLabels: ["Off", "On"], + default: 0 + } + }, + "mag.promptoverwriteschemes": { + schema: { + title: "Prompt When Overwriting Scheme", + description: "Whether or not to display a warning when attempting to save a color, cursor, or mouse scheme with the same name as an existing scheme.", + enum: [0,1], + enumLabels: ["Off", "On"], + default: 1 + } + }, + "mag.size": { + schema: { + title: "Magnification Level", + description: "How many times to magnify content when magnification is enabled.", + type: "number", + default: 1.10, + oneOf: [ + { minimum: 1, maximum: 1.9, multipleOf: 0.1}, + { minimum: 2, maximum: 3.75, multipleOf: 0.25}, + { minimum: 4, type: "integer"} + ] + } + }, + "mag.speechbutton": { + schema: { + title: "Enable Speech", + description: "Whether or not to announce onscreen text using text-to-speech.", + enum: [0,1], + enumLabels: ["Off", "On"], + default: 1 + } + }, + "mag.splitlocation": { + schema: { + title: "Split Location", + description: "The location where the 'split' view should appear when it is turned on.", + enum: [0,1,2,3], + enumLabels: [ + "Bottom", + "Top", + "Right", + "Left" + ], + default: 0 + } + }, + "mag.splitsize": { + schema: { + title: "Split Size", + description: "The size of the magnified portion of the screen in the 'split' view.", + enum: [0,1,3], + enumLabels: ["Quarter", "Half", "Custom"], + default: 1 + } + }, + "mag.splitbordercolor": { + schema: { + title: "Split Border Color", + description: "The color of the border between the magnified and unmagnified portions of the screen in the 'split' view.", + enum: [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15 + ], + enumLabels: [ + "Black", + "White", + "Medium Gray", + "Light Gray", + "Dark Red", + "Red", + "Dark Yellow", + "Yellow", + "Dark Green", + "Green", + "Dark Cyan", + "Cyan", + "Dark Blue", + "Blue", + "Dark Magenta", + "Magenta" + ], + default: 4 + } + + }, + "mag.splitframesize": { + schema: { + title: "Split Frame Size", + description: "The width of the frame border in the 'split' view.", + type: "integer", + minimum: 2, + maximum: 12, + default: 8 + } + }, + "mag.splitwindowheight": { + schema: { + title: "Split Window Height", + description: "The height of the magnified portion of the 'split' view when a horizontal (top or bottom) position is selected. Expressed as a percentage of screen height.", + type: "integer", + minimum: 0, + maximum: 100, + default: 50 + } + }, + "mag.splitwindowwidth": { + schema: { + title: "Split Window Width", + description: "The width of the magnified portion of the 'split' view when a vertical (left or right) position is selected. Expressed as a percentage of screen width.", + type: "integer", + minimum: 0, + maximum: 100, + default: 50 + } + }, + "mag.startmagicminimized": { + schema: { + title: "Start Minimized", + description: "Whether or not to minimize MAGic on startup.", + enum: [0,1], + enumLabels: ["Off", "On"], + default: 0 + } + }, + "mag.startmagnified": { + schema: { + title: "Start Magnified", + description: "Whether or not to turn magnification on when MAGic starts.", + enum: [0,1], + enumLabels: ["Off", "On"], + default: 1 + } + }, + "mag.TextViewerCharSpace": { + schema: { + title: "Text Viewer Character Spacing", + description: "The character spacing used by the text viewer.", + type: "integer", + minimum: 0, + maximum: 20, + default: 0 + } + }, + // There is no easy way to understand or set custom color values, use the indexed color scheme instead. + //"mag.TextViewerFontBkgColor": {}, + "mag.TextViewerFontBkgColorIndex": { + schema: { + title: "Text Viewer Font Background Color", + description: "The background color to use behind the text in the text viewer.", + // 15 is "Custom", but we have no way to set that, so we leave it out. + enum: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14], + enumLabels: [ + "Light Blue", + "Deep Sky Blue", + "Blue", + "Dark Blue", + "Purple Blue", + "Fuchsia", + "Red", + "Orange", + "Yellow", + "Lime Green", + "Green", + "Olive Drab", + "Saddle Brown", + "Burly Wood", + "Black", + "White" + ], + default: 14 // White + } + }, + // There is no easy way to understand or set custom color values, use the indexed color scheme instead. + //"mag.TextViewerFontColor": {}, + "mag.TextViewerFontColorIndex": { + schema: { + title: "Text Viewer Font Color", + description: "The font color to use in the text viewer.", + // 15 is "Custom", but we have no way to set that, so we leave it out. + enum: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14], + enumLabels: [ + "Light Blue", + "Deep Sky Blue", + "Blue", + "Dark Blue", + "Purple Blue", + "Fuchsia", + "Red", + "Orange", + "Yellow", + "Lime Green", + "Green", + "Olive Drab", + "Saddle Brown", + "Burly Wood", + "Black", + "White" + ], + default: 13 // Black + } + }, + "mag.TextViewerFontFaceName": { + schema: { + title: "Text Viewer Font", + description: "The font to use when displaying text using the text viewer.", + type: "string", + default: "Arial" + } + }, + "mag.TextViewerFontSize": { + schema: { + title: "Text Viewer Font Size", + description: "The size of the font to use when displaying text using the text viewer.", + type: "integer", + default: 50 + } + }, + "mag.TextViewerFontStyle": { + schema: { + title: "Text Viewer Font Style", + description: "The font style to use with the text viewer.", + enum: [0,1,2], + enumLabels: ["Original", "Regular", "Bold"], + default: 0 + } + + }, + "mag.TextViewerFontEffect": { + schema: { + title: "Text Viewer Font Effect", + description: "The font effect to use in the text viewer.", + enum: [0,1,2], + enumLabels: ["Small Caps", "All Caps", "None"], + default: 2 + } + }, + "mag.TextViewerOn": { + schema: { + title: "Enable Text Viewer", + description: "Whether or not to enable the text viewer.", + enum: [0,1], + enumLabels: ["Off", "On"], + default: 1 + } + }, + "mag.TextViewerPosition": { + schema: { + title: "Text Viewer Position", + description: "The position of text viewer content onscreen.", + enum: [0,1], + enumLabels: ["Bottom", "Top"], + default: 0 + } + }, + "mag.TextViewerSpotlightStyle": { + schema: { + title: "Text Viewer Spotlight Style", + description: "The style of 'spotlight' to use with the text viewer.", + enum: [0,1,2], + enumLabels: ["Box", "Underline", "Block"], + default: 0 + } + }, + "mag.TextViewerSpotlightTransparency": { + schema: { + title: "Text Viewer Spotlight Transparency", + description: "How transparent the text viewer 'spotlight' should be.", + type: "integer", + minimum: 0, + maximum: 100, + default: 0 + } + }, + // There is no easy way to understand or set custom color values, use the indexed color scheme instead. + //"mag.TextViewerSpotlightColor": {}, + "mag.TextViewerSpotlightColorIndex": { + schema: { + title: "Text Viewer Spotlight Color", + description: "The spotlight color to use in the text viewer.", + // 15 is "Custom", but we have no way to set that, so we leave it out. + enum: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14], + enumLabels: [ + "Light Blue", + "Deep Sky Blue", + "Blue", + "Dark Blue", + "Purple Blue", + "Fuchsia", + "Red", + "Orange", + "Yellow", + "Lime Green", + "Green", + "Olive Drab", + "Saddle Brown", + "Burly Wood", + "Black", + "White" + ], + default: 6 // Red + } + }, + // There is no easy way to understand or set custom color values, use the indexed color scheme instead. + //"mag.TextViewerSpotlightWordColor": {}, + "mag.TextViewerSpotlightWordColorIndex": { + schema: { + title: "Text Viewer Spotlight Text Color", + description: "The spotlight text color to use in the text viewer.", + // 15 is "Custom", but we have no way to set that, so we leave it out. + enum: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14], + enumLabels: [ + "Light Blue", + "Deep Sky Blue", + "Blue", + "Dark Blue", + "Purple Blue", + "Fuchsia", + "Red", + "Orange", + "Yellow", + "Lime Green", + "Green", + "Olive Drab", + "Saddle Brown", + "Burly Wood", + "Black", + "White" + ], + default: 8 // Yellow + } + }, + "mag.TextViewerSpotlightThickness": { + schema: { + title: "Text Viewer Spotlight Thickness", + description: "The thickness (line width) used by the text viewer 'spotlight' when a relevant style (such as 'Box') is selected.", + type: "integer", + minimum: 0, + maximum: 10, + default: 0 + } + }, + "mag.TextViewerTrackingMode": { + schema: { + title: "Text Viewer Tracking Mode", + description: "The tracking mode to use with the text viewer.", + enum: [0,1], + enumLabels: ["Auto Advance", "Center"], + default: 0 + } + }, + "mag.TextViewerWordSpace": { + schema: { + title: "Text Viewer Word Spacing", + description: "The word spacing used by the text viewer.", + type: "integer", + minimum: 0, + maximum: 20, + default: 0 + } + }, + "mag.typingecho": { + schema: { + title: "Typing Echo Unit", + description: "The 'unit' to announce when echoing typed text (words, characters, or both).", + enum: [1,2,3], + // In JAWS, zero is also an option, but the MAGic interface doesn't allow you to select that, so it's omitted here. + enumLabels: [ + "Characters", + "Words", + "Characters and Words" + ], + default: 1 + } + }, + "mag.typeechobutton": { + schema: { + title: "Enable Typing Echo", + description: "Whether or not to enable the typing echo when announcing content.", + enum: [0,1], + enumLabels: ["Off", "On"], + default: 1 + } + }, + "mag.viewtype": { + schema: { + title: "View Type", + description: "How to display magnified content when magnification is active.", + enum: [0, 1, 2, 3, 4, 5], + enumLabels: [ + "Full", + "Split", + "Overlay", + "Lens", + "Dynamic Lens", + "Multi-Monitor" + ], + default: 0 + } + }, + "Magnification": { + "schema": { + "title": "Magnification", + "description": "Set up magnification level", + "type": "integer", + "default": 200, + "minimum": 100, + "maximum": 1600 + } + }, + "MagnificationMode": { + "schema": { + "title": "Magnifier position", + "description": "Position of the magnified area", + "default": 2, + "enum": [ + 2, + 3 + ], + "enumLabels": [ + "Top Half", + "Full Screen", + "Lens" + ] + } + }, + "Options.SkipILM": { + schema: { + title: "Skip License Manager Dialog", + description: "Whether or not to display the license manager on startup.", + enum: [0,1], + enumLabels: ["Off", "On"], + default: 0 + } + }, + "scheme.ColorSchemeModified": { + schema: { + title: "Color Scheme Modified", + description: "Whether or not the chosen color scheme has been modified from its defaults.", + enum: [0,1], + enumLabels: ["Off", "On"], + default: 0 + } + }, + "scheme.Color_Scheme": { + schema: { + title: "Color Scheme", + description: "The color scheme to use.", + enum: [ + "Invert Brightness+Color", + "Autumn Colors+Color", + "Double Invert+Color", + "Grayscale+Color", + "Grayscale (Inverted)+Color", + "Green on Black+Color", + "Invert Colors+Color", + "Yellow on Black+Color" + ], + enumLabels: [ + "Invert Brightness", + "Autumn Colors", + "Double Invert", + "Grayscale", + "Grayscale (Inverted)", + "Green on Black", + "Invert Colors", + "Yellow on Black" + ], + default: "Invert Brightness+Color" + } + }, + "scheme.CursorSchemeModified": { + schema: { + title: "Cursor Scheme Modified", + description: "Whether or not the chosen cursor scheme has been modified from its defaults.", + enum: [0,1], + enumLabels: ["Off", "On"], + default: 0 + } + }, + "scheme.Cursor_Scheme": { + schema: { + title: "Cursor Scheme", + description: "How to represent the text cursor.", + enum: [ + "Sky Blue Oval+Cursor", + "Flat Orange Triangle+Cursor", + "Light Blue Long Oval+Cursor", + "Little Red Triangle+Cursor", + "Purple Long Box+Cursor" + ], + enumLabels: [ + "Sky Blue Oval", + "Flat Orange Triangle", + "Light Blue Long Oval", + "Little Red Triangle", + "Purple Long Box" + ], + default: "Sky Blue Oval+Cursor" + } + }, + "scheme.MouseSchemeModified": { + schema: { + title: "Mouse Scheme Modified", + description: "Whether or not the chosen mouse scheme has been modified from its defaults.", + enum: [ 0, 1 ], + enumLabels: ["Off", "On"], + default: 0 + } + }, + "scheme.Mouse_Scheme": { + schema: { + title: "Mouse Scheme", + description: "How to represent the mouse pointer.", + enum: [ + "3D Blue with Oval+Mouse", + "Big Green+Mouse", + "Bold with Short Cross+Mouse", + "Bold Yellow+Mouse", + "Bold Yellow with Full Cross+Mouse", + "MAGic Key Mouse Finder+Mouse", + "Scope When Moving+Mouse" + ], + enumLabels: [ + "3D Blue with Oval", + "Big Green", + "Bold with Short Cross", + "Bold Yellow", + "Bold Yellow with Full Cross", + "MAGic Key Mouse Finder", + "Scope When Moving" + ], + default: "3D Blue with Oval+Mouse" + } + } + }, + "capabilitiesTransformations": { + "mag\\.size": "http://registry\\.gpii\\.net/common/magnification", + "mag\\.TextViewerFontSize": "http://registry\\.gpii\\.net/common/fontSize", + "mag\\.startmagnified": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/common/magnification/enabled", + "true": 1, + "false": 0 + } + }, + "scheme\\.Mouse_Scheme": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/common/showCrosshairs", + "true": "Bold Yellow with Full Cross+Mouse", + "false": "3D Blue with Oval+Mouse" + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/magnification": "mag\\.size", + "http://registry\\.gpii\\.net/common/fontSize": "mag\\.TextViewerFontSize", + "http://registry\\.gpii\\.net/common/showCrosshairs": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "scheme\\.Mouse_Scheme", + "match": [ + { + inputValue: "Bold with Short Cross+Mouse", + outputValue: true + }, + { + inputValue: "Bold Yellow with Full Cross+Mouse", + outputValue: true + }, + { + inputValue: "Scope When Moving+Mouse", + outputValue: true + } + ], + noMatch: { + outputUndefinedValue: true + } + } + } + } + } + // MAGic also supports a subset of JAWS settings using its built-in "JAWS lite". + // "configuration2": { + // "type": "gpii.settingsHandlers.INISettingsHandler", + // "liveness": "manualRestart", + // "options": { + // "filename": "${{environment}.APPDATA}\\Freedom Scientific\\MAGIC\\14.0\\Settings\\enu\\DEFAULT.JCF" + // }, + // "supportedSettings": { + // "Options.AllowMouseEchoWhenMuted": { + // "schema": { + // "title": "Allow Mouse Echo", + // "description": "Whether to echo mouse movement when otherwise muted.", + // "enum": [ + // 0, + // 1 + // ], + // "enumLabels": [ + // "off", + // "on" + // ], + // "default": 0 + // } + // }, + // "Options.AllowSpeechOnDemandWhenMuted": { + // "schema": { + // "title": "Allow Reading Commands", + // "description": "Whether to allow speech on demand when otherwise muted.", + // "enum": [ + // 0, + // 1 + // ], + // "enumLabels": [ + // "off", + // "on" + // ], + // "default": 1 + // } + // }, + // "Options.AllowTypingEchoWhenMuted": { + // "schema": { + // "title": "Allow Typing Echo", + // "description": "Whether to echo typed keys when otherwise muted.", + // "enum": [ + // 0, + // 1 + // ], + // "enumLabels": [ + // "off", + // "on" + // ], + // "default": 0 + // } + // }, + // "Options.IndicateCaps": { + // "schema": { + // "title": "Indicate Caps", + // "description": "When to indicate the presence of capital letters.", + // "type": "number", + // "default": 0, + // "enum": [ + // 0, + // 1, + // 2, + // 3 + // ], + // "enumLabels": [ + // "Off", + // "indicate caps when spelling or when navigating by character", + // "also indicate caps when reading by words", + // "also indicate caps when reading by lines" + // ] + // } + // }, + // "Options.InitialNumlockState": { + // "schema": { + // "title": "Initial State of NumLock Key", + // "description": "The initial state of the NumLock key.", + // "type": "number", + // "enum": [ + // 0, + // 1, + // 2 + // ], + // "enumLabels": [ + // "off at startup", + // "on at startup", + // "leave unmodified" + // ], + // "default": 0 + // } + // }, + // "Options.MouseMovementStopsSpeech": { + // "schema": { + // "title": "Mouse Echo Interrupt", + // "description": "Whether to stop speaking when the mouse is moved.", + // "enum": [ + // 0, + // 1 + // ], + // "enumLabels": [ + // "off", + // "on" + // ], + // "default": 1 + // } + // }, + // "Options.MouseSpeechDelay": { + // "schema": { + // "title": "Mouse Echo Delay", + // "description": "How long to delay announcing content under the mouse, in milliseconds. Defaults to 0 (no delay).", + // "type": "integer", + // "default": 0 + // } + // }, + // "Options.MouseSpeechEchoUnit": { + // "schema": { + // "title": "Mouse Echo Unit", + // "description": "The unit of text to announce when the mouse pauses over text in a control. Defaults to reading the whole line.", + // "enum": [ + // 0, + // 1, + // 2, + // 3 + // ], + // "enumLabels": [ + // "character", + // "word", + // "line", + // "paragraph" + // ], + // "default": 2 + // } + // }, + // "Options.MouseSpeechEnabled": { + // "schema": { + // "title": "Enable Mouse Echo", + // "description": "Whether or not to enable the mouse echo, i.e. reading content under the mouse pointer.", + // "enum": [ + // 0, + // 1 + // ], + // "enumLabels": [ + // "off", + // "on" + // ], + // "default": 0 + // } + // }, + // "Options.TypingEcho": { + // "schema": { + // "title": "Typing echo", + // "description": "How to announce typed words and/or characters.", + // "type": "number", + // "default": 1, + // "enum": [ + // 0, + // 1, + // 2, + // 3 + // ], + // "enumLabels": [ + // "don't announce characters or words", + // "announce characters", + // "announce words", + // "announce characters and words" + // ] + // } + // } + // }, + // "capabilitiesTransformations": { + // "Options\\.TypingEcho": { + // // (1=keyEcho, 2=wordEcho, 3=key&wordEcho) + // "transform": { + // "type": "fluid.transforms.condition", + // "condition": { + // "transform": { + // "type": "fluid.transforms.binaryOp", + // "leftPath": "http://registry\\.gpii\\.net/common/keyEcho", + // "rightPath": "http://registry\\.gpii\\.net/common/wordEcho", + // "operator": "&&" + // } + // }, + // "true": 3, + // "false": { + // "transform": { + // "type": "fluid.transforms.condition", + // "conditionPath": "http://registry\\.gpii\\.net/common/keyEcho", + // "true": 1, + // "false": { + // "transform": { + // "type": "fluid.transforms.condition", + // "conditionPath": "http://registry\\.gpii\\.net/common/wordEcho", + // "true": 2 + // } + // } + // } + // } + // } + // } + // }, + // "inverseCapabilitiesTransformations": { + // "http://registry\\.gpii\\.net/common/keyEcho": { + // "transform": { + // "type": "valueMapper", + // "defaultInputPath": "Options\\.TypingEcho", + // // (1=keyEcho, 2=wordEcho, 3=key&wordEcho) + // "match": [ + // { + // "inputValue": 1, + // "outputValue": true + // }, + // { + // "inputValue": 3, + // "outputValue": true + // } + // ], + // "noMatch": { + // outputUndefinedValue: true + // } + // } + // }, + // "http://registry\\.gpii\\.net/common/wordEcho": { + // "transform": { + // "type": "valueMapper", + // "defaultInputPath": "Options\\.TypingEcho", + // // (1=keyEcho, 2=wordEcho, 3=key&wordEcho) + // "match": [ + // { + // "inputValue": 2, + // "outputValue": true + // }, + // { + // "inputValue": 3, + // "outputValue": true + // } + // ], + // "noMatch": { + // outputUndefinedValue: true + // } + // } + // } + // } + // } */ + }, + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "verifySettings": true, + "retryOptions": { + "rewriteEvery": 0, + "numRetries": 20 + }, + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "Magic.exe" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\MAGic140.exe\\}\"" + } + ], + "setFalse": [ + // In MAGic 14.0, the following processes show up in the task manager under the app's grouping: + // magutil.exe, C:\Program Files\Freedom Scientific\MAGic\14.0\MagUtil + // jhookldr.exe, C:\Program Files\Freedom Scientific\MAGic\14.0\MagUtil\x86 + // mag.exe, C:\Program Files\Freedom Scientific\MAGic\14.0 + // MagHook32.exe, C:\Program Files\Freedom Scientific\MAGic\14.0\x86 + // Magic.exe, C:\Program Files\Freedom Scientific\MAGic\14.0 + // Unloader.exe, C:\Program Files\Freedom Scientific\MAGic\14.0 + // + // Also found outside the grouping are these stragglers: + // fsSynth32.exe, C:\Program Files (x86)\Freedom Scientific\Shared\fsSynth\2.0 + // fsATProxy.exe, C:\Program Files\Freedom Scientific\MAGic\14.0 + // + // + { + "type": "gpii.windows.closeProcessByName", + "filename": "Unloader.exe" + }, + { + "type": "gpii.windows.closeProcessByName", + "filename": "Magic.exe" + }, + { + "type": "gpii.windows.closeProcessByName", + "filename": "MagHook32.exe" + }, + { + "type": "gpii.windows.closeProcessByName", + "filename": "fsATProxy.exe" + }, + { + "type": "gpii.windows.closeProcessByName", + "filename": "fsSynth32.exe" + }, + { + "type": "gpii.windows.closeProcessByName", + "filename": "mag.exe" + }, + { + "type": "gpii.windows.closeProcessByName", + "filename": "jhookldr.exe" + }, + { + "type": "gpii.windows.closeProcessByName", + "filename": "magutil.exe" + } + ] + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.registryKeyExists", + "hKey": "HKEY_LOCAL_MACHINE", + "path": "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\MAGic140.exe", + "subPath": "", + "dataType": "REG_SZ" + } + ] + }, + "com.microsoft.windows.brightness": { + "name": "Windows brightness settings", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.windows.wmiSettingsHandler", + "liveness": "live", + "options": { + "Brightness": { + "namespace": "root\\WMI", + "get": { "query": "SELECT CurrentBrightness FROM WmiMonitorBrightness" }, + "set": { + "className": "WmiMonitorBrightnessMethods", + "method": "WmiSetBrightness", + "params": [0xFFFFFFFF, "$value"], + "returnVal": ["uint", 0] + }, + "settingType": "uint" + } + }, + "capabilitiesTransformations": { + "Brightness": { + "value": "http://registry\\.gpii\\.net/common/screenBrightness" + } + }, + "supportedSettings": { + "Brightness": { + "schema": { + "title": "Brightness", + "description": "Set the screen brightness.", + "type": "object", + "properties": { + "value": { + "type": "number", + "default": 0, + "minimum": 0, + "maximum": 100 + } + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/screenBrightness": "Brightness.value" + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.wmiSettingSupported", + "namespace": "root\\WMI", + "query": "SELECT CurrentBrightness FROM WmiMonitorBrightness" + } + ] + }, + "com.microsoft.windows.desktopBackground": { + "name": "Windows desktop background personalization", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configureWallpaperStyle": { + "type": "gpii.windows.registrySettingsHandler", + "liveness": "liveRestart", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Control Panel\\Desktop", + "dataTypes": { + "TileWallpaper": "REG_SZ", + "WallpaperStyle": "REG_SZ", + "Scaling": "REG_SZ" + } + }, + "supportedSettings": { + "TileWallpaper": { + "schema": { + "title": "Desktop wallpaper tiling", + "description": "Sets the wallpaper to tiling style.", + "enum": [ + "0", + "1" + ], + "enumLabels": [ + "off", + "on" + ] + } + }, + "WallpaperStyle": { + "schema": { + "title": "Desktop wallpaper style", + "description": "Sets the wallpaper style.", + "enum": [ + "0", + "2", + "6", + "10", + "22" + ], + "enumLabels": [ + "None", + "Stretch", + "Fill", + "Fit", + "Span" + ] + } + }, + // TODO: Use this as an early test case for user-facing vs. settings handler facing work. + "Scaling": { + "schema": { + "title": "Desktop wallpaper scaling", + "description": "Sets the wallpaper to scaling type.", + "enum": [ + "Fill", + "Fit", + "Stretch", + "Tile", + "Center", + "Span" + ], + "enumLabels": [ + "Fill", + "Fit", + "Stretch", + "Tile", + "Center", + "Span" + ] + } + } + }, + "capabilitiesTransformations": { + "TileWallpaper": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.desktopBackground.Scaling", + "match": { + "Fill": "0", + "Fit": "0", + "Stretch": "0", + "Tile": "1", + "Center": "0", + "Span": "0" + }, + "noMatch": { + "outputValue": "0" + } + } + }, + "WallpaperStyle": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.desktopBackground.Scaling", + "match": { + "Fill": "6", + "Fit": "10", + "Stretch": "2", + "Tile": "0", + "Center": "0", + "Span": "22" + }, + "noMatch": { + "outputValue": "6" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.desktopBackground.Scaling": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "WallpaperStyle", + "match": { + "6": "Fill", + "10": "Fit", + "2": "Stretch", + "0": { + "outputValue": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "TileWallpaper", + "match": { + "1": "Tile", + "0": "Center" + }, + "noMatch": { + "outputValue": "Tile" + } + } + } + }, + "22": "Span" + }, + "noMatch": { + "outputValue": "Fill" + } + } + } + } + }, + "configureImage": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETDESKWALLPAPER", + "setAction": "SPI_SETDESKWALLPAPER", + "uiParam": 260, + "pvParam": { + "type": "array", + "valueType": "TCHAR", + "length": 260 + } + }, + // TODO: Discuss somehow simplifying the structure of SPI settings for easier UI input. + "supportedSettings": { + "ImageConfig": { + "schema": { + "title": "Desktop background wallpaper", + "description": "The path to the image to be set as the new desktop wallpaper.", + "type": "object", + "properties": { + "path": { "type": "string", "required": false}, + "value": { + "type": "string", + "default": "%SystemRoot%\\Web\\Wallpaper\\Windows\\img0.jpg" + } + } + } + } + }, + "capabilitiesTransformations": { + "ImageConfig": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.desktopBackground.Image", + "path": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": "pvParam" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.desktopBackground.Image": "ImageConfig.value" + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.desktopBackgroundColor": { + "name": "Windows desktop background color", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configureImage": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETDESKWALLPAPER", + "setAction": "SPI_SETDESKWALLPAPER", + "uiParam": 260, + "pvParam": { + "type": "array", + "valueType": "TCHAR", + "length": 260 + } + }, + // TODO: Discuss somehow simplifying the structure of SPI settings for easier UI input. + "supportedSettings": { + "ImageConfig": { + "schema": { + "title": "Desktop background wallpaper", + "description": "The path to the image to be set as the new desktop wallpaper.", + "type": "object", + "properties": { + "path": { "type": "string", "required": false}, + "value": { + "type": "string", + "required": true, + "default": "" + } + } + } + } + }, + "capabilitiesTransformations": { + "ImageConfig": { + "value": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": "" + } + }, + "path": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": "pvParam" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.desktopBackgroundColor.Image": "ImageConfig.value" + } + }, + "configureSolidColor": { + "type": "gpii.windows.nativeSettingsHandler", + "liveness": "live", + "options": { + "functionName": "SolidColor" + }, + "supportedSettings": { + "SolidColorConfig": { + "schema": { + "title": "Desktop background color", + "description": "The color to be set as desktop background.", + "type": "object", + "additionalProperties": false, + "properties": { + "r": { + "type": "integer", + "minimum": 0, + "maximum": 255, + "default": 0, + "required": true + }, + "g": { + "type": "integer", + "minimum": 0, + "maximum": 255, + "default": 0, + "required": true + }, + "b": { + "type": "integer", + "minimum": 0, + "maximum": 255, + "default": 0, + "required": true + } + } + } + } + }, + "capabilitiesTransformations": { + "SolidColorConfig": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.desktopBackgroundColor.SolidColor" + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.desktopBackgroundColor.SolidColor": "SolidColorConfig.value" + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.cursors": { + "name": "Windows Cursors", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.windows.registrySettingsHandler", + "liveness": "liveRestart", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Control Panel\\Cursors", + "dataTypes": { + "AppStarting": "REG_SZ", + "Arrow": "REG_SZ", + "Crosshair": "REG_SZ", + "Hand": "REG_SZ", + "Help": "REG_SZ", + "IBeam": "REG_SZ", + "No": "REG_SZ", + "NWPen": "REG_SZ", + "SizeAll": "REG_SZ", + "SizeNESW": "REG_SZ", + "SizeNS": "REG_SZ", + "SizeNWSE": "REG_SZ", + "SizeWE": "REG_SZ", + "UpArrow": "REG_SZ", + "Wait": "REG_SZ" + } + }, + "supportedSettings": { + "AppStarting": { + schema: { + title: "Application Startup Cursor", + description: "The animated cursor that displays while an application is starting up.", + type: "string", + default: "%SystemRoot%\\cursors\\wait_i.ani" + } + }, + "Arrow": { + schema: { + title: "Arrow Cursor", + description: "The 'arrow' cursor icon.", + type: "string", + default: "%SystemRoot%\\cursors\\arrow_i.cur" + } + }, + "Crosshair": { + schema: { + title: "Crosshair", + description: "The 'crosshair' cursor to use.", + type: "string" + } + }, + "Hand": { + schema: { + title: "Hand Cursor", + description: "The 'hand' cursor icon.", + type: "string", + default: "%SystemRoot%\\cursors\\aero_link.cur" + } + }, + "Help": { + schema: { + title: "Help Cursor", + description: "The 'help' cursor icon.", + type: "string", + default: "%SystemRoot%\\cursors\\help_i.cur" + } + }, + "IBeam": { + schema: { + title: "IBeam", + description: "The 'IBeam' cursor to use.", + type: "string" + } + }, + "No": { + schema: { + title: "'No' Cursor", + description: "The cursor icon used to indicate that the resource under the pointer cannot be clicked.", + type: "string", + default: "%SystemRoot%\\cursors\\no_l.cur" + } + }, + "NWPen": { + schema: { + title: "Pen Cursor", + description: "The 'pen' cursor icon.", + type: "string", + default: "%SystemRoot%\\cursors\\pen_i.cur" + } + }, + "SizeAll": { + schema: { + title: "'Resize All' Cursor", + description: "The 'resize all' cursor icon.", + type: "string", + default: "%SystemRoot%\\cursors\\move_i.cur" + } + }, + "SizeNESW": { + schema: { + title: "'Resize NESW' Cursor", + description: "The 'resize NESW' cursor icon.", + type: "string", + default: "%SystemRoot%\\cursors\\size1_i.cur" + } + }, + "SizeNS": { + schema: { + title: "'Resize Height' Cursor", + description: "The 'resize height' cursor icon.", + type: "string", + default: "%SystemRoot%\\cursors\\size4_i.cur" + } + }, + "SizeNWSE": { + schema: { + title: "'Resize NWSE' Cursor", + description: "The 'resize NWSE' cursor icon.", + type: "string", + default: "%SystemRoot%\\cursors\\size2_i.cur" + } + }, + "SizeWE": { + schema: { + title: "'Resize WE' Cursor", + description: "The 'resize WE' cursor icon.", + type: "string", + default: "%SystemRoot%\\cursors\\size3_i.cur" + } + }, + "UpArrow": { + schema: { + title: "'Up Arrow' Cursor", + description: "The 'up arrow' cursor icon.", + type: "string", + default: "%SystemRoot%\\cursors\\up_i.cur" + } + }, + "Wait": { + schema: { + title: "'Wait' Cursor", + description: "The cursor icon to display while the system is busy.", + type: "string", + default: "%SystemRoot%\\cursors\\busy_i.ani" + } + } + }, + "capabilitiesTransformations": { + "AppStarting": { + "transform": { + "type": "fluid.transforms.quantize", + "inputPath": "http://registry\\.gpii\\.net/common/cursorSize", + "ranges": [ + { + "upperBound": 0.333, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "", + "Black": "%SystemRoot%\\cursors\\wait_r.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\wait_i.cur" + }, + "noMatch": { + "outputValue": "" + } + } + } + }, { + "upperBound": 0.666, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\wait_m.cur", + "Black": "%SystemRoot%\\cursors\\wait_rm.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\wait_im.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\wait_im.cur" + } + } + } + }, { + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\wait_l.cur", + "Black": "%SystemRoot%\\cursors\\wait_rl.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\wait_il.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\wait_l.cur" + } + } + } + } + ] + } + }, + "Arrow": { + "transform": { + "type": "fluid.transforms.quantize", + "inputPath": "http://registry\\.gpii\\.net/common/cursorSize", + "ranges": [ + { + "upperBound": 0.333, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "", + "Black": "%SystemRoot%\\cursors\\arrow_r.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\arrow_i.cur" + }, + "noMatch": { + "outputValue": "" + } + } + } + }, { + "upperBound": 0.666, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\arrow_m.cur", + "Black": "%SystemRoot%\\cursors\\arrow_rm.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\arrow_im.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\arrow_m.cur" + } + } + } + }, { + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\arrow_l.cur", + "Black": "%SystemRoot%\\cursors\\arrow_rl.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\arrow_il.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\arrow_l.cur" + } + } + } + } + ] + } + }, + "Crosshair": { + "transform": { + "type": "fluid.transforms.quantize", + "inputPath": "http://registry\\.gpii\\.net/common/cursorSize", + "ranges": [ + { + "upperBound": 0.333, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "", + "Black": "%SystemRoot%\\cursors\\cross_r.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\cross_i.cur" + }, + "noMatch": { + "outputValue": "" + } + } + } + }, { + "upperBound": 0.666, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\cross_m.cur", + "Black": "%SystemRoot%\\cursors\\cross_rm.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\cross_im.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\cross_m.cur" + } + } + } + }, { + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\cross_l.cur", + "Black": "%SystemRoot%\\cursors\\cross_rl.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\cross_il.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\cross_l.cur" + } + } + } + } + ] + } + }, + "Hand": { + "transform": { + "type": "fluid.transforms.quantize", + "inputPath": "http://registry\\.gpii\\.net/common/cursorSize", + "ranges": [ + { + "upperBound": 0.333, + "output": "%SystemRoot%\\cursors\\aero_link.cur" + }, { + "upperBound": 0.666, + "output": "%SystemRoot%\\cursors\\aero_link_im.cur" + }, { + "output": "%SystemRoot%\\cursors\\aero_link_l.cur" + } + ] + } + }, + "Help": { + "transform": { + "type": "fluid.transforms.quantize", + "inputPath": "http://registry\\.gpii\\.net/common/cursorSize", + "ranges": [ + { + "upperBound": 0.333, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "", + "Black": "%SystemRoot%\\cursors\\help_r.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\help_i.cur" + }, + "noMatch": { + "outputValue": "" + } + } + } + }, { + "upperBound": 0.666, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\help_m.cur", + "Black": "%SystemRoot%\\cursors\\help_rm.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\help_im.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\help_m.cur" + } + } + } + }, { + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\help_l.cur", + "Black": "%SystemRoot%\\cursors\\help_rl.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\help_il.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\help_l.cur" + } + } + } + } + ] + } + }, + "IBeam": { + "transform": { + "type": "fluid.transforms.quantize", + "inputPath": "http://registry\\.gpii\\.net/common/cursorSize", + "ranges": [ + { + "upperBound": 0.333, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "", + "Black": "%SystemRoot%\\cursors\\beam_r.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\beam_i.cur" + }, + "noMatch": { + "outputValue": "" + } + } + } + }, { + "upperBound": 0.666, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\beam_m.cur", + "Black": "%SystemRoot%\\cursors\\beam_rm.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\beam_im.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\beam_m.cur" + } + } + } + }, { + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\beam_l.cur", + "Black": "%SystemRoot%\\cursors\\beam_rl.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\beam_il.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\beam_l.cur" + } + } + } + } + ] + } + }, + "No": { + "transform": { + "type": "fluid.transforms.quantize", + "inputPath": "http://registry\\.gpii\\.net/common/cursorSize", + "ranges": [ + { + "upperBound": 0.333, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "", + "Black": "%SystemRoot%\\cursors\\no_r.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\no_i.cur" + }, + "noMatch": { + "outputValue": "" + } + } + } + }, { + "upperBound": 0.666, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\no_m.cur", + "Black": "%SystemRoot%\\cursors\\no_rm.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\no_im.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\no_m.cur" + } + } + } + }, { + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\no_l.cur", + "Black": "%SystemRoot%\\cursors\\no_rl.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\no_il.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\no_l.cur" + } + } + } + } + ] + } + }, + "NWPen": { + "transform": { + "type": "fluid.transforms.quantize", + "inputPath": "http://registry\\.gpii\\.net/common/cursorSize", + "ranges": [ + { + "upperBound": 0.333, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "", + "Black": "%SystemRoot%\\cursors\\pen_r.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\pen_i.cur" + }, + "noMatch": { + "outputValue": "" + } + } + } + }, { + "upperBound": 0.666, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\pen_m.cur", + "Black": "%SystemRoot%\\cursors\\pen_rm.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\pen_im.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\pen_m.cur" + } + } + } + }, { + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\pen_l.cur", + "Black": "%SystemRoot%\\cursors\\pen_rl.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\pen_il.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\pen_l.cur" + } + } + } + } + ] + } + }, + "SizeAll": { + "transform": { + "type": "fluid.transforms.quantize", + "inputPath": "http://registry\\.gpii\\.net/common/cursorSize", + "ranges": [ + { + "upperBound": 0.333, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "", + "Black": "%SystemRoot%\\cursors\\move_r.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\move_i.cur" + }, + "noMatch": { + "outputValue": "" + } + } + } + }, { + "upperBound": 0.666, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\move_m.cur", + "Black": "%SystemRoot%\\cursors\\move_rm.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\move_im.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\move_m.cur" + } + } + } + }, { + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\move_l.cur", + "Black": "%SystemRoot%\\cursors\\move_rl.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\move_il.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\move_l.cur" + } + } + } + } + ] + } + }, + "SizeNESW": { + "transform": { + "type": "fluid.transforms.quantize", + "inputPath": "http://registry\\.gpii\\.net/common/cursorSize", + "ranges": [ + { + "upperBound": 0.333, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "", + "Black": "%SystemRoot%\\cursors\\size1_r.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\size1_i.cur" + }, + "noMatch": { + "outputValue": "" + } + } + } + }, { + "upperBound": 0.666, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\size1_m.cur", + "Black": "%SystemRoot%\\cursors\\size1_rm.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\size1_im.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\size1_m.cur" + } + } + } + }, { + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\size1_l.cur", + "Black": "%SystemRoot%\\cursors\\size1_rl.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\size1_il.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\size1_l.cur" + } + } + } + } + ] + } + }, + "SizeNS": { + "transform": { + "type": "fluid.transforms.quantize", + "inputPath": "http://registry\\.gpii\\.net/common/cursorSize", + "ranges": [ + { + "upperBound": 0.333, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "", + "Black": "%SystemRoot%\\cursors\\size4_r.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\size4_i.cur" + }, + "noMatch": { + "outputValue": "" + } + } + } + }, { + "upperBound": 0.666, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\size4_m.cur", + "Black": "%SystemRoot%\\cursors\\size4_rm.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\size4_im.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\size4_m.cur" + } + } + } + }, { + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\size4_l.cur", + "Black": "%SystemRoot%\\cursors\\size4_rl.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\size4_il.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\size4_l.cur" + } + } + } + } + ] + } + }, + "SizeNWSE": { + "transform": { + "type": "fluid.transforms.quantize", + "inputPath": "http://registry\\.gpii\\.net/common/cursorSize", + "ranges": [ + { + "upperBound": 0.333, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "", + "Black": "%SystemRoot%\\cursors\\size2_r.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\size2_i.cur" + }, + "noMatch": { + "outputValue": "" + } + } + } + }, { + "upperBound": 0.666, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\size2_m.cur", + "Black": "%SystemRoot%\\cursors\\size2_rm.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\size2_im.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\size2_m.cur" + } + } + } + }, { + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\size2_l.cur", + "Black": "%SystemRoot%\\cursors\\size2_rl.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\size2_il.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\size2_l.cur" + } + } + } + } + ] + } + }, + "SizeWE": { + "transform": { + "type": "fluid.transforms.quantize", + "inputPath": "http://registry\\.gpii\\.net/common/cursorSize", + "ranges": [ + { + "upperBound": 0.333, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "", + "Black": "%SystemRoot%\\cursors\\size3_r.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\size3_i.cur" + }, + "noMatch": { + "outputValue": "" + } + } + } + }, { + "upperBound": 0.666, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\size3_m.cur", + "Black": "%SystemRoot%\\cursors\\size3_rm.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\size3_im.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\size3_m.cur" + } + } + } + }, { + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\size3_l.cur", + "Black": "%SystemRoot%\\cursors\\size3_rl.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\size3_il.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\size3_l.cur" + } + } + } + } + ] + } + }, + "UpArrow": { + "transform": { + "type": "fluid.transforms.quantize", + "inputPath": "http://registry\\.gpii\\.net/common/cursorSize", + "ranges": [ + { + "upperBound": 0.333, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "", + "Black": "%SystemRoot%\\cursors\\up_r.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\up_i.cur" + }, + "noMatch": { + "outputValue": "" + } + } + } + }, { + "upperBound": 0.666, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\up_m.cur", + "Black": "%SystemRoot%\\cursors\\up_rm.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\up_im.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\up_m.cur" + } + } + } + }, { + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\up_l.cur", + "Black": "%SystemRoot%\\cursors\\up_rl.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\up_il.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\up_l.cur" + } + } + } + } + ] + } + }, + "Wait": { + "transform": { + "type": "fluid.transforms.quantize", + "inputPath": "http://registry\\.gpii\\.net/common/cursorSize", + "ranges": [ + { + "upperBound": 0.333, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "", + "Black": "%SystemRoot%\\cursors\\busy_r.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\busy_i.cur" + }, + "noMatch": { + "outputValue": "" + } + } + } + }, { + "upperBound": 0.666, + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\busy_m.cur", + "Black": "%SystemRoot%\\cursors\\busy_rm.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\busy_im.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\busy_m.cur" + } + } + } + }, { + "output": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "White": "%SystemRoot%\\cursors\\busy_l.cur", + "Black": "%SystemRoot%\\cursors\\busy_rl.cur", + "ReverseBlack": "%SystemRoot%\\cursors\\busy_il.cur" + }, + "noMatch": { + "outputValue": "%SystemRoot%\\cursors\\busy_l.cur" + } + } + } + } + ] + } + } + }, + "inverseCapabilitiesTransformations": { + "transform": [ + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "Arrow", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/cursorSize", + "match": { + "": 0.32, + "%SystemRoot%\\cursors\\arrow_r.cur": 0.32, + "%SystemRoot%\\cursors\\arrow_i.cur": 0.32, + "%SystemRoot%\\cursors\\arrow_m.cur": 0.65, + "%SystemRoot%\\cursors\\arrow_rm.cur": 0.65, + "%SystemRoot%\\cursors\\arrow_im.cur": 0.65, + "%SystemRoot%\\cursors\\arrow_l.cur": 1, + "%SystemRoot%\\cursors\\arrow_rl.cur": 1, + "%SystemRoot%\\cursors\\arrow_il.cur": 1 + } + }, + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "Arrow", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/cursorColor", + "match": { + "": "White", + "%SystemRoot%\\cursors\\arrow_r.cur": "Black", + "%SystemRoot%\\cursors\\arrow_i.cur": "ReverseBlack", + "%SystemRoot%\\cursors\\arrow_m.cur": "White", + "%SystemRoot%\\cursors\\arrow_rm.cur": "Black", + "%SystemRoot%\\cursors\\arrow_im.cur": "ReverseBlack", + "%SystemRoot%\\cursors\\arrow_l.cur": "White", + "%SystemRoot%\\cursors\\arrow_rl.cur": "Black", + "%SystemRoot%\\cursors\\arrow_il.cur": "ReverseBlack" + } + } + ] + } + } + }, + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "setTrue": [ + { + "type": "gpii.windows.spiSettingsHandler.updateCursors" + } + ], + "getState": [ + { + "type": "gpii.processReporter.neverRunning" + } + ] + } + } + }, + "stop": [], + "update": [ + "settings.configure", + "start" + ], + "restore": [ + "settings.configure", + { + "type": "gpii.windows.spiSettingsHandler.updateCursors" + } + ], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.colorFilters": { + "name": "Windows Built-in Color Fiters", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "capabilities": [ + ], + "settingsHandlers": { + "configure": { + "type": "gpii.windows.systemSettingsHandler", + "liveness": "live", + "options": { + "CheckResult": true + }, + "supportedSettings": { + "SystemSettings_Accessibility_ColorFiltering_IsEnabled": { + "schema": { + "title": "Enable color filtering", + "description": "Enable or disables Windows color filters.", + "type": "object", + "properties": { + "value": { + "type": "boolean", + "default": false + } + } + } + }, + "SystemSettings_Accessibility_ColorFiltering_FilterType": { + "schema": { + "title": "Color filter type", + "description": "Selects the Windows color filter to be used.", + "type": "object", + "properties": { + "value": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "enumLabels": [ + "Grayscale", + "Inverted", + "Grayscale inverted", + "Red-green (green weak, deuteranopia)", + "Red-green (red weak, protanopia)", + "Blue-yellow (tritanopia)" + ], + "default": 1 + } + } + } + }, + "SystemSettings_Accessibility_ColorFiltering_IsShortcutKeyEnabled": { + "schema": { + "title": "Shortcut key for color filters", + "description": "Allow the shortcut key (Windows + Ctrl + C) to toggle color filter on or off.", + "type": "object", + "properties": { + "value": { + "type": "boolean", + "default": false + } + } + } + } + }, + "capabilitiesTransformations": { + "SystemSettings_Accessibility_ColorFiltering_IsEnabled": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.colorFilters.FilterType", + "defaultOutputPath": "value", + "match": [ + { + "inputValue": 0, + "outputValue": false + } + ], + "noMatch": { + "outputValue": true + } + } + }, + "SystemSettings_Accessibility_ColorFiltering_FilterType": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.colorFilters.FilterType", + "defaultOutputPath": "value", + "match": [ + { + "inputValue": 1, + "outputValue": 0 + }, + { + "intputValue": 2, + "outputValue": 1 + }, + { + "inputValue": 3, + "outputValue": 2 + }, + { + "inputValue": 4, + "outputValue": 3 + }, + { + "inputValue": 5, + "outputValue": 4 + }, + { + "inputValue": 6, + "outputValue": 5 + } + ], + "noMatch": { + "outputValue": 0 + } + } + }, + "SystemSettings_Accessibility_ColorFiltering_IsShortcutKeyEnabled": { + "transform": { + "type": "fluid.transforms.value", + "inputPath": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.colorFilters.ShortcutKeyEnable", + "outputPath": "value" + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.colorFilters.Enable": "SystemSettings_Accessibility_ColorFiltering_IsEnabled.value", + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.colorFilters.FilterType": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "SystemSettings_Accessibility_ColorFiltering_IsEnabled.value", + "defaultOutputPath": "value", + "match": [ + { + "inputValue": 0, + "outputValue": 0 + }, + { + "inputValue": 1, + "outputValue": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "SystemSettings_Accessibility_ColorFiltering_FilterType.value", + "defaultOutputPath": "value", + "match": [ + { + "inputValue": 0, + "outputValue": 1 + }, + { + "intputValue": 1, + "outputValue": 2 + }, + { + "inputValue": 2, + "outputValue": 3 + }, + { + "inputValue": 3, + "outputValue": 4 + }, + { + "inputValue": 4, + "outputValue": 5 + }, + { + "inputValue": 5, + "outputValue": 6 + } + ], + "noMatch": { + "outputValue": 0 + } + } + } + } + ], + "noMatch": { + "outputValue": 0 + } + } + }, + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.colorFilters.ShortcutKeyEnable": "SystemSettings_Accessibility_ColorFiltering_IsShortcutKeyEnabled.value" + } + } + }, + "update": [ + "settings.configure" + ], + "configure": [ + "settings.configure" + ], + "restore": [ + "settings.configure" + ], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.filterKeys": { + "name": "Windows FilterKeys", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETFILTERKEYS", + "setAction": "SPI_SETFILTERKEYS", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "FILTERKEYS" + } + }, + // TODO: Discuss somehow simplifying the structure of SPI settings for easier UI input. + "supportedSettings": { + "FilterKeysEnable": { + "schema": { + "title": "Filter keys", + "description": "Enable/Disable filter keys", + "type": "object", + "properties": { + "path": { "type": "string", "required": false}, + "value": { + "required": true, + "type": "boolean", + "default": false + } + } + } + }, + "SlowKeysInterval": { + "schema": { + "title": "Slow keys interval", + "description": "Slow keys interval time in milliseconds", + "type": "object", + "properties": { + "path": { "type": "string", "required": false}, + "value": { + "required": true, + "type": "integer", + "default": 0 // 0 means no interval time + } + } + } + } + // TODO: Reenable this once we can confirm a safe range of values that work with the SPI settings handler. +// "BounceKeysInterval": { +// "schema": { +// "title": "Bounce keys interval", +// "description": "Bounce keys interval time in milliseconds", +// "type": "object", +// "properties": { +// "path": { "type": "string", "required": true}, +// "value": { +// "required": true, +// "type": "integer", +// "default": 0 +// } +// } +// } +// } + }, + "capabilitiesTransformations": { + "FilterKeysEnable": { + "value": { + "transform": { + "type": "fluid.transforms.condition", + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/debounce/enabled", + "left": false, + "rightPath": "http://registry\\.gpii\\.net/common/slowKeys/enabled", + "right": false, + "operator": "||" + } + }, + "true": true, + "false": false + } + }, + "path": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": "pvParam.dwFlags.FKF_FILTERKEYSON" + } + } + }, + "SlowKeysInterval": { + "value": { + "transform": { + "type": "fluid.transforms.condition", + "condition": false, // If there's no setting, make sure it's counted as false. + "conditionPath": "http://registry\\.gpii\\.net/common/slowKeys/enabled", + "true": { + "transform": { + "type": "fluid.transforms.linearScale", + "input": 0.5, // The default if there is now slow key interval set. + "inputPath": "http://registry\\.gpii\\.net/common/slowKeysInterval", + "factor": 1000 + } + }, + "false": 0 + } + }, + "path": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": "pvParam.iWaitMSec" + } + } + } + // TODO: Reenable this once we can confirm a safe range of values that work with the SPI settings handler. +// "BounceKeysInterval": { +// "value": { +// "transform": { +// "type": "fluid.transforms.condition", +// "condition": false, // If there's no setting, make sure it's counted as false. +// "conditionPath": "http://registry\\.gpii\\.net/common/slowKeys/enabled", +// // If close keys is enabled, bounce keys should be disabled. +// "true": 0, +// "false": { +// "transform": { +// "type": "fluid.transforms.condition", +// "condition": false, // If there's no setting, make sure it's counted as false. +// "conditionPath": "http://registry\\.gpii\\.net/common/debounce/enabled", +// "true": 1000, +// "false": 0 +// } +// } +// } +// }, +// "path": { +// "transform": { +// "type": "fluid.transforms.literalValue", +// "input": "pvParam.iBounceMSec" +// } +// } +// } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/slowKeys/enabled": { + "transform": { + "type": "fluid.transforms.condition", + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "SlowKeysInterval.value", + "right": 0, + "operator": ">" + } + }, + "truePath": "FilterKeysEnable.value" + } + }, + "http://registry\\.gpii\\.net/common/slowKeysInterval": { + "transform": { + "type": "fluid.transforms.condition", + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "SlowKeysInterval.value", + "right": 0, + "operator": ">" + } + }, + "true": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "SlowKeysInterval.value", + "factor": 0.001 + } + } + } + }, + "http://registry\\.gpii\\.net/common/debounce/enabled": { + "transform": { + "type": "fluid.transforms.condition", + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "BounceKeysInterval.value", + "left": 0, + "right": 0, + "operator": ">" + } + }, + "truePath": "FilterKeysEnable.value" + } + }, + "http://registry\\.gpii\\.net/common/debounceInterval": { + "transform": { + "type": "fluid.transforms.condition", + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "BounceKeysInterval.value", + "left": 0, + "right": 0, + "operator": ">" + } + }, + "true": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "BounceKeysInterval.value", + "factor": 0.001 + } + } + } + } + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.highContrast": { + "name": "Windows High Contrast", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure-spi": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETHIGHCONTRAST", + "setAction": "SPI_SETHIGHCONTRAST", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "HIGHCONTRAST" + } + }, + // TODO: Discuss somehow simplifying the structure of SPI settings for easier UI input. + "supportedSettings": { + "HighContrastOn": { + "schema": { + "title": "High Contrast", + "description": "Whether to enable/disable High Contrast", + "type": "object", + "properties": { + "path": { "type": "string", "required": false}, + "value": { + "required": true, + "type": "boolean", + "default": false + } + } + } + } + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/highContrast/enabled", + "http://registry\\.gpii\\.net/common/highContrastTheme" + ], + "capabilitiesTransformations": { + "HighContrastOn": { + // GPII-4505 - We are going to apply a few edge cases of logic here to support both: + // 1. The fact that the QSS only sends up the high contrast theme setting at the moment, and is + // unlikely to change this soon, using the special value "regular-contrast" to determine + // whether or not the high contrast is enabled. ( If the value is regular-contrast, it should + // not be enabled. ) + // 2. The fact that probably the correct way to determine if the high contrast theme is on is the + // actual windows settings which turns it on and off, and which will be captured during the + // process of a capture tool snapshot. In this case, if `enabled` is equal to false, the + // high contrast really shouldn't turn on, depsite a high contrast theme setting being saved. + // + // To work around both these use cases for the near future, we will turn on the high contrast + // if highContrast.enabled is not false and if highContrastTheme is not regular-contrast. Do + // note that this will cause problems for users who go their preference set up by doing a system + // capture when high contrast was off, and then beginning to use the qss. But it's unlikely this + // problem will present itself anytime soon. + // + // + // ON + // "http://registry.gpii.net/common/highContrastTheme": "yellow-black", + // "http://registry.gpii.net/common/highContrast/enabled": true + // + // OFF + // "http://registry.gpii.net/common/highContrastTheme": "yellow-black", + // "http://registry.gpii.net/common/highContrast/enabled": false + // + // OFF + // "http://registry.gpii.net/common/highContrastTheme": "regular-contrast", + // "http://registry.gpii.net/common/highContrast/enabled": true + // + // OFF + // "http://registry.gpii.net/common/highContrastTheme": "regular-contrast", + // "http://registry.gpii.net/common/highContrast/enabled": false + // + // ON + // "http://registry.gpii.net/common/highContrastTheme": "yellow-black" + // + // OFF + // "http://registry.gpii.net/common/highContrastTheme": "regular-contrast" + // + // ON + // "http://registry.gpii.net/common/highContrast/enabled": true + // + // OFF + // "http://registry.gpii.net/common/highContrast/enabled": false + // + // (highContrast.enabled !== false) && (highContrastTheme !== "regular-contrast") + "value": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": true, // If `enabled` is missing use `true` to support QSS case + "leftPath": "http://registry\\.gpii\\.net/common/highContrast/enabled", + "operator": "!==", + "right": false + } + }, + "operator": "&&", + "right": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": "", + "leftPath": "http://registry\\.gpii\\.net/common/highContrastTheme", + "operator": "!==", + "right": "regular-contrast" + } + } + } + }, + "path": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": "pvParam.dwFlags.HCF_HIGHCONTRASTON" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/highContrast/enabled": "HighContrastOn.value" + } + }, + "configure-registry": { + // This sets the high-contrast theme. + "type": "gpii.windows.registrySettingsHandler", + "liveness": "live", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes", + "dataTypes": { + "LastHighContrastTheme": "REG_SZ" + } + }, + "supportedSettings": { + "LastHighContrastTheme": { + "schema": { + "title": "High Contrast theme", + "description": "High Contrast Theme", + "type": "string", + "default": "%SystemRoot%\\resources\\Ease of Access Themes\\hcwhite.theme", + "enum": [ + "%SystemRoot%\\resources\\Ease of Access Themes\\hcwhite.theme", + "%SystemRoot%\\resources\\Ease of Access Themes\\hcblack.theme", + "%SystemRoot%\\resources\\Ease of Access Themes\\hc1.theme", + "%SystemRoot%\\resources\\Ease of Access Themes\\blackOnYellow.theme", + "%SystemRoot%\\resources\\Ease of Access Themes\\hc2.theme", + "%SystemRoot%\\resources\\Ease of Access Themes\\blackOnBrown.theme", + "%SystemRoot%\\resources\\Ease of Access Themes\\grayOnDark.theme", + "%SystemRoot%\\resources\\Ease of Access Themes\\grayOnWhite.theme" + ], + "enumLabels": [ + "Black on White", + "White on Black", + "Yellow on Black", + "Black on Yellow", + "Lime on Black", + "Black on Brown", + "Grey on Dark", + "Grey on White" + ] + } + } + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/highContrast/enabled", + "http://registry\\.gpii\\.net/common/highContrastTheme" + ], + "capabilitiesTransformations": { + "LastHighContrastTheme": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/highContrastTheme", + "match": { + "black-white": "%SystemRoot%\\resources\\Ease of Access Themes\\hcwhite.theme", + "white-black": "%SystemRoot%\\resources\\Ease of Access Themes\\hcblack.theme", + "yellow-black": "%SystemRoot%\\resources\\Ease of Access Themes\\hc1.theme", + "black-yellow": "%SystemRoot%\\resources\\Ease of Access Themes\\blackOnYellow.theme", + "lime-black": "%SystemRoot%\\resources\\Ease of Access Themes\\hc2.theme", + "black-brown": "%SystemRoot%\\resources\\Ease of Access Themes\\blackOnBrown.theme", + "grey-black": "%SystemRoot%\\resources\\Ease of Access Themes\\grayOnDark.theme", + "grey-white": "%SystemRoot%\\resources\\Ease of Access Themes\\grayOnWhite.theme" + } + } + } + } + }, + "configure-theme": { + // This is used to specify the theme file that Windows will use when restoring high-contrast. The + // file this setting points to is created by gpii, containing the currently unsaved theme settings. + "type": "gpii.windows.registrySettingsHandler", + "liveness": "live", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\HighContrast", + "dataTypes": { + "Pre-High Contrast Scheme": "REG_SZ" + } + }, + "supportedSettings": { + "Pre-High Contrast Scheme": { + "schema": { + "title": "Previous Scheme", + "description": "The scheme that was used prior to selecting the high contrast theme.", + "type": "string", + "default": "C:\\Windows\\resources\\Themes\\aero.theme" + } + } + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/highContrast/enabled", + "http://registry\\.gpii\\.net/common/highContrastTheme" + ], + "capabilitiesTransformations": { + "Pre-High Contrast Scheme": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": "${{environment}.LOCALAPPDATA}\\Microsoft\\Windows\\Themes\\Morphic.theme" + } + } + } + } + }, + "configure": [ + // Set the "restore" theme. + "settings.configure-theme", + // Set the high-contrast theme. + "settings.configure-registry", + { + // Save the current theme, and set the new high-contrast one. + "type": "gpii.windows.spiSettingsHandler.setHighContrastTheme", + "newTheme": "${{registry}HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\LastHighContrastTheme}", + "currentTheme": "${{registry}HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\CurrentTheme}", + "saveAs": "${{registry}HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\HighContrast\\Pre-High Contrast Scheme}" + }, + // Apply high-contrast. + "settings.configure-spi", + { + // Make sure the colours are applied for custom themes. + "type": "gpii.windows.spiSettingsHandler.applyCustomTheme", + "themeFile": "${{registry}HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\HighContrast\\LastSet}" + } + ], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.audioDescription": { + "name": "Windows video audio description feature", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETAUDIODESCRIPTION", + "setAction": "SPI_SETAUDIODESCRIPTION", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "AUDIODESCRIPTION" + } + }, + "supportedSettings": { + "AudioDescriptionOn": { + "schema": { + "title": "Video audio description", + "description": "Hear descriptions of what's happening in videos", + "type": "object", + "properties": { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + "value": { + "default": 0, + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ] + } + } + } + } + }, + "capabilitiesTransformations": { + "AudioDescriptionOn": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.audioDescription.AudioDescriptionEnabled", + "path": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": "pvParam.Enabled" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.audioDescription.AudioDescriptionEnabled": "AudioDescriptionOn.value" + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.notificationDuration": { + "name": "Windows notification duration", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETMESSAGEDURATION", + "setAction": "SPI_SETMESSAGEDURATION", + "uiParam": 0, + "pvParam": { + "type": "UINT" + } + }, + "supportedSettings": { + "Duration": { + "schema": { + "title": "Notification duration", + "description": "Changes the time system notifications are displayed", + "type": "object", + properties: { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + "value": { + "type": "number", + "default": 5, + "enum": [ + 5, + 7, + 15, + 30, + 60, + 300 + ], + "enumLabels": [ + "5s", + "7s", + "15s", + "30s", + "60s", + "300s" + ] + } + } + } + } + }, + "capabilitiesTransformations": { + "Duration": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.notificationDuration.NotificationDuration", + "path": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": "pvParam" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.notificationDuration.NotificationDuration": "Duration.value" + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.language": { + "name": "Windows Display Language", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure1": { + "type": "gpii.windows.registrySettingsHandler", + "liveness": "liveRestart", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Control Panel\\Desktop\\MuiCached", + "dataTypes": { + "MachinePreferredUILanguages": "REG_SZ" + } + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/language" + ], + "capabilitiesTransformations": { + "MachinePreferredUILanguages": { + "transform": { + "type": "fluid.transforms.value", + "inputPath": "http://registry\\.gpii\\.net/common/language" + } + } + }, + "supportedSettings": { + "MachinePreferredUILanguages": { + "schema": { + "title": "The Machine Preferred UI Language", + "description": "Machine Preferred UI language.", + "type": "string" + } + } + } + }, + "configure2": { + "type": "gpii.windows.registrySettingsHandler", + "liveness": "liveRestart", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Control Panel\\Desktop", + "dataTypes": { + "PreferredUILanguages": "REG_SZ" + } + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/language" + ], + "capabilitiesTransformations": { + "PreferredUILanguages": "http://registry\\.gpii\\.net/common/language" + }, + "supportedSettings": { + "PreferredUILanguages": { + "schema": { + "title": "Preferred UI Language", + "description": "Preffered UI language.", + "type": "string" + } + } + } + }, + "configure3": { + "type": "gpii.windows.registrySettingsHandler", + "liveness": "liveRestart", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Control Panel\\International\\User Profile", + "dataTypes": { + "Languages": "REG_MULTI_SZ" + } + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/language" + ], + "capabilitiesTransformations": { + "Languages": [{ + "transform": { + "type": "fluid.transforms.value", + "inputPath": "http://registry\\.gpii\\.net/common/language" + } + }] + }, + "supportedSettings": { + "Languages": { + "schema": { + "title": "Language", + "description": "Language.", + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "configure": [ + "settings.configure1", + "settings.configure2", + "settings.configure3", + { + "type": "gpii.windows.updateLanguage", + "currentLanguage": "${{registry}.HKEY_CURRENT_USER\\Control Panel\\Desktop\\PreferredUILanguages}" + } + ], + "restore": [ + "settings.configure1", + "settings.configure2", + "settings.configure3", + { + "type": "gpii.windows.updateLanguage", + "currentLanguage": "${{registry}.HKEY_CURRENT_USER\\Control Panel\\Desktop\\PreferredUILanguages}" + } + ], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.toggleKeys": { + "name": "Windows Toggle keys feature", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETTOGGLEKEYS", + "setAction": "SPI_SETTOGGLEKEYS", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "TOGGLEKEYS" + } + }, + "supportedSettings": { + "ToggleKeysOn": { + "schema": { + "title": "ToggleKeys accesibility feature", + "description": "Enable/Disable ToggleKeys feature", + "type": "object", + "properties": { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + "value": { + "type": "boolean", + "default": false, + "required": true + } + } + } + } + }, + "capabilitiesTransformations": { + "ToggleKeysOn": { + "value": "http://registry\\.gpii\\.net/common/toggleKeys", + "path": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": "pvParam.dwFlags.TKF_TOGGLEKEYSON" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/toggleKeys": "ToggleKeysOn.value" + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.underlineMenuShortcuts": { + "name": "Underline menu shortcuts", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configureShortcuts": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETKEYBOARDCUES", + "setAction": "SPI_SETKEYBOARDCUES", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + } + }, + "supportedSettings": { + "UnderlineMenuShortcutsOn": { + "schema": { + "title": "Enable/Disable underlaying menu shortcuts", + "description": "Displays a underline showing which is the shortcut to active a menu item", + "type": "object", + "properties": { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + "value": { + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + } + } + } + }, + "capabilitiesTransformations": { + "UnderlineMenuShortcutsOn": { + "value": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.underlineMenuShortcuts.Enabled", + "true": 1, + "false": 0 + } + }, + "path": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": "pvParam" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.underlineMenuShortcuts.Enabled": "UnderlineMenuShortcutsOn.value" + } + }, + "keyboardPreferred": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETKEYBOARDPREF", + "setAction": "SPI_SETKEYBOARDPREF", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + } + }, + "supportedSettings": { + "KeyboardPreferenceOn": { + "schema": { + "title": "Keyboard as preferred input method", + "description": "Set the keyboard as the preferred input method", + "type": "object", + "properties": { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + "value": { + "type": "boolean", + "default": false + } + } + } + } + }, + "capabilitiesTransformations": { + "KeyboardPreferenceOn": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.underlineMenuShortcuts.KeyboardPrefEnabled", + "path": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": { + "get": "pvParam", + "set": "uiParam" + } + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.underlineMenuShortcuts.KeyboardPrefEnabled": "KeyboardPreferenceOn.value" + } + } + }, + "configure": [ + "settings.configureShortcuts", + "settings.keyboardPreferred" + ], + "restore": [ + "settings.keyboardPreferred", + "settings.configureShortcuts" + ], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.shortcutWarningMessage": { + "name": "Warning messages for shortcuts", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.windows.registrySettingsHandler", + "liveness": "liveRestart", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Control Panel\\Accessibility", + "dataTypes": { + "Warning Sounds": "REG_DWORD" + } + }, + "supportedSettings": { + "Warning Sounds": { + "schema": { + "title": "Shortcut warning messages", + "description": "Enable/Disable warning messages when changing a setting with a shortcut", + "enum": [0,1], + "enumLabels": ["Off", "On"] + } + } + }, + "capabilitiesTransformations": { + "Warning Sounds": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.shortcutWarningMessage.Enabled", + "true": 1, + "false": 0 + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.shortcutWarningMessage.Enabled": "Warning Sounds.value" + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.shortcutWarningSound": { + "name": "Warning sounds for shortcuts", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.windows.registrySettingsHandler", + "liveness": "liveRestart", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Control Panel\\Accessibility", + "dataTypes": { + "Sound on Activation": "REG_DWORD" + } + }, + "supportedSettings": { + "Sound on Activation": { + "schema": { + "title": "Shortcut warning sounds", + "description": "Enable/Disable warning sounds when changing a setting with a shortcut", + "enum": [0,1], + "enumLabels": ["Off", "On"], + "default": 0 + } + } + }, + "capabilitiesTransformations": { + "Sound on Activation": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.shortcutWarningSound.Enabled", + "false": 0, + "true": 1 + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.shortcutWarningSound.Enabled": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "Sound on Activation", + "right": 0, + "operator": "!==" + } + } + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.magnifier": { + "name": "Windows Built-in Screen Magnifier", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + "settingsHandlers": { + "configure": { + "type": "gpii.windows.registrySettingsHandler", + "liveness": "liveRestart", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\ScreenMagnifier", + "dataTypes": { + "Magnification": "REG_DWORD", + "Invert": "REG_DWORD", + "FollowFocus": "REG_DWORD", + "FollowCaret": "REG_DWORD", + "FollowMouse": "REG_DWORD", + "FollowNarrator": "REG_DWORD", + "MagnificationMode": "REG_DWORD", + "FadeToMagIcon": "REG_DWORD", + "ZoomIncrement": "REG_DWORD", + "UseBitmapSmoothing": "REG_DWORD", + "LensHeight": "REG_DWORD", + "LensWidth": "REG_DWORD" + } + }, + "supportedSettings": { + "Invert": { + "schema": { + "title": "Invert Colours", + "description": "Enable colour inversion for Magnifier", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "Magnification": { + "schema": { + "title": "Magnification", + "description": "Set up magnification level", + "type": "number", + "default": 200, + "minimum": 100, + "maximum": 1600 + } + }, + "FollowFocus": { + "schema": { + "title": "Magnifier follows focus", + "description": "Magnifier follows the keyboard focus", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "FollowCaret": { + "schema": { + "title": "Magnifier follows caret", + "description": "Magnifier follows the text insertion point", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "FollowMouse": { + "schema": { + "title": "Magnifier follows mouse", + "description": "Magnifier follows the mouse pointer", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "FollowNarrator": { + "schema": { + "title": "Magnifier follows Narrator", + "description": "Magnifier follows the Narrator cursor", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "MagnificationMode": { + "schema": { + "title": "Magnifier position", + "description": "Position of the magnified area", + "default": 2, + "enum": [ + 1, + 2, + 3 + ], + "enumLabels": [ + "Docked", + "FullScreen", + "Lens" + ] + } + }, + "FadeToMagIcon": { + "schema": { + "title": "Collapse into magnifiying glass", + "description": "Collapse Magnifier window into a floating transparent magnifying glass.", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "ZoomIncrement": { + "schema": { + "title": "Zoom Increment", + "description": "Change zoom increments.", + "type": "number", + "default": 100, + "enum": [ + 25, + 50, + 100, + 150, + 200, + 400 + ], + "enumLabels": [ + "25%", + "50%", + "100% (default)", + "150%", + "200%", + "400%" + ] + } + }, + "UseBitmapSmoothing": { + "schema": { + "title": "Smooth edges of images and text", + "description": "Enables/Disables smoothing for edges", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "LensHeight": { + "schema": { + "title": "Lens Height", + "description": "Modifies the height of the magnifier lens.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 30 + } + }, + "LensWidth": { + "schema": { + "title": "Lens Width", + "description": "Modifies the width of the magnifier lens.", + "type": "integer", + "minimum": 10, + "maximum": 100, + "default": 30 + } + } + }, + "capabilitiesTransformations": { + "Invert": { + "transform": { + "type": "gpii.transformer.booleanToNumber", + "inputPath": "http://registry\\.gpii\\.net/common/invertColours" + } + }, + "Magnification": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/magnification", + "factor": 100 + } + } + } + }, + "transform": [ + { + "type": "fluid.transforms.arrayToSetMembership", + "inputPath": "http://registry\\.gpii\\.net/common/tracking", + "outputPath": "", + "presentValue": 1, + "missingValue": 0, + "options": { + "focus": "FollowFocus", + "caret": "FollowCaret", + "mouse": "FollowMouse" + } + } + ], + "MagnificationMode": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/magnifierPosition", + "match": { + "FullScreen": 2, + "Lens": 3, + "LeftHalf": 1, + "RightHalf": 1, + "TopHalf": 1, + "BottomHalf": 1, + "Custom": 2 + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/invertColours": "Invert", + "transform": [ + { + "type": "fluid.transforms.linearScale", + "inputPath": "Magnification", + "outputPath": "http://registry\\.gpii\\.net/common/magnification", + "factor": 0.01 + }, + { + "type": "fluid.transforms.setMembershipToArray", + "inputPath": "", + "outputPath": "http://registry\\.gpii\\.net/common/tracking", + "presentValue": 1, + "missingValue": 0, + "options": { + "FollowFocus": "focus", + "FollowMouse": "mouse", + "FollowCaret": "caret" + } + }, + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "MagnificationMode", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/magnifierPosition", + "match": { + "2": "FullScreen", + "3": "Lens", + "1": "TopHalf" + } + } + ] + } + }, + "configureSystemSettings": { + "type": "gpii.windows.systemSettingsHandler", + "liveness": "live", + "options": { + "Async": true, + "CheckResult": true + }, + "supportedSettings": { + "SystemSettings_Accessibility_Magnifier_IsAutoStartEnabled": { + "schema": { + "title": "Auto Start Magnifier", + "description": "Start Magnifier after sign-in.", + "type": "object", + "properties": { + "value": { + "type": "boolean", + "default": false + } + } + } + }, + "SystemSettings_Accessibility_Magnifier_IsAutoStartOnLogonDesktopEnabled": { + "schema": { + "title": "Auto Start Magnifier for everyone", + "description": "Start Magnifier before sign-in.", + "type": "object", + "properties": { + "value": { + "type": "boolean", + "default": false + } + } + } + } + }, + "capabilitiesTransformations": { + "SystemSettings_Accessibility_Magnifier_IsAutoStartEnabled": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.magnifier.EnableAutoStart" + }, + "SystemSettings_Accessibility_Magnifier_IsAutoStartOnLogonDesktopEnabled": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.magnifier.EnableSystemAutoStart" + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.magnifier.EnableAutoStart": "SystemSettings_Accessibility_Magnifier_IsAutoStartEnabled.value", + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.magnifier.EnableSystemAutoStart": "SystemSettings_Accessibility_Magnifier_IsAutoStartOnLogonDesktopEnabled.value" + } + } + }, + "update": [ + "settings.configure", + "settings.configureSystemSettings" + ], + "configure": [ + "settings.configure", + "settings.configureSystemSettings" + ], + "restore": [ + "settings.configure", + "settings.configureSystemSettings" + ], + "launchHandlers": { + "launcher": { + "type": "gpii.windows.enableRegisteredAT", + "options": { + "registryName": "magnifierpane", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "Magnify.exe" + } + ] + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.mouseKeys": { + "name": "Windows MouseKeys", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETMOUSEKEYS", + "setAction": "SPI_SETMOUSEKEYS", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "MOUSEKEYS" + } + }, + // TODO: Discuss somehow simplifying the structure of SPI settings for easier UI input. + "supportedSettings": { + "MouseKeysOn": { + "schema": { + "title": "Mouse keys", + "description": "Enable/Disable mouse keys", + "type": "object", + "properties": { + "path": { "type": "string", "required": false}, + "value": { + "required": true, + "type": "boolean", + "default": false + } + } + } + }, + "MaxSpeed": { + "schema": { + "title": "Mouse keys speed", + "description": "Speed of mouse keys", + "type": "object", + "properties": { + "path": { "type": "string", "required": false}, + "value": { + "type": "number", + "minimum": 10, + "maximum": 360 + } + } + } + } + // This is temporarily being commented out due to a missing capabilities transform. + // Until GPII-3119 is complete, SPI settings must have this transform which includes + // their param `path`. + // "Acceleration": { + // "schema": { + // "title": "Mouse keys acceleration", + // "description": "Acceleration of mouse keys", + // "type": "object", + // "properties": { + // "path": { "type": "string"}, + // "value": { + // "type": "number", + // "minimum": -1000, + // // TODO: Not clear how to describe this setting + // "maximum": 1000 + // } + // } + // } + // } + }, + "capabilitiesTransformations": { + "MouseKeysOn": { + "value": "http://registry\\.gpii\\.net/common/mouseEmulation/enabled", + "path": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": "pvParam.dwFlags.MKF_MOUSEKEYSON" + } + } + }, + "MaxSpeed": { + "value": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/cursorSpeed", + "factor": 350, + "offset": 10 + } + } + } + }, + "path": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": "pvParam.iMaxSpeed" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/mouseEmulation/enabled": "MouseKeysOn.value", + "http://registry\\.gpii\\.net/common/cursorSpeed": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "MaxSpeed.value", + "factor": 0.00285714285714, + "offset": -0.0285714285714 + } + } + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.mouseSettings": { + "name": "Windows Mouse settings", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configureMousePrimaryButton": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETMOUSEBUTTONSWAP", + "setAction": "SPI_SETMOUSEBUTTONSWAP", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + }, + "fWinIni": "SPIF_UPDATEINIFILE|SPIF_SENDCHANGE" + }, + // TODO: Discuss somehow simplifying the structure of SPI settings for easier UI input. + "supportedSettings": { + "SwapMouseButtonsConfig": { + "schema": { + "title": "Swap mouse primary button", + "description": "Swap mouse left/right buttons", + "type": "object", + "properties": { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + // This one is already in use in the QSS and can't be booleanised yet. + "value": { + "required": true, + "enum": [0,1], + "enumLabels": ["Off", "On"], + "default": 0 + } + } + } + } + }, + "capabilitiesTransformations": { + "SwapMouseButtonsConfig": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.SwapMouseButtons", + "path": { + "literalValue": { + "get": "pvParam", + "set": "uiParam" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.SwapMouseButtons": "SwapMouseButtonsConfig.value" + } + }, + "configureScrollWheelMode": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETWHEELSCROLLLINES", + "setAction": "SPI_SETWHEELSCROLLLINES", + "uiParam": 0, + "pvParam": { + "type": "UINT" + }, + "fWinIni": "SPIF_UPDATEINIFILE|SPIF_SENDCHANGE" + }, + // TODO: Discuss somehow simplifying the structure of SPI settings for easier UI input. + "supportedSettings": { + "ScrollWheelModeConfig": { + "schema": { + "title": "Mouse scroll wheel mode", + "description": "Changes the number of lines to scroll when moving the scrollwheel.", + "type": "object", + "properties": { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + "value": { + "required": true, + "default": 3, + "oneOf": [ + { + "title": "Range of accepted regular values", + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + { + "title": "Values with special meaning", + "enum": [4294967295], + "enumLabels": ["full-page"] + } + ] + } + } + } + } + }, + "capabilitiesTransformations": { + "ScrollWheelModeConfig": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.ScrollLines", + "path": { + "literalValue": { + "get": "pvParam", + "set": "uiParam" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.ScrollLines": "ScrollWheelModeConfig.value" + } + }, + "configureScrollInactiveWindow": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETMOUSEWHEELROUTING", + "setAction": "SPI_SETMOUSEWHEELROUTING", + "uiParam": 0, + "pvParam": { + "type": "UINT" + }, + "fWinIni": "SPIF_UPDATEINIFILE|SPIF_SENDCHANGE" + }, + // TODO: Discuss somehow simplifying the structure of SPI settings for easier UI input. + "supportedSettings": { + "ScrollFocusRoutingConfig": { + "schema": { + "title": "Mouse scroll focus routing", + "description": "Changes the way mouse scrolling is passed to applications.", + "type": "object", + "properties": { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + "value": { + "required": true, + "enum": [0, 1], + "enumLabels": ["RoutingFocus", "RoutingHybrid"] + } + } + } + } + }, + "capabilitiesTransformations": { + "ScrollFocusRoutingConfig": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.ScrollRouting", + "path": { + "literalValue": "pvParam" + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.ScrollRouting": "ScrollFocusRoutingConfig.value" + } + }, + "configureMouseCursorShadow": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETCURSORSHADOW", + "setAction": "SPI_SETCURSORSHADOW", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + }, + "fWinIni": "SPIF_UPDATEINIFILE|SPIF_SENDCHANGE" + }, + // TODO: Discuss somehow simplifying the structure of SPI settings for easier UI input. + "supportedSettings": { + "MouseCursorShadowEnable": { + "schema": { + "title": "Mouse cursor shadow", + "description": "Enables or disables mouse cursor shadow.", + "type": "object", + "properties": { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + "value": { + "required": true, + "type": "boolean", + "default": false + } + } + } + } + }, + "capabilitiesTransformations": { + "MouseCursorShadowEnable": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.EnableCursorShadow", + "path": { + "literalValue": "pvParam" + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.EnableCursorShadow": "MouseCursorShadowEnable.value" + } + }, + "configureHorizontalScrollChars": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETWHEELSCROLLCHARS", + "setAction": "SPI_SETWHEELSCROLLCHARS", + "uiParam": 0, + "pvParam": { + "type": "UINT" + }, + "fWinIni": "SPIF_UPDATEINIFILE|SPIF_SENDCHANGE" + }, + // TODO: Discuss somehow simplifying the structure of SPI settings for easier UI input. + "supportedSettings": { + "ScrollCharsConfig": { + "schema": { + "title": "Mouse wheel horizontal scroll", + "description": "Changes the number of chars that are scrolled horizontally.", + "type": "object", + "properties": { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + "value": { + "required": true, + "type": "integer", + "default": 3, + "minimum": 1, + "maximum": 100 + } + } + } + } + }, + "capabilitiesTransformations": { + "ScrollCharsConfig": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.ScrollChars", + "path": { + "literalValue": { + "get": "pvParam", + "set": "uiParam" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.ScrollChars": "ScrollCharsConfig.value" + } + }, + "configureActiveWindowTracking": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETACTIVEWINDOWTRACKING", + "setAction": "SPI_SETACTIVEWINDOWTRACKING", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + }, + "fWinIni": "SPIF_UPDATEINIFILE|SPIF_SENDCHANGE" + }, + // TODO: Discuss somehow simplifying the structure of SPI settings for easier UI input. + "supportedSettings": { + "WindowsTrackingConfig": { + "schema": { + "title": "Mouse Windows tracking", + "description": "Windows are focused when mouse is stopped over them.", + "type": "object", + "properties": { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + "value": { + "required": true, + "type": "boolean", + "default": false + } + } + } + } + }, + "capabilitiesTransformations": { + "WindowsTrackingConfig": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.WindowsTrackingEnabled", + "path": { + "literalValue": "pvParam" + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.WindowsTrackingEnabled": "WindowsTrackingConfig.value" + } + }, + "configureActiveWindowZOrder": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETACTIVEWNDTRKZORDER", + "setAction": "SPI_SETACTIVEWNDTRKZORDER", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + }, + "fWinIni": "SPIF_UPDATEINIFILE|SPIF_SENDCHANGE" + }, + // TODO: Discuss somehow simplifying the structure of SPI settings for easier UI input. + "supportedSettings": { + "ActiveZOrder": { + "schema": { + "title": "Windows Tracking", + "description": "Windows receive focus when mouse hovers over them.", + "type": "object", + "properties": { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + "value": { + "required": true, + "type": "boolean", + "default": false + } + } + } + } + }, + "capabilitiesTransformations": { + "ActiveZOrder": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.WindowsTrackingEnabled", + "path": { + "literalValue": "pvParam" + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.WindowsTrackingEnabled": "ActiveZOrder.value" + } + }, + "configureWindowsArrangement": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETWINARRANGING", + "setAction": "SPI_SETWINARRANGING", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + }, + "fWinIni": "SPIF_UPDATEINIFILE|SPIF_SENDCHANGE" + }, + // TODO: Discuss somehow simplifying the structure of SPI settings for easier UI input. + "supportedSettings": { + "WindowsArrangement": { + "schema": { + "title": "Configures Window arrangement", + "description": "Enable/Disables the automatic Windows arrangment when dragging windows to border with mouse.", + "type": "object", + "properties": { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + "value": { + "required": true, + "type": "boolean", + "default": true + } + } + } + } + }, + "capabilitiesTransformations": { + "WindowsArrangement": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.AutoWinArrangement", + "path": { + "literalValue": { + "get": "pvParam", + "set": "uiParam" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.AutoWinArrangement": "WindowsArrangement.value" + } + }, + "configureMouseDoubleClickTime": { + "type": "gpii.windows.nativeSettingsHandler", + "liveness": "live", + "options": { + "functionName": "DoubleClickTime" + }, + "supportedSettings": { + "DoubleClickTimeConfig": { + "schema": { + "title": "Double click time", + "description": "Changes the amount of time that can elapse between a first click and a second click for the OS to consider the mouse action a double-click.", + "type": "object", + "properties": { + "value": { + "type": "integer", + "default": 500, + "minimum": 500, + "maximum": 5000 + } + } + } + } + }, + "capabilitiesTransformations": { + "DoubleClickTimeConfig": { + value: "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.DoubleClickTime" + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.DoubleClickTime": "DoubleClickTimeConfig.value" + } + }, + "configurePointerSpeed": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETMOUSESPEED", + "setAction": "SPI_SETMOUSESPEED", + "uiParam": 0, + "pvParam": { + "type": "UINT" + }, + "fWinIni": "SPIF_UPDATEINIFILE|SPIF_SENDCHANGE" + }, + // TODO: Discuss somehow simplifying the structure of SPI settings for easier UI input. + "supportedSettings": { + "PointerSpeedConfig": { + "schema": { + "title": "Configures pointer speed", + "description": "Selects a pointer speed.", + "type": "object", + "properties": { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + "value": { + "required": true, + "type": "integer", + "minimum": 1, + "maximum": 20, + "default": 10 + } + } + } + } + }, + "capabilitiesTransformations": { + "PointerSpeedConfig": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.PointerSpeed", + "path": { + "literalValue": "pvParam" + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.PointerSpeed": "PointerSpeedConfig.value" + } + }, + "configurePointerPrecision": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETMOUSE", + "setAction": "SPI_SETMOUSE", + "uiParam": 0, + "pvParam": { + "type": "array", + "valueType": "INT", + "length": 3 + }, + "fWinIni": "SPIF_UPDATEINIFILE|SPIF_SENDCHANGE" + }, + // TODO: Discuss somehow simplifying the structure of SPI settings for easier UI input. + "supportedSettings": { + "EnhancePrecisionConfig": { + "schema": { + "title": "Enhance pointer precision", + "description": "Enable/Disables enhanced pointer precision.", + "type": "object", + "properties": { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + "value": { + "required": true, + "type": "boolean", + "default": true + } + } + } + } + }, + "capabilitiesTransformations": { + "EnhancePrecisionConfig": { + "value": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.EnhancePrecision", + "condition": true, + "true": [6, 10, 1], + "false": [0, 0, 1] + } + }, + "path": { + "literalValue": "pvParam" + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.EnhancePrecision": "EnhancePrecisionConfig.value" + } + }, + "configureSnapToDefButton": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETSNAPTODEFBUTTON", + "setAction": "SPI_SETSNAPTODEFBUTTON", + "uiParam": 0, + "pvParam": { + "type": "UINT" + }, + "fWinIni": "SPIF_UPDATEINIFILE|SPIF_SENDCHANGE" + }, + // TODO: Discuss somehow simplifying the structure of SPI settings for easier UI input. + "supportedSettings": { + "SnapToDefaultButtonConfig": { + "schema": { + "title": "Snap To", + "description": "Automatically move pointer to the default button in a dialogue.", + "type": "object", + "properties": { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + "value": { + "required": true, + "type": "boolean", + "default": false + } + } + } + } + }, + "capabilitiesTransformations": { + "SnapToDefaultButtonConfig": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.SnapToDefaultButton", + "path": { + "literalValue": { + "get": "pvParam", + "set": "uiParam" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.SnapToDefaultButton": "SnapToDefaultButtonConfig.value" + } + }, + "configureHidePointer": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETMOUSEVANISH", + "setAction": "SPI_SETMOUSEVANISH", + "uiParam": 0, + "pvParam": { + "type": "INT" + }, + "fWinIni": "SPIF_UPDATEINIFILE|SPIF_SENDCHANGE" + }, + // TODO: Discuss somehow simplifying the structure of SPI settings for easier UI input. + "supportedSettings": { + "HidePointerConfig": { + "schema": { + "title": "Hide pointer while typing", + "description": "Enable/Disables hiding the pointer while typing.", + "type": "object", + "properties": { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + "value": { + "required": true, + "type": "boolean", + "default": true + } + } + } + } + }, + "capabilitiesTransformations": { + "HidePointerConfig": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.HidePointer", + "path": { + "literalValue": "pvParam" + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.HidePointer": "HidePointerConfig.value" + } + }, + "configureMouseSonar": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETMOUSESONAR", + "setAction": "SPI_SETMOUSESONAR", + "uiParam": 0, + "pvParam": { + "type": "UINT" + }, + "fWinIni": "SPIF_UPDATEINIFILE|SPIF_SENDCHANGE" + }, + // TODO: Discuss somehow simplifying the structure of SPI settings for easier UI input. + "supportedSettings": { + "MouseSonarConfig": { + "schema": { + "title": "Mouse sonar", + "description": "Enable/Disable showing mouse location when pressing the CTRL key.", + "type": "object", + "properties": { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + "value": { + "required": true, + "type": "boolean", + "default": false + } + } + } + } + }, + "capabilitiesTransformations": { + "MouseSonarConfig": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.MouseSonar", + "path": { + "literalValue": "pvParam" + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.MouseSonar": "MouseSonarConfig.value" + } + }, + "configureDoubleClickWidth": { + "type": "gpii.windows.nativeSettingsHandler", + "liveness": "live", + "options": { + "functionName": "DoubleClickWidth" + }, + "supportedSettings": { + "DoubleClickWidthConfig": { + // This setting has no documented range, see https://issues.gpii.net/browse/GPII-3732 for explanation of current range. + "schema": { + "title": "Double-click rectangle width", + "description": "Changes the width of the rectangle in which a second click should fall for OS to consider the mouse action a double-click.", + "type": "object", + "properties": { + "value": { + "type": "integer", + "default": 4, + "minimum": 1, + "maximum": 64, + "multipleOf": 1 + } + } + } + } + }, + "capabilitiesTransformations": { + "DoubleClickWidthConfig": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.DoubleClickWidth" + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.DoubleClickWidth": "DoubleClickWidthConfig.value" + } + }, + "configureDoubleClickHeight": { + "type": "gpii.windows.nativeSettingsHandler", + "liveness": "live", + "options": { + "functionName": "DoubleClickHeight" + }, + "supportedSettings": { + "DoubleClickHeightConfig": { + // This setting has no documented range, see https://issues.gpii.net/browse/GPII-3732 for explanation of current range. + "schema": { + "title": "Double-click rectangle height", + "description": "Changes the height of the rectangle in which a second click should fall for OS to consider the mouse action a double-click.", + "type": "object", + "properties": { + "value": { + "type": "integer", + "default": 4, + "minimum": 1, + "maximum": 64, + "multipleOf": 1 + } + } + } + } + }, + "capabilitiesTransformations": { + "DoubleClickHeightConfig": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.DoubleClickHeight" + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.mouseSettings.DoubleClickHeight": "DoubleClickHeightConfig.value" + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.mouseTrailing": { + "name": "Windows Mouse Trailing", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETMOUSETRAILS", + "setAction": "SPI_SETMOUSETRAILS", + "uiParam": 0, + "pvParam": { + "type": "UINT" + } + }, + // TODO: Discuss somehow simplifying the structure of SPI settings for easier UI input. + "supportedSettings": { + "MouseTrails": { + "schema": { + // Exactly the same schema as mouseTrailing common term + "title": "Mouse trails", + "description": "Amount of mouse trailing", + "type": "object", + "properties": { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + "value": { + "required": true, + "type": "integer", + "minimum": 0, + "maximum": 10, + "default": 0 + } + } + } + } + }, + "capabilitiesTransformations": { + "MouseTrails": { + "value": "http://registry\\.gpii\\.net/common/mouseTrailing", + "path": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": { + "get": "pvParam", + "set": "uiParam" + } + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/mouseTrailing": "MouseTrails.value" + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.narrator": { + "name": "Windows Built-in Narrator", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled" + ], + "settingsHandlers": { + "configure": { + "type": "gpii.windows.registrySettingsHandler", + "liveness": "liveRestart", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\Narrator", + "dataTypes": { + "SpeechSpeed": "REG_DWORD", + "SpeechPitch": "REG_DWORD", + "InteractionMouse": "REG_DWORD", + "CoupleNarratorCursorKeyboard": "REG_DWORD", + "FollowInsertion": "REG_DWORD", + "EchoChars": "REG_DWORD", + "EchoWords": "REG_DWORD", + "IntonationPause": "REG_DWORD", + "ReadHints": "REG_DWORD", + "PlayAudioCues": "REG_DWORD", + "NarratorCursorHighlight": "REG_DWORD", + "FastKeyEntryEnabled": "REG_DWORD", + "ReadingWithIntent": "REG_DWORD", + "ErrorNotificationType": "REG_DWORD", + "CoupleNarratorCursorMouse": "REG_DWORD", + "LockNarratorKeys": "REG_DWORD" + } + }, + "supportedSettings": { + // CoupleNarratorCursorKeyboard, FollowInsertion, and InteractionMouse are mapped to one common term, maybe we should have individual schemas, maybe not. + "CoupleNarratorCursorKeyboard": { + "schema": { + "title": "Couple Narrator Cursor Keyboard", + "description": "Whether to couple the narrator cursor to the keyboard", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "CoupleNarratorCursorMouse": { + "schema": { + "title": "Narrator cursor follow the mouse", + "description": "Have the narrator cursor follow the mouse.", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "EchoChars": { + "schema": { + "title": "Echo Characters", + "description": "Whether to echo characters typed.", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "EchoWords": { + "schema": { + "title": "Echo Words", + "description": "Whether to echo words typed.", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "ErrorNotificationType": { + "schema": { + "title": "Speak narrator errors", + "description": "Narrator announces errors such as 'no next landmark' or 'no next item.'", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "FastKeyEntryEnabled": { + "schema": { + "title": "Touch keyboard keys activated on finger lift", + "description": "Make key insertion work on finger lift on touch keyboards", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + // CoupleNarratorCursorKeyboard, FollowInsertion, and InteractionMouse are mapped to one common term, maybe we should have individual schemas, maybe not. + "FollowInsertion": { + "schema": { + "title": "Couple Narrator Cursor Keyboard", + "description": "Whether to couple the narrator cursor to the keyboard.", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + // CoupleNarratorCursorKeyboard, FollowInsertion, and InteractionMouse are mapped to one common term, maybe we should have individual schemas, maybe not. + "InteractionMouse": { + "schema": { + "title": "Interaction Mouse", + "description": "Whether to follow mouse interactions.", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "IntonationPause": { + "schema": { + "title": "Narrator intonation pauses", + "description": "Enable intonation pauses for Narrator", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "LockNarratorKeys": { + "schema": { + "title": "Lock Narrator keys", + "description": "Lock the Narrator key so you don't have to press it with each command.", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "NarratorCursorHighlight": { + "schema": { + "title": "Cursor Highlight", + "description": "Enable cursor highlighting with a blue focus box", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "PlayAudioCues": { + "schema": { + "title": "Read audio cues", + "description": "Enable sound playing when you do things like perform a Narrator command or when suggestions are available", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "ReadingWithIntent": { + "schema": { + "title": "Emphasize formatted text", + "description": "Narrator’s voice will put more emphasis on formatted text.", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "ReadHints": { + "schema": { + "title": "Read hints for controls and buttons", + "description": "Enable narrator announcing how to interact with items", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "SpeechSpeed": { + "schema": { + "title": "Speech Speed", + "description": "The speed at which text is announced.", + "type": "integer", + "minimum": 0, + "maximum": 20, + "default": 10 + } + }, + "SpeechPitch": { + "schema": { + "title": "Speech Pitch", + "description": "The pitch at which text is announced.", + "minimum": 0, + "maximum": 20 + } + } + }, + "capabilitiesTransformations": { + "SpeechSpeed": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.quantize", + "inputPath": "http://registry\\.gpii\\.net/common/speechRate", + /* https://msdn.microsoft.com/library/ee431826 + * The speed range is -10 to +10. 0 being normal, -10 is a third of normal and +10 + * is three times normal. The registry value is actually 0 to 20. + * So this maps values between (default-speechRate / 3) and (default-speechRage * 3) + * into values between 0 and 20. + */ + "ranges": [ + { + "upperBound": 50, + "output": 0 + }, + { + "upperBound": 150, + "output": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/speechRate", + "factor": 0.066666666666667 + } + } + }, + { + "upperBound": 450, + "output": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/speechRate", + "factor": 0.033333333333, + "offset": 5 + } + } + }, + { + "output": 20 + } + ] + } + } + } + }, + "SpeechPitch": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 20, + "offset": 0 + } + }, + "transform": [ + { + "type": "fluid.transforms.arrayToSetMembership", + "inputPath": "http://registry\\.gpii\\.net/common/trackingTTS", + "outputPath": "", + "presentValue": 1, + "missingValue": 0, + "options": { + "focus": "CoupleNarratorCursorKeyboard", + "caret": "FollowInsertion", + "mouse": "InteractionMouse" + } + } + ], + "EchoChars": { + "transform": { + "type": "fluid.transforms.condition", + "false": 0, + "true": 1, + "conditionPath": "http://registry\\.gpii\\.net/common/keyEcho" + } + }, + "EchoWords": { + "transform": { + "type": "fluid.transforms.condition", + "false": 0, + "true": 1, + "conditionPath": "http://registry\\.gpii\\.net/common/wordEcho" + } + }, + "ReadingWithIntent": { + "transform": { + "type": "fluid.transforms.condition", + "false": 0, + "true": 1 + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/speechRate": { + "transform": { + "type": "fluid.transforms.condition", + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "SpeechSpeed", + "right": 10, + "operator": "<" + } + }, + "true": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "SpeechSpeed", + "factor": 10, + "offset": 50 + } + }, + "false": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "SpeechSpeed", + "factor": 30, + "offset": -150 + } + } + } + }, + "http://registry\\.gpii\\.net/common/pitch": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "SpeechPitch", + "factor": 0.05, + "offset": 0 + } + }, + "transform": [ + { + "type": "fluid.transforms.setMembershipToArray", + "inputPath": "", + "outputPath": "http://registry\\.gpii\\.net/common/trackingTTS", + "presentValue": 1, + "missingValue": 0, + "options": { + "CoupleNarratorCursorKeyboard": "focus", + "FollowInsertion": "caret", + "InteractionMouse": "mouse" + } + } + ], + "http://registry\\.gpii\\.net/common/keyEcho": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "EchoChars", + "right": 0, + "operator": "!==" + } + }, + "http://registry\\.gpii\\.net/common/wordEcho": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "EchoWords", + "right": 0, + "operator": "!==" + } + } + } + }, + "configureNoRoam": { + "type": "gpii.windows.registrySettingsHandler", + "liveness": "live", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\Narrator\\NoRoam", + "dataTypes": { + "SpeechVolume": "REG_DWORD", + "SpeechVoice": "REG_SZ", + "ShowKeyboardIntroduction": "REG_DWORD", + "ShowBrowserSelection": "REG_DWORD", + "ContextVerbosityLevel": "REG_DWORD", + "RenderContextBeforeElement": "REG_DWORD", + "DuckAudio": "REG_DWORD", + "WinEnterLaunchEnabled": "REG_DWORD", + "VerbosityLevel": "REG_DWORD", + "DetailedFeedback": "REG_DWORD" + } + }, + "supportedSettings": { + "SpeechVolume": { + "schema": { + "title": "Speech volume", + "description": "Changes the Narrator voice volume", + "type": "number", + "minimum": 0.01, + "maximum": 1 + } + }, + "SpeechVoice": { + "schema": { + "title": "Speech voice", + "description": "Select which voice is going to be used for Narrator", + "default": "Microsoft David - English (United States)", + "enum": [ + "Microsoft David - English (United States)", + "Microsoft Zira - English (United States)", + "Microsoft Mark - English (United States)", + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ], + "enumLabels": [ + "Microsoft David - English (United States)", + "Microsoft Zira - English (United States)", + "Microsoft Mark - English (United States)", + "Microsoft David Desktop", + "Microsoft Zira Desktop" + ] + } + }, + // TODO: Confirm this is still useful, it doesn't seem to appear in the Narrator options. + "ShowKeyboardIntroduction": { + "schema": { + "title": "Show Keyboard Introduction", + "description": "Whether or not to show the keyboard introduction.", + "enum": [true, false], + "enumLabels": ["On", "Off"] + } + }, + "ShowBrowserSelection": { + "schema": { + "title": "Show Browser Selection", + "description": "Whether or not to show the browser selection.", + "enum": [true, false], + "enumLabels": ["On", "Off"] + } + }, + "ContextVerbosityLevel": { + "schema": { + "title": "Context Verbosity Level", + "description": "Changes the amount of contextual reading for controls and buttons", + "default": 2, + "enum": [0, 1, 2, 3, 4, 5], + "enumLabels": [ + "No context reading", + "Sounds only", + "Immediate context", + "Immediate context name and type", + "Full context of new control", + "Full context of both the old control and new control" + ] + } + }, + "RenderContextBeforeElement": { + "schema": { + "title": "Contextual reading order", + "description": "Choose whether you want to hear context before or after the current item", + "default": 1, + "enum": [0, 1], + "enumLabels": ["Before", "After"] + } + }, + "DuckAudio": { + "schema": { + "title": "Lower other sounds", + "description": "Enable/Disable lowering other sounds while narrator is speaking", + "type": "boolean", + "default": true + } + }, + WinEnterLaunchEnabled: { + "schema": { + "title": "Start with shortcut", + "description": "Allow the shortcut key to start Narrator", + "type": "boolean", + "default": true + } + }, + "VerbosityLevel": { + "schema": { + "title": "Text verbosity level", + "description": "Changes the level of detail Narrator provides about text.", + "default": 1, + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "enumLabels": [ + "0 - Text Only", + "1 - Headers and errors", + "2 - Basic information", + "3 - Other annotations", + "4 - Extended formatting", + "5 - Layout and animation info" + ] + } + }, + "DetailedFeedback": { + "schema": { + "title": "Additional feedback", + "description": "Use caps lock + E to send additional feedback", + "enum": [0, 1], + "enumLabels": ["Off", "On"], + "default": 1 + } + } + }, + "capabilitiesTransformations": { + "SpeechVolume": { + // The common term is 0->1, we're 0.01->1 + transform: { + type: "fluid.transforms.binaryOp", + left: 0.01, + right: { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/volumeTTS", + "right": 0.99, + "operator": "*" + } + } + } + }, + "ShowKeyboardIntroduction": "http://registry\\.gpii\\.net/common/speakTutorialMessages", + "ShowBrowserSelection": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": false + } + }, + "ContextVerbosityLevel": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.narrator.ControlContextVerbosity", + "RenderContextBeforeElement": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.narrator.ReadContextBeforeControls", + "DuckAudio": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.narrator.LowerAppsVolume", + "WinEnterLaunchEnabled": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.narrator.StartWithShortcut", + "VerbosityLevel": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.narrator.TextVerbosityLevel" + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.narrator.ControlContextVerbosity": "ContextVerbosityLevel", + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.narrator.ReadContextBeforeControls": "RenderContextBeforeElement", + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.narrator.LowerAppsVolume": "DuckAudio", + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.narrator.StartWithShortcut": "WinEnterLaunchEnabled", + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.narrator.TextVerbosityLevel": "VerbosityLevel" + } + }/*, + * This settings handler block has been commented out since these settings have become + * unstable as a result of https://issues.gpii.net/browse/GPII-4231. + * https://issues.gpii.net/browse/GPII-4462 will track work to potentially rewrite this + * settings handler back in C# and remove the instability. It's also possible + * that this instability has resulted from missing "update"/"configure"/"restore" blocks as seen + * with the magnifier + "configureSystemSettings": { + "type": "gpii.windows.systemSettingsHandler", + "liveness": "live", + "capabilities": [], + "options": { + "Async": true, + "CheckResult": true + }, + "supportedSettings": { + "SystemSettings_Accessibility_Narrator_IsAutoStartEnabled": { + "schema": { + "title": "Auto Start Narrator", + "description": "Start Narrator after sign-in.", + "type": "object", + "properties": { + "value": { + "enum": [0, 1], + "enumLabels": ["Off", "On"], + "default": 0 + } + } + } + }, + "SystemSettings_Accessibility_Narrator_IsAutoStartOnLogonDesktopEnabled": { + "schema": { + "title": "Auto Start Narrator for everyone", + "description": "Start Narrator before sign-in.", + "type": "object", + "properties": { + "value": { + "enum": [0, 1], + "enumLabels": ["Off", "On"], + "default": 0 + } + } + } + } + }, + "capabilitiesTransformations": { + "SystemSettings_Accessibility_Narrator_IsAutoStartEnabled": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.narrator.EnableAutoStart" + }, + "SystemSettings_Accessibility_Narrator_IsAutoStartOnLogonDesktopEnabled": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.narrator.EnableSystemAutoStart" + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.narrator.EnableAutoStart": "SystemSettings_Accessibility_Narrator_IsAutoStartEnabled.value", + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.narrator.EnableSystemAutoStart": "SystemSettings_Accessibility_Narrator_IsAutoStartOnLogonDesktopEnabled.value" + } + } +*/ + }, + "launchHandlers": { + "launcher": { + "type": "gpii.windows.enableRegisteredAT", + "options": { + "registryName": "Narrator", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "Narrator.exe" + } + ] + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.nightScreen": { + "name": "Windows night light settings", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.windows.systemSettingsHandler", + "liveness": "live", + "options": { + "Async": true, + "CheckResult": true + }, + "capabilities": [ + "display.screenEnhancement.nightScreen", + "applications.com\\.microsoft\\.windows\\.nightScreen.id" + ], + "capabilitiesTransformations": { + "SystemSettings_Display_BlueLight_ManualToggleQuickAction": { + "value": "http://registry\\.gpii\\.net/common/nightScreen" + } + }, + "supportedSettings": { + "SystemSettings_Display_BlueLight_ManualToggleQuickAction": { + "schema": { + "title": "Night Screen", + "description": "Reduce the blue emitted from the screen.", + "type": "object", + "properties": { + "value": { + "type": "boolean", + "default": false + } + } + } + } + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.soundSentry": { + "name": "Windows 10 SoundSentry", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "capabilities": [ + ], + "settingsHandlers": { + "configure": { + "type": "gpii.windows.registrySettingsHandler", + "liveness": "liveRestart", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Control Panel\\Accessibility\\SoundSentry", + "dataTypes": { + "WindowsEffect": "REG_SZ" + } + }, + "supportedSettings": { + "WindowsEffect": { + "schema": { + "title": "SoundSentry", + "description": "Selects the desired visual notification for sounds.", + "enum": [ + "0", + "1", + "2", + "3" + ], + "enumLabels": [ + "No visual alert", + "Flash the title of the active window", + "Flash the active window", + "Flash the entire screen" + ] + } + } + }, + "capabilitiesTransformations": { + }, + "inverseCapabilitiesTransformations": { + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.onscreenKeyboard": { + "name": "Windows Built-in Onscreen Keyboard", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/onScreenKeyboard/enabled" + ], + "settingsHandlers": { + "configure": { + "type": "gpii.windows.registrySettingsHandler", + "liveness": "liveRestart", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\Osk", + "dataTypes": { + "NavigationMode": "REG_DWORD", + "ClickSound": "REG_DWORD", + "ShowClearKeyboard": "REG_DWORD", + "ShowNumPad": "REG_DWORD", + "Mode": "REG_DWORD", + "HoverPeriod": "REG_DWORD", + "ScanInterval": "REG_DWORD", + "UseDevice": "REG_DWORD", + "UseKB": "REG_DWORD", + "ScanKey": "REG_DWORD", + "UseMouse": "REG_DWORD", + "UseTextPrediction": "REG_DWORD", + "InsertSpace": "REG_DWORD", + "Dock": "REG_DWORD" + } + }, + "supportedSettings": { + "NavigationMode": { + // TODO: Weird supported setting - How do we provide such use case? + "schema": { + "title": "On-Screen Keyboard", + "description": "Enable/Disable On-Screen Keyboard", + "enum": [0, 1], + "enumLabels": ["Off", "On"] + } + }, + "ClickSound": { + "schema": { + "title": "Use click sound", + "description": "Makes click sound when pressing OSK keys", + "enum": [0, 1], + "enumLabels": ["Off", "On"], + "default": 0 + } + }, + "ShowClearKeyboard": { + "schema": { + "title": "Show position keys", + "description": "Show keys to make it easier to move around the screen", + "enum": [0, 1], + "enumLabels": ["Off", "On"], + "default": 0 + } + }, + "ShowNumPad": { + "schema": { + "title": "Numeric key pad", + "description": "Turn on numeric key pad", + "enum": [0, 1], + "enumLabels": ["Off", "On"], + "default": 0 + } + }, + "Mode": { + "schema": { + "title": "OSK keys mode", + "description": "Select how OSK keys are pressed", + "enum": [ + 1, + 2, + 3 + ], + "enumLabels": [ + "Use click to press the OSK keys", + "Hover over keys", + "Scan through keys" + ], + "default": 1 + } + }, + "HoverPeriod": { + "schema": { + "title": "Hover duration", + "description": "Changes the ammount of time for considering a click when hovering over a key", + "type": "integer", + "default": 1000, + "minimum": 500, + "maximum": 3000 + } + }, + "ScanInterval": { + "schema": { + "title": "Scanning speed", + "description": "Changes the wait interval while scanning keys", + "type": "integer", + "default": 1000, + "minimum": 500, + "maximum": 3000 + } + }, + "UseDevice": { + "schema": { + "title": "Scan using gaming device", + "description": "Use joystick, game pad or other gaming device", + "enum": [0, 1], + "enumLabels": ["Off", "On"], + "default": 1 + } + }, + "UseKB": { + "schema": { + "title": "Scan using keyboard key", + "description": "Use specific keyboard key for scanning", + "enum": [0, 1], + "enumLabels": ["Off", "On"], + "default": 0 + } + }, + "ScanKey": { + "schema": { + "title": "Scan keyboard key", + "description": "Keyboard key using for scanning", + "enum": [ + 32, + 13, + 113, + 114, + 115, + 116, + 117, + 118, + 119, + 120, + 123 + ], + "enumLabels": [ + "Space Bar Key", + "Enter", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F12" + ], + "default": 32 + } + }, + "UseMouse": { + "schema": { + "title": "Scan using mouse click", + "description": "Use mouse clicks to for scanning", + "enum": [0, 1], + "enumLabels": ["Off", "On"], + "default": 0 + } + }, + "UseTextPrediction": { + "schema": { + "title": "Text prediction", + "description": "Use OSK text prediction to show suggestions while typing", + "enum": [0, 1], + "enumLabels": ["Off", "On"], + "default": 0 + } + }, + "InsertSpace": { + "schema": { + "title": "Auto insert space", + "description": "Insert space after predicted word", + "enum": [0, 1], + "enumLabels": ["Off", "On"], + "default": 0 + } + }, + "Dock": { + "schema": { + "title": "Dock", + "description": "Dock the OSK at the bottom of the screen", + "enum": [0, 1], + "enumLabels": ["Off", "On"], + "default": 0 + } + } + }, + "capabilitiesTransformations": { + "NavigationMode": { + "literalValue": 0 + }, + "ShowClearKeyboard": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.onscreenKeyboard.ShowPositionKeys", + "Mode": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.onscreenKeyboard.KeyPressMode", + "UseDevice": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.onscreenKeyboard.UseDeviceForScan", + "UseKB": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.onscreenKeyboard.UseKBForScan", + "UseMouse": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.onscreenKeyboard.UseMouseForScan" + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.onscreenKeyboard.ShowPositionKeys": "ShowClearKeyboard", + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.onscreenKeyboard.KeyPressMode": "Mode", + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.onscreenKeyboard.UseDeviceForScan": "UseDevice", + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.onscreenKeyboard.UseKBForScan": "UseKBForScan", + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.onscreenKeyboard.UseMouseForScan": "UseMouseForScan" + } + } + }, + "launchHandlers": { + "launcher": { + "type": "gpii.windows.enableRegisteredAT", + "options": { + "registryName": "osk", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "osk.exe" + } + ] + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.screenDPI": { + "name": "Windows DPI", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=6.2" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.windows.displaySettingsHandler", + "liveness": "live", + "supportedSettings": { + "screen-dpi": { + "schema": { + "title": "Screen DPI", + "description": "Screen DPI of the default display.", + "type": "number", + // This is a relative value, where '0' is the recommended setting. + "minimum": -3, + "maximum": 5 + } + } + }, + "capabilitiesTransformations": { + "screen-dpi": "http://registry\\.gpii\\.net/common/DPIScale" + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.screenResolution": { + "name": "Windows Screen Resolution", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configuration": { + "type": "gpii.windows.displaySettingsHandler", + "liveness": "liveRestart", + "supportedSettings": { + "screen-resolution": { + "schema": { + "title": "Screen resolution", + "description": "Screen resolution of the default display.", + "type": "object", + // Although in other contexts we've accepted an array, the windows display settings handler + // throws a hard fail unless this is an object. See: + // + // https://github.com/GPII/windows/blob/97953e9b2f19fa0524d5c667291e4e3e6641fb9f/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js#L223 + // + "properties": { + "width": { type: "integer", required: true }, + "height": { type: "integer", required: true }, + "additionalProperties": false + } + } + } + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.stickyKeys": { + "name": "Windows StickyKeys", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.windows.spiSettingsHandler", + "liveness": "live", + "options": { + "getAction": "SPI_GETSTICKYKEYS", + "setAction": "SPI_SETSTICKYKEYS", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "STICKYKEYS" + } + }, + // TODO: Make a general pattern for SPI settings. + "supportedSettings": { + "StickyKeysOn": { + schema: { + "title": "Sticky Keys On", + "description": "Whether or not sticky keys should be turned on.", + "type": "object", + "properties": { + "path": { + oneOf: [ + { "type": "string"}, + { + "type": "object", + "properties": { + "get": { "type": "string", "required": true }, + "set": { "type": "string", "required": true } + } + } + ], + "required": false + }, + "value": { "type": "boolean", "required": true} + } + } + } + }, + "capabilitiesTransformations": { + "StickyKeysOn": { + "value": "http://registry\\.gpii\\.net/common/stickyKeys", + "path": { + "transform": { + "type": "fluid.transforms.literalValue", + "input": "pvParam.dwFlags.SKF_STICKYKEYSON" + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/stickyKeys": "StickyKeysOn.value" + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.touchPadSettings": { + "name": "Windows precision touchpad settings", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.windows.systemSettingsHandler", + "liveness": "live", + "options": { + "Async": true, + "CheckResult": true + }, + "capabilities": [], + "capabilitiesTransformations": { + "SystemSettings_Input_Touch_SetActivationTimeout": { + "value": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.touchPadSettings.Sensitivity" + } + }, + "supportedSettings": { + "SystemSettings_Input_Touch_SetActivationTimeout": { + "schema": { + "title": "Touchpad sensitivity", + "description": "Changes Windows 10 precision touchpad sensitivity.", + "type": "object", + "properties": { + "value": { + + "default": "Medium sensitivity", + "enum": [ + "Low sensitivity", + "Medium sensitivity", + "High sensitivity", + "Most sensitive" + ], + "enumLabels": [ + "Low sensitivity", + "Medium sensitivity", + "High sensitivity", + "Most sensitive" + ] + } + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.windows\\.touchPadSettings.Sensitivity": "SystemSettings_Input_Touch_SetActivationTimeout.value" + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.typingEnhancement": { + "name": "Windows typing autocorrect", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.windows.registrySettingsHandler", + "liveness": "live", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\TabletTip\\1.7", + "dataTypes": { + "EnableAutoShiftEngage": "REG_DWORD", + "EnableAutocorrection": "REG_DWORD", + "EnableCompatibilityKeyboard": "REG_DWORD", + "EnableDesktopModeAutoInvoke": "REG_DWORD", + "EnableDoubleTapSpace": "REG_DWORD", + "EnableKeyAudioFeedback": "REG_DWORD", + "EnablePredictionSpaceInsertion": "REG_DWORD", + "EnableShiftLock": "REG_DWORD", + "EnableSpellchecking": "REG_DWORD", + "EnableTextPrediction": "REG_DWORD" + } + }, + "supportedSettings": { + // TODO: confirm this is still relevant, update details, and add defaults. + "EnableAutoShiftEngage": { + "schema": { + "title": "Auto Shift", + "description": "Automatically engage the shift key when ???.", + "enum": [true, false], + "enumLabels": ["On", "Off"] + } + }, + "EnableAutocorrection": { + "schema": { + "title": "Autocorrect Misspelled Words", + "description": "Autocorrect misspelled words while typing.", + "enum": [true, false], + "enumLabels": ["On", "Off"], + "default": true + } + }, + // TODO: confirm this is still relevant, update details, and add defaults. + "EnableCompatibilityKeyboard": { + "schema": { + "title": "Enable Compatibility Keyboard", + "description": "Enable compatibility keyboard.", + "enum": [true, false], + "enumLabels": ["On", "Off"] + } + }, + // TODO: confirm this is still relevant, update details, and add defaults. + "EnableDesktopModeAutoInvoke": { + "schema": { + "title": "Enable Desktop Mode Auto-invoke", + "description": "Enable desktop mode auto-invoke.", + "enum": [true, false], + "enumLabels": ["On", "Off"] + } + }, + "EnableDoubleTapSpace": { + "schema": { + "title": "Period On Spacebar Double Tap", + "description": "Add a period after the spacebar is double-tapped.", + "enum": [true, false], + "enumLabels": ["On", "Off"], + "default": true + } + }, + // TODO: confirm this is still relevant, update details, and add defaults. + "EnableKeyAudioFeedback": { + "schema": { + "title": "Enable Key Audio Feedback", + "description": "Play a sound when a key is pressed.", + "enum": [true, false], + "enumLabels": ["On", "Off"] + } + }, + "EnablePredictionSpaceInsertion": { + "schema": { + "title": "Add Space After Suggestion", + "description": "Add a space after choosing a text suggestion.", + "enum": [true, false], + "enumLabels": ["On", "Off"], + "default": true + } + }, + // TODO: confirm this is still relevant, update details, and add defaults. + "EnableShiftLock": { + "schema": { + "title": "Enable Shift Lock", + "description": "Enable shift lock.", + "enum": [true, false], + "enumLabels": ["On", "Off"] + } + }, + "EnableSpellchecking": { + "schema": { + "title": "Highlight Misspelled Words", + "description": "Highlight misspelled words while typing.", + "enum": [true, false], + "enumLabels": ["On", "Off"], + "default": true + } + }, + "EnableTextPrediction": { + "schema": { + "title": "Show Text Suggestions", + "description": "Show text suggestions while typing.", + "enum": [true, false], + "enumLabels": ["On", "Off"], + "default": true + } + } + }, + "capabilities": [], + "capabilitiesTransformations": {}, + "inverseCapabilitiesTransformations": {} + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.windows.volumeControl": { + "name": "Windows 10 system volume", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.windows.nativeSettingsHandler", + "liveness": "live", + "options": { + "functionName": "Volume" + }, + "supportedSettings": { + "Volume": { + "schema": { + "title": "System volume", + "description": "Changes the current system volume.", + "type": "object", + "properties": { + "value": { + "type": "number", + "default": 0.5, + "minimum": 0, + "maximum": 1, + "multipleOf": 0.01 + } + } + } + } + }, + "capabilitiesTransformations": { + "Volume": { + "value": "http://registry\\.gpii\\.net/common/volume" + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/volume": "Volume.value" + } + } + }, + "start": [], + "stop": [], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ], + "isRunning": [] + }, + "com.office.windowsOneNoteLearningTools": { + "name": "LearningTools for OneNote 2016", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.windows.registrySettingsHandler", + "liveness": "manualRestart", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "64:Software\\Microsoft\\OneNoteLearningTools", + "dataTypes": { + "DictationLanguage": "REG_DWORD", + "ReadingComprehensionDefaultFont": "REG_SZ", + "IsReadingComprehensionFontWide": "REG_DWORD", + "ReadingComprehensionTheme": "REG_DWORD", + "ReadingComprehensionFontSize": "REG_DWORD", + "SynthRate": "REG_DWORD", + "SynthVoices": "REG_SZ" + } + }, + "supportedSettings": { + "DictationLanguage": { + "schema": { + "title": "Dictation Language", + "description": "Voice dictation language", + "type": "number", + "enum": [ + 1031, + 3082, + 1036, + 1033, + 1040, + 1041, + 1046, + 1049 + ], + "enumLabels": [ + "German", + "Spanish", + "French", + "English", + "Italian", + "Japanese", + "Portuguese", + "Russian" + ], + "default": 1033 + } + }, + "ReadingComprehensionDefaultFont": { + "schema": { + "title": "Font Spacing", + "description": "The font in which text is preferred to be presented", + "type": "string", + "enum": [ "Calibri", "Sitka Small" ], + "enumLabels": [ "Calibri", "Sitka Small" ], + "default": "Calibri" + } + }, + "IsReadingComprehensionFontWide": { + "schema": { + "title": "Font Spacing", + "description": "Set wider font spacing for reading", + "type": "number", + "enum": [1, 0], + "enumLabels": ["Default", "Wide"], + "default": 0 + } + }, + "ReadingComprehensionTheme": { + "schema": { + "title": "Theme", + "description": "Set the theme for reading", + "type": "number", + "enum": [0, 1, 2, 3, 4, 5], + "enumLabels": ["White", "Reverse (High-Contrast)", "Yellow", "Pink", "Green", "Blue"], + "default": 0 + } + }, + "ReadingComprehensionFontSize": { + "schema": { + "title": "Font Size", + "description": "The font size in which text is preferred to be presented", + "type": "number", + "minimum": 12, + "maximum": 96, + "default": 48, + "multipleOf" : 2 + } + }, + "SynthRate": { + "schema": { + "title": "Synth rate", + "description": "The font size in which text is preferred to be presented", + "type": "number", + "enum": [ + 4294967286, + 4294967289, + 4294967291, + 4294967294, + 0, + 2, + 4, + 6, + 8 + ], + "enumLabels": ["-4", "-3", "-2", "-1", "0", "1", "2", "4", "6"], + "default": 0 + } + }, + "SynthVoices": { + "schema": { + "title": "Synth voice", + "description": "The voice that will be used for text reading", + "type": "string", + "enum": [ + "[{\"Lang\":\"zh\",\"Voice\":\"Microsoft Tracy Desktop\"}]", // - Hong kong + "[{\"Lang\":\"zh\",\"Voice\":\"Microsoft Hanhan Desktop\"}]", // - Taiwan + "[{\"Lang\":\"zh\",\"Voice\":\"Microsoft HuiHui Desktop\"}]", // - People’s Chinese Republic + + "[{\"Lang\":\"en\",\"Voice\":\"Microsoft Zira Desktop\"}]", // - US + "[{\"Lang\":\"en\",\"Voice\":\"Microsoft David Desktop\"}]", // - US + + "[{\"Lang\":\"en\",\"Voice\":\"Microsoft Helena Desktop\"}]", // - United Kingdom + "[{\"Lang\":\"fr\",\"Voice\":\"Microsoft Hortense Desktop\"}]", // - France + "[{\"Lang\":\"de\",\"Voice\":\"Microsoft Hedda Desktop\"}]", // - Germany + "[{\"Lang\":\"it\",\"Voice\":\"Microsoft Elsa Desktop\"}]", // - Italy + "[{\"Lang\":\"ja\",\"Voice\":\"Microsoft Haruka Desktop\"}]", // - Japan + "[{\"Lang\":\"ko\",\"Voice\":\"Microsoft Heami Desktop\"}]", // - Korea + "[{\"Lang\":\"pl\",\"Voice\":\"Microsoft Paulina Desktop\"}]", // - Poland + "[{\"Lang\":\"pt\",\"Voice\":\"Microsoft Maria Desktop\"}]", // - Portugal + "[{\"Lang\":\"ru\",\"Voice\":\"Microsoft Irina Desktop\"}]", // - Russia + "[{\"Lang\":\"es\",\"Voice\":\"Microsoft Sabina Desktop\"}]", // - Mexico + "[{\"Lang\":\"es\",\"Voice\":\"Microsoft Helena Desktop\"}]" // - Spain + ], + "enumLabels": [ + "ZH - Microsoft Tracy", + "ZH - Microsoft Hanhan", + "ZH - Microsoft HuiHui", + "EN - Microsoft Zira", + "EN - Microsoft David", + "EN - Microsoft Helena", + "FR - Microsoft Hortense", + "DE - Microsoft Hedda", + "IT - Microsoft Elsa", + "JA - Microsoft Haruka", + "KO - Microsoft Heami", + "PL - Microsoft Paulina", + "PT - Microsoft Maria", + "RU - Microsoft Irina", + "ES - Microsoft Sabina", + "ES - Microsoft Helena" + ] + } + } + }, + "capabilities": [ + ], + "capabilitiesTransformations": { + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.registryKeyExists", + "hKey": "HKEY_LOCAL_MACHINE", + "path": "64:SOFTWARE\\Microsoft\\Office\\OneNote\\AddIns\\LearningTools.OneNoteLearningTools", + "subPath": "FriendlyName", + "dataType": "REG_SZ" + } + ], + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "ONENOTE.exe" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\OneNote.exe\\}\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "ONENOTE.exe" + } + ] + } + } + } + }, + "com.office.windowsWordHome365LearningTools": { + "name": "LearningTools for Microsoft Office 2016 Word Home 365 edition for Windows 10", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.windows.registrySettingsHandler", + "liveness": "manualRestart", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "64:Software\\Microsoft\\Office\\16.0\\Word\\Options", + "dataTypes": { + "ReadingModeColumnWidth": "REG_DWORD", + "ReadingModePageColor": "REG_DWORD", + "ReadingModePrintedPage": "REG_DWORD", + "ReadAloudVoiceId": "REG_SZ", + "ReadAloudVoiceRate": "REG_DWORD", + "ReadingModeSyllables": "REG_DWORD", + "ReadingModeTextSpacing": "REG_DWORD" + } + }, + "supportedSettings": { + "ReadingModeColumnWidth": { + "schema": { + "title": "Column mode", + "description": "Column mode in which to display text", + "type": "number", + "enum": [0, 1, 2], + "enumLabels": ["Narrow", "Default", "Wide"], + "default": 0 + } + }, + "ReadingModePageColor": { + "schema": { + "title": "Page color", + "description": "The color that will be used as page background", + "type": "number", + "enum": [0, 1, 2], + "enumLabels": ["None", "Sepia", "Reverse (High-Contrast)"], + "default": 0 + } + }, + "ReadingModePrintedPage": { + "schema": { + "title": "Design", + "description": "Select between column desing and page design", + "type": "number", + "enum": [0, 1], + "enumLabels": ["Column Layout", "Printed Layout"], + "default": 0 + } + }, + "ReadAloudVoiceId": { + "schema": { + "title": "Voice Id", + "description": "The voice that will be used for text reading", + "type": "string", + "enum": [ + "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech_OneCore\\Voices\\Tokens\\MSTTS_V110_esES_PabloM", + "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech_OneCore\\Voices\\Tokens\\MSTTS_V110_esES_HelenaM", + "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech_OneCore\\Voices\\Tokens\\MSTTS_V110_esES_LauraM", + + "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech_OneCore\\Voices\\Tokens\\MSTTS_V110_enUS_DavidM", + "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech_OneCore\\Voices\\Tokens\\MSTTS_V110_enUS_MarkM", + "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech_OneCore\\Voices\\Tokens\\MSTTS_V110_enUS_ZiraM" + ], + "enumLabels": ["Pablo", "Helena", "Laura", "David", "Mark", "Zira"], + "default": "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech_OneCore\\Voices\\Tokens\\MSTTS_V110_enUS_DavidM" + } + }, + "ReadAloudVoiceRate": { + "schema": { + "title": "Font Size", + "description": "The font size in which text is preferred to be presented", + "type": "integer", + // TODO: We should present the value in a homogeneus speech rate + "default": 10, + "minimum": 0, + "maximum": 20 + } + }, + "ReadingModeSyllables": { + "schema": { + "title": "Synth rate", + "description": "The font size in which text is preferred to be presented", + "type": "number", + "enum": [0, 1], + "enumLabels": ["False", "True"], + "default": 0 + } + }, + "ReadingModeTextSpacing": { + "schema": { + "title": "Synth rate", + "description": "The font size in which text is preferred to be presented", + "type": "number", + "enum": [0, 1], + "enumLabels": ["False", "True"], + "default": 0 + } + } + }, + "capabilities": [ + ], + "capabilitiesTransformations": { + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.registryKeyExists", + "hKey": "HKEY_LOCAL_MACHINE", + "path": "64:SOFTWARE\\Microsoft\\Office\\ClickToRun\\Configuration", + "subPath": "O365HomePremRetail.TenantId", + "dataType": "REG_SZ" + } + ], + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "WINWORD.exe" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\Winword.exe\\}\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "WINWORD.exe" + } + ] + } + } + } + }, + "com.office.windowsWordPro365LearningTools": { + "name": "LearningTools for Microsoft Office 2016 Word Pro 365 edition for Windows 10", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.windows.registrySettingsHandler", + "liveness": "manualRestart", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "64:Software\\Microsoft\\Office\\16.0\\Word\\Options", + "dataTypes": { + "ReadingModeColumnWidth": "REG_DWORD", + "ReadingModePageColor": "REG_DWORD", + "ReadingModePrintedPage": "REG_DWORD", + "ReadAloudVoiceId": "REG_SZ", + "ReadAloudVoiceRate": "REG_DWORD", + "ReadingModeSyllables": "REG_DWORD", + "ReadingModeTextSpacing": "REG_DWORD" + } + }, + "supportedSettings": { + "ReadingModeColumnWidth": { + "schema": { + "title": "Column mode", + "description": "Column mode in which to display text", + "type": "number", + "enum": [0, 1, 2], + "enumLabels": ["Narrow", "Default", "Wide"], + "default": 0 + } + }, + "ReadingModePageColor": { + "schema": { + "title": "Page color", + "description": "The color that will be used as page background", + "type": "number", + "enum": [0, 1, 2], + "enumLabels": ["None", "Sepia", "Reverse (High-Contrast)"], + "default": 0 + } + }, + "ReadingModePrintedPage": { + "schema": { + "title": "Design", + "description": "Select between column desing and page design", + "type": "number", + "enum": [0, 1], + "enumLabels": ["Column Layout", "Printed Layout"], + "default": 0 + } + }, + "ReadAloudVoiceId": { + "schema": { + "title": "Voice Id", + "description": "The voice that will be used for text reading", + "enum": [ + "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\TTS_MS_EN-US_DAVID_11.0", + "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\TTS_MS_EN-US_ZIRA_11.0", + + "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\TTS_MS_ES-ES_HELENA_11.0" + ], + "enumLabels": ["David", "Zira", "Helena"], + "default": "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\TTS_MS_EN-US_DAVID_11.0" + } + }, + "ReadAloudVoiceRate": { + "schema": { + "title": "Font Size", + "description": "The font size in which text is preferred to be presented", + "type": "integer", + // TODO: We should present the value in a homogeneous speech rate + "default": 10, + "minimum": 0, + "maximum": 20 + } + }, + "ReadingModeSyllables": { + "schema": { + "title": "Synth rate", + "description": "The font size in which text is preferred to be presented", + "type": "number", + "enumLabels": ["False", "True"], + "enum": [0, 1], + "default": 0 + } + }, + "ReadingModeTextSpacing": { + "schema": { + "title": "Synth rate", + "description": "The font size in which text is preferred to be presented", + "type": "number", + "enum": [0, 1], + "enumLabels": ["False", "True"], + "default": 0 + } + } + }, + "capabilities": [ + ], + "capabilitiesTransformations": { + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.registryKeyExists", + "hKey": "HKEY_LOCAL_MACHINE", + "path": "64:SOFTWARE\\Microsoft\\Office\\ClickToRun\\Configuration", + "subPath": "O365ProPlusRetail.TenantId", + "dataType": "REG_SZ" + } + ], + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "WINWORD.exe" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\Winword.exe\\}\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "WINWORD.exe" + } + ] + } + } + } + }, + "com.texthelp.readWriteGold": { + "name": "Read&Write 12", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "capabilities": [], + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.XMLHandler", + "liveness": "manualRestart", + "options": { + "filename": "${{environment}.APPDATA}\\Texthelp\\ReadAndWrite\\12\\RWSettings.xml", + "encoding": "utf-8", + "xml-tag": "" + }, + "capabilitiesTransformations": { + "ApplicationSettings": "ApplicationSettings" + }, + "supportedSettings": { + // General + "ApplicationSettings.AppBar.IconSize.$t": { + "schema": { + "title": "Button size", + "description": "Changes the application panel buttons size.", + "enum": [ + "Medium", + "Small", + "Large" + ], + "enumLabels": [ + "Medium", + "Small", + "Large" + ], + "default": "Medium" + } + }, + "ApplicationSettings.AppBar.ToolbarIconSet.$t": { + "schema": { + "title": "Button style", + "description": "Changes the application panel buttons style.", + "enum": [ + "Clear", + "Color" + ], + "enumLabels": [ + "Clear", + "Color" + ], + "default": "Clear" + } + }, + "ApplicationSettings.AppBar.ShowText.$t": { + "schema": { + "title": "Show text on toolbar", + "description": "Shows icons text in the application toolbar.", + "type": "boolean", + "default": false + } + }, + // TODO: Add a sensible default. + "ApplicationSettings.AppBar.optToolbarBackColour.$t": { + "schema": { + "title": "Toolbar color", + "description": "Changes the amount of time for considering a click when hovering over a key.", + "type": "string" + } + }, + "ApplicationSettings.AppBar.RunOnStartUp.$t": { + "schema": { + "title": "Launch on Windows startup", + "description": "Automatically launches the application at windows startup.", + "type": "boolean", + "default": true + } + }, + // Check It + "ApplicationSettings.Spelling.OrderByContext.$t": { + "schema": { + "title": "Order suggestions by context", + "description": "Suggestions are ordered by context.", + "type": "boolean", + "default": true + } + }, + "ApplicationSettings.Spelling.SpeakSpellingOnHover.$t": { + "schema": { + "title": "Speak when mouse pointer hovers over a word", + "description": "When mouse pointer hovers over a word spell the hovered word.", + "type": "boolean", + "default": true + } + }, + "ApplicationSettings.Spelling.ShowCheckIt.$t": { + "schema": { + "title": "Classic Spell Check", + "description": "Uses the old program interface for spell checking.", + "type": "boolean", + "default": false + } + }, + "ApplicationSettings.Spelling.RightClickMSWord.$t": { + "schema": { + "title": "Microsoft Word right click spell check", + "description": "Allows right click Read&Write spell check in Microsoft Word.", + "type": "boolean", + "default": false + } + }, + // Check It - Vocabulary - TODO: We need to comsume an array to build a mask + // "ApplicationSettings.Spelling.optVocabMask.$t": {}, + // + // Dictionary + "ApplicationSettings.Dictionary.WebDictionary.$t": { + "schema": { + "title": "Web Dictionary", + "description": "Select the web dictionary to use.", + "enum": [ + "Google Dictionary", + "Bing", + "Dictionary.com" + ], + "enumLabels": [ + "Google Dictionary", + "Bing", + "Dictionary.com" + ], + "default": "Google Dictionary" + } + }, + "ApplicationSettings.Dictionary.PopupDictionary.$t": { + "schema": { + "title": "Pop-up Dictionary", + "description": "Displays a popup dictionary when hovering selected words.", + "type": "boolean", + "default": false + } + }, + "ApplicationSettings.Dictionary.ToggleImages.$t": { + "schema": { + "title": "Toggle Images", + "description": "Toggles image section in dictionary popup window.", + "type": "boolean", + "default": false + } + }, + // Highlights + "ApplicationSettings.StudySkills.CollectYellowHighlight.$t": { + "schema": { + "title": "Collect Yellow color", + "description": "Collects your highlighted sections that have been marked with Yellow.", + "type": "boolean", + "default": true + } + }, + "ApplicationSettings.StudySkills.CollectBlueHighlight.$t": { + "schema": { + "title": "Collect Blue color", + "description": "Collects your highlighted sections that have been marked with Blue.", + "type": "boolean", + "default": true + } + }, + "ApplicationSettings.StudySkills.CollectGreenHighlight.$t": { + "schema": { + "title": "Collect Green color", + "description": "Collects your highlighted sections that have been marked with Green.", + "type": "boolean", + "default": true + } + }, + "ApplicationSettings.StudySkills.CollectPinkHighlight.$t": { + "schema": { + "title": "Collect Pink color", + "description": "Collects your highlighted sections that have been marked with Pink.", + "type": "boolean", + "default": true + } + }, + "ApplicationSettings.StudySkills.CollectOrder.$t": { + "schema": { + "title": "Highlights order", + "description": "Changes the order in which highlights are displayed.", + "enum": [ + "Color", + "Position", + "Time" + ], + "enumLabels": [ + "Color", + "Position", + "Time" + ], + "default": "Color" + } + }, + "ApplicationSettings.StudySkills.ColorSeparator.$t": { + "schema": { + "title": "Colors separator", + "description": "Element used for color separation.", + "enum": [ + "Paragraph", + "None", + "Page" + ], + "enumLabels": [ + "Paragraph", + "None", + "Page" + ], + "default": "Paragraph" + } + }, + "ApplicationSettings.StudySkills.HighlightSeparator.$t": { + "schema": { + "title": "Highlights separator", + "description": "Element used for highlight separation.", + "enum": [ + "Line", + "None", + "Space", + "Tab" + ], + "enumLabels": [ + "Line", + "None", + "Space", + "Tab" + ], + "default": "Line" + } + }, + "ApplicationSettings.StudySkills.MultiDocHighlighting.$t": { + "schema": { + "title": "Collect from multiple documents", + "description": "Makes the application collect your highlights from multiple documents.", + "type": "boolean", + "default": true + } + }, + // Prediction + "ApplicationSettings.Prediction.DisplayAlphabetic.$t": { + "schema": { + "title": "Alphabetic display", + "description": "Display words alphabetically.", + "type": "boolean", + "default": false + } + }, + "ApplicationSettings.Prediction.FollowCursor.$t": { + "schema": { + "title": "Follow the cursor while typing", + "description": "Instructs the system to have the Prediction window follow where the cursor goes.", + "type": "boolean", + "default": false + } + }, + "ApplicationSettings.Prediction.AutoHeightAdjust.$t": { + "schema": { + "title": "Auto height adjust", + "description": "Instructs the system to have the Prediction window change size depending on the amount of words appearing.", + "type": "boolean", + "default": false + } + }, + "ApplicationSettings.Prediction.winPredictOneWordAhead.$t": { + "schema": { + "title": "Predict one word ahead", + "description": "Instructs the system to predict a word ahead.", + "type": "boolean", + "default": true + } + }, + "ApplicationSettings.Prediction.InsertSpaceAfterPred.$t": { + "schema": { + "title": "Insert a space after predictions", + "description": "Instructs the system to automatically insert a space after a word when it is inserted.", + "type": "boolean", + "default": true + } + }, + "ApplicationSettings.Prediction.UseSpellingForPredictions.$t": { + "schema": { + "title": "Use spelling suggestions for the prediction list", + "description": "Allows the system to suggest words even if you start to spell them wrong.", + "type": "boolean", + "default": true + } + }, + // Prediction Speech + "ApplicationSettings.Prediction.SpeakPredictionOnMouseHover.$t": { + "schema": { + "title": "Speak when hovering", + "description": "Speaks when the mouse pointer hovers over a word", + "type": "boolean", + "default": true + } + }, + // This two settings are excluyent, need to encode that + // ===================================================== + "ApplicationSettings.Prediction.SpeakOnWordClick.$t": { + "schema": { + "title": "Speak the word when clicked", + "description": "Speaks the word clicked in the prediction list.", + "type": "boolean", + "default": false + } + }, + "ApplicationSettings.Prediction.InsertOnWordClick.$t": { + "schema": { + "title": "Insert the word when clicked", + "description": "Inserts the word clicked in the prediction list.", + "type": "boolean", + "default": true + } + }, + // ===================================================== + // Prediction Learning + "ApplicationSettings.Prediction.SpellCheckBeforeLearn.$t": { + "schema": { + "title": "Spell check each word before learning", + "description": "System always spell check each word before learning it. Turn off for making the system learn custom words.", + "type": "boolean", + "default": true + } + }, + "ApplicationSettings.Prediction.AutoLearnAsType.$t": { + "schema": { + "title": "Automatically learn as I type", + "description": "Prediction will learn anything typed by the user when using prediction.", + "type": "boolean", + "default": true + } + }, + // Scanning + "ApplicationSettings.Scanning.dpi.$t": { + "schema": { + "title": "Resolution", + "description": "DPI resolution in which the scan is going to be performed.", + "enum": [ + 300, + 600 + ], + "enumLabels": [ + "300 DPI", + "600 DPI" + ], + "default": 300 + } + }, + "ApplicationSettings.Scanning.colormode.$t": { + "schema": { + "title": "Color mode", + "description": "Specifies the color mode used to scan the image.", + "enum": [ + "Color", + "GrayScale", + "BlackAndWhite" + ], + "enumLabels": [ + "Full color", + "GrayScale", + "Monochrome" + ], + "default": "Color" + } + }, + "ApplicationSettings.Scanning.HideInterface.$t": { + "schema": { + "title": "Hide device settings", + "description": "Make the scanning easier and faster. You can turn this off for getting device info.", + "type": "boolean", + "default": true + } + }, + // Device Setup (Advanced settigns) + "ApplicationSettings.Scanning.Countdown.$t": { + "schema": { + "title": "Use Countdown", + "description": "Automatically scans using a countdown interval.", + "type": "boolean", + "default": true + } + }, + "ApplicationSettings.Scanning.CountdownInterval.$t": { + "schema": { + "title": "Countdown seconds interval", + "description": "Sets the number of seconds of the interval.", + "type": "integer", + "minimum": 0, + "maximum": 99, + "default": 6 + } + }, + "ApplicationSettings.Scanning.ADF.$t": { + "schema": { + "title": "Automatic Document Feeder", + "description": "Enable if you are using an Automatic Document Feeder.", + "type": "boolean", + "default": false + } + }, + "ApplicationSettings.Scanning.Duplex.$t": { + "schema": { + "title": "Duplex", + "description": "Enable you are using an 'double side scanner'.", + "type": "boolean", + "default": false + } + }, + // Save Options + "ApplicationSettings.Scanning.AutoSave.$t": { + "schema": { + "title": "Use save location", + "description": "Use the supplied location to save the scanned documents.", + "type": "boolean", + "default": false + } + }, + "ApplicationSettings.Scanning.DefaultFilePath.$t": { + "schema": { + "title": "File path", + "description": "Default location for storing the scanned documents.", + "type": "string", + // Should be a way to capture the $HOME environmental variable + "default": "%HOME%\\Documents" + } + }, + "ApplicationSettings.Scanning.DefaultFileName.$t": { + "schema": { + "title": "File name", + "description": "Default file name for the scanned document.", + "type": "string", + "default": "Scan" + } + }, + // PDF Output + "ApplicationSettings.Scanning.PdfTextOnly.$t": { + "schema": { + "title": "Text only PDF output", + "description": "Scan to PDF to text only.", + "type": "boolean", + "default": false + } + }, + "ApplicationSettings.Scanning.MSWordRetainFormat.$t": { + "schema": { + "title": "Plain text Word output", + "description": "Scan to Word in plain text.", + "type": "boolean", + "default": false + } + }, + "ApplicationSettings.Scanning.CoverFirstPage.$t": { + "schema": { + "title": "Title in ePub Output", + "description": "Threat heading as book title for ePub output.", + "type": "boolean", + "default": true + } + }, + // Output Types + "ApplicationSettings.Scanning.EnableEpub.$t": { + "schema": { + "title": "Enable ePup", + "description": "Enable ePub as output format.", + "type": "boolean", + "default": false + } + }, + "ApplicationSettings.Scanning.EnableWeb.$t": { + "schema": { + "title": "Enable Web", + "description": "Enable Web as output format.", + "type": "boolean", + "default": false + } + }, + // Screen Masking + "ApplicationSettings.ScreenMasking.ScreenMask.$t": { + "schema": { + "title": "Screen Masking mode", + "description": "Specifies the mode in which 'Screen Masking' tool will operate.", + "enum": [ + "TintScreen", + "UnderlineTypingLine", + "TintTypingLine", + "SystemBackground", + "UnderlineCursor" + ], + "enumLabels": [ + "TintScreen", + "UnderlineTypingLine", + "TintTypingLine", + "SystemBackground", + "UnderlineCursor" + ], + "default": "TintScreen" + } + }, + // Screen Masking -> Tint whole screen subsettings + "ApplicationSettings.ScreenMasking.TintScreenColour.$t": { + "schema": { + "title": "Whole screen color mask", + "description": "Selects the color for tinting the screen.", + "type": "string", + "default": "#80FFFF" + } + }, + "ApplicationSettings.ScreenMasking.TintScreenOpacity.$t": { + "schema": { + "title": "Whole screen mask opacity", + "description": "Selects the opacity for the screen mask.", + "type": "number", + "minimum": 0, + "maximum": 1, + "default": 0.3 + } + }, + "ApplicationSettings.ScreenMasking.ReadingLight.$t": { + "schema": { + "title": "Reading ruler", + "description": "Enable/Disable reading ruler.", + "type": "boolean", + "default": false + } + }, + "ApplicationSettings.ScreenMasking.ReadingLightColour.$t": { + "schema": { + "title": "Reading ruler color", + "description": "Selects the color for the reading ruler.", + "type": "string", + "default": "#00FFFFFF" + } + }, + "ApplicationSettings.ScreenMasking.ReadingLightOpacity.$t": { + "schema": { + "title": "Reading ruler opacity", + "description": "Selects the opacity for the reading ruler.", + "type": "number", + "minimum": 0, + "maximum": 1, + "default": 0.3 + } + }, + "ApplicationSettings.ScreenMasking.ReadingLightHeight.$t": { + "schema": { + "title": "Reading ruler height", + "description": "Selects the height for the reading ruler.", + "type": "integer", + "minimum": 0, + "maximum": 300, + "default": 100 + } + }, + // Screen Masking -> Underline typing line + "ApplicationSettings.ScreenMasking.UnderlineTypingLineColour.$t": { + "schema": { + "title": "Underline typing line color", + "description": "Selects the color for the underline typing line.", + "type": "string", + "default": "#80FFFF" + } + }, + "ApplicationSettings.ScreenMasking.UnderlineTypingLineOpacity.$t": { + "schema": { + "title": "Underline typing line opacity", + "description": "Selects the opacity for the underline typing line.", + "type": "number", + "minimum": 0, + "maximum": 1, + "default": 0.5 + } + }, + "ApplicationSettings.ScreenMasking.UnderlineTypingLineHeight.$t": { + "schema": { + "title": "Underline typing line height", + "description": "Selects the height for the underline typing line.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 5 + } + }, + // Screen Masking -> Tint typing line + "ApplicationSettings.ScreenMasking.TintTypingLineColour.$t": { + "schema": { + "title": "Typing line mask color", + "description": "Selects the color for the typing line mask.", + "type": "string", + "default": "#80FFFF" + } + }, + "ApplicationSettings.ScreenMasking.TintTypingLineOpacity.$t": { + "schema": { + "title": "Typing line mask opacity", + "description": "Selects the opacity for the typing line mask.", + "type": "number", + "minimum": 0, + "maximum": 1, + "default": 0.3 + } + }, + // Screen Masking -> Change page color + "ApplicationSettings.ScreenMasking.SystemBackgroundColour.$t": { + "schema": { + "title": "Change page color", + "description": "Changes the page color of text fields.", + "type": "string", + "default": "#80FFFF" + } + }, + // Screen Masking -> Underline cursor + "ApplicationSettings.ScreenMasking.UnderlineCursorColour.$t": { + "schema": { + "title": "Underline cursor color", + "description": "Changes the color for the underline cursor.", + "type": "string", + "default": "#80FFFF" + } + }, + "ApplicationSettings.ScreenMasking.UnderlineCursorOpacity.$t": { + "schema": { + "title": "Underline cursor line opacity", + "description": "Selects the opacity for underline cursor line.", + "type": "number", + "minimum": 0, + "maximum": 1, + "default": 0.5 + } + }, + "ApplicationSettings.ScreenMasking.UnderlineCursorHeight.$t": { + "schema": { + "title": "Underline cursor line height", + "description": "Selects the height for underline cursor line.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 5 + } + }, + // Screenshot Reader + "ApplicationSettings.Screenshot.CaptureBy.$t": { + "schema": { + "title": "Capture operation", + "description": "Specifies the type of operation that is needed for making a screenshot.", + "enum": [ + "Drawing Rectangle", + "Drawing Freehand", + "Hover" + ], + "enumLabels": [ + "Drawing Rectangle", + "Drawing Freehand", + "Hover" + ], + "default": "Drawing Rectangle" + } + }, + "ApplicationSettings.Screenshot.ScreenshotTo.$t": { + "schema": { + "title": "Capture target", + "description": "Specifies the capture target of the screenshot operation.", + "enum": [ + "Window", + "Microsoft Word", + "Text Reader" + ], + "enumLabels": [ + "Window", + "Microsoft Word", + "Text Reader" + ], + "default": "Window" + } + }, + "ApplicationSettings.Screenshot.AppendToMsWordDoc.$t": { + "schema": { + "title": "Add to active Microsoft Word document", + "description": "Automatically add the screenshot to the current active Microsoft Word document.", + "type": "boolean", + "default": true + } + }, + // Speech + "ApplicationSettings.Speech.CurrentVoice.$t": { + "schema": { + "title": "Voice", + "description": "Selects the human voice for speaking.", + "enum":[ + "Mexican Spanish Paulina - Vocalizer", + "US Ava - Vocalizer", + "US Tom - Vocalizer", + "Microsoft David Desktop - English (United States)", + "Microsoft Zira Desktop - English (United States)" + ], + "enumLabels": [ + "Mexican Spanish Paulina - Vocalizer", + "US Ava - Vocalizer", + "US Tom - Vocalizer", + "Microsoft David Desktop - English (United States)", + "Microsoft Zira Desktop - English (United States)" + ], + "default": "US Ava - Vocalizer" + } + }, + "ApplicationSettings.Speech.VoiceSpeed.$t": { + "schema": { + "title": "Voice speed", + "description": "Selects the speed of reading.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "ApplicationSettings.Speech.UseVoiceSpeedHotKey.$t": { + "schema": { + "title": "Use arrow keys to change speed", + "description": "This will allow you to use the Up (\u2b06) and Down (\u2b07) keys on your keyboard to change the voice speed when it is reading.", + "type": "boolean", + "default": true + } + }, + "ApplicationSettings.Speech.WordPause.$t": { + "schema": { + "title": "Word pause", + "description": "Selects the word pause time while reading.", + "type": "integer", + "minimum": 0, + "maximum": 1000, + "default": 0, + // TODO: Verify if it's the proper check + "multipleOf": 100 + } + }, + "ApplicationSettings.Speech.VoicePitch.$t": { + "schema": { + "title": "Voice pitch", + "description": "Selects the pitch of the reading voice.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + // Speech -> Auto Read + "ApplicationSettings.Speech.SpeakAsIType.$t": { + "schema": { + "title": "Speak as I type", + "description": "Enable speaking while the user is typing.", + "type": "boolean", + "default": true + } + }, + // Speech -> Auto Read -> Speak as I type + "ApplicationSettings.Speech.SpeakOnEachLetter.$t": { + "schema": { + "title": "Speak on each letter", + "description": "System will speak each written letter.", + "type": "boolean", + "default": false + } + }, + // Speech -> Auto Read -> Speak as I type + "ApplicationSettings.Speech.SpeakOnEachWord.$t": { + "schema": { + "title": "Speak on each word", + "description": "System will speak each written word.", + "type": "boolean", + "default": true + } + }, + // Speech -> Auto Read -> Speak as I type + "ApplicationSettings.Speech.SpeakOnEachSentence.$t": { + "schema": { + "title": "Speak on each sentence", + "description": "System will speak each written sentence.", + "type": "boolean", + "default": true + } + }, + // Speech -> Screen reading + "ApplicationSettings.Speech.ScreenReadingEnabled.$t": { + "schema": { + "title": "Screen reading", + "description": "Enables screen reading.", + "type": "boolean", + "default": true + } + }, + // Speech -> Screen reading -> Items to read + // TODO: Setting is stored in an array format in XML, we need to check how + // XMLSettingsHandler handles this case. + // "ApplicationSettings.Speech.ScreenReadingItems.$t": { + // "schema": { + // "title": "Items to read", + // "description": "Selects the kind of items that shoud be readed.", + // "type": "array", + // "default": [ + // "Buttons", + // "Lists", + // "Menus", + // "Text", + // "Other" + // ] + // } + // }, + // Speech -> Read by + "ApplicationSettings.Speech.SelectionType.$t": { + "schema": { + "title": "Read by", + "description": "Selects the reading unit.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "Words", + "Sentences", + "Paragraphs" + ], + "default": 1 + } + }, + // Speech -> Continuous reading + "ApplicationSettings.Speech.ContinuousReading.$t": { + "schema": { + "title": "Continuous reading", + "description": "It instructs the system to keep reading until the end of the text.", + "type": "boolean", + "default": true + } + }, + // Speech -> Read the web + "ApplicationSettings.Speech.ReadTheWeb.$t": { + "schema": { + "title": "Read the web", + "description": "Encourages the system to read webpages.", + "type": "boolean", + "default": true + } + }, + // Speech -> Speech highlighting + "ApplicationSettings.Speech.HighlightingMethod.$t": { + "schema": { + "title": "Read by", + "description": "Selects the reading unit.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "Speak with highlighting in document", + "Speak with highlighting in text reader", + "Speak using one word display" + ], + "default": 0 + } + }, + // Speech -> Speech highlighting -> Font + "ApplicationSettings.Speech.TextReaderOneWordFontName.$t": { + "schema": { + "title": "One word display font", + "description": "Selects the reading font for the 'one word display'", + "enum": [ + "Adobe Arabic", + "Adobe Fan Heiti Std", + "Adobe Gothic Std", + "Adobe Hebrew", + "Adobe Heiti Std", + "Adobe Ming Std", + "Adobe Myungjo Std", + "Adobe Pi Std", + "Adobe Song Std", + "Adobe Thai", + "Arial", + "Bahnschrift", + "Calibri", + "Cambria", + "Cambria Math", + "Candara", + "Comic Sans MS", + "Consolas", + "Constantia", + "Corbel", + "Courier New", + "Courier Std", + "Ebrima", + "Franklin Gothic", + "Gabriola", + "Gadugi", + "Georgia", + "Global Monospace", + "Global Sans Serif", + "Global Serif", + "Global User Interface", + "HoloLens MDL2 Assets", + "Impact", + "Ink Free", + "Javanese Text", + "Kozuka Gothic Pr6N", + "Kozuka Mincho Pr6N", + "Leelawadee UI", + "Lucida Console", + "Lucida Sans Unicode", + "Malgun Gothic", + "Marlett", + "Microsoft Himalaya", + "Microsoft JhengHei", + "Microsoft JhengHei UI", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Sans Serif", + "Microsoft Tai Le", + "Microsoft YaHei", + "Microsoft YaHei UI", + "Microsoft Yi Baiti", + "MingLiUHKSCS-ExtB", + "MingLiU-ExtB", + "Minion Pro", + "Mongolian Baiti", + "MS Gothic", + "MS PGothic", + "MS UI Gothic", + "MV Boli", + "Myanmar Text", + "Myriad Pro", + "Nirmala UI", + "NSimSun", + "Open Sans", + "Palatino Linotype", + "PMingLiu-ExtB", + "Segoe MDL2 Assets", + "Segoe Print", + "Segoe Script", + "Segoe UI", + "Segoe UI Emoji", + "Segoe UI Historic", + "Segoe UI Symbol", + "SimSun", + "Simsun-ExtB", + "Sitka Banner", + "Sitka Display", + "Sitka Heading", + "Sitka Small", + "Sitka Subheading", + "Sitka Text", + "Sylfaen", + "Symbol", + "Tahoma", + "Times New Roman", + "Trebuchet MS", + "Verdana", + "Webdings", + "Wingdings", + "Yu Gothic", + "Yu Gothic UI" + ], + "enumLabels": [ + "Adobe Arabic", + "Adobe Fan Heiti Std", + "Adobe Gothic Std", + "Adobe Hebrew", + "Adobe Heiti Std", + "Adobe Ming Std", + "Adobe Myungjo Std", + "Adobe Pi Std", + "Adobe Song Std", + "Adobe Thai", + "Arial", + "Bahnschrift", + "Calibri", + "Cambria", + "Cambria Math", + "Candara", + "Comic Sans MS", + "Consolas", + "Constantia", + "Corbel", + "Courier New", + "Courier Std", + "Ebrima", + "Franklin Gothic", + "Gabriola", + "Gadugi", + "Georgia", + "Global Monospace", + "Global Sans Serif", + "Global Serif", + "Global User Interface", + "HoloLens MDL2 Assets", + "Impact", + "Ink Free", + "Javanese Text", + "Kozuka Gothic Pr6N", + "Kozuka Mincho Pr6N", + "Leelawadee UI", + "Lucida Console", + "Lucida Sans Unicode", + "Malgun Gothic", + "Marlett", + "Microsoft Himalaya", + "Microsoft JhengHei", + "Microsoft JhengHei UI", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Sans Serif", + "Microsoft Tai Le", + "Microsoft YaHei", + "Microsoft YaHei UI", + "Microsoft Yi Baiti", + "MingLiUHKSCS-ExtB", + "MingLiU-ExtB", + "Minion Pro", + "Mongolian Baiti", + "MS Gothic", + "MS PGothic", + "MS UI Gothic", + "MV Boli", + "Myanmar Text", + "Myriad Pro", + "Nirmala UI", + "NSimSun", + "Open Sans", + "Palatino Linotype", + "PMingLiu-ExtB", + "Segoe MDL2 Assets", + "Segoe Print", + "Segoe Script", + "Segoe UI", + "Segoe UI Emoji", + "Segoe UI Historic", + "Segoe UI Symbol", + "SimSun", + "Simsun-ExtB", + "Sitka Banner", + "Sitka Display", + "Sitka Heading", + "Sitka Small", + "Sitka Subheading", + "Sitka Text", + "Sylfaen", + "Symbol", + "Tahoma", + "Times New Roman", + "Trebuchet MS", + "Verdana", + "Webdings", + "Wingdings", + "Yu Gothic", + "Yu Gothic UI" + ], + "default": "Comic Sans MS" + } + }, + "ApplicationSettings.Speech.optSpeechHighTRFontName.$t": { + "schema": { + "title": "Text reader display font", + "description": "Selects the font for the 'text reader tool'.", + "enum": [ + "Adobe Arabic", + "Adobe Fan Heiti Std", + "Adobe Gothic Std", + "Adobe Hebrew", + "Adobe Heiti Std", + "Adobe Ming Std", + "Adobe Myungjo Std", + "Adobe Pi Std", + "Adobe Song Std", + "Adobe Thai", + "Arial", + "Bahnschrift", + "Calibri", + "Cambria", + "Cambria Math", + "Candara", + "Comic Sans MS", + "Consolas", + "Constantia", + "Corbel", + "Courier New", + "Courier Std", + "Ebrima", + "Franklin Gothic", + "Gabriola", + "Gadugi", + "Georgia", + "Global Monospace", + "Global Sans Serif", + "Global Serif", + "Global User Interface", + "HoloLens MDL2 Assets", + "Impact", + "Ink Free", + "Javanese Text", + "Kozuka Gothic Pr6N", + "Kozuka Mincho Pr6N", + "Leelawadee UI", + "Lucida Console", + "Lucida Sans Unicode", + "Malgun Gothic", + "Marlett", + "Microsoft Himalaya", + "Microsoft JhengHei", + "Microsoft JhengHei UI", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Sans Serif", + "Microsoft Tai Le", + "Microsoft YaHei", + "Microsoft YaHei UI", + "Microsoft Yi Baiti", + "MingLiUHKSCS-ExtB", + "MingLiU-ExtB", + "Minion Pro", + "Mongolian Baiti", + "MS Gothic", + "MS PGothic", + "MS UI Gothic", + "MV Boli", + "Myanmar Text", + "Myriad Pro", + "Nirmala UI", + "NSimSun", + "Open Sans", + "Palatino Linotype", + "PMingLiu-ExtB", + "Segoe MDL2 Assets", + "Segoe Print", + "Segoe Script", + "Segoe UI", + "Segoe UI Emoji", + "Segoe UI Historic", + "Segoe UI Symbol", + "SimSun", + "Simsun-ExtB", + "Sitka Banner", + "Sitka Display", + "Sitka Heading", + "Sitka Small", + "Sitka Subheading", + "Sitka Text", + "Sylfaen", + "Symbol", + "Tahoma", + "Times New Roman", + "Trebuchet MS", + "Verdana", + "Webdings", + "Wingdings", + "Yu Gothic", + "Yu Gothic UI" + ], + "enumLabels": [ + "Adobe Arabic", + "Adobe Fan Heiti Std", + "Adobe Gothic Std", + "Adobe Hebrew", + "Adobe Heiti Std", + "Adobe Ming Std", + "Adobe Myungjo Std", + "Adobe Pi Std", + "Adobe Song Std", + "Adobe Thai", + "Arial", + "Bahnschrift", + "Calibri", + "Cambria", + "Cambria Math", + "Candara", + "Comic Sans MS", + "Consolas", + "Constantia", + "Corbel", + "Courier New", + "Courier Std", + "Ebrima", + "Franklin Gothic", + "Gabriola", + "Gadugi", + "Georgia", + "Global Monospace", + "Global Sans Serif", + "Global Serif", + "Global User Interface", + "HoloLens MDL2 Assets", + "Impact", + "Ink Free", + "Javanese Text", + "Kozuka Gothic Pr6N", + "Kozuka Mincho Pr6N", + "Leelawadee UI", + "Lucida Console", + "Lucida Sans Unicode", + "Malgun Gothic", + "Marlett", + "Microsoft Himalaya", + "Microsoft JhengHei", + "Microsoft JhengHei UI", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Sans Serif", + "Microsoft Tai Le", + "Microsoft YaHei", + "Microsoft YaHei UI", + "Microsoft Yi Baiti", + "MingLiUHKSCS-ExtB", + "MingLiU-ExtB", + "Minion Pro", + "Mongolian Baiti", + "MS Gothic", + "MS PGothic", + "MS UI Gothic", + "MV Boli", + "Myanmar Text", + "Myriad Pro", + "Nirmala UI", + "NSimSun", + "Open Sans", + "Palatino Linotype", + "PMingLiu-ExtB", + "Segoe MDL2 Assets", + "Segoe Print", + "Segoe Script", + "Segoe UI", + "Segoe UI Emoji", + "Segoe UI Historic", + "Segoe UI Symbol", + "SimSun", + "Simsun-ExtB", + "Sitka Banner", + "Sitka Display", + "Sitka Heading", + "Sitka Small", + "Sitka Subheading", + "Sitka Text", + "Sylfaen", + "Symbol", + "Tahoma", + "Times New Roman", + "Trebuchet MS", + "Verdana", + "Webdings", + "Wingdings", + "Yu Gothic", + "Yu Gothic UI" + ], + "default": "Comic Sans MS" + } + }, + // Speech -> Speech highlighting -> Font size + "ApplicationSettings.Speech.TextReaderOneWordFontSize.$t": { + "schema": { + "title": "One word display font size", + "description": "Selects the font size for the 'one word display'.", + "type": "integer", + "minimum": 10, + "maximum": 50, + "default": 14 + } + }, + "ApplicationSettings.Speech.optSpeechHighTRFontSize.$t": { + "schema": { + "title": "Text reader font size", + "description": "Selects the font size for the 'text reader tool'.", + "type": "integer", + "minimum": 10, + "maximum": 50, + "default": 14 + } + }, + // Speech -> Speech highlighting -> Highlighting colors + "ApplicationSettings.Speech.HighlightingBackColour.$t": { + "schema": { + "title": "Highlighted background color", + "description": "Selects the color used as background for highlighting.", + "enum": [ + "Blue", + "Turquoise", + "Chartreuse", // Bright green + "Violet", + "Red", + "Yellow", + "White", + "DarkBlue", + "Teal", + "DarkGreen", + "Purple", + "DarkRed", + "Olive", + "LightGray", + "Gray" + ], + "enumLabels": [ + "Blue", + "Turquoise", + "Brigh green", + "Violet", + "Red", + "Yellow", + "White", + "Dark Blue", + "Teal", + "Dark Green", + "Purple", + "Dark Red", + "Olive green", + "Dimmed Text", + "Gray" + ], + "default": "Yellow" + } + }, + "ApplicationSettings.Speech.HighlightingForeColour.$t": { + "schema": { + "title": "Highlighted foreground color", + "description": "Selects the color used as foreground for highlighting.", + "enum": [ + "Yellow", + "Red", + "Violet", + "Chartreuse", + "Turquoise", + "Blue", + "Black", + "LightYellow", + "Orange", + "LightGreen", + "SkyBlue", + "White", + // This color is only used in case of the following setting value: + // "ApplicationSettings.Speech.HighlightingMethod.$t": "Speak with highlighting in text reader" + "RoyalBlue" + ], + "enumLabels": [ + "Yellow", + "Red", + "Violet", + "Bright green", + "Turquoise", + "Blue", + "Black", + "Pale yellow", + "Orange", + "Light Green", + "Sky blue", + "White", + // This color is only used in case of the following setting value: + // "ApplicationSettings.Speech.HighlightingMethod.$t": "Speak with highlighting in text reader" + "Royal blue" + ], + "default": "Yellow" + } + }, + // Translation + "ApplicationSettings.Translation.FromLanguage.$t": { + "schema": { + "title": "Translate from", + "description": "Selects the language from which translate text.", + "enum":[ + "en", + "af", + "sq", + "ar", + "be", + "bn", + "bg", + "ca", + "zh", + "hr", + "cs", + "da", + "nl", + "et", + "tl", + "fi", + "fr", + "gl", + "de", + "el", + "gu", + "ht", + "he", + "hi", + "hu", + "is", + "id", + "ga", + "it", + "ja", + "kn", + "ko", + "lv", + "lt", + "mk", + "ms", + "mt", + "no", + "fa", + "pl", + "pt", + "ro", + "ru", + "sr", + "sk", + "sl", + "es", + "sw", + "sv", + "ta", + "te", + "th", + "tr", + "uk", + "ur", + "vi", + "cy", + "yi" + ], + "enumLabels": [ + "English", + "Afrikaans", + "Albanian", + "Arabic", + "Belarusian", + "Bengali", + "Bulgarian", + "Catalan", + "Chinese", + "Croatian", + "Czech", + "Danish", + "Dutch", + "Estonian", + "Filipino", + "Finnish", + "French", + "Galician", + "German", + "Greek", + "Gujarati", + "Haitian Creole", + "Hebrew", + "Hindi", + "Hungarian", + "Icelandic", + "Indonesian", + "Irish", + "Italian", + "Japanese", + "Kannada", + "Korean", + "Latvian", + "Lithuanian", + "Macedonian", + "Malay", + "Maltese", + "Norwegian", + "Persian", + "Polish", + "Portuguese", + "Romanian", + "Russian", + "Serbian", + "Slovak", + "Slovenian", + "Spanish", + "Swahili", + "Swedish", + "Tamil", + "Telugu", + "Thai", + "Turkish", + "Ukrainian", + "Urdu", + "Vietnamese", + "Welsh", + "Yiddish" + ], + "default": "en" + } + }, + "ApplicationSettings.Translation.ToLanguage.$t": { + "schema": { + "title": "Translate to", + "description": "Selects the target language in which text should be translated.", + "enum":[ + "en", + "af", + "sq", + "ar", + "be", + "bn", + "bg", + "ca", + "zh", + "hr", + "cs", + "da", + "nl", + "et", + "tl", + "fi", + "fr", + "gl", + "de", + "el", + "gu", + "ht", + "he", + "hi", + "hu", + "is", + "id", + "ga", + "it", + "ja", + "kn", + "ko", + "lv", + "lt", + "mk", + "ms", + "mt", + "no", + "fa", + "pl", + "pt", + "ro", + "ru", + "sr", + "sk", + "sl", + "es", + "sw", + "sv", + "ta", + "te", + "th", + "tr", + "uk", + "ur", + "vi", + "cy", + "yi" + ], + "enumLabels": [ + "English", + "Afrikaans", + "Albanian", + "Arabic", + "Belarusian", + "Bengali", + "Bulgarian", + "Catalan", + "Chinese", + "Croatian", + "Czech", + "Danish", + "Dutch", + "Estonian", + "Filipino", + "Finnish", + "French", + "Galician", + "German", + "Greek", + "Gujarati", + "Haitian Creole", + "Hebrew", + "Hindi", + "Hungarian", + "Icelandic", + "Indonesian", + "Irish", + "Italian", + "Japanese", + "Kannada", + "Korean", + "Latvian", + "Lithuanian", + "Macedonian", + "Malay", + "Maltese", + "Norwegian", + "Persian", + "Polish", + "Portuguese", + "Romanian", + "Russian", + "Serbian", + "Slovak", + "Slovenian", + "Spanish", + "Swahili", + "Swedish", + "Tamil", + "Telugu", + "Thai", + "Turkish", + "Ukrainian", + "Urdu", + "Vietnamese", + "Welsh", + "Yiddish" + ], + "default": "es" + } + }, + // Vocabulary List + // TODO: Verify with Microsoft word installation + "ApplicationSettings.Vocabulary.VocabListWindow.$t": { + "schema": { + "title": "Show Vocabulary list window", + "description": "Displays the vocabulary list window. This lets you edit the current vocabulary list before creating a doc with it.", + "type": "boolean", + "default": false + } + }, + "ApplicationSettings.Vocabulary.VocabListImages.$t": { + "schema": { + "title": "Include pictures", + "description": "Include pictures of the words present in the vocabulary list.", + "type": "boolean", + "default": true + } + }, + "ApplicationSettings.Vocabulary.VocabListDefinitions.$t": { + "schema": { + "title": "Include definitions", + "description": "Include definitions of the words present the vocabulary list.", + "type": "boolean", + "default": true + } + }, + // Voice Note + "ApplicationSettings.VoiceNote.InsertInDoc.$t": { + "schema": { + "title": "Insert voice note directly on document", + "description": "Allows you to insert a voice note directly in the current working document if supported.", + "type": "boolean", + "default": false + } + } + } + } + }, + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{environment}.SystemDrive}\\Program Files (x86)\\Texthelp\\Read And Write 12\\ReadAndWrite.exe\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "ReadAndWrite.exe", + "options": { + "closeWindow": true + } + } + ], + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "ReadAndWrite.exe" + } + ] + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.registryKeyExists", + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Texthelp\\Voices", + "subPath": "DefaultTokenId", + "dataType": "REG_SZ" + } + ] + }, + "net.gpii.test.speechControl": { + // This solution does not exist in real life and has been added strictly for user testing/demoing of a solution + // which requires the OS to be restarted in order to respond to changed settings (e.g. DPI on Windows 7) + "name": "GPII Test solution for speech control of the computer", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.settingsHandlers.noSettings", + "liveness": "OSRestart", + "capabilitiesTransformations": { + "sc": "http://registry\\.gpii\\.net/common/speechControl" + }, + "supportedSettings": { + "sc": { + "schema": { + "title": "Speech Control", + "description": "Whether to enable/disable voice commands for computer.", + "type": "boolean", + "default": false + } + } + } + } + }, + "configure": [], + "restore": [], + "start": [], + "stop": [], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ], + "isRunning": [] + }, + "net.gpii.explode": { + // This solution exists only to support manual and automated testing of GPII's error support. It references the + // "exploding settings handler" which will trigger a failure using a configurable strategy during either the + // get, set or launch settingsHandler action + "name": "GPII Test solution for triggering settingsHandler errors", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure": { + "type": "gpii.settingsHandlers.exploding", + "liveness": "live", + "supportedSettings": { + "explodeMethod": { + "schema": { + "title": "Explode Method", + "description": "The strategy to be used when exploding", + "enum": [ + "reject", + "fail", + "throw" + ], + "enumLabels": [ + "reject", + "fail", + "throw" + ] + } + }, + "explodeOn": { + "schema": { + "title": "Explode On", + "description": "Whether to explode during the get or set settingsHandler method", + "enum": [ + "get", + "set", + "launch", + "stop" + ], + "enumLabels": [ + "get", + "set", + "launch", + "stop" + ] + } + } + } + } + }, + "launchHandlers": { + "launch": { + "type": "gpii.settingsHandlers.exploding", + "options": { + "launchHandler": true, + // We tunnel this through since we blundered in the initial design of settings handlers in not contextualising them. + // This is preferable to the other approaches of i) Rewriting the matchmaker/lifecycle manager so that it routes other + // settings to launch handlers, or ii) Rewriting the settingshandler contract so that it supports arbitrary interception + // Real solutions should never read directly from the session in this way + "preferences": "${{session}.preferences}" + } + } + }, + "isRunning": [ + "launchers.launch" + ], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "net.gpii.uioPlus": { + "name": "UIO+", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.webSockets", + "liveness": "live", + "options": { + "path": "net.gpii.uioPlus" + }, + "supportedSettings": { + "characterSpace": { + "schema": { + "title": "Character Space", + "description": "Adjust the space between characters.", + "type": "number", + "default": 0, + "minimum": -0.1, + "maximum": 1, + "multipleOf": 0.1 + } + }, + "contrastTheme": { + "schema": { + "title": "Contrast Theme", + "description": "Change text and background colors.", + "default": "default", + "enum": [ + "default", + "bw", + "wb", + "by", + "yb", + "lgdg", + "gw", + "bbr" + ], + "enumLabels": [ + "Default Theme", + "Black Foreground on a White Background", + "White Foreground on a Black Background", + "Black Foreground on a Yellow Background", + "Yellow Foreground on a Black Background", + "Light Gray Foreground on a Dark Gray Background", + "Gray Foreground on a White Background", + "Black Foreground on a Brown Background" + ] + } + }, + "fontSize": { + "schema": { + "title": "Font Size", + "description": "Adjust text size.", + "type": "number", + "default": 1, + "minimum": 0.25, + "maximum": 5 + } + }, + "inputsLargerEnabled": { + "schema": { + "title": "Enhance Inputs", + "description": "Emphasize links, buttons, menus, textfields, and other inputs.", + "type": "boolean", + "default": false + } + }, + "lineSpace": { + "schema": { + "title": "Line Space", + "description": "Adjusts the spacing between lines of text.", + "type": "number", + "default": 1, + "minimum": 0.7, + "maximum": 3, + "multipleOf": 0.1 + } + }, + "selectionTheme": { + "schema": { + "title": "Selection Theme", + "description": "Change the highlight colour for text selections.", + "default": "default", + "enum": [ + "default", + "yellow", + "green", + "pink" + ], + "enumLabels": [ + "Default Selection Highlight", + "Yellow Selection Highlight", + "Green Selection Highlight", + "Pink Selection Highlight" + ] + } + }, + "selfVoicingEnabled": { + "schema": { + "title": "Self Voicing", + "description": "Let the device read site content aloud.", + "type": "boolean", + "default": false + } + }, + "simplifiedUiEnabled": { + "schema": { + "title": "Simplified UI", + "description": "Only display the main content.", + "type": "boolean", + "default": false + } + }, + "syllabificationEnabled": { + "schema": { + "title": "Syllabification", + "description": "Display words broken down into their syllables.", + "type": "boolean", + "default": false + } + }, + "tableOfContentsEnabled": { + "schema": { + "title": "Table of Contents", + "description": "Create a table of contents.", + "type": "boolean", + "default": false + } + }, + "wordSpace": { + "schema": { + "title": "Word Spacing", + "description": "The spacing between words.", + "type": "number", + "default": 0, + "minimum": -0.3, + "maximum": 3, + "multipleOf": 0.1 + } + } + }, + "capabilitiesTransformations": { + "characterSpace": { + "transform": { + "type": "fluid.transforms.round", + "scale": 1, + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "offset": -1, + "inputPath": "http://registry\\.gpii\\.net/common/characterSpace" + } + } + } + }, + "contrastTheme": { + "transform": { + // Use the same conditions for applying a high + // contrast theme as specified for the + // "com.microsoft.windows.highContrast" solution: + // (highContrast.enabled !== false) && (highContrastTheme !== "regular-contrast") + "type": "fluid.transforms.condition", + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/highContrast/enabled", + "left": true, + "operator": "!==", + "right": false + } + }, + "operator": "&&", + "right": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": "", + "leftPath": "http://registry\\.gpii\\.net/common/highContrastTheme", + "operator": "!==", + "right": "regular-contrast" + } + } + } + }, + "true": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/highContrastTheme", + "defaultOutputValue": "default", + "match": { + "black-white": "bw", + "white-black": "wb", + "black-yellow": "by", + "yellow-black": "yb", + "grey-black": "lgdg", + "grey-white": "gw", + "black-brown": "bbr" + } + } + }, + "false": "default" + } + }, + "fontSize": { + "transform": { + "type": "fluid.transforms.round", + "scale": 1, + "input": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/fontSize", + "right": 12, + "operator": "/" + } + } + } + }, + "inputsLargerEnabled": "http://registry\\.gpii\\.net/common/inputsLarger/enabled", + "lineSpace": "http://registry\\.gpii\\.net/common/lineSpace", + "selectionTheme": "http://registry\\.gpii\\.net/common/highlightColor", + "selfVoicingEnabled": "http://registry\\.gpii\\.net/common/selfVoicing/enabled", + "simplifiedUiEnabled": "http://registry\\.gpii\\.net/common/simplifiedUi/enabled", + "syllabificationEnabled": "http://registry\\.gpii\\.net/common/syllabification/enabled", + "tableOfContentsEnabled": "http://registry\\.gpii\\.net/common/tableOfContents", + "wordSpace": { + "transform": { + "type": "fluid.transforms.round", + "scale": 1, + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "offset": 1, + "inputPath": "http://registry\\.gpii\\.net/common/wordSpace" + } + } + } + } + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/characterSpace": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "characterSpace", + "offset": 1 + } + }, + "http://registry\\.gpii\\.net/common/highContrast/enabled": { + "transform": { + "type": "fluid.transforms.condition", + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "contrastTheme", + "operator": "!==", + "right": "default" + } + }, + "true": "true", + "false": "false" + } + }, + "http://registry\\.gpii\\.net/common/highContrastTheme": { + "transform": { + "type": "fluid.transforms.condition", + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "contrastTheme", + "operator": "!==", + "right": "default" + } + }, + "true": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "contrastTheme", + "defaultOutputValue": "default", + "match": { + "bw": "black-white", + "wb": "white-black", + "by": "black-yellow", + "yb": "yellow-black", + "lgdg":"grey-black", + "gw": "grey-white", + "bbr": "black-brown" + } + } + }, + "false": "regular-contrast" + } + }, + "http://registry\\.gpii\\.net/common/fontSize": { + "transform": { + "type": "fluid.transforms.round", + "scale": 2, + "input": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "fontSize", + "right": 12, + "operator": "*" + } + } + } + }, + "http://registry\\.gpii\\.net/common/inputsLarger/enabled": "inputsLargerEnabled", + "http://registry\\.gpii\\.net/common/lineSpace": "lineSpace", + "http://registry\\.gpii\\.net/common/highlightColor": "selectionTheme", + "http://registry\\.gpii\\.net/common/selfVoicing/enabled": "selfVoicingEnabled", + "http://registry\\.gpii\\.net/common/simplifiedUi/enabled": "simplifiedUiEnabled", + "http://registry\\.gpii\\.net/common/syllabification/enabled": "syllabificationEnabled", + "http://registry\\.gpii\\.net/common/tableOfContents": "tableOfContentsEnabled", + "http://registry\\.gpii\\.net/common/wordSpace": { + "transform": { + "type": "fluid.transforms.linearScale", + "offset": -1, + "inputPath": "wordSpace" + } + } + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "org.nvda-project": { + "name": "NVDA Screen Reader", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configs": { + "type": "gpii.settingsHandlers.INISettingsHandler", + "liveness": "manualRestart", + "options": { + "filename": "${{environment}.APPDATA}\\nvda\\nvda.ini" + }, + "supportedSettings": { + "audio.audioDuckingMode": { + "schema": { + "title": "Audio Ducking Mode", + "description": "Whether NVDA should lower the volume of other applications while NVDA is speaking or all the time while NVDA is running.", + "enum": [ + 0, + 1, + 2 + ], + "enumLabels": [ + "No Ducking", + "Duck when Outputting Speech and Sounds", + "Always Duck" + ], + "default": 0 + } + }, + "braille.autoTether": { + "schema": { + "title": "Tether Braille", + "description": "Whether the braille display will follow the system focus or whether it follows the navigator object / review cursor.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "braille.cursorBlink": { + "schema": { + "title": "Blink Cursor", + "description": "Whether the cursor should blink", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "braille.cursorBlinkRate": { + "schema": { + "title": "Cursor Blink Rate", + "description": "The blink rate of the cursor in milliseconds.", + "type": "integer", + "minimum": 200, + "maximum": 2000, + "default": 500 + } + }, + "braille.cursorShapeFocus": { + "schema": { + "title": "Cursor Shape for Focus", + "description": "The shape (dot pattern) of the braille cursor. The selection indicator is not affected by this option.", + "enum": [ + 192, + 128, + 255 + ], + "enumLabels": [ + "Dots 7 and 8", + "Dot 8", + "All Dots" + ], + "default": 192 + } + }, + "braille.cursorShapeReview": { + "schema": { + "title": "Cursor Shape for Review", + "description": "The shape (dot pattern) of the braille cursor. The selection indicator is not affected by this option.", + "enum": [ + 192, + 128, + 255 + ], + "enumLabels": [ + "Dots 7 and 8", + "Dot 8", + "All Dots" + ], + "default": 128 + } + }, + "braille.display": { + "schema": { + "title": "Braille Display", + "description": "Which of the available braillle displays to use.", + "enum": [ + "auto", + "brltty", + "ecoBraille", + "hedoMobilLine", + "hedoProfiLine", + "lilli", + "papenmeier", + "papenmeier_serial", + "seika", + "noBraille" + ], + "enumLabels": [ + "Automatic", + "brltty", + "EcoBraille displays", + "hedo MobilLine USB", + "hedo ProfiLine USB", + "MDV Lilli", + "Papenmeier BRAILLEX newer models", + "Papenmeier BRAILLEX older models", + "Seika braille displays", + "No braille" + ], + "default": "noBraille" + } + }, + "braille.expandAtCursor": { + "schema": { + "title": "Expand Contracted Braille", + "description": "Whether to expand the word that is under the cursor into non-contracted computer braille.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "braille.focusContextPresentation": { + "schema": { + "title": "Focus Context Presentation", + "description": "Choose what context information NVDA will show on the braille display when an object gets focus.", + "enum": [ + "changedContext", + "fill", + "scroll" + ], + "enumLabels": [ + "Fill Display for Context Changes", + "Always Fill Display", + "Only when Scrolling Back" + ], + "default": "changedContext" + } + }, + "braille.inputTable": { + "schema": { + "title": "Input Table", + "description": "Complementary to the output table option. The chosen table will be used to translate braille entered on your braille display's Perkins-style keyboard into text. NVDA currently only supports computer braille input so only 8 dot computer braille tables will be shown. You can move from braille table to braille table in the list by using the arrow keys. Note that this option is only useful if your braille display has a Perkins-style keyboard and this feature is supported by the braille display driver. If input is not supported on a display which does have a braille keyboard this will be noted in the Supported Braille Displays section.", + // TODO: If we need to support braille devices, we'll have to reconcile this with our larger support for languages. + "enum": [ + "ar-ar-g1.utb", + "as-in-g1.utb", + "be-in-g1.utb", + "bg.ctb", + "ckb-g1.ctb", + "cy-cy-g1.utb", + "cy-cy-g2.ctb", + "cs-comp8.utb", + "cs-g1.ctb", + "da-dk-g08.ctb", + "da-dk-g16.ctb", + "da-dk-g18.ctb", + "da-dk-g26.ctb", + "da-dk-g28.ctb", + "de-de-comp8.ctb", + "de-de-g0.utb", + "de-de-g1.ctb", + "de-de-g2.ctb", + "el.ctb", + "en-gb-comp8.ctb", + "en-gb-g1.utb", + "en-GB-g2.ctb", + "en-ueb-g1.ctb", + "en-ueb-g2.ctb", + "en-us-comp6.ctb", + "en-us-comp8-ext.utb", + "en-us-g1.ctb", + "en-us-g2.ctb", + "eo-g1.ctb", + "Es-Es-G0.utb", + "es-g1.ctb", + "et-g0.utb", + "ethio-g1.ctb", + "fa-ir-comp8.ctb", + "fa-ir-g1.utb", + "fi.utb", + "fi-fi-8dot.ctb", + "fr-bfu-comp6.utb", + "fr-bfu-comp8.utb", + "fr-bfu-g2.ctb", + "ga-g1.utb", + "ga-g2.ctb", + "gu-in-g1.utb", + "gr-bb.ctb", + "he.ctb", + "hi-in-g1.utb", + "hr-comp8.utb", + "hr-g1.ctb", + "hu-hu-comp8.ctb", + "hu-hu-g1.ctb", + "hu-hu-g2.ctb", + "is.ctb", + "it-it-comp6.utb", + "it-it-comp8.utb", + "ka-in-g1.utb", + "ko-2006-g1.ctb", + "ko-2006-g2.ctb", + "ko-g1.ctb", + "ko-g2.ctb", + "ks-in-g1.utb", + "lt.ctb", + "lt-6dot.utb", + "Lv-Lv-g1.utb", + "ml-in-g1.utb", + "mn-in-g1.utb", + "mn-MN-g1.utb", + "mn-MN-g2.ctb", + "mr-in-g1.utb", + "nl-BE-g0.utb", + "nl-NL-g0.utb", + "no-no-comp8.ctb", + "No-No-g0.utb", + "No-No-g1.ctb", + "No-No-g2.ctb", + "No-No-g3.ctb", + "np-in-g1.utb", + "or-in-g1.utb", + "pl-pl-comp8.ctb", + "Pl-Pl-g1.utb", + "pt-pt-comp8.ctb", + "Pt-Pt-g1.utb", + "Pt-Pt-g2.ctb", + "pu-in-g1.utb", + "ro.ctb", + "ru.ctb", + "ru-ru-g1.utb", + "sa-in-g1.utb", + "Se-Se.ctb", + "Se-Se-g1.utb", + "sk-g1.ctb", + "sl-si-comp8.ctb", + "sl-si-g1.utb", + "sr-g1.ctb", + "ta-ta-g1.ctb", + "te-in-g1.utb", + "tr.ctb", + "uk.utb", + "unicode-braille.utb", + "vi-g1.ctb", + "zhcn-g1.ctb", + "zhcn-g2.ctb", + "zh-hk.ctb", + "zh-tw.ctb" + ], + "enumLabels": [ + "Arabic grade 1", + "Assamese grade 1", + "Bengali grade 1", + "Bulgarian 8 dot computer braille", + "Central Kurdish grade 1", + "Welsh grade 1", + "Welsh grade 2", + "Czech 8 dot computer braille", + "Czech grade 1", + "Danish 8 dot computer braille", + "Danish 6 dot grade 1", + "Danish 8 dot grade 1", + "Danish 6 dot grade 2", + "Danish 8 dot grade 2", + "German 8 dot computer braille", + "German grade 0", + "German grade 1", + "German grade 2", + "Greek (Greece)", + "English (U.K.) 8 dot computer braille", + "English (U.K.) grade 1", + "English (U.K.) grade 2", + "Unified English Braille Code grade 1", + "Unified English Braille Code grade 2", + "English (U.S.) 6 dot computer braille", + "English (U.S.) 8 dot computer braille", + "English (U.S.) grade 1", + "English (U.S.) grade 2", + "Esperanto grade 1", + "Spanish 8 dot computer braille", + "Spanish grade 1", + "Estonian grade 0", + "Ethiopic grade 1", + "Persian 8 dot computer braille", + "Persian grade 1", + "Finnish 6 dot", + "Finnish 8 dot computer braille", + "French (unified) 6 dot computer braille", + "French (unified) 8 dot computer braille", + "French (unified) grade 2", + "Irish grade 1", + "Irish grade 2", + "Gujarati grade 1", + "Koine Greek", + "Hebrew 8 dot computer braille", + "Hindi grade 1", + "Croatian 8 dot computer braille", + "Croatian grade 1", + "Hungarian 8 dot computer braille", + "Hungarian grade 1", + "Hungarian grade 2", + "Icelandic 8 dot computer braille", + "Italian 6 dot computer braille", + "Italian 8 dot computer braille", + "Kannada grade 1", + "Korean grade 1 (2006)", + "Korean grade 2 (2006)", + "Korean grade 1", + "Korean grade 2", + "Kashmiri grade 1", + "Lithuanian 8 dot", + "Lithuanian 6 dot", + "Latvian grade 1", + "Malayalam grade 1", + "Manipuri grade 1", + "Mongolian grade 1", + "Mongolian grade 2", + "Marathi grade 1", + "Dutch (Belgium)", + "Dutch (Netherlands)", + "Norwegian 8 dot computer braille", + "Norwegian grade 0", + "Norwegian grade 1", + "Norwegian grade 2", + "Norwegian grade 3", + "Nepali grade 1", + "Oriya grade 1", + "Polish 8 dot computer braille", + "Polish grade 1", + "Portuguese 8 dot computer braille", + "Portuguese grade 1", + "Portuguese grade 2", + "Punjabi grade 1", + "Romanian", + "Russian braille for computer code", + "Russian grade 1", + "Sanskrit grade 1", + "Swedish 8 dot computer braille", + "Swedish grade 1", + "Slovak grade 1", + "Slovenian 8 dot computer braille", + "Slovenian grade 1", + "Serbian grade 1", + "Tamil grade 1", + "Telugu grade 1", + "Turkish grade 1", + "Ukrainian", + "Unicode braille", + "Vietnamese grade 1", + "Chinese (China Mandarin) grade 1", + "Chinese (China Mandarin) grade 2", + "Chinese (Hong Kong Cantonese)", + "Chinese (Taiwan Mandarin)" + ], + "default": "en-ueb-g1.ctb" + } + }, + "braille.messageTimeout": { + "schema": { + "title": "Message timeout (sec)", + "description": "How long in seconds NVDA messages are displayed on the braille display. Specifying 0 disables displaying of these messages completely.", + "type": "integer", + "minimum": 0, + "maximum": 20, + "default": 4 + } + }, + "braille.noMessageTimeout": { + "schema": { + "title": "Show Messages Indefinitely", + "description": "Allow NVDA messages to be displayed on the braille display indefinitely.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "braille.readByParagraph": { + "schema": { + "title": "Display Braille by Paragraph", + "description": "Whether to displayed braille by paragraphs instead of lines. If this is set the next and previous line commands will move by paragraph accordingly. This means that you do not have to scroll the display at the end of each line even where more text would fit on the display. This may allow for more fluent reading of large amounts of text. It is disabled by default.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "braille.showCursor": { + "schema": { + "title": "Show Braille Cursor", + "description": "Whether to display the cursor on a braille display. It applies to the system caret and review cursor but not to the selection indicator.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "braille.tetherTo": { + "schema": { + "title": "Tether Braille", + "description": "Whether the braille display will follow the system focus or whether it follows the navigator object / review cursor.", + "enum": [ + "focus", + "review", + "autoTether" + ], + "enumLabels": [ + "Focus", + "Review", + "Auto Tether" + ], + "default": "focus" + } + }, + // TODO: If we want to support braille devices, we'll have to reconcile this with our larger support for languages. + "braille.translationTable": { + "schema": { + "title": "Output Table", + "description": "The braille output table to use in translating text into braille to be presented on your braille display.", + "enum": [ + "ar-ar-g1.utb", + "as-in-g1.utb", + "be-in-g1.utb", + "bg.ctb", + "ckb-g1.ctb", + "cy-cy-g1.utb", + "cy-cy-g2.ctb", + "cs-comp8.utb", + "cs-g1.ctb", + "da-dk-g08.ctb", + "da-dk-g16.ctb", + "da-dk-g18.ctb", + "da-dk-g26.ctb", + "da-dk-g28.ctb", + "de-de-comp8.ctb", + "de-de-g0.utb", + "de-de-g1.ctb", + "de-de-g2.ctb", + "el.ctb", + "en-gb-comp8.ctb", + "en-gb-g1.utb", + "en-GB-g2.ctb", + "en-ueb-g1.ctb", + "en-ueb-g2.ctb", + "en-us-comp6.ctb", + "en-us-comp8-ext.utb", + "en-us-g1.ctb", + "en-us-g2.ctb", + "eo-g1.ctb", + "Es-Es-G0.utb", + "es-g1.ctb", + "et-g0.utb", + "ethio-g1.ctb", + "fa-ir-comp8.ctb", + "fa-ir-g1.utb", + "fi.utb", + "fi-fi-8dot.ctb", + "fr-bfu-comp6.utb", + "fr-bfu-comp8.utb", + "fr-bfu-g2.ctb", + "ga-g1.utb", + "ga-g2.ctb", + "gu-in-g1.utb", + "gr-bb.ctb", + "he.ctb", + "hi-in-g1.utb", + "hr-comp8.utb", + "hr-g1.ctb", + "hu-hu-comp8.ctb", + "hu-hu-g1.ctb", + "hu-hu-g2.ctb", + "is.ctb", + "it-it-comp6.utb", + "it-it-comp8.utb", + "ka-in-g1.utb", + "ko-2006-g1.ctb", + "ko-2006-g2.ctb", + "ko-g1.ctb", + "ko-g2.ctb", + "ks-in-g1.utb", + "lt.ctb", + "lt-6dot.utb", + "Lv-Lv-g1.utb", + "ml-in-g1.utb", + "mn-in-g1.utb", + "mn-MN-g1.utb", + "mn-MN-g2.ctb", + "mr-in-g1.utb", + "nl-BE-g0.utb", + "nl-NL-g0.utb", + "no-no-comp8.ctb", + "No-No-g0.utb", + "No-No-g1.ctb", + "No-No-g2.ctb", + "No-No-g3.ctb", + "np-in-g1.utb", + "or-in-g1.utb", + "pl-pl-comp8.ctb", + "Pl-Pl-g1.utb", + "pt-pt-comp8.ctb", + "Pt-Pt-g1.utb", + "Pt-Pt-g2.ctb", + "pu-in-g1.utb", + "ro.ctb", + "ru.ctb", + "ru-ru-g1.utb", + "sa-in-g1.utb", + "Se-Se.ctb", + "Se-Se-g1.utb", + "sk-g1.ctb", + "sl-si-comp8.ctb", + "sl-si-g1.utb", + "sr-g1.ctb", + "ta-ta-g1.ctb", + "te-in-g1.utb", + "tr.ctb", + "uk.utb", + "unicode-braille.utb", + "vi-g1.ctb", + "zhcn-g1.ctb", + "zhcn-g2.ctb", + "zh-hk.ctb", + "zh-tw.ctb" + ], + "enumLabels": [ + "Arabic grade 1", + "Assamese grade 1", + "Bengali grade 1", + "Bulgarian 8 dot computer braille", + "Central Kurdish grade 1", + "Welsh grade 1", + "Welsh grade 2", + "Czech 8 dot computer braille", + "Czech grade 1", + "Danish 8 dot computer braille", + "Danish 6 dot grade 1", + "Danish 8 dot grade 1", + "Danish 6 dot grade 2", + "Danish 8 dot grade 2", + "German 8 dot computer braille", + "German grade 0", + "German grade 1", + "German grade 2", + "Greek (Greece)", + "English (U.K.) 8 dot computer braille", + "English (U.K.) grade 1", + "English (U.K.) grade 2", + "Unified English Braille Code grade 1", + "Unified English Braille Code grade 2", + "English (U.S.) 6 dot computer braille", + "English (U.S.) 8 dot computer braille", + "English (U.S.) grade 1", + "English (U.S.) grade 2", + "Esperanto grade 1", + "Spanish 8 dot computer braille", + "Spanish grade 1", + "Estonian grade 0", + "Ethiopic grade 1", + "Persian 8 dot computer braille", + "Persian grade 1", + "Finnish 6 dot", + "Finnish 8 dot computer braille", + "French (unified) 6 dot computer braille", + "French (unified) 8 dot computer braille", + "French (unified) grade 2", + "Irish grade 1", + "Irish grade 2", + "Gujarati grade 1", + "Koine Greek", + "Hebrew 8 dot computer braille", + "Hindi grade 1", + "Croatian 8 dot computer braille", + "Croatian grade 1", + "Hungarian 8 dot computer braille", + "Hungarian grade 1", + "Hungarian grade 2", + "Icelandic 8 dot computer braille", + "Italian 6 dot computer braille", + "Italian 8 dot computer braille", + "Kannada grade 1", + "Korean grade 1 (2006)", + "Korean grade 2 (2006)", + "Korean grade 1", + "Korean grade 2", + "Kashmiri grade 1", + "Lithuanian 8 dot", + "Lithuanian 6 dot", + "Latvian grade 1", + "Malayalam grade 1", + "Manipuri grade 1", + "Mongolian grade 1", + "Mongolian grade 2", + "Marathi grade 1", + "Dutch (Belgium)", + "Dutch (Netherlands)", + "Norwegian 8 dot computer braille", + "Norwegian grade 0", + "Norwegian grade 1", + "Norwegian grade 2", + "Norwegian grade 3", + "Nepali grade 1", + "Oriya grade 1", + "Polish 8 dot computer braille", + "Polish grade 1", + "Portuguese 8 dot computer braille", + "Portuguese grade 1", + "Portuguese grade 2", + "Punjabi grade 1", + "Romanian", + "Russian braille for computer code", + "Russian grade 1", + "Sanskrit grade 1", + "Swedish 8 dot computer braille", + "Swedish grade 1", + "Slovak grade 1", + "Slovenian 8 dot computer braille", + "Slovenian grade 1", + "Serbian grade 1", + "Tamil grade 1", + "Telugu grade 1", + "Turkish grade 1", + "Ukrainian", + "Unicode braille", + "Vietnamese grade 1", + "Chinese (China Mandarin) grade 1", + "Chinese (China Mandarin) grade 2", + "Chinese (Hong Kong Cantonese)", + "Chinese (Taiwan Mandarin)" + ], + "default": "en-ueb-g1.ctb" + } + }, + "braille.wordWrap": { + "schema": { + "title": "Avoid splitting words when possible", + "description": "If this is enabled a word which is too large to fit at the end of the braille display will not be split. Instead there will be some blank space at the end of the display. When you scroll the display you will be able to read the entire word. This is sometimes called \"word wrap\". Note that if the word is too large to fit on the display even by itself the word must still be split. If this is disabled as much of the word as possible will be displayed but the rest will be cut off. When you scroll the display you will then be able to read the rest of the word. Enabling this may allow for more fluent reading but generally requires you to scroll the display more.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "documentFormatting.detectFormatAfterCursor": { + "schema": { + "title": "Report Formatting Changes after the Cursor", + "description": "Whether to try and detect all the formatting changes on a line as it speaks it even if doing this may slow down NVDA's performance. By default, NVDA will detect the formatting at the position of the System caret / Review Cursor and in some instances may detect formatting on the rest of the line only if it is not going to cause a performance decrease. Enable this option while proof reading documents in applications such as Microsoft Word where formatting is important.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "documentFormatting.includeLayoutTables": { + "schema": { + "title": "Announce Layout Tables", + "description": "Whether to announce layout tables. When on, NVDA will treat these as normal tables, announcing them based on Document Formatting Settings and locating them with quick navigation commands. When off, they will not be announced nor found with quick navigation. However, the content of the tables will still be included as normal text. This option is turned off by default.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "documentFormatting.reportAlignment": { + "schema": { + "title": "Announce Alignment", + "description": "Whether to announce the text alignment.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "documentFormatting.reportBlockQuotes": { + "schema": { + "title": "Announce Block quotes", + "description": "Whether to announce block quotes.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "documentFormatting.reportBorderColor": { + "schema": { + "title": "Announce Cell Border Colors", + "description": "Whether to announce cell border colors.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "documentFormatting.reportBorderStyle": { + "schema": { + "title": "Announce Cell Border Styles", + "description": "Whether to announce cell border styles.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "documentFormatting.reportClickable": { + "schema": { + "title": "Announce Clickable Items", + "description": "Whether to announce items that can be clicked.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "documentFormatting.reportColor": { + "schema": { + "title": "Announce Font Colors", + "description": "Whether to announce font colors.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "documentFormatting.reportComments": { + "schema": { + "title": "Announce Comments", + "description": "Whether to announce comments.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "documentFormatting.reportEmphasis": { + "schema": { + "title": "Announce Font Emphasis", + "description": "Whether to announce font emphasis.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "documentFormatting.reportFontAttributes": { + "schema": { + "title": "Announce Font Attributes", + "description": "Whether to announce font attributes.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "documentFormatting.reportFontName": { + "schema": { + "title": "Announce Font Name", + "description": "Whether to announce font names.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "documentFormatting.reportFontSize": { + "schema": { + "title": "Announce Font Size", + "description": "Whether to announce font sizes.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "documentFormatting.reportFrames": { + "schema": { + "title": "Announce Frames", + "description": "Whether to announce frames.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "documentFormatting.reportHeadings": { + "schema": { + "title": "Announce Headings", + "description": "Whether to announce headings.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "documentFormatting.reportLandmarks": { + "schema": { + "title": "Announce Landmarks", + "description": "Whether to announce landmarks.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "documentFormatting.reportLineIndentation": { + "schema": { + "title": "Announce Line Indentation", + "description": "Whether to announce line indentation,", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "documentFormatting.reportLineIndentationWithTones": { + "schema": { + "title": "Announce Line Indentation with Tones", + "description": "Whether to announce line indentation with tones.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "documentFormatting.reportLineNumber": { + "schema": { + "title": "Announce Line Numbers", + "description": "Whether to announce line numbers.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "documentFormatting.reportLineSpacing": { + "schema": { + "title": "Announce Line Spacing", + "description": "Whether to announce line spacing.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "documentFormatting.reportLinks": { + "schema": { + "title": "Announce Links", + "description": "Whether to announce links.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "documentFormatting.reportLists": { + "schema": { + "title": "Announce Lists", + "description": "Whether to announce lists.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "documentFormatting.reportPage": { + "schema": { + "title": "Announce Pages", + "description": "Whether to announce pages.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "documentFormatting.reportParagraphIndentation": { + "schema": { + "title": "Announce Paragraph Indentation", + "description": "Whether to announce paragraph indentation.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "documentFormatting.reportRevisions": { + "schema": { + "title": "Announce Editor Revisions", + "description": "Whether to announce editor revisions.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "documentFormatting.reportSpellingErrors": { + "schema": { + "title": "Announce Spelling Errors", + "description": "Whether to announce spelling errors.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "documentFormatting.reportStyle": { + "schema": { + "title": "Announce Font Styles", + "description": "Whether to announce font styles.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "documentFormatting.reportTableCellCoords": { + "schema": { + "title": "Announce Cell Coordinates", + "description": "Whether to announce cell coordinates.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "documentFormatting.reportTableHeaders": { + "schema": { + "title": "Announce Row/Column Headers", + "description": "Whether to announce row/column headers of tables.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "documentFormatting.reportTables": { + "schema": { + "title": "Announce Tables", + "description": "Whether to announce tables.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "general.askToExit": { + "schema": { + "title": "Show Options on Exit", + "description": "Whether or not an options dialog should appear when you exit NVDA. If enabled, a dialog will appear when you attempt to exit NVDA asking whether you want to exit restart or restart with add-ons disabled. When disabled, NVDA will exit immediately.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "general.language": { + "schema": { + "title": "Language", + "description": "The language that NVDA's user interface and messages should be shown in. There are many languages however the default option is \"User Default Windows\". This option tells NVDA to use the language that Windows is currently set to.", + + // TODO: These cannot both be correctly alphabetised. + "enum": [ + "af_ZA", + "am", + "an", + "ar", + "bg", + "ca", + "ckb", + "cs", + "da", + "de", + "de_CH", + "el", + "en", + "es", + "es_CO", + "fa", + "fi", + "fr", + "ga", + "he", + "hi", + "hr", + "hu", + "id", + "is", + "it", + "ja", + "ka", + "kn", + "ko", + "ky", + "lt", + "mk", + "mn", + "my", + "nb_NO", + "ne", + "nl", + "nn_NO", + "pa", + "pl", + "pt_BR", + "pt_PT", + "ro", + "ru", + "sk", + "sl", + "sq", + "sr", + "sv", + "tl", + "th", + "tr", + "uk", + "ur", + "vi", + "zh_CN", + "zh_HK", + "zh_TW", + "Windows" + ], + "enumLabels": [ + "Afrikkans(South Africa)", + "Amharic", + "Aragonese", + "Arabic", + "Bulgarian", + "Catalan", + "Central Kurdish", + "Czech", + "Danish", + "German", + "German(Switzerland)", + "Greek", + "English", + "Spanish", + "Spanish (Columbia)", + "Persian", + "Finnish", + "French", + "Irish", + "Galician", + "Herbrew", + "Hindi", + "Croatian", + "Hungarian", + "Indonesian", + "Icelandic", + "Italian", + "Japanese", + "Georgian", + "Kannada", + "Korean", + "Kyrgyz", + "Lithuanian", + "Macedonian", + "Mongolian", + "Burmese", + "Norwegian Bokmal (Norway)", + "Nepali", + "Dutch", + "Norwegian Nynorsk (Norway)", + "Punjabi", + "Polish", + "Portugese (Brazil)", + "Portugese (Portugal)", + "Romanian", + "Russian", + "Slovak", + "Slovenian", + "Albanian", + "Serbian (Latin)", + "Swedish", + "Tamil", + "Thai", + "Turkish", + "Ukranian", + "Urdu", + "Vietnamese", + "Chinese (simplified China)", + "Chinese (Traditional Hong Kong SAR)", + "Chinese (Traditional Taiwan)", + "User default" + ], + "default": "Windows" + } + }, + "general.playStartAndExitSounds": { + "schema": { + "title": "Play Sounds on Startup or Exit", + "description": "Whether NVDA should play sounds when it starts or exits.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "general.saveConfigurationOnExit": { + "schema": { + "title": "Save Configuration on Exit", + "description": "Whether to automatically save the current configuration when you exit NVDA.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "general.showWelcomeDialogAtStartup": { + "schema": { + "title": "Show Welcome Dialog on Startup", + "description": "Whether or not to show the welcome dialog on startup.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "inputComposition.alwaysIncludeShortCharacterDescriptionInCandidateName": { + "schema": { + "title": "Always include short character description when announcing candidates", + "description": "Whether or not NVDA should provide a short description for each character in a candidate either when its selected or when its automatically read when the candidate list appears. Note that for locales such as Chinese the announcement of extra character descriptions for the selected candidate is not affected by this option. This option may be useful for Korean and japanese input methods.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "inputComposition.announceSelectedCandidate": { + "schema": { + "title": "Announce Selected Candidate", + "description": "Whether NVDA should announce the selected candidate when a candidate list appears or when the selection is changed. For input methods where the selection can be changed with the arrow keys (such as Chinese New Phonetic) this is necessary but for some input methods it may be more efficient typing with this option turned off. Note that even with this option off the review cursor will still be placed on the selected candidate allowing you to use object navigation / review to manually read this or other candidates.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "inputComposition.autoReportAllCandidates": { + "schema": { + "title": "Announce All Available Candidates", + "description": "Whether or not all visible candidates should be announced automatically when a candidate list appears or its page is changed. Having this option on for pictographic input methods such as chinese New ChangJie or Boshiami is useful as you can automatically hear all symbols and their numbers and you can choose one right away. However for phonetic input methods such as chinese New Phonetic it may be more useful to turn this option off as all the symbols will sound the same and you will have to use the arrow keys to navigate the list items individually to gain more information from the character descriptions for each candidate.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "inputComposition.reportCompositionStringChanges": { + "schema": { + "title": "Announce Changes to the Composition String", + "description": "After reading or precomposition data has been combined into a valid pictographic symbol most input methods place this symbol into a composition string for temporary storage along with other combined symbols before they are finally inserted into the document. This option allows you to choose whether or not NVDA should announce new symbols as they appear in the composition string. This option is on by default.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "inputComposition.reportReadingStringChanges": { + "schema": { + "title": "Announce Changes to the Reading String", + "description": "Some input methods such as Chinese New Phonetic and New ChangJie have a reading string (sometimes known as a precomposition string). You can choose whether or not NVDA should announce new characters being typed into this reading string with this option. This option is on by default. Note some older input methods such as Chinese ChangJie may not use the reading string to hold precomposition characters but instead use the composition string directly. Please see the next option for configuring announcing of the composition string.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "keyboard.alertForSpellingErrors": { + "schema": { + "title": "Play sound for spelling errors while typing", + "description": "Whether to play a short buzzer sound when a word you type contains a spelling error.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "keyboard.allowSkimReadingInSayAll": { + "schema": { + "title": "Allow skim reading in Say All", + "description": "If on, certain navigation commands (such as quick navigation in browse mode or moving by line or paragraph) do not stop Say All rather Say All jumps to the new position and continues reading.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "keyboard.beepForLowercaseWithCapslock": { + "schema": { + "title": "Beep if typing lowercase letters when caps lock is on", + "description": "When enabled, a warning beep will be heard if a letter is typed with the shift key while caps lock is on. Generally typing shifted letters with caps lock is unintentional and is usually due to not realising that caps lock is enabled. Therefore it can be quite helpful to be warned about this.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "keyboard.handleInjectedKeys": { + "schema": { + "title": "Handle keys from other applications", + "description": "Whether key presses generated by applications such as on-screen keyboards and speech recognition software should be processed by NVDA. This option is on by default though certain users may wish to turn this off such as those typing Vietnamese with the Unikey typing software as it will cause incorrect character input.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "keyboard.keyboardLayout": { + "schema": { + "title": "Keyboard Layout", + "description": "What keyboard layout NVDA should use. Currently the two that come with NVDA are Desktop and Laptop.", + "enum": [ + "desktop", + "laptop" + ], + "enumLabels": [ + "Desktop", + "Laptop" + ], + "default": "desktop" + } + }, + "keyboard.speakCommandKeys": { + "schema": { + "title": "Announce Command Keys", + "description": "Whether to announce all non-character keys you type on the keyboard. This includes key combinations such as control plus another letter.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "keyboard.speakTypedCharacters": { + "schema": { + "title": "Announce Typed Characters", + "description": "Whether to announce all characters you type on the keyboard.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "keyboard.speakTypedWords": { + "schema": { + "title": "Announce Typed Words", + "description": "Whether to announce words you type on the keyboard.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "keyboard.speechInterruptForCharacters": { + "schema": { + "title": "Interrupt Speech for Typed Characters", + "description": "Whether or not to interrupt speech each time a character is typed. This is on by default.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "keyboard.speechInterruptForEnter": { + "schema": { + "title": "Interrupt Speech for Enter Key", + "description": "Whether to interrupt speech each time the Enter key is pressed. On by default.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "keyboard.useCapsLockAsNVDAModifierKey": { + "schema": { + "title": "Use CapsLock as an NVDA Modifier Key", + "description": "Whether or not to use the caps lock key as an NVDA modifier key.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "keyboard.useExtendedInsertAsNVDAModifierKey": { + "schema": { + "title": "Use Extended Insert as an NVDA Modifier Key", + "description": "Whether or not to use the extended insert key (usually found above the arrow keys near home and end) as an NVDA modifier key.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "keyboard.useNumpadInsertAsNVDAModifierKey": { + "schema": { + "title": "Use Numpad Insert as an NVDA Modifier Key", + "description": "Whether or not to use the insert key on the number pad as an NVDA modifier key.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "mouse.audioCoordinates_detectBrightness": { + "schema": { + "title": "Brightness controls audio coordinates volume", + "description": "Whether the volume of the audio coordinates beeps is controled by how bright the screen is under the mouse. This setting is turned off by default.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "mouse.audioCoordinatesOnMouseMove": { + "schema": { + "title": "Play audio coordinates when mouse moves", + "description": "Whether NVDA should play beeps as the mouse moves so that the user can work out where the mouse is in regards to the dimensions of the screen. The higher the mouse is on the screen the higher the pitch of the beeps. The further left or right the mouse is located on the screen the further left or right the sound will be played (assuming the user has stereo speakers or headphones).", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "mouse.enableMouseTracking": { + "schema": { + "title": "Mouse Tracking", + "description": "Whether to announce the text currently under the mouse pointer as you move it around the screen. This allows you to find things on the screen by physically moving the mouse rather than trying to find them through object navigation.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "mouse.mouseTextUnit": { + "schema": { + "title": "Text Unit Resolution", + "description": "If NVDA is set to announce the text under the mouse as you move it this option allows you to choose exactly how much text will be spoken. The options are character word line and paragraph.", + "enum": [ + "character", + "word", + "line", + "paragraph" + ], + "enumLabels": [ + "Character", + "Word", + "Line", + "Paragraph" + ], + "default": "paragraph" + } + }, + "mouse.reportMouseShapeChanges": { + "schema": { + "title": "Announce Mouse Shape Changes", + "description": "Whether to announce the shape of the mouse pointer each time it changes. The mouse pointer in Windows changes shape to convey certain information such as when something is editable or when something is loading et cetera.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "mouse.reportObjectRoleOnMouseEnter": { + "schema": { + "title": "Announce Role when Mouse Enters Object", + "description": "Whether to announce the role (type) of object as the mouse moves inside it.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "presentation.guessObjectPositionInformationWhenUnavailable": { + "schema": { + "title": "Guess object position information when unavailable", + "description": "If announcing of object position information is turned on this option allows NVDA to guess object position information when it is otherwise unavailable for a particular control. When on NVDA will announce position information for more controls such as menus and toolbars however this information may be slightly inaccurate.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "presentation.reportAutoSuggestionsWithSound": { + "schema": { + "title": "Play a sound when auto-suggestions appear", + "description": "Toggle announcement of auto-suggestions, which are lists of suggested entries based on the text entered into certain edit fields and documents.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "presentation.reportDynamicContentChanges": { + "schema": { + "title": "Announce dynamic content changes", + "description": "Toggles the announcement of new content in particular objects such as terminals and the history control in chat programs.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "presentation.reportHelpBalloons": { + "schema": { + "title": "Announce Help Balloons", + "description": "Whether to report help balloons as they appear. Help Balloons are like tool tips but are usually larger in size and are associated with system events such as a network cable being unplugged.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "presentation.reportKeyboardShortcuts": { + "schema": { + "title": "Announce Object Shortcut Keys", + "description": "Whether to announce the shortcut key that is associated with a certain object or control when it is announced. For example, the File menu on a menu bar may have a shortcut key of alt+f.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "presentation.reportObjectDescriptions": { + "schema": { + "title": "Announce Object Descriptions", + "description": "Whether to announce the description along with objects.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "presentation.reportObjectPositionInformation": { + "schema": { + "title": "Announce Object Position Information", + "description": "Whether to announce an object's position (e.g. 1 of 4) when moving to the object with the focus or object navigation.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "presentation.reportTooltips": { + "schema": { + "title": "Announce Tooltips", + "description": "Whether to announce tool tips as they appear. Many Windows and controls show a small message (or tool tip) when you move the mouse pointer over them or sometimes when you move the focus to them.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "presentation.progressBarUpdates.progressBarOutputMode": { + "schema": { + "title": "Progress Bar Output", + "description": "Whether/how to announce progress bar updates. It has the following options: Off: Progress bars will not be announced as they change. Speak: This option tells NVDA to speak the progress bar in percentages. Each time the progress bar changes NVDA will speak the new value. Beep: This tells NVDA to beep each time the progress bar changes. The higher the beep the closer the progress bar is to completion. Beep and speak: This option tells NVDA to both beep and speak when a progress bar updates.", + "enum": [ + "off", + "speak", + "beep", + "speak and beep" + ], + "enumLabels": [ + "Off", + "Speak", + "Beep", + "Speak and Beep" + ], + "default": "beep" + } + }, + "presentation.progressBarUpdates.reportBackgroundProgressBars": { + "schema": { + "title": "Announce Background Progress Bars", + "description": "Whether to keep announcing a progress bar even if it is not physically in the foreground. If you minimize or switch away from a window that contains a progress bar NVDA will keep track of it allowing you to do other things while NVDA tracks the progress bar.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "reviewCursor.followCaret": { + "schema": { + "title": "Follow System Carat", + "description": "Whether to automatically move the review cursor to the position of the system caret each time the system caret moves.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "reviewCursor.followFocus": { + "schema": { + "title": "Follow System Focus", + "description": "Whether to place the review cursor in the same object as the current system focus whenever the focus changes.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "reviewCursor.followMouse": { + "schema": { + "title": "Follow Mouse Cursor", + "description": "Whether the review cursor should follow the mouse as it moves.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "reviewCursor.simpleReviewMode": { + "schema": { + "title": "Simple Review Mode", + "description": "When enabled NVDA will filter the hierarchy of objects that can be navigated to exclude objects that aren't of interest to the user; e.g. invisible objects and objects used only for layout purposes.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "speech.beepSpeechModePitch": { + "schema": { + "title": "Speech Mode Beep Pitch", + "description": "The pitch of the beep in speech mode.", + "type": "integer", + "minimum": 50, + "maximum": 11025, + "default": 10000 + } + }, + "speech.espeak.autoDialectSwitching": { + "schema": { + "title": "eSpeak Automatic Dialect Switching", + "description": "When using eSpeak, whether or not dialect changes should be made rather than just actual language changes. For example if reading in an English U.S. voice but a document specifies that some text is in English U.K. then the synthesizer will switch accents if this option is enabled. This option is disabled by default.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "speech.espeak.autoLanguageSwitching": { + "schema": { + "title": "eSpeak Automatic Language Switching", + "description": "When using eSpeak, whether NVDA should switch speech synthesizer languages automatically if the text being read specifies its language. This option is enabled by default. Currently only the eSpeak synthesizer supports automatic language switching.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "speech.espeak.beepForCapitals": { + "schema": { + "title": "eSpeak Beep for Capitals", + "description": "When using eSpeak, whether to make a small beep each time NVDA encounters a capitalized character by itself. Like the \"say cap for capitals\" setting, this is useful for Synthesizers that can't change their pitch for capital letters.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "speech.espeak.capPitchChange": { + "schema": { + "title": "eSpeak Capital Pitch Change Percentage", + "description": "When using eSpeak, the amount that the pitch of the voice will change when speaking a capital letter. This value is a percentage where a negative value lowers the pitch and a positive value raises it. For no pitch change you would use 0.", + "type": "integer", + "minimum": -100, + "maximum": 100, + "default": 30 + } + }, + "speech.espeak.inflection": { + "schema": { + "title": "Voice Inflection", + "description": "How much inflection (rise and fall in pitch) the synthesizer should use to speak with. (The only synthesizer that supports this option at the present time is eSpeak).", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 75 + } + }, + "speech.espeak.pitch": { + "schema": { + "title": "eSpeak Pitch", + "description": "The voice pitch eSpeak should use.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 40 + } + }, + "speech.espeak.rate": { + "schema": { + "title": "eSpeak Speech Rate", + "description": "The rate at which eSpeak should speak.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 30 + } + }, + "speech.espeak.rateBoost": { + "schema": { + "title": "eSpeak Speech Rate Boost", + "description": "Whether to use the \"sonic\" library to speed up the speech rate.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "speech.espeak.sayCapForCapitals": { + "schema": { + "title": "Announce Capitals in eSpeak", + "description": "Whether eSpeak should say the word \"cap\" before any capital letter when spoken as an individual character, such as when spelling a word.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "speech.espeak.trustVoiceLanguage": { + "schema": { + "title": "eSpeak Trust Voice Language", + "description": "Whether the current voice's language can be trusted when processing symbols and characters. This is on by default. If you find that NVDA is reading punctuation in the wrong language for a particular synthesizer or voice you may wish to turn this off to force NVDA to use its global language setting instead.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "speech.espeak.useSpellingFunctionality": { + "schema": { + "title": "eSpeak Use Spelling Functionality", + "description": "Some words consist of only one character but the pronunciation is different depending on whether the character is being spoken as an individual character (such as when spelling) or a word. For example in English \"a\" is both a letter and a word and is pronounced differently in each case. This option allows the synthesizer to differentiate between these two cases if the synthesizer supports this. Most synthesizers do support it. This option should generally be enabled. However some Microsoft Speech API synthesizers do not implement this correctly and behave strangely when it is enabled. If you are having problems with the pronunciation of individual characters try disabling this option.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "speech.espeak.voice": { + "schema": { + "title": "ESpeak Voice", + "description": "The voice to use when using the 'eSpeak' engine.", + // TODO: Figure out a way to more cleanly reuse material between language-supporting features. Common term? + "enum": [ + "af", "sq", "am", "ar", "an", "hy-arevela", "hyw", "as", "az", "eu", "bn", "bpy", "bs", + "bg", "ca", "zh-yue", "zh-cmn", "hr", "cs", "da", "nl", "en-us", "en-029", "en-gb", + "en-gb-x-gbclan", "en-gb-x-rp", "en-gb-scotland", "en-gb-x-gbcwmd", "eo", "et", "fi", + "fr-be", "fr-fr", "fr-ch", "ga", "gd", "ka", "de", "grc", "el", "kl", "gn", "gu", "ht", + "hak", "hi", "hu", "is", "id", "ia", "it", "ja", "quc", "kn", "kk", "kok", "ko", "ku", "ky", + "la", "lv", "lfn", "lt", "jbo", "mk", "ms", "ml", "mt", "mr", "my", "mi", "nci", "ne", "no", + "or", "om", "pap", "fa-Latn", "fa", "pl", "pt-br", "pt-pt", "pa", "ro", "ru-LV", "ru", "sr", + "tn", "shn", "sd", "si", "sk", "sl", "es-419", "es", "sw", "sv", "ta", "tt", "te", "tr", + "ur", "vi-vn-x-central", "vi", "vi-vn-x-south", "cy" + ], + "enumLabels": [ + "Afrikaans", "Albanian", "Amharic", "Arabic", "Aragonese", "Armenian (East Armenia)", + "Armenian (West Armenia)", "Assamese", "Azerbaijani", "Basque", "Bengali", + "Bishnupriya Manipuri", "Bosnian", "Bulgarian", "Catalan", "Chinese (Cantonese)", + "Chinese (Mandarin)", "Croatian", "Czech", "Danish", "Dutch", "English (America)", + "English (Caribbean)", "English (Great Britain)", "English (Lancaster)", + "English (Received Pronunciation)", "English (Scotland)", "English (West Midlands)", + "Esperanto", "Estonian", "Finnish", "French (Belgium)", "French (France)", + "French (Switzerland)", "Gaelic (Irish)", "Gaelic (Scottish)", "Georgian", "German", + "Greek (Ancient)", "Greek", "Greenlandic", "Guarani", "Gujarati", "Haitian Creole", + "Hakka Chinese", "Hindi", "Hungarian", "Icelandic", "Indonesian", "Interlingua", + "Italian", "Japanese", "K'iche'", "Kannada", "Kazakh", "Konkani", "Korean", "Kurdish", + "Kyrgyz", "Latin", "Latvian", "Lingua Franca Nova", "Lithuanian", "Lojban", "Macedonian", + "Malay", "Malayalam", "Maltese", "Marathi", "Myanmar (Burmese)", "Māori", + "Nahuatl (Classical)", "Nepali", "Norwegian Bokmål", "Oriya", "Oromo", "Papiamento", + "Persian (Pinglish)", "Persian", "Polish", "Portuguese (Brazil)", "Portuguese (Portugal)", + "Punjabi", "Romanian", "Russian (Latvia)", "Russian", "Serbian", "Setswana", + "Shan (Tai Yai)", "Sindhi", "Sinhala", "Slovak", "Slovenian", "Spanish (Latin America)", + "Spanish (Spain)", "Swahili", "Swedish", "Tamil", "Tatar", "Telugu", "Turkish", "Urdu", + "Vietnamese (Central)", "Vietnamese (Northern)", "Vietnamese (Southern)", "Welsh" + ] + } + }, + "speech.espeak.volume": { + "schema": { + "title": "eSpeak Volume", + "description": "The volume eSpeak should use.", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 100 + } + }, + "speech.oneCore.autoDialectSwitching": { + "schema": { + "title": "oneCore Automatic Dialect Switching", + "description": "When using oneCore, whether or not dialect changes should be made rather than just actual language changes. For example if reading in an English U.S. voice but a document specifies that some text is in English U.K. then the synthesizer will switch accents if this option is enabled. This option is disabled by default.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "speech.oneCore.autoLanguageSwitching": { + "schema": { + "title": "OneCore Automatic Language Switching", + "description": "When using oneCore, whether NVDA should switch speech synthesizer languages automatically if the text being read specifies its language. This option is enabled by default. Currently only the eSpeak synthesizer supports automatic language switching.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "speech.oneCore.beepForCapitals": { + "schema": { + "title": "oneCore Beep for Capitals", + "description": "When using oneCore, whether to make a small beep each time NVDA encounters a capitalized character by itself. Like the \"say cap for capitals\" setting this is useful for Synthesizers that can't change their pitch for capital letters.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "speech.oneCore.capPitchChange": { + "schema": { + "title": "OneCore Capital Pitch Change Percentage", + "description": "When using oneCore, the amount that the pitch of the voice will change when speaking a capital letter. This value is a percentage where a negative value lowers the pitch and a positive value raises it. For no pitch change you would use 0.", + "type": "integer", + "minimum": -100, + "maximum": 100, + "default": 30 + } + }, + "speech.oneCore.pitch": { + "schema": { + "title": "OneCore Voice Pitch", + "description": "When using oneCore, the pitch of the current voice, from 0 to 100 (0 being the lowest pitch and 100 being the highest).", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "speech.oneCore.rate": { + "schema": { + "title": "OneCore Speech Rate", + "description": "The speech rate, from 0 to 100 (0 being the slowest, 100 being the fastest).", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "speech.oneCore.sayCapForCapitals": { + "schema": { + "title": "OneCore Announce Capitals", + "description": "Whether to say the word \"cap\" before any capital letter when spoken as an individual character such as when spelling. Usually NVDA raises the pitch slightly for any capital letter but some synthesizers may not support this well so perhaps this option may be of use.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "speech.oneCore.trustVoiceLanguage": { + "schema": { + "title": "OneCore Trust Voice Language", + "description": "Whether the current voice's language can be trusted when processing symbols and characters. This is on by default. If you find that NVDA is reading punctuation in the wrong language for a particular synthesizer or voice you may wish to turn this off to force NVDA to use its global language setting instead.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "speech.oneCore.useSpellingFunctionality": { + "schema": { + "title": "OneCore Use Spelling Functionality", + "description": "Some words consist of only one character but the pronunciation is different depending on whether the character is being spoken as an individual character (such as when spelling) or a word. For example in English \"a\" is both a letter and a word and is pronounced differently in each case. This option allows the synthesizer to differentiate between these two cases if the synthesizer supports this. Most synthesizers do support it. This option should generally be enabled. However some Microsoft Speech API synthesizers do not implement this correctly and behave strangely when it is enabled. If you are having problems with the pronunciation of individual characters try disabling this option.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "speech.oneCore.voice": { + "schema": { + "title": "OneCore Voice", + "description": "The voice to use.", + "enum": [ + "Microsoft David", + "Microsoft Zira", + "Microsoft Mark" + ], + "enumLabels": [ + "Microsoft David", + "Microsoft Zira", + "Microsoft Mark" + ], + "default": "Microsoft David" + } + }, + "speech.oneCore.volume": { + "schema": { + "title": "OneCore Speech Volume", + "description": "The speech volume, from 0 to 100 (0 being the lowest volume and 100 being the highest).", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 100 + } + }, + "speech.outputDevice": { + "schema": { + "title": "Speech Output Device", + "description": "The sound card that NVDA should instruct the selected synthesizer to speak through.", + // Hard code this to the only known good value. + "enum": ["Microsoft Sound Mapper"], + "enumLabels": ["Microsoft Sound Mapper"] + } + }, + "speech.sapi5.beepForCapitals": { + "schema": { + "title": "SAPI5 Beep for Capitals", + "description": "When using sapi5, whether to make a small beep each time NVDA encounters a capitalized character by itself. Like the \"say cap for capitals\" setting this is useful for Synthesizers that can't change their pitch for capital letters.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "speech.sapi5.capPitchChange": { + "schema": { + "title": "SAPI5 Capital Pitch Change Percentage", + "description": "The amount that the pitch of the voice will change when speaking a capital letter. This value is a percentage where a negative value lowers the pitch and a positive value raises it. For no pitch change you would use 0.", + "type": "integer", + "minimum": -100, + "maximum": 100, + "default": 30 + } + }, + "speech.sapi5.pitch": { + "schema": { + "title": "SAPI5 Voice Pitch", + "description": "The pitch of the current voice, from 0 to 100 (0 being the lowest pitch and 100 being the highest).", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "speech.sapi5.rate": { + "schema": { + "title": "SAPI5 Speech Rate", + "description": "The speech rate, from 0 to 100 (0 being the slowest, 100 being the fastest).", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 50 + } + }, + "speech.sapi5.sayCapForCapitals": { + "schema": { + "title": "SAPI5 Announce Capitals", + "description": "Whether to say the word \"cap\" before any capital letter when spoken as an individual character such as when spelling. Usually NVDA raises the pitch slightly for any capital letter but some synthesizers may not support this well so perhaps this option may be of use.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "speech.sapi5.trustVoiceLanguage": { + "schema": { + "title": "SAPI5 Trust Voice Language", + "description": "Whether the current voice's language can be trusted when processing symbols and characters. This is on by default. If you find that NVDA is reading punctuation in the wrong language for a particular synthesizer or voice you may wish to turn this off to force NVDA to use its global language setting instead.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "speech.sapi5.useSpellingFunctionality": { + "schema": { + "title": "SAPI5 Use Spelling Functionality", + "description": "Some words consist of only one character but the pronunciation is different depending on whether the character is being spoken as an individual character (such as when spelling) or a word. For example in English \"a\" is both a letter and a word and is pronounced differently in each case. This option allows the synthesizer to differentiate between these two cases if the synthesizer supports this. Most synthesizers do support it. This option should generally be enabled. However some Microsoft Speech API synthesizers do not implement this correctly and behave strangely when it is enabled. If you are having problems with the pronunciation of individual characters try disabling this option.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "speech.sapi5.voice": { + "schema": { + "title": "SAPI5 Voice", + "description": "The voice to use.", + "enum": [ + "Microsoft David Desktop - English (United States)", + "Microsoft Zira Desktop - English (United States)" + ], + "enumLabels": [ + "Microsoft David Desktop - English (United States)", + "Microsoft Zira Desktop - English (United States)" + ], + "default": "Microsoft David Desktop - English (United States)" + } + }, + "speech.sapi5.volume": { + "schema": { + "title": "SAPI5 Speech Volume", + "description": "The speech volume, from 0 to 100 (0 being the lowest volume and 100 being the highest).", + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 100 + } + }, + "speech.silence.autoDialectSwitching": { + "schema": { + "title": "\"No Speech\" Automatic Dialect Switching", + "description": "When using \"no speech\", whether or not dialect changes should be made rather than just actual language changes. For example if reading in an English U.S. voice but a document specifies that some text is in English U.K. then the synthesizer will switch accents if this option is enabled. This option is disabled by default.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "speech.silence.autoLanguageSwitching": { + "schema": { + "title": "\"No Speech\" Automatic Language Switching", + "description": "When using \"no speech\", whether NVDA should switch speech synthesizer languages automatically if the text being read specifies its language. This option is enabled by default. Currently only the eSpeak synthesizer supports automatic language switching.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "speech.silence.beepForCapitals": { + "schema": { + "title": "\"No Speech\" Beep for Capitals", + "description": "When using oneCore, whether to make a small beep each time NVDA encounters a capitalized character by itself. Like the \"say cap for capitals\" setting this is useful for Synthesizers that can't change their pitch for capital letters.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "speech.silence.capPitchChange": { + "schema": { + "title": "\"No Speech\" Capital Pitch Change Percentage", + "description": "When using \"no speech\", the amount that the pitch of the voice will change when speaking a capital letter. This value is a percentage where a negative value lowers the pitch and a positive value raises it. For no pitch change you would use 0.", + "type": "integer", + "minimum": -100, + "maximum": 100, + "default": 30 + } + }, + "speech.silence.sayCapForCapitals": { + "schema": { + "title": "\"No Speech\" Announce Capitals", + "description": "Whether to say the word \"cap\" before any capital letter when spoken as an individual character such as when spelling. Usually NVDA raises the pitch slightly for any capital letter but some synthesizers may not support this well so perhaps this option may be of use.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "speech.silence.trustVoiceLanguage": { + "schema": { + "title": "\"No Speech\" Trust Voice Language", + "description": "Whether the current voice's language can be trusted when processing symbols and characters. This is on by default. If you find that NVDA is reading punctuation in the wrong language for a particular synthesizer or voice you may wish to turn this off to force NVDA to use its global language setting instead.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "speech.silence.useSpellingFunctionality": { + "schema": { + "title": "\"No Speech\" Use Spelling Functionality", + "description": "Some words consist of only one character but the pronunciation is different depending on whether the character is being spoken as an individual character (such as when spelling) or a word. For example in English \"a\" is both a letter and a word and is pronounced differently in each case. This option allows the synthesizer to differentiate between these two cases if the synthesizer supports this. Most synthesizers do support it. This option should generally be enabled. However some Microsoft Speech API synthesizers do not implement this correctly and behave strangely when it is enabled. If you are having problems with the pronunciation of individual characters try disabling this option.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "speech.symbolLevel": { + "schema": { + "title": "Punctuation/Symbol Level", + "description": "The amount of punctuation and other symbols that should be spoken as words. This option applies to all synthesizers not just the currently active synthesizer.", + "enum": [ + 0, + 100, + 200, + 300 + ], + "enumLabels": [ + "None", + "Some", + "Most", + "All" + ], + "default": 100 + } + }, + "speech.synth": { + "schema": { + "title": "Synthesizer", + "description": "The synthesizer you wish NVDA to use for speech output. The \"No speech\" option allows you to use NVDA with no speech output what so ever. This may be useful for someone who wishes to only use NVDA with Braille, or perhaps to sighted developers who only wish to use the Speech Viewer.", + "enum": [ + "auto", + "espeak", + "sapi4", + "sapi5", + "oneCore", + "silence" + ], + "enumLabels": [ + "auto", + "eSpeak", + "sapi4", + "sapi5", + "oneCore", + "No Speech" + ], + "default": "auto" + } + }, + "speechViewer.autoPositionWindow": { + "schema": { + "title": "Speech Viewer", + "description": "Whether to enable the speech viewer (in real time).", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "speechViewer.showSpeechViewerAtStartup": { + "schema": { + "title": "Show Speech Viewer on Startup", + "description": "Whether to enable the speech viewer on startup.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "touch.touchTyping": { + "schema": { + "title": "Touch Typing Mode", + "description": "Whether to require a single or double key press to type a literal character.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "update.autoCheck": { + "schema": { + "title": "Automatically Check for NVDA Updates", + "description": "If this is enabled NVDA will automatically check for updated versions of NVDA and inform you when an update is available. You can also manually check for updates by selecting Check for updates under Help in the NVDA menu.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "update.startupNotification": { + "schema": { + "title": "Notify of Pending Update on Startup", + "description": "Whether to notify about an update that is waiting to be installed when NVDA is started.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "uwpOcr.language": { + "schema": { + "title": "Recognition Language", + "description": "The language to use for text recognition.", + "enum": [ + "English(United States)" + ], + "enumLabels": [ + "English(United States)" + ], + "default": "English(United States)" + } + }, + "virtualBuffers.autoPassThroughOnCaretMove": { + "schema": { + "title": "Automatic focus mode for caret movement", + "description": "This option when checked allows NVDA to enter and leave focus mode when using arrow keys. For example if arrowing down a webpage and you land on an edit box NVDA will automatically bring you into focus mode. If you arrow out of the edit box NVDA will put you back in browse mode.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "False" + } + }, + "virtualBuffers.autoPassThroughOnFocusChange": { + "schema": { + "title": "Automatic focus mode for focus changes", + "description": "Whether to invoke focus mode if focus changes. For example when on a web page if you press tab and you land on a form if this option is checked focus mode will automatically be invoked.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "virtualBuffers.autoSayAllOnPageLoad": { + "schema": { + "title": "Automatic Say All on page load", + "description": "Whether to automatically read a page after it loads in browse mode. This option is enabled by default.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "virtualBuffers.linesPerPage": { + "schema": { + "title": "Lines per Page", + "description": "The amount of lines you will move by when pressing page up or page down while in browse mode.", + "type": "integer", + "minimum": 5, + "maximum": 150, + "default": 25 + } + }, + "virtualBuffers.maxLineLength": { + "schema": { + "title": "Maximum Line Length", + "description": "The maximum length of a line in browse mode (in characters).", + "type": "integer", + "minimum": 10, + "maximum": 250, + "default": 100 + } + }, + "virtualBuffers.passThroughAudioIndication": { + "schema": { + "title": "Audio indication of focus and browse modes", + "description": "Whether to play special sounds when NVDA switches between browse mode and focus mode rather than speaking the change.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "virtualBuffers.trapNonCommandGestures": { + "schema": { + "title": "Trap all non-command gestures from reaching the document", + "description": "Enabled by default this option allows you to choose if gestures (such as key presses) that do not result in an NVDA command and are not considered to be a command key in general should be trapped from going through to the document you are currently focused on. As an example if enabled if the letter j was pressed it would be trapped from reaching the document even though it is not a quick navigation command nor is it likely to be a command in the application itself.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + }, + "virtualBuffers.useScreenLayout": { + "schema": { + "title": "Use Screen Layout", + "description": "Whether content in browse mode should place content such as links and other fields on their own line or if it should keep them in the flow of text as it is visually shown. If the option is enabled then things will stay as they are visually shown but if it is disabled then fields will be placed on their own line.", + "enum": [ + "True", + "False" + ], + "enumLabels": [ + "True", + "False" + ], + "default": "True" + } + } + }, + "capabilitiesTransformations": { + "keyboard\\.speakTypedCharacters": { + transform: { + type: "fluid.transforms.condition", + conditionPath: "http://registry\\.gpii\\.net/common/keyEcho", + "true": "True", + "false": "False" + } + }, + "keyboard\\.speakTypedWords": { + transform: { + type: "fluid.transforms.condition", + conditionPath: "http://registry\\.gpii\\.net/common/wordEcho", + "true": "True", + "false": "False" + } + }, + "presentation\\.reportHelpBalloons": { + transform: { + type: "fluid.transforms.condition", + conditionPath: "http://registry\\.gpii\\.net/common/speakTutorialMessages", + "true": "True", + "false": "False" + } + }, + "speech\\.espeak\\.pitch": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 100 + } + }, + "speech\\.espeak\\.rate": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/speechRate", + "factor": 0.1 + } + } + } + }, + "speech\\.espeak\\.rateBoost": { + "transform": { + type: "fluid.transforms.condition", + condition: { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "http://registry\\.gpii\\.net/common/speechRate", + "operator": ">", + "right": 390 + } + }, + "true": "True", + "false": "False" + } + }, + "speech\\.espeak\\.sayCapForCapitals": { + transform: { + type: "fluid.transforms.condition", + conditionPath: "http://registry\\.gpii\\.net/common/announceCapitals", + "true": "True", + "false": "False" + } + }, + // TODO: Figure out a way to reuse this material between language-supporting features. Common term? + "speech\\.espeak\\.voice": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "match": { + "en": "en-us", + "en-GB": "en-gb", + "en-US": "en-us", + "en-scotland": "en-gb-scotland", + "af": "af", + "bg": "bg", + "bs": "bs", + "ca": "ca", + "cs": "cs", + "cy": "cy", + "da": "da", + "de": "de", + "el": "el", + "eo": "eo", + "es": "es", + "es-419": "es-la", + "et": "et", + "fi": "fi", + "fr": "fr", + "fr-BE": "fr-be", + "hi": "hi", + "hr": "hr", + "hu": "hu", + "hy": "hy", + "hy-arevmda": "hy-west", + "id": "id", + "is": "is", + "it": "it", + "ka": "ka", + "kn": "kn", + "ku": "ku", + "la": "la", + "lv": "lv", + "mk": "mk", + "ml": "ml", + "nl": "nl", + "no": "no", + "pl": "pl", + "pt-BR": "pt", + "pt-PT": "pt-pt", + "ro": "ro", + "ru": "ru", + "sk": "sk", + "sq": "sq", + "sr": "sr", + "sv": "sv", + "sw": "sw", + "ta": "ta", + "tr": "tr", + "vi": "vi", + "zh-cmn": "zh", + "cmn": "zh", + "zh-yue": "zh-yue" + }, + "noMatch": { + outputValue: "en-us" + } + } + }, + "speech\\.espeak\\.volume": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volumeTTS", + "factor": 100 + } + }, + + "speech\\.oneCore\\.pitch": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 100 + } + }, + "speech\\.oneCore\\.rate": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/speechRate", + "factor": 0.1 + } + } + } + }, + "speech\\.oneCore\\.sayCapForCapitals": { + transform: { + type: "fluid.transforms.condition", + conditionPath: "http://registry\\.gpii\\.net/common/announceCapitals", + "true": "True", + "false": "False" + } + }, + // TODO: "speech\\.oneCore\\.voice" cannot be updated to reflect the current language without additional research, i.e. installing all languages. + "speech\\.oneCore\\.volume": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volumeTTS", + "factor": 100 + } + }, + + "speech\\.sapi5\\.pitch": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/pitch", + "factor": 100 + } + }, + "speech\\.sapi5\\.rate": { + "transform": { + "type": "fluid.transforms.round", + "input": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/speechRate", + "factor": 0.1 + } + } + } + }, + "speech\\.sapi5\\.sayCapForCapitals": { + transform: { + type: "fluid.transforms.condition", + conditionPath: "http://registry\\.gpii\\.net/common/announceCapitals", + "true": "True", + "false": "False" + } + }, + // TODO: "speech\\.sapi5\\.voice" cannot be updated to reflect the current language without additional research, i.e. installing all languages. + "speech\\.sapi5\\.volume": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "http://registry\\.gpii\\.net/common/volumeTTS", + "factor": 100 + } + }, + + "speech\\.silence\\.sayCapForCapitals": { + transform: { + type: "fluid.transforms.condition", + conditionPath: "http://registry\\.gpii\\.net/common/announceCapitals", + "true": "True", + "false": "False" + } + }, + "speech\\.symbolLevel": { + "transform": { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "match": { + "none": 0, + "some": 100, + "most": 200, + "all": 300 + } + } + }, + + "transform": [ + { + "type": "fluid.transforms.arrayToSetMembership", + "inputPath": "http://registry\\.gpii\\.net/common/trackingTTS", + "outputPath": "", + "presentValue": "True", + "missingValue": "False", + "options": { + "focus": "reviewCursor\\.followFocus", + "caret": "reviewCursor\\.followCaret", + "mouse": "reviewCursor\\.followMouse" + } + }, + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled", + "match": { + // Explicitly set the voice to eSpeak so that we can control the language as well. + "true": { + "outputValue": { + "transform": [ + { + "type": "fluid.transforms.literalValue", + "input": "espeak", + "outputPath": "speech\\.synth" + }, + { + "type": "fluid.transforms.literalValue", + "input": "Microsoft Sound Mapper", + "outputPath": "speech\\.outputDevice" + } + ] + } + }, + "false": { + "outputValue": { + "transform": [ + { + "type": "fluid.transforms.literalValue", + "input": "silence", + "outputPath": "speech\\.synth" + }, + { + "type": "fluid.transforms.literalValue", + "input": "Microsoft Sound Mapper", + "outputPath": "speech\\.outputDevice" + } + ] + } + } + } + } + ] + }, + "inverseCapabilitiesTransformations": { + "http://registry\\.gpii\\.net/common/speakTutorialMessages": "presentation\\.reportHelpBalloons", + "http://registry\\.gpii\\.net/common/keyEcho": "keyboard\\.speakTypedCharacters", + "http://registry\\.gpii\\.net/common/wordEcho": "keyboard\\.speakTypedWords", + // TODO: eSpeak is the preferred voice and is the only one we perform an inverse transform of at the moment. If we decide to flesh this out better, we will need a better transform here. + "http://registry\\.gpii\\.net/common/announceCapitals": "speech\\.espeak\\.sayCapForCapitals", + "transform": [ + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "virtualBuffers\\.autoSayAllOnPageLoad.value", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/readingUnit", + "match": { + "true": "all", + "false": "sentence" + } + }, + { + "type": "fluid.transforms.setMembershipToArray", + "inputPath": "", + "outputPath": "http://registry\\.gpii\\.net/common/trackingTTS", + "presentValue": true, + "missingValue": false, + "options": { + "reviewCursor\\.followFocus": "focus", + "reviewCursor\\.followCaret": "caret", + "reviewCursor\\.followMouse": "mouse" + } + }, + { + "type": "fluid.transforms.valueMapper", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/punctuationVerbosity", + "defaultInputPath": "speech\\.symbolLevel", + "match": { + "0": "none", + "100": "some", + "200": "most", + "300": "all" + } + }, + { + "type": "fluid.transforms.condition", + "outputPath": "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled", + "false": true, + "true": false, + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "speech\\.synth", + "operator": "===", + "right": "silence" + } + }, + "right": { + "transform": { + "type": "fluid.transforms.binaryOp", + "leftPath": "speech\\.outputDevice", + "operator": "===", + "right": "Microsoft Sound Mapper" + } + }, + "operator": "&&" + } + } + }, + { + "type": "fluid.transforms.condition", + "outputPath": "http://registry\\.gpii\\.net/common/speechRate", + "condition": { + "transform": { + "type": "fluid.transforms.binaryOp", + "right": true, + "operator": "===", + "leftPath": "speech\\.espeak\\.rateBoost" + } + }, + "false": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "speech\\.espeak\\.rate", + "factor": 3.1, + "offset": 80 + } + }, + "true": { + "transform": { + "type": "fluid.transforms.binaryOp", + "left": { + "transform": { + "type": "fluid.transforms.linearScale", + "inputPath": "speech\\.espeak\\.rate", + "factor": 3.1, + "offset": 80 + } + }, + "operator": "*", + "right": 3 + } + } + }, + // TODO: Figure out a way to reuse this material between language-supporting features. Common term? + { + "type": "fluid.transforms.valueMapper", + "defaultInputPath": "speech\\.espeak\\.voice", + "defaultOutputPath": "http://registry\\.gpii\\.net/common/auditoryOutLanguage", + "match": { + "en-us": "en-US", + "en-gb-scotland": "en-scotland", + "af": "af", + "bg": "bg", + "bs": "bs", + "ca": "ca", + "cs": "cs", + "cy": "cy", + "da": "da", + "de": "de", + "el": "el", + "eo": "eo", + "es": "es", + "es-la": "es-419", + "et": "et", + "fi": "fi", + "fr": "fr", + "fr-be": "fr-BE", + "hi": "hi", + "hr": "hr", + "hu": "hu", + "hy": "hy", + "hy-west": "hy-arevmda", + "id": "id", + "is": "is", + "it": "it", + "ka": "ka", + "kn": "kn", + "ku": "ku", + "la": "la", + "lv": "lv", + "mk": "mk", + "ml": "ml", + "nl": "nl", + "no": "no", + "pt": "pt-BR", + "pt-pt": "pt-PT", + "ro": "ro", + "ru": "ru", + "sk": "sk", + "sq": "sq", + "sr": "sr", + "sv": "sv", + "sw": "sw", + "ta": "ta", + "tr": "tr", + "vi": "vi", + "zh": "zh-cmn", + "zh-yue": "zh-yue" + } + } + ] + } + } + }, + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "nvda.exe" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe\\}\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "nvda_service.exe" + }, + { + "type": "gpii.windows.closeProcessByName", + "filename": "nvda.exe" + } + ] + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.registryKeyExists", + "hKey": "HKEY_LOCAL_MACHINE", + "path": "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe", + "subPath": "", + "dataType": "REG_SZ" + } + ] + }, + "trace.easyOne.communicator.windows": { + "name": "EasyOne Communicator Windows", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/simplifiedUi/enabled" + ], + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "setTrue": { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\firefox.exe\\}\" http://easyone.gpii.net/user/${{gpiiKey}}" + }, + "setFalse": { + "type": "gpii.windows.closeProcessByName", + "filename": "firefox.exe" + }, + "getState": [ + { + "type": "gpii.processReporter.neverRunning" + } + ] + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "trace.easyOne.sudan.windows": { + "name": "EasyOne Communicator Sudan", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "capabilities": [], + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "setTrue": { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\firefox.exe\\}\" http://easyone.gpii.net/sudan" + }, + "setFalse": { + "type": "gpii.windows.closeProcessByName", + "filename": "firefox.exe" + }, + "getState": [ + { + "type": "gpii.processReporter.neverRunning" + } + ] + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "webinsight.webAnywhere.windows": { + "name": "Web Anywhere", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/screenReaderTTS/enabled" + ], + "launchHandlers": { + "launcher": { + "type": "gpii.launchHandlers.flexibleHandler", + "options": { + "setTrue": { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\firefox.exe\\}\" \"http://webanywhere.cs.washington.edu/beta/?starting_url=http%3A%2F%2Fcloud4all.info\"" + }, + "setFalse": { + "type": "gpii.windows.closeProcessByName", + "filename": "firefox.exe" + }, + "getState": [ + { + "type": "gpii.processReporter.neverRunning" + } + ] + } + } + }, + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + }, + "com.microsoft.office": { + "name": "Microsoft Office", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "settingsHandlers": { + "configure.common-tabletmode": { + "type": "gpii.windows.registrySettingsHandler", + "liveness": "liveRestart", + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\Office\\16.0\\Common", + "dataTypes": { + "OverrideTabletMode": "REG_DWORD", + "OverridePointerMode": "REG_DWORD" + } + }, + "capabilitiesTransformations": { + "OverrideTabletMode": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.office.tabletMode", + "true": 2, + "false": 1 + } + }, + "OverridePointerMode": { + "transform": { + "type": "fluid.transforms.condition", + "conditionPath": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.office.tabletMode", + "true": 2, + "false": 1 + } + } + }, + "supportedSettings": { + "OverrideTabletMode": { + "schema": { + "title": "Enable Tablet Mode", + "description": "Display larger ribbon buttons that are easier to touch", + "type": "number", + "enum": [ + 1, + 2 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + }, + "OverridePointerMode": { + "schema": { + "title": "Enable Tablet Mode", + "description": "Display larger ribbon buttons that are easier to touch", + "type": "number", + "enum": [ + 1, + 2 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 1 + } + } + } + }, + "configure.ribbons": { + "type": "gpii.settingsHandlers.remoteFileSettingsHandler.office", + "liveness": "live", + "options": { + "settings": { + "word-ribbon": { + "path": "${{environment}.LOCALAPPDATA}\\Microsoft\\Office\\Word.officeUI", + "url": "https://raw.githubusercontent.com/GPII/gpii-assets/master/dataBlocks/officeRibbon/Word%20%value.xml", + "cache": true + }, + "excel-ribbon": { + "path": "${{environment}.LOCALAPPDATA}\\Microsoft\\Office\\Excel.officeUI", + "url": "https://raw.githubusercontent.com/GPII/gpii-assets/master/dataBlocks/officeRibbon/Excel%20%value.xml", + "cache": true + } + } + }, + "capabilitiesTransformations": { + "word-ribbon": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.office.word-ribbon", + "excel-ribbon": "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.office.excel-ribbon" + }, + "supportedSettings": { + "word-ribbon": { + "schema": { + "title": "Word Ribbon Layout", + "description": "Specifies the custom layout of the ribbon and quick access toolbar for Word", + "enum": [ + "StandardSet", + "Basics+StandardSet", + "Essentials+StandardSet", + "Basics+Essentials+StandardSet" + ], + "enumLabels": [ + "Standard Set", + "Basics and Standard Set", + "Essentials and Standard Set", + "Basic, Essentials, and Standard Set" + ], + "default": "StandardSet" + } + }, + "excel-ribbon": { + "schema": { + "title": "Excel Ribbon Layout", + "description": "Specifies the custom layout of the ribbon and quick access toolbar for Excel", + "enum": [ + "StandardSet", + "Basics+StandardSet", + "Essentials+StandardSet", + "Basics+Essentials+StandardSet" + ], + "enumLabels": [ + "Standard Set", + "Basics and Standard Set", + "Essentials and Standard Set", + "Basic, Essentials, and Standard Set" + ], + "default": "StandardSet" + } + } + } + } + }, + "configure": [ + "settings.configure.ribbons", + "settings.configure.common-tabletmode", + { + type: "gpii.windows.office.reloadRibbon", + application: "Word" + } + ], + "restore": [ + "settings.configure.ribbons", + "settings.configure.common-tabletmode", + { + type: "gpii.windows.office.reloadRibbon", + application: "Word" + } + ], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + } +} diff --git a/tests/AcceptanceTests.js b/tests/AcceptanceTests.js deleted file mode 100644 index e4d910685..000000000 --- a/tests/AcceptanceTests.js +++ /dev/null @@ -1,316 +0,0 @@ -/* - -GPII Acceptance Testing - -Copyright 2013 Raising the Floor International -Copyright 2013 OCAD University - -Licensed under the New BSD license. You may not use this file except in -compliance with this License. - -You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt -*/ - -/*global require*/ - -"use strict"; - -var fluid = require("infusion"), - http = require("http"), - gpii = fluid.registerNamespace("gpii"), - kettle = fluid.require("kettle", require), - jqUnit = fluid.require("jqUnit"), - child_process = require('child_process'); - -fluid.registerNamespace("gpii.acceptanceTesting"); - -// Definition and defaults of exec component -fluid.defaults("gpii.acceptanceTesting.exec", { - gradeNames: ["autoInit", "fluid.eventedComponent"], - events: { - onExecExit: null - }, - invokers: { - exec: { - funcName: "gpii.acceptanceTesting.exec.exec", - args: ["{that}", "{arguments}.0", "{arguments}.1"] - } - }, - timeout: 500, - maxTimeouts: 6 -}); - -gpii.acceptanceTesting.exec.exec = function (that, processSpec, expected) { - var command = processSpec.command, - timeout = that.options.timeout, - maxTimeouts = that.options.maxTimeouts; - - fluid.log("Exec'ing: ", command); - - child_process.exec(command, function (err, stdout, stderr) { - if (stderr) { - fluid.log("stderr from command '", command, "': ", stderr); - } - if (stdout.trim() === expected) { - that.events.onExecExit.fire(true, processSpec); - } else { - processSpec.count = processSpec.count || 0; - - if (processSpec.count >= maxTimeouts) { - that.events.onExecExit.fire(false, processSpec); - return; - } - - processSpec.count++; - setTimeout(function () { - gpii.acceptanceTesting.exec.exec(that, processSpec, expected); - }, timeout); - } - }); -}; - -//Definition and defaults of http request component -fluid.defaults("gpii.acceptanceTesting.httpReq", { - gradeNames: ["autoInit", "fluid.eventedComponent"], - events: { - onLogin: null, - onLogout: null - }, - invokers: { - login: { - funcName: "gpii.acceptanceTesting.httpReq.send", - args: ["{that}.options.requestOptions", { - token: "{arguments}.0", - action: "login" - }, "{that}.events.onLogin.fire"] - }, - logout: { - funcName: "gpii.acceptanceTesting.httpReq.send", - args: ["{that}.options.requestOptions", { - token: "{arguments}.0", - action: "logout" - }, "{that}.events.onLogout.fire"] - } - }, - requestOptions: { - host: "localhost", - port: 8081, - path: "/user/%token/%action" - } -}); - -gpii.acceptanceTesting.httpReq.send = function (requestOptions, pathTermMap, callback) { - var roptions = fluid.copy(requestOptions); - roptions.path = fluid.stringTemplate(roptions.path, pathTermMap); - http.get(roptions, function(response) { - var data = ""; - - response.on("data", function (chunk) { - data += chunk; - }); - response.on("close", function(err) { - if (err) { - jqUnit.assertFalse("Got an error on request to " + roptions.path + ": " + - err.message, true); - } - fluid.log("Connection to the server was closed"); - }); - response.on("end", function() { - callback(data); - }); - }).on('error', function(err) { - jqUnit.assertFalse("Got an error on request to " + roptions.path + ": " + err.message, true); - }); -}; - -/* -* Sets the settings given in the json paramater. The content of the json passed -* is the values to set in a format similar to the content of 'initialState' -*/ -gpii.acceptanceTesting.getSettings = function (payload) { - var ret = {}; - fluid.each(payload, function (handlerBlock, handlerID) { - ret[handlerID] = fluid.invokeGlobalFunction(handlerID + ".get", [handlerBlock]); - }); - return ret; -}; - -gpii.acceptanceTesting.removeOptionsBlocks = function (payload) { - var togo = fluid.copy(payload); - return fluid.transform(togo, function (settingHandler) { - return fluid.transform(settingHandler, function (solutionBlocks) { - return fluid.transform(solutionBlocks, function (solutionBlock) { - return {settings: solutionBlock.settings}; - }); - }); - }); -}; - -gpii.acceptanceTesting.startServer = function (testDef, tests) { - tests.events.createServer.fire(); -}; - -gpii.acceptanceTesting.snapshotSettings = function (testDef, settingsStore) { - settingsStore.orig = gpii.acceptanceTesting.getSettings(testDef.settingsHandlers); -}; - -gpii.acceptanceTesting.loginRequestListen = function (data) { - jqUnit.assertNotEquals("Successful login message returned " + data, -1, - data.indexOf("was successfully logged in.")); -}; - -gpii.acceptanceTesting.checkConfiguration = function (testDef) { - var config = gpii.acceptanceTesting.getSettings(testDef.settingsHandlers); - var noOptions = gpii.acceptanceTesting.removeOptionsBlocks( - testDef.settingsHandlers); - jqUnit.assertDeepEq("Checking that settings are set", config, noOptions); -}; - -gpii.acceptanceTesting.onExecExit = function (result, processSpec) { - jqUnit.assertTrue("Checking the process with command: " + processSpec, result); -}; - -gpii.acceptanceTesting.logoutRequestListen = function (data) { - jqUnit.assertNotEquals("Successful logout message returned " + data, -1, - data.indexOf("was successfully logged out.")); -}; - -gpii.acceptanceTesting.checkRestoredConfiguration = function (testDef, settingsStore) { - var currentSettings = gpii.acceptanceTesting.getSettings( - testDef.settingsHandlers); - jqUnit.assertDeepEq("Checking that settings are properly reset", - settingsStore.orig, currentSettings); -}; - -gpii.acceptanceTesting.buildSingleTestFixture = function (testDef) { - var processes = testDef.processes; - var testDefRef = "{tests}.options.testDef"; - // Storing state, start server, logging in and checking that - // configuration is set - var testFixture = { - name: testDef.name, - //number of asserts is 4 + number of checks for running processes (both on login and logout) - expect: 4 + testDef.processes.length*2, - sequence: [ { - func: "gpii.acceptanceTesting.startServer", - args: [ testDefRef, "{tests}" ] - }, { - func: "gpii.acceptanceTesting.snapshotSettings", - args: [ testDefRef, "{tests}.settingsStore" ] - }, { - func: "{httpReq}.login", - args: [ testDefRef + ".token" ] - }, { - listener: "gpii.acceptanceTesting.loginRequestListen", - event: "{httpReq}.events.onLogin" - }, { - func: "gpii.acceptanceTesting.checkConfiguration", - args: [ testDefRef ] - }] - }; - // For each process, run the command, then check that we get the - // expected output - fluid.each(processes, function (process, pindex) { - testFixture.sequence.push({ - func: "{exec}.exec", - args: [ testDefRef + ".processes." + pindex, testDefRef + ".processes." + pindex + ".expectConfigured" ] - }, { - listener: "gpii.acceptanceTesting.onExecExit", - event: "{exec}.events.onExecExit" - }); - }); - //Logout, check that configuration is properly restored - testFixture.sequence.push({ - func: "{httpReq}.logout", - args: [ testDefRef + ".token" ] - }, { - listener: "gpii.acceptanceTesting.logoutRequestListen", - event: "{httpReq}.events.onLogout" - }); - - // Check that the processes are in the expected state after logout - fluid.each(processes, function (process, pindex) { - testFixture.sequence.push({ - func: "{exec}.exec", - args: [ testDefRef + ".processes." + pindex, testDefRef + ".processes." + pindex + ".expectRestored" ] - }, { - listener: "gpii.acceptanceTesting.onExecExit", - event: "{exec}.events.onExecExit" - }); - }); - - testFixture.sequence.push({ - func: "gpii.acceptanceTesting.checkRestoredConfiguration", - args: [ testDefRef, "{tests}.settingsStore"] - }); - - return testFixture; -}; - -//holds the GPII server to run the integration testing against -fluid.defaults("gpii.acceptanceTesting.server", { - gradeNames: ["autoInit", "fluid.littleComponent", "{that}.buildServerGrade"], - invokers: { - buildServerGrade: { - funcName: "fluid.identity", - args: "{gpii.acceptanceTesting.testCaseHolder}.options.serverName" - } - } -}); - -fluid.defaults("gpii.acceptanceTesting.testCaseHolder", { - gradeNames: ["autoInit", "fluid.test.testCaseHolder"], - members: { - settingsStore: {} - }, - events: { - createServer: null - }, - components: { - server: { - type: "gpii.acceptanceTesting.server", - createOnEvent: "createServer" - } - } -}); - -//definition of tests, sequence, etc. -fluid.defaults("gpii.acceptanceTesting.testEnvironment", { - gradeNames: ["fluid.test.testEnvironment", "autoInit"], - components: { - httpReq: { - type: "gpii.acceptanceTesting.httpReq" - }, - exec: { - type: "gpii.acceptanceTesting.exec" - } - } -}); - - -gpii.acceptanceTesting.runTests = function (testDefs, gpiiConfig) { - var serverName = kettle.config.createDefaults(gpiiConfig); - var tests = fluid.transform(testDefs, function (testDef) { - return { - type: "gpii.acceptanceTesting.testEnvironment", - options: { - components: { - tests: { - type: "gpii.acceptanceTesting.testCaseHolder", - options: { - testDef: gpii.lifecycleManager.resolver().resolve(testDef), - serverName: serverName, - modules: [ { - name: "Full login/logout cycle", - tests: [ gpii.acceptanceTesting.buildSingleTestFixture(testDef) ] - }] - } - } - } - } - }; - }); - - fluid.test.runTests(tests); -}; diff --git a/tests/CloseConflictingAppsTests.js b/tests/CloseConflictingAppsTests.js new file mode 100644 index 000000000..972c16291 --- /dev/null +++ b/tests/CloseConflictingAppsTests.js @@ -0,0 +1,221 @@ +/* + * GPII Tests for ensuring that two conflicting apps will not be launched + * + * These are integration tests for ensuring that eg. two screenreaders will not be active at the same time on + * the system. Besides the obvious case of checking that the MM doesn't launch two screenreaders on login, + * it is also tested that an already running screenreader will be closed if the matchmaker finds that another + * (conflicting) matchmaker should be launched. + * + * Copyright 2016 Raising The Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.conflictingApps"); + +gpii.tests.conflictingApps.jawsHandlerEntry = function (running) { + return { + "com.freedomscientific.jaws": [{ + "settings": { + "running": running + }, + "options": { + "verifySettings": true, + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "jfw.exe" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\JAWS2019.exe\\}\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "jfw.exe", + "options": { + "closeWindow": true + } + } + ] + } + }] + }; +}; + +gpii.tests.conflictingApps.NVDAHandlerEntry = function (running) { + return { + "org.nvda-project": [{ + "settings": { + "running": running + }, + "options": { + "verifySettings": true, + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "nvda" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe\\}\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "nvda_service.exe" + },{ + "type": "gpii.windows.closeProcessByName", + "filename": "nvda.exe" + } + ] + } + }] + }; +}; + +gpii.tests.conflictingApps.testDefs = [ + { + name: "Only one screenreader is launched", + gpiiKey: "screenreader_common", + initialState: { + "gpii.launchHandlers.flexibleHandler": fluid.extend({}, + gpii.tests.conflictingApps.jawsHandlerEntry(false), + gpii.tests.conflictingApps.NVDAHandlerEntry(false)) + }, + settingsHandlers: { + "gpii.settingsHandlers.INISettingsHandler": { + "com.freedomscientific.jaws": [ + { + "settings": { + "Options.SayAllIndicateCaps": 1, + "Options.TypingEcho": 2 + }, + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF" + } + } + ] + }, + "gpii.settingsHandlers.JAWSSettingsHandler": { + "com.freedomscientific.jaws": [ + { + "settings": { + // NOTE: Commented until GPII-4336 is addressed + // ============================================ + // "ENU-Global.Rate": 400, + // "ENU-Global.Punctuation": 3, + // "ENU-Global.Pitch": 11, + // "ENU-Message.Rate": 400, + // "ENU-Message.Punctuation": 3, + // "ENU-Message.Pitch": 11, + // "ENU-Keyboard.Rate": 400, + // "ENU-Keyboard.Punctuation": 3, + // "ENU-Keyboard.Pitch": 11, + // "ENU-PCCursor.Rate": 400, + // "ENU-PCCursor.Punctuation": 3, + // "ENU-PCCursor.Pitch": 11, + // "ENU-JAWSCursor.Rate": 400, + // "ENU-JAWSCursor.Punctuation": 3, + // "ENU-JAWSCursor.Pitch": 11, + // "ENU-MenuAndDialog.Rate": 400, + // "ENU-MenuAndDialog.Punctuation": 3, + // "ENU-MenuAndDialog.Pitch": 11 + }, + "options": { + "defaultSettingsFilePath": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", + "voiceProfilesDirPath": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\VoiceProfiles" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": fluid.extend({}, + gpii.tests.conflictingApps.jawsHandlerEntry(true), + gpii.tests.conflictingApps.NVDAHandlerEntry(false)) + } + }, { + name: "Conflicting screenreader (jaws) is closed is Only one screenreader is launched", + gpiiKey: "screenreader_common", + initialState: { + "gpii.launchHandlers.flexibleHandler": fluid.extend({}, + gpii.tests.conflictingApps.jawsHandlerEntry(true), + gpii.tests.conflictingApps.NVDAHandlerEntry(false)) + }, + settingsHandlers: { + + "gpii.settingsHandlers.INISettingsHandler": { + "com.freedomscientific.jaws": [ + { + "settings": { + "Options.SayAllIndicateCaps": 1, + "Options.TypingEcho": 2 + }, + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF" + } + } + ] + }, + "gpii.settingsHandlers.JAWSSettingsHandler": { + "com.freedomscientific.jaws": [ + { + "settings": { + // NOTE: Commented until GPII-4336 is addressed + // ============================================ + // "ENU-Global.Rate": 400, + // "ENU-Global.Punctuation": 3, + // "ENU-Global.Pitch": 11, + // "ENU-Message.Rate": 400, + // "ENU-Message.Punctuation": 3, + // "ENU-Message.Pitch": 11, + // "ENU-Keyboard.Rate": 400, + // "ENU-Keyboard.Punctuation": 3, + // "ENU-Keyboard.Pitch": 11, + // "ENU-PCCursor.Rate": 400, + // "ENU-PCCursor.Punctuation": 3, + // "ENU-PCCursor.Pitch": 11, + // "ENU-JAWSCursor.Rate": 400, + // "ENU-JAWSCursor.Punctuation": 3, + // "ENU-JAWSCursor.Pitch": 11, + // "ENU-MenuAndDialog.Rate": 400, + // "ENU-MenuAndDialog.Punctuation": 3, + // "ENU-MenuAndDialog.Pitch": 11 + }, + "options": { + "defaultSettingsFilePath": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", + "voiceProfilesDirPath": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\VoiceProfiles" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": fluid.extend({}, + gpii.tests.conflictingApps.jawsHandlerEntry(true), + gpii.tests.conflictingApps.NVDAHandlerEntry(false)) + } + } +]; + +module.exports = gpii.test.runTests({ + testDefs: "gpii.tests.conflictingApps.testDefs", + configName: "gpii.tests.multiScreenreader.config", + configPath: "%gpii-universal/tests/configs" +}, ["gpii.test.integration.testCaseHolder.windows"]); diff --git a/tests/DevelopmentTests.js b/tests/DevelopmentTests.js new file mode 100644 index 000000000..65ced2d2e --- /dev/null +++ b/tests/DevelopmentTests.js @@ -0,0 +1,52 @@ +/* + * GPII Flow Manager Development Tests + * + * Copyright 2013 OCAD University + * Copyright 2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); +require("./shared/DevelopmentTestDefs.js"); + +gpii.loadTestingSupport(); + +gpii.tests.development.buildTestDefs = function (testDefs) { + return fluid.transform(testDefs, function (testDefIn) { + var testDef = fluid.extend(true, {}, testDefIn, { + config: { + configName: "gpii.config.development.local", + configPath: "%gpii-universal/gpii/configs/shared" + } + }); + + return testDef; + }); +}; + +// With all-in-local config, nonexistent GPII keys are able to key in and key out. +// Note that with untrusted config, nonexistent GPII keys can only key in with client credentials that have privilege +// to create nonexistent GPII keys and prefs safes. +gpii.tests.development.nonexistentKeyInTestDefs = [{ + name: "Flow Manager test: Key in and key out with a nonexistent GPII key", + expect: 2, + gpiiKey: "nonexistent_gpii_key", + sequenceGrade: "gpii.tests.development.commonTestSequence" +}]; + +gpii.test.runCouchTestDefs(gpii.tests.development.buildTestDefs(gpii.tests.development.testDefs)); +gpii.test.runCouchTestDefs(gpii.tests.development.buildTestDefs(gpii.tests.development.nonexistentKeyInTestDefs)); diff --git a/tests/DeviceReporterErrorTests.js b/tests/DeviceReporterErrorTests.js new file mode 100644 index 000000000..76f9e8492 --- /dev/null +++ b/tests/DeviceReporterErrorTests.js @@ -0,0 +1,74 @@ +/* + * GPII Tests to ensure that failing device reporter is handled properly + * + * Copyright 2015 Raising the FLoor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.defaults("gpii.tests.deviceReporterErrorTests.testCaseHolder", { + gradeNames: ["gpii.test.testCaseHolder"], + distributeOptions: { + "development.installedSolutionsPath": { + "record": "%gpii-universal/tests/data/faultyDeviceReport.jsonx", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path" + } + } +}); + +gpii.tests.deviceReporterErrorTests.testDefCommon = { + config: { + configName: "gpii.config.development.local", + configPath: "%gpii-universal/gpii/configs/shared" + }, + gradeNames: "gpii.tests.deviceReporterErrorTests.testCaseHolder" +}; + +gpii.tests.deviceReporterErrorTests.testDefs = [ + { + name: "Login fails on error in Device Reporter and reports to login", + expect: 4, + gpiiKey: "testUser1", + config: { + configName: "gpii.config.development.local", + configPath: "%gpii-universal/gpii/configs/shared" + }, + gradeNames: "gpii.tests.deviceReporterErrorTests.testCaseHolder", + sequence: [{ + func: "{loginRequest}.send" + }, { + event: "{loginRequest}.events.onComplete", + listener: "kettle.test.assertErrorResponse", + args: { + message: "Received 500 error when logging in with corrupt device reporter", + errorTexts: ["Failed to read deviceReporter", "Parse error"], + string: "{arguments}.0", + request: "{loginRequest}" + } + }] + }]; + +gpii.tests.deviceReporterErrorTests.buildAllTestDefs = function () { + return fluid.transform(gpii.tests.deviceReporterErrorTests.testDefs, function (testDef) { + return fluid.extend(true, {}, gpii.tests.deviceReporterErrorTests.testDefCommon, testDef); + }); +}; + +gpii.test.runCouchTestDefs(gpii.tests.deviceReporterErrorTests.testDefs); diff --git a/tests/ErrorTests.js b/tests/ErrorTests.js new file mode 100644 index 000000000..130cc9b05 --- /dev/null +++ b/tests/ErrorTests.js @@ -0,0 +1,290 @@ +/* + * GPII Error Test Definitions + * + * Copyright 2018 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + jqUnit = fluid.require("node-jqunit", require, "jqUnit"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +// These tests execute the login cycle which is expected to terminate with an error, +// as well as notifying the "userErrors" event + +fluid.registerNamespace("gpii.tests.errors"); + +gpii.tests.errors.testLoginResponse = function (data, nativeResponse, method) { + jqUnit.assertNotEquals("HTTP response should be in error", 200, nativeResponse.statusCode); + var parsed = JSON.parse(data); + jqUnit.assertTrue("Response error field is set", parsed.isError); + jqUnit.assertTrue("Response error message is correct", parsed.message.includes( + "Error occurred during login: Exploding settings handler has failed on " + method + " method")); +}; + +gpii.tests.errors.recordError = function (holder, error) { + holder.lastError.push(error.messageKey); +}; + +gpii.tests.errors.recordClear = function (holder) { + holder.cleared = true; +}; + +gpii.tests.errors.checkUserError = function (holder, expected) { + jqUnit.assertValue("User error should have been reported", holder.lastError); + jqUnit.assertDeepEq("User error should be as expected", expected, holder.lastError); +}; + +gpii.tests.errors.checkCleared = function (holder) { + jqUnit.assertTrue("LifecycleManager queue should have been cleared", holder.cleared); +}; + +gpii.tests.errors.preferencesFilter = function (payload, testCaseHolder) { + fluid.set(payload, ["contexts", "gpii-default", "preferences", + "http://registry.gpii.net/applications/net.gpii.explode", "explodeMethod"], testCaseHolder.options.explodeMethod); + return payload; +}; + +fluid.defaults("gpii.tests.errors.mixin", { + members: { + lastError: [], + cleared: false + }, + distributeOptions: { + receiveUserError: { + target: "{testCaseHolder flowManager gpii.userErrors}.options.listeners.userError", + record: { + func: "gpii.tests.errors.recordError", + args: ["{gpii.tests.errors.mixin}", "{arguments}.0"] + } + }, + clearQueue: { + target: "{testCaseHolder lifecycleManager}.options.listeners.onClearActionQueue", + record: { + func: "gpii.tests.errors.recordClear", + args: ["{gpii.tests.errors.mixin}"] + } + }, + preferencesServerAdvisor: { + record: { + namespace: "errorTestsFilter", + priority: "after:encoding", + funcName: "gpii.tests.errors.preferencesFilter", + args: ["{arguments}.0", "{gpii.tests.errors.mixin}"] + }, + target: "{testCaseHolder flowManager prefsServerDataSource preferencesDataSourceImpl}.options.listeners.onRead" + } + }, + listeners: { + "onDestroy.clearExplodingState": "gpii.tests.errors.clearExplodingState" + } +}); + +gpii.tests.errors.clearExplodingState = function () { + gpii.settingsHandlers.exploding.runningState.running = false; +}; + +gpii.tests.errors.expectedError = function (method) { + return "Exploding settings handler has failed on " + method + " method"; +}; + +fluid.defaults("gpii.tests.errors.prepAndFireLoginSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { funcName: "fluid.log", args: ["prep sequence hit"]}, + { + funcName: "kettle.test.pushInstrumentedErrors", + args: "kettle.requestUncaughtExceptionHandler" + }, + { + func: "{loginRequest}.send" + } + ] +}); + +fluid.defaults("gpii.tests.errors.loginWithErrorSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { funcName: "fluid.log", args: ["login with error sequence"]}, + { + event: "{loginRequest}.events.onComplete", + listener: "kettle.test.assertErrorResponse", + args: { + message: "Login response should be in error", + string: "{arguments}.0", + request: "{loginRequest}", + errorTexts: [ + "Error occurred during login:", + "@expand:gpii.tests.errors.expectedError({testCaseHolder}.options.expectedMethodFail)" + ] + } + } + ] +}); + +/** A variant middle sequence applied to gpii.tests.errors.coreTestDef when testing explosion on logout **/ +fluid.defaults("gpii.tests.errors.logoutWithErrorSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { funcName: "fluid.log", args: ["logout with error sequence"]}, + { + event: "{loginRequest}.events.onComplete", + listener: "kettle.test.assertResponse", + args: { + message: "Login should be successful", + string: "{arguments}.0", + plainText: true, + request: "{loginRequest}", + expected: "User with GPII key explodeLaunchHandlerStop was successfully logged in." + } + }, + { + func: "{logoutRequest}.send" + }, + { + event: "{logoutRequest}.events.onComplete", + listener: "kettle.test.assertErrorResponse", + args: { + message: "Logout response should be in error", + string: "{arguments}.0", + request: "{logoutRequest}", + errorTexts: [ + "Error occurred during logout:", + "@expand:gpii.tests.errors.expectedError({testCaseHolder}.options.expectedMethodFail)" + ] + } + } + ] +}); + +fluid.defaults("gpii.tests.errors.checkAndFinishSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { funcName: "fluid.log", args: ["check sequence hit"]}, + { + funcName: "gpii.tests.errors.checkUserError", + args: ["{gpii.tests.errors.mixin}", "{testCaseHolder}.options.expectedError"] + }, + { + funcName: "gpii.tests.errors.checkCleared", + args: ["{gpii.tests.errors.mixin}"] + }, + { + funcName: "kettle.test.popInstrumentedErrors", + args: "kettle.requestUncaughtExceptionHandler" + } + ] +}); + + +fluid.defaults("gpii.tests.errors.loginErrorSequenceGrade", { + gradeNames: ["gpii.test.couchSequenceGrade"], + sequenceElements: { + prep: { + gradeNames: "gpii.tests.errors.prepAndFireLoginSequence", + priority: "after:startServer" + }, + loginWithError: { + gradeNames: "gpii.tests.errors.loginWithErrorSequence", + priority: "after:prep" + }, + check: { + gradeNames: "gpii.tests.errors.checkAndFinishSequence", + priority: "after:loginWithError" + } + } +}); + +fluid.defaults("gpii.tests.errors.logoutErrorSequenceGrade", { + gradeNames: ["gpii.test.couchSequenceGrade"], + sequenceElements: { + prep: { + gradeNames: "gpii.tests.errors.prepAndFireLoginSequence", + priority: "after:startServer" + }, + logoutWithError: { + gradeNames: "gpii.tests.errors.logoutWithErrorSequence", + priority: "after:prep" + }, + check: { + gradeNames: "gpii.tests.errors.checkAndFinishSequence", + priority: "after:logoutWithError" + } + } +}); + +/** The core testDef options used by all tests. The sequence elements are added via a sequenceGrade option by our test runner. **/ +gpii.tests.errors.coreTestDef = { + name: "Flow Manager error tests", + expect: 7, + config: { + configName: "gpii.config.development.local", + configPath: "%gpii-universal/gpii/configs/shared" + }, + sequence: [], + gradeNames: ["gpii.test.integration.testCaseHolder.windows", "gpii.tests.errors.mixin"], + gpiiKey: null, + expectedError: [], + expectedMethodFail: null, + explodeMethod: null +}; + +// TODO: Note that we cannot test explodeSettingsHandlerGet since the core does not currently generate any calls +// to settings handler "Get" methods (except the synthetic one for launch handlers). This will be revisited +// when the capture tool/snapshotter is implemented for GPII-228 + +gpii.tests.errors.testVariants = [{ + gpiiKey: "explodeSettingsHandlerSet", + expectedError: ["WriteSettingFail", "KeyInFail"], + expectedMethodFail: "set" +}, { + gpiiKey: "explodeLaunchHandlerStart", + expectedError: ["StartApplicationFail", "KeyInFail"], + expectedMethodFail: "launch" +}, { + gpiiKey: "explodeLaunchHandlerStop", + expectedError: ["StopApplicationFail", "KeyInFail"], + expectedMethodFail: "stop", + expect: 9 +}]; + +gpii.tests.errors.explodeMethodVariants = ["reject", "throw", "fail"]; + +gpii.tests.errors.buildTestDefs = function (coreTestDef, testVariants, explodeMethodVariants) { + return fluid.flatten(fluid.transform(explodeMethodVariants, function (explodeMethodVariant) { + return fluid.transform(testVariants, function (testVariant) { + var testDef = Object.assign(fluid.copy(coreTestDef), testVariant); + testDef.name += " - GPII key " + testVariant.gpiiKey + ", explode method " + explodeMethodVariant; + testDef.explodeMethod = explodeMethodVariant; + return testDef; + }); + })); +}; + +gpii.tests.errors.testDefs = gpii.tests.errors.buildTestDefs( + gpii.tests.errors.coreTestDef, + gpii.tests.errors.testVariants, + gpii.tests.errors.explodeMethodVariants +); + +gpii.loadTestingSupport(); + +gpii.test.testDefToErrorEnvironment = function (testDef) { + var sequenceGrade = testDef.gpiiKey === "explodeLaunchHandlerStop" ? "gpii.tests.errors.logoutErrorSequenceGrade" : "gpii.tests.errors.loginErrorSequenceGrade"; + return gpii.test.testDefToEnvironment(testDef, "gpii.test.couchEnvironment", sequenceGrade); +}; + +gpii.test.runErrorTestDefs = function (testDefs) { + gpii.test.runTestDefs(testDefs, gpii.test.testDefToErrorEnvironment); +}; + +gpii.test.runErrorTestDefs(gpii.tests.errors.testDefs); diff --git a/tests/IntegrationTests.js b/tests/IntegrationTests.js new file mode 100644 index 000000000..4485abbda --- /dev/null +++ b/tests/IntegrationTests.js @@ -0,0 +1,34 @@ +/** +GPII Integration Tests + +Copyright 2014 Lucendo Development Ltd. + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.contextAware.makeChecks({ + "gpii.contexts.test.integration": { + value: true + } +}); + +fluid.require("%gpii-universal"); +fluid.logObjectRenderChars = 102400; + +gpii.loadTestingSupport(); + +gpii.test.runSuitesWithFiltering(require("./platform/index-windows.js"), __dirname, ["gpii.test.integration.testCaseHolder.windows"]); +gpii.test.runSuitesWithFiltering(require("./platform/index-linux.js"), __dirname, ["gpii.test.integration.testCaseHolder.linux"]); +gpii.test.runSuitesWithFiltering(require("./platform/index-android.js"), __dirname, ["gpii.test.integration.testCaseHolder.android"]); diff --git a/tests/JournalIntegrationTests.js b/tests/JournalIntegrationTests.js new file mode 100644 index 000000000..b0b574ede --- /dev/null +++ b/tests/JournalIntegrationTests.js @@ -0,0 +1,718 @@ +/** +GPII Journal Integration Tests + +Copyright 2016 Raising the Floor - International + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +var jqUnit = require("node-jqunit"); +var kettle = require("kettle"); +kettle.loadTestingSupport(); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.journal"); + +gpii.tests.journal.testSpec = fluid.require("%gpii-universal/tests/platform/windows/windows-builtIn-testSpec.js"); + +// The os_win entry forms the spine of our test. This user has 4 application-specific preferences encoded +// for Windows built-in a11y features - mouse trailing (SPI), high contrast (SPI), large cursors (registry), magnifier (registry) +// our test strategy involves overriding the mock implementation for the large cursors solutions registry entry to instead invoke an +// exploding settings handler +gpii.tests.journal.testDef = gpii.tests.windows.builtIn[0]; + +gpii.tests.journal.initialSettings = { + "gpii.windows.spiSettingsHandler": { + "com.microsoft.windows.mouseTrailing": [{ + "settings": { + "MouseTrails": { + "value": 20 + } + }, + "options": { + "mockSync": true, + "getAction": "SPI_GETMOUSETRAILS", + "setAction": "SPI_SETMOUSETRAILS" + } + }] + }, + "gpii.windows.registrySettingsHandler": { + "com.microsoft.windows.magnifier": [{ // magnifier stuff + "settings": { + "Invert": 1, + "Magnification": 200, + "MagnificationMode": 4, + "FollowFocus": 0, + "FollowCaret": 1, + "FollowMouse": 1, + "FollowNarrator": 1, + "FadeToMagIcon": 0, + "ZoomIncrement": 100, + "UseBitmapSmoothing": 1, + "LensHeight": 30, + "LensWidth": 30 + }, + "options": { + "mockSync": true, + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\ScreenMagnifier", + "dataTypes": { + "Magnification": "REG_DWORD", + "Invert": "REG_DWORD", + "FollowFocus": "REG_DWORD", + "FollowCaret": "REG_DWORD", + "FollowMouse": "REG_DWORD", + "FollowNarrator": "REG_DWORD", + "MagnificationMode": "REG_DWORD", + "FadeToMagIcon": "REG_DWORD", + "ZoomIncrement": "REG_DWORD", + "UseBitmapSmoothing": "REG_DWORD", + "LensHeight": "REG_DWORD", + "LensWidth": "REG_DWORD" + } + } + } + ] + }, + "gpii.windows.displaySettingsHandler": { + "com.microsoft.windows.screenResolution": [{ + "settings": { + "screen-resolution": { + "width": 800, + "height": 600 + }, + "screen-dpi": 1 + } + }] + }, + "gpii.windows.systemSettingsHandler": { + "com.microsoft.windows.nightScreen": [ + { + "settings": { + "SystemSettings_Display_BlueLight_ManualToggleQuickAction": { + "value": false + } + } + } + ], + "com.microsoft.windows.touchPadSettings": [ + { + "settings": { + "SystemSettings_Input_Touch_SetActivationTimeout": { + "value": "Medium sensitivity" + } + } + } + ]/*, + * These settings have been commented out for https://issues.gpii.net/browse/GPII-4462 + "com.microsoft.windows.narrator": [ + { + "settings": { + "SystemSettings_Accessibility_Narrator_IsAutoStartEnabled": { + "value": false + }, + "SystemSettings_Accessibility_Narrator_IsAutoStartOnLogonDesktopEnabled": { + "value": false + } + } + } + ] + */ + }, + "gpii.windows.enableRegisteredAT": { + "com.microsoft.windows.magnifier": [{ + "settings": { + "running": false + }, + "options": { + "registryName": "magnifierpane", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "Magnify.exe" + } + ] + } + }] + }, + "gpii.windows.nativeSettingsHandler": { + "com.microsoft.windows.mouseSettings": [ + { + "settings": { + "DoubleClickTimeConfig": { + "value": 500 + } + }, + "options": { + "functionName": "DoubleClickTime" + } + }, + { + "settings": { + "DoubleClickWidthConfig": { + "value": 4 + } + }, + "options": { + "functionName": "DoubleClickWidth" + } + }, + { + "settings": { + "DoubleClickHeightConfig": { + "value": 4 + } + }, + "options": { + "functionName": "DoubleClickHeight" + } + } + // TODO: Disabled due to GPII-4518 + //{ + // "settings": { + // "Volume": { + // "value": 0.5 + // } + // }, + // "options": { + // "functionName": "Volume" + // } + //} + ] + } +}; + +gpii.tests.journal.settingsAfterCrash = { + "gpii.windows.enableRegisteredAT": { + "com.microsoft.windows.magnifier": [{ + "settings": { + "running": false + }, + "options": { + "registryName": "magnifierpane", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "Magnify.exe" + } + ] + } + }] + } +}; + +fluid.defaults("gpii.tests.journal.solutionsRegistryAdvisor", { + gradeNames: "fluid.modelComponent", + model: { + throwOnNext: false, + explodeOnNext: true + }, + distributeOptions: { + "tests.journal.solutionsRegistry": { + record: { + namespace: "journalFilter", + priority: "after:encoding", + funcName: "gpii.tests.journal.solutionsRegistryFilter" + }, + target: "{testCaseHolder flowManager solutionsRegistryDataSource}.options.listeners.onRead" + } + } +}); + +fluid.defaults("gpii.tests.journal.listJournalsRequestComponent", { + gradeNames: "kettle.test.request.http", + path: "/journal/journals.html" +}); + +// A base holder which doesn't harbour the exploding settings handler. Derived from the standard integration +// testCaseHolder in testing/src/Integration.js. Defines new requests and overrides of the solutions registry and mock +// settings handlers. +fluid.defaults("gpii.tests.journal.baseTestCaseHolder", { + gradeNames: ["gpii.test.integration.testCaseHolder.windows"], + gpiiKey: gpii.tests.journal.testDef.gpiiKey, + settingsHandlers: gpii.tests.journal.testDef.settingsHandlers, + config: { + configName: gpii.tests.journal.testSpec.configName, + configPath: gpii.tests.journal.testSpec.configPath + }, + distributeOptions: { + "tests.journal.fastSPI": { + record: { + "gpii.windows.spiSettingsHandler": { + delaying: false // defeat this annoying built-in mock behaviour so we can run these tests faster + } + }, + target: "{that mockSettingsHandlers}.options.settingsHandlers" + } + }, + components: { + restoreJournalRequest: { + type: "kettle.test.request.http", + options: { + path: "/journal/restore/%journalId" + } + }, + listJournalsRequest: { + type: "gpii.tests.journal.listJournalsRequestComponent" + }, + loginRequest2: { + type: "gpii.test.common.loginRequestComponent" + }, + logoutRequest2: { + type: "gpii.test.common.logoutRequestComponent" + }, + listJournalsRequest2: { + type: "gpii.tests.journal.listJournalsRequestComponent" + } + } +}); + +fluid.defaults("gpii.tests.journal.testCaseHolder", { + gradeNames: ["gpii.tests.journal.baseTestCaseHolder"], + events: { + FLUID5931wait: null, + onInitialSettingsComplete: null + }, + components: { + solutionsRegistryAdvisor: { + type: "gpii.tests.journal.solutionsRegistryAdvisor" + }, + // Override this component from gpii.test.integration.testCaseHolder to include the exploding handler + mockSettingsHandlers: { + type: "gpii.tests.integration.mockSettingsHandlerRegistry.journal" + } + } +}); + +// Used on the first run where the settings handler crashes the whole system +gpii.tests.journal.solutionsRegistryOverlay = { + win32: { + "com.microsoft.windows.cursors": { + settingsHandlers: { + explode: { + type: "gpii.tests.journal.explodingSettingsHandler", + supportedSettings: { + AppStarting: {} + }, + capabilitiesTransformations: { + AppStarting: { + transform: { + type: "fluid.transforms.value", + inputPath: "http://registry\\.gpii\\.net/common/cursorSize", + outputPath: "value" + } + } + } + }, + maybeThrow: { + type: "gpii.tests.journal.throwingSettingsHandler", + supportedSettings: { + AppStarting: {} + }, + capabilitiesTransformations: { + AppStarting: { + transform: { + type: "fluid.transforms.value", + inputPath: "http://registry\\.gpii\\.net/common/cursorSize", + outputPath: "value" + } + } + } + }, + configure: { + supportedSettings: { + AppStarting: {} + } + } + }, + restore: ["settings.maybeThrow", "settings.configure"], + // It's necessary to execute settings.maybeThrow otherwise its entry does not appear in the snapshotted solutions + // registry block entered into the journal, and hence it cannot be found on the next turn + configure: ["settings.configure", "settings.maybeThrow", "settings.explode"] + } + } +}; + +gpii.tests.journal.solutionsRegistryFilter = function (payload) { + return fluid.extend(true, {}, payload, gpii.tests.journal.solutionsRegistryOverlay); +}; + +fluid.defaults("gpii.tests.integration.mockSettingsHandlerRegistry.journal", { + gradeNames: "gpii.test.integration.mockSettingsHandlerRegistry.windows", + components: { + explodingSettingsHandler: { + type: "gpii.tests.journal.explodingSettingsHandler" + }, + throwingSettingsHandler: { + type: "gpii.tests.journal.throwingSettingsHandler" + } + }, + listeners: { + "onCreate.populateMocks": "gpii.tests.journal.populateMockSettingsHandlers" + } +}); + +// A settings handler with its "set" action overridden to have a globally destructive side-effect - +// the entire Kettle server hosting the FlowManager, etc. will be destroyed +fluid.defaults("gpii.tests.journal.explodingSettingsHandler", { + gradeNames: "gpii.test.integration.mockSettingsHandler", + invokers: { + set: { + funcName: "gpii.tests.journal.settingsHandlerExplode", + args: ["{gpii.test.configuration}", "{gpii.tests.journal.solutionsRegistryAdvisor}", "{that}", "{arguments}.0"] + } + } +}); + +// A settings handler which synchronously throws an Error during its "set" action +fluid.defaults("gpii.tests.journal.throwingSettingsHandler", { + gradeNames: "gpii.test.integration.mockSettingsHandler", + invokers: { + set: { + funcName: "gpii.tests.journal.settingsHandlerThrow", + args: ["{gpii.tests.journal.solutionsRegistryAdvisor}", "{that}", "{arguments}.0"] + } + } +}); + +// Reach upwards into the global configuration's server and destroy it +gpii.tests.journal.settingsHandlerExplode = function (configuration, advisor, that, payload) { + fluid.log("EXPLODING SETTINGS HANDLER EXECUTING with id " + that.id + " and payload ", payload); + if (advisor.model.explodeOnNext) { + fluid.invokeLater(function () { // invoke later so that we do not race with construction - TODO: framework bug + configuration.server.destroy(); + fluid.log("Server destroyed"); + }); + advisor.applier.change("explodeOnNext", false); + } + // null out our payload, compensate for GPII-1223/GPII-1891 + payload["com.microsoft.windows.cursors"][0].settings = {}; + // Beat jqUnit's failure handler to ignore the various errors falling out from this process + // kettle.test.pushInstrumentedErrors(function () { + // fluid.log("Received expected failure from destroying active server: ", arguments); + //}); + // TODO: failure of Ungarism here - invokers need to be eliminated from the framework + return gpii.settingsHandlers.invokeSettingsHandler(that.setImpl, payload); +}; + +gpii.tests.journal.settingsHandlerThrow = function (advisor, that, payload) { + if (advisor.model.throwOnNext) { + advisor.applier.change("throwOnNext", false); + throw new Error("Failure triggered during settings handler action"); + }; + payload["com.microsoft.windows.cursors"][0].settings = {}; + return gpii.settingsHandlers.invokeSettingsHandler(that.setImpl, payload); +}; + +gpii.tests.journal.populateMockSettingsHandlers = function (that) { + gpii.test.integration.mockSettingsHandlerRegistry.populateOne(that, that.explodingSettingsHandler, + "gpii.tests.journal.explodingSettingsHandler"); + gpii.test.integration.mockSettingsHandlerRegistry.populateOne(that, that.throwingSettingsHandler, + "gpii.tests.journal.throwingSettingsHandler"); +}; + +gpii.tests.journal.logDestroyed = function (loginRequest, testCaseHolder) { + fluid.log("Received expected server destroyed event"); + // Remove KettleTestUtils.http native handler to this which will trigger a test failure + loginRequest.nativeRequest.removeAllListeners("error"); + loginRequest.nativeRequest.on("error", function (err) { + fluid.log("Received expected hangup error from login request", err); + }); + jqUnit.assert("Reached checkpoint for destruction of server"); + var mocks = fluid.queryIoCSelector(fluid.rootComponent, "gpii.test.integration.mockSettingsHandler", true); + fluid.each(mocks, function (mock) { + mock.settingsStore = {}; + }); + fluid.log("CLEARED " + mocks.length + " mock settingsHandlers"); + // Arbitrarily wait as a result of FLUID5931 bug preventing immediate reconstruction of the server + fluid.invokeLater(testCaseHolder.events.FLUID5931wait.fire); +}; + +gpii.tests.journal.stashJournalId = function (component) { + component.stashedStartTime = Date.now(); + component.stashedJournalId = ">" + new Date(component.stashedStartTime).toISOString(); +}; + +gpii.tests.journal.checkJournalsList = function (markup, component, expectCrashed) { + console.log("Got markup of " + markup); + var match = /a href=".*\/%3E(.*)"/.exec(markup); + console.log("Got " + match.length + " matches"); + var firstDate = decodeURIComponent(match[1]); + // Relaxed requirements on GPII-2522 due to lack of control over CI environment - see https://github.com/GPII/gpii-app/pull/19 + var expectedStart = component.stashedStartTime, expectedEnd = Date.now(); + var firstTime = Date.parse(firstDate); + // Add further diagnostics for GPII-2522 + fluid.log("Parsed link date " + firstDate + " to time " + firstTime + " expected to be in range [" + + expectedStart + ", " + expectedEnd + "]"); + jqUnit.assertTrue("Received correct journal time in journal list markup", + firstTime >= expectedStart && firstTime <= expectedEnd); + // See: http://stackoverflow.com/questions/1979884/how-to-use-javascript-regex-over-multiple-lines + var snapshots = markup.match(/

                            ([\s\S]*?)<\/p>/g); + fluid.log("Acquired " + snapshots.length + " snapshots"); + var isCrashed = snapshots[0].indexOf("crashed session") !== -1; + jqUnit.assertEquals("First snapshot has expected crashed status", expectCrashed, isCrashed); +}; + +// Stashes the special members used by gpii.test.checkConfiguration into the testCaseHolder +gpii.tests.journal.stashInitial = function (settingsHandlersPayload, settingsStore, testCaseHolder) { + // Like the effect of gpii.test.snapshotSettings + settingsStore.orig = fluid.transform(settingsHandlersPayload, gpii.settingsHandlers.extractSettingsBlocks); + // Like the effect of gpii.test.expandSettings with 2 blocks missing + var settingsHandlers = fluid.copy(testCaseHolder.options.settingsHandlers); + // We eliminate the last blocks since our initial settings state does not include them, and the blocks + // with values all `undefined` will confuse jqUnit.assertDeepEq in gpii.test.checkConfiguration + settingsHandlers["gpii.windows.spiSettingsHandler"] = fluid.filterKeys(settingsHandlers["gpii.windows.spiSettingsHandler"], "com.microsoft.windows.mouseTrailing"); + settingsHandlers["gpii.windows.registrySettingsHandler"] = fluid.filterKeys(settingsHandlers["gpii.windows.registrySettingsHandler"], "com.microsoft.windows.magnifier"); + testCaseHolder.settingsHandlers = settingsHandlers; +}; + +fluid.defaults("gpii.tests.journal.normalLoginSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { func: "gpii.tests.journal.stashJournalId", + args: "{testCaseHolder}" + }, + { + func: "{loginRequest2}.send" + }, + { + event: "{loginRequest2}.events.onComplete", + listener: "gpii.test.loginRequestListen" + }, + { // TODO: As well as FLUID-5903, implement "parameterised grades" of some kind + func: "{logoutRequest2}.send" + }, + { + event: "{logoutRequest2}.events.onComplete", + listener: "gpii.test.logoutRequestListen" + }, + { + func: "{listJournalsRequest2}.send" + }, + { + event: "{listJournalsRequest2}.events.onComplete", + listener: "gpii.tests.journal.checkJournalsList", + args: ["{arguments}.0", "{testCaseHolder}", false] + } + ] +}); + +fluid.defaults("gpii.tests.journal.stashInitialJournalSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { func: "gpii.tests.journal.stashJournalId", + args: "{testCaseHolder}" + }, + { + func: "gpii.test.setSettings", + args: [gpii.tests.journal.initialSettings, "{nameResolver}", "{testCaseHolder}.events.onInitialSettingsComplete.fire"] + }, + { + event: "{tests}.events.onInitialSettingsComplete", + listener: "fluid.identity" + }, + { + func: "gpii.tests.journal.stashInitial", + args: [gpii.tests.journal.initialSettings, "{testCaseHolder}.settingsStore", "{testCaseHolder}"] + }, + { + func: "{loginRequest}.send" + }, + { // As a result of the exploding settings handler, the attempt to login will destroy the server + event: "{configuration}.server.events.afterDestroy", + listener: "gpii.tests.journal.logDestroyed", + args: ["{loginRequest}", "{testCaseHolder}"] + }, + { + event: "{testCaseHolder}.events.FLUID5931wait", + listener: "fluid.identity" + } + ] +}); + +fluid.defaults("gpii.tests.journal.journalRestorationSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { + func: "gpii.test.setSettings", + args: [gpii.tests.journal.settingsAfterCrash, "{nameResolver}", "{testCaseHolder}.events.onInitialSettingsComplete.fire"] + }, + { + event: "{tests}.events.onInitialSettingsComplete", + listener: "fluid.identity" + }, + { + func: "{listJournalsRequest}.send" + }, + { + event: "{listJournalsRequest}.events.onComplete", + listener: "gpii.tests.journal.checkJournalsList", + args: ["{arguments}.0", "{testCaseHolder}", true] + }, + { + func: "{gpii.tests.journal.solutionsRegistryAdvisor}.applier.change", + args: ["throwOnNext", true] + }, + { + func: "{restoreJournalRequest}.send", + args: [null, { + termMap: { + journalId: "{testCaseHolder}.stashedJournalId" + } + }] + }, + { + event: "{restoreJournalRequest}.events.onComplete", + listener: "gpii.test.assertJSONResponseSubset", + args: [{ + message: "Successful response from journal restoration", + string: "{arguments}.0", + request: "{restoreJournalRequest}", + expected: { + message: "The system's settings were restored from a snapshot" + } + }] + } + ] +}); + +fluid.defaults("gpii.tests.journal.goodSequenceGrade", { + gradeNames: ["gpii.test.couchSequenceGrade"], + sequenceElements: { + stashInitial: { + priority: "after:startCouch", + gradeNames: "gpii.tests.journal.stashInitialJournalSequence" + }, + restartServer: { + gradeNames: "gpii.test.startServerSequence", + priority: "after:stashInitial" + }, + journalRestore: { + priority: "after:restartServer", + gradeNames: "gpii.tests.journal.journalRestorationSequence" + }, + checkSequence: { + priority: "after:journalRestore", + gradeNames: "gpii.test.checkSequence" + }, + normalLogin: { + priority: "after:checkSequence", + gradeNames: "gpii.tests.journal.normalLoginSequence" + } + } +}); + +fluid.defaults("gpii.tests.journal.goodCaseHolder", { + gradeNames: ["gpii.tests.journal.testCaseHolder"], + modules: [{ + name: "Journal Restoration Tests", + tests: [{ + name: "Journal state and restoration", + expect: 11, + sequenceGrade: "gpii.tests.journal.goodSequenceGrade", + sequence: [] + }] + }] +}); + +// A little stub that emulates some of the testdef unpacking that previously would have been handled by +// `kettle.test.bootstrapServer`. +fluid.defaults("gpii.tests.journal.baseEnvironment", { + gradeNames: ["gpii.test.couchEnvironment"], + configurationName: "@expand:kettle.config.createDefaults({that}.tests.options.config)" +}); + +fluid.defaults("gpii.tests.journal.goodEnvironment", { + gradeNames: ["gpii.tests.journal.baseEnvironment"], + configurationName: "@expand:kettle.config.createDefaults({that}.tests.options.config)", + components: { + tests: { + type: "gpii.tests.journal.goodCaseHolder" + } + } +}); + +fluid.defaults("gpii.tests.journal.badJournalRestorationSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { + funcName: "kettle.test.pushInstrumentedErrors", + args: "kettle.requestUncaughtExceptionHandler" + }, + { + func: "{restoreJournalRequest}.send", + args: [null, { + termMap: { + journalId: "Invalid journal ID" + } + }] + }, + { + event: "{restoreJournalRequest}.events.onComplete", + listener: "kettle.test.assertErrorResponse", + args: [{ + message: "Received 500 error when asking for bad journal file", + errorTexts: "The only supported formats for journalId are", + statusCode: 500, + string: "{arguments}.0", + request: "{restoreJournalRequest}" + }] + }, + { + func: "kettle.test.popInstrumentedErrors" + } + ] +}); + +fluid.defaults("gpii.tests.journal.badSequenceGrade", { + gradeNames: ["gpii.test.couchSequenceGrade"], + sequenceElements: { + badJournalRestore: { + priority: "after:startCouch", + gradeNames: "gpii.tests.journal.badJournalRestorationSequence" + }, + normalLogin: { + priority: "after:badJournalRestore", + gradeNames: "gpii.tests.journal.normalLoginSequence" + } + } +}); + +fluid.defaults("gpii.tests.journal.badCaseHolder", { + gradeNames: ["gpii.tests.journal.baseTestCaseHolder"], + modules: [{ + name: "Bad Journal Restoration Tests", + tests:[{ + name: "Restoration of bad journal file doesn't jam system", + expect: 7, + sequenceGrade: "gpii.tests.journal.badSequenceGrade", + sequence: [] + }] + }] +}); + +fluid.defaults("gpii.tests.journal.badEnvironment", { + gradeNames: ["gpii.tests.journal.baseEnvironment"], + components: { + tests: { + type: "gpii.tests.journal.badCaseHolder" + } + } +}); + +fluid.test.runTests([ + "gpii.tests.journal.goodEnvironment", + "gpii.tests.journal.badEnvironment" +]); diff --git a/tests/MultiSettingsHandlerTests.js b/tests/MultiSettingsHandlerTests.js new file mode 100644 index 000000000..27fa1d842 --- /dev/null +++ b/tests/MultiSettingsHandlerTests.js @@ -0,0 +1,68 @@ +/* + * GPII Tests for supporting multiple settings handlers + * + * This is tested here, as the support for this is spread over multiple components and hence only + * the individual parts of the process can be tested via unit tests. The tests here are integration + * style tests ensuring that the expected settings are set on a user login, etc. + * + * Copyright 2016 Raising The Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.multiSHSupport"); + +gpii.tests.multiSHSupport.testDefs = [ + { + name: "Multiple settings handlers with common terms only", + gpiiKey: "multiSHCommonOnly", + settingsHandlers: { + "gpii.settingsHandlers.JSONSettingsHandler": { + "some.app.id": [ + { + "settings": { + "tracking": [ + "mouse", + "caret" + ], + "magnification": 1.5 + }, + "options": { + "filename": "/tmp/fakemag1.settings.json" + } + }, { // high contrast settings + "settings": { + "crazyColor": true + }, + "options": { + "filename": "/tmp/fakemag2.settings.json" + } + } + ] + } + } + } +]; + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.multiSHSupport.testDefs", + configName: "gpii.tests.multiSH.config", + configPath: "%gpii-universal/tests/configs" +}, ["gpii.test.integration.testCaseHolder.windows"], + module, require, gpii.test.runSnapshotTestDefs); diff --git a/tests/PSPIntegrationTests.js b/tests/PSPIntegrationTests.js new file mode 100644 index 000000000..a499cd59d --- /dev/null +++ b/tests/PSPIntegrationTests.js @@ -0,0 +1,49 @@ +/** +GPII PSP Integration Tests + +Copyright 2017 Raising the Floor - International + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); +require("./shared/PSPIntegrationTestDefs.js"); + +gpii.tests.pspIntegration.buildTestDefs = function (testDefs) { + return fluid.transform(testDefs, function (testDef) { + return fluid.extend(true, { + gradeNames: [ + "gpii.tests.pspIntegration.testCaseHolder.common.linux", + "gpii.test.common.lifecycleManagerReceiver" + ], + config: { + configName: "gpii.tests.acceptance.pspIntegration.config", + configPath: "%gpii-universal/tests/configs" + }, + distributeOptions: { + "acceptance.defaultSettings": { + "record": { + args: testDef.defaultSettings + }, + "target": "{that defaultSettingsLoader}.options.invokers.get" + } + } + }, testDef); + }); +}; +var builtApplySettingsTestDefs = gpii.tests.pspIntegration.buildTestDefs(gpii.tests.pspIntegration.applyPrefsTestDefs); +gpii.test.runCouchTestDefs(builtApplySettingsTestDefs); + +var builtReadSettingsTestDefs = gpii.tests.pspIntegration.buildTestDefs(gpii.tests.pspIntegration.readPrefsTestDefs); +gpii.test.runCouchTestDefs(builtReadSettingsTestDefs); diff --git a/tests/PayloadSizeTest.js b/tests/PayloadSizeTest.js new file mode 100644 index 000000000..73a70978c --- /dev/null +++ b/tests/PayloadSizeTest.js @@ -0,0 +1,56 @@ +/* + * GPII-1880 Test Definitions + * + * Copyright 2017 Raising The Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + jqUnit = fluid.require("node-jqunit", require, "jqUnit"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.payloadSize"); + +gpii.tests.payloadSize.testLoginResponse = function (data) { + jqUnit.assertEquals("Response is correct", "User with GPII key giant was successfully logged in.", data); +}; + +gpii.tests.payloadSize.testLogoutResponse = function (data) { + jqUnit.assertEquals("Response is correct", "User with GPII key giant was successfully logged out.", data); +}; + +gpii.tests.payloadSize.testDefs = [{ + name: "Payload size limit tests > 100kb", + expect: 2, + config: { + configName: "gpii.config.development.local", + configPath: "%gpii-universal/gpii/configs/shared" + }, + gradeNames: ["gpii.test.testCaseHolder"], + gpiiKey: "giant", + + sequence: [{ + func: "{loginRequest}.send" + }, { + event: "{loginRequest}.events.onComplete", + listener: "gpii.tests.payloadSize.testLoginResponse" + }, { + func: "{logoutRequest}.send" + }, { + event: "{logoutRequest}.events.onComplete", + listener: "gpii.tests.payloadSize.testLogoutResponse" + }] +}]; + +gpii.test.runCouchTestDefs(gpii.tests.payloadSize.testDefs); diff --git a/tests/ResetAtStartTests.js b/tests/ResetAtStartTests.js new file mode 100644 index 000000000..a7f968c45 --- /dev/null +++ b/tests/ResetAtStartTests.js @@ -0,0 +1,23 @@ +/* + * Reset on System Startup Tests with All in Local Config + * + * Copyright 2018 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +require("./shared/ResetDefaultSettingsTestDefs.js"); + +gpii.test.runCouchTestDefs(gpii.tests.resetDefaultSettings.buildTestDefs(gpii.tests.resetDefaultSettings.resetAtStartTestCases, { + configName: "gpii.tests.acceptance.resetAtStart.config", + configPath: "%gpii-universal/tests/configs" +})); diff --git a/tests/SuppressHttpEndpointsTests.js b/tests/SuppressHttpEndpointsTests.js new file mode 100644 index 000000000..37dd61c8e --- /dev/null +++ b/tests/SuppressHttpEndpointsTests.js @@ -0,0 +1,23 @@ +/* + * Suppress HTTP Endpoints Tests + * + * Copyright 2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +require("./shared/SuppressHttpEndpointsTestDefs.js"); + +gpii.test.runCouchTestDefs(gpii.tests.suppressHttpEndpoints.buildTestDefs({ + configName: "gpii.tests.acceptance.suppressHttpEndpoints.config.json5", + configPath: "%gpii-universal/tests/configs" +})); diff --git a/tests/UntrustedBrowserChannelTests.js b/tests/UntrustedBrowserChannelTests.js new file mode 100644 index 000000000..3d81b987e --- /dev/null +++ b/tests/UntrustedBrowserChannelTests.js @@ -0,0 +1,37 @@ +/** + * GPII Untrusted Flow Manager Browser Channel Tests + * + * Copyright 2013 OCAD University + * Copyright 2015 Emergya + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.require("%flowManager/test/shared/BrowserChannelTestDefs.js"); + +fluid.registerNamespace("gpii.tests.untrusted.flowManager.browserChannel"); + +gpii.tests.untrusted.flowManager.browserChannel.testDefs = fluid.transform(gpii.tests.flowManager.browserChannel.testDefs, function (testDefIn) { + var testDef = fluid.extend(true, {}, testDefIn, { + config: { + configName: "gpii.tests.acceptance.untrusted.browserChannel.config", + configPath: "%gpii-universal/tests/configs" + } + }); + return testDef; +}); + +gpii.test.runCouchTestDefs(gpii.tests.untrusted.flowManager.browserChannel.testDefs); diff --git a/tests/UntrustedDevelopmentTests.js b/tests/UntrustedDevelopmentTests.js new file mode 100644 index 000000000..f6f87f220 --- /dev/null +++ b/tests/UntrustedDevelopmentTests.js @@ -0,0 +1,82 @@ +/* + * GPII Untrusted Flow Manager Development Tests + * + * Copyright 2015 OCAD University + * Copyright 2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); +require("./shared/DevelopmentTestDefs.js"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.untrusted.development"); + +gpii.tests.untrusted.development.buildTestDefs = function (testDefs) { + return fluid.transform(testDefs, function (testDefIn) { + var testDef = fluid.extend(true, {}, testDefIn, { + config: { + configName: "gpii.config.untrusted.development.local", + configPath: "%gpii-universal/gpii/configs/shared" + } + }); + + return testDef; + }); +}; + +// With untrusted config, nonexistent GPII keys can only key in with client credentials that have privilege +// to create nonexistent GPII keys and prefs safes. +// Note that with all-in-local config, nonexistent GPII keys are able to key in and key out. +gpii.tests.development.nonexistentKeyInWithPrivTestDefs = [{ + name: "Flow Manager test: Key in and key out with a nonexistent GPII key (with privileges)", + expect: 2, + gpiiKey: "nonexistent_gpii_key", + distributeOptions: { + "test.clientCredentialFilePath": { + "record": "%gpii-universal/tests/data/clientCredentials/nova.json", + "target": "{that gpii.flowManager.untrusted settingsDataSource}.options.clientCredentialFilePath", + priority: "after:flowManager.clientCredentialFilePath" + } + }, + sequenceGrade: "gpii.tests.development.commonTestSequence" +}]; + +gpii.tests.development.nonexistentKeyInWithoutPrivTestDefs = [{ + name: "Flow Manager test: Key in and key out with a nonexistent GPII key (without privileges)", + expect: 3, + gpiiKey: "nonexistent_gpii_key", + gradeNames: ["gpii.test.testCaseHolder"], + sequence: [{ + // login with a nonexistent GPII key + func: "{loginRequest}.send" + }, { + event: "{loginRequest}.events.onComplete", + listener: "kettle.test.assertErrorResponse", + args: { + errorTexts: "server_error while executing HTTP POST on url", + statusCode: 401, + string: "{arguments}.0", + request: "{loginRequest}" + } + }] +}]; + +gpii.test.runCouchTestDefs(gpii.tests.untrusted.development.buildTestDefs(gpii.tests.development.testDefs)); +gpii.test.runCouchTestDefs(gpii.tests.untrusted.development.buildTestDefs(gpii.tests.development.nonexistentKeyInWithPrivTestDefs)); +gpii.test.runCouchTestDefs(gpii.tests.untrusted.development.buildTestDefs(gpii.tests.development.nonexistentKeyInWithoutPrivTestDefs)); diff --git a/tests/UntrustedPSPIntegrationTests.js b/tests/UntrustedPSPIntegrationTests.js new file mode 100644 index 000000000..29da5bf92 --- /dev/null +++ b/tests/UntrustedPSPIntegrationTests.js @@ -0,0 +1,367 @@ +/* + * GPII Untrusted PSP Integration Tests + * + * Copyright 2017 Raising the floor - international + * Copyright 2018 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + jqUnit = fluid.registerNamespace("jqUnit"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); +require("./shared/PSPIntegrationTestDefs.js"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.untrusted.pspIntegration"); + +// The test preferences server that adds an endpoint for verifying raw preferences on the cloud +fluid.defaults("gpii.tests.untrusted.pspIntegration.preferencesServer", { + gradeNames: ["gpii.preferencesServer"], + requestHandlers: { + rawPreferencesGet: { + route: "/rawPreferences/:gpiiKey", + method: "get", + type: "gpii.tests.untrusted.pspIntegration.preferencesServer.rawGet.handler" + } + } +}); + +fluid.defaults("gpii.tests.untrusted.pspIntegration.preferencesServer.rawGet.handler", { + gradeNames: ["kettle.request.http"], + invokers: { + handleRequest: { + funcName: "gpii.tests.untrusted.pspIntegration.preferencesServer.rawGet.handler.getRawPreferences", + args: ["{preferencesServer}", "{that}", "{that}.req.params.gpiiKey"] + } + } +}); + +gpii.tests.untrusted.pspIntegration.preferencesServer.rawGet.handler.getRawPreferences = function (preferencesServer, request, gpiiKey) { + var prefsPromise = preferencesServer.preferencesService.getPreferencesByGpiiKey(gpiiKey); + prefsPromise.then(request.events.onSuccess.fire, request.events.onError.fire); +}; + +// Request component for retrieving raw preferences from the cloud +fluid.defaults("gpii.test.untrusted.pspIntegration.rawPrefsRequest", { // named oddly to avoid name conflicts with component whose member name is `loginRequest` + gradeNames: "kettle.test.request.http", + port: 8084, + path: "/rawPreferences/%gpiiKey", + termMap: { + gpiiKey: "{testCaseHolder}.options.gpiiKey" + } +}); + +// Add raw preferences request components to testCaseHolder +fluid.defaults("gpii.tests.untrusted.pspIntegration.testCaseHolder", { + gradeNames: [ + "gpii.tests.pspIntegration.testCaseHolder.common.linux" + ], + components: { + rawPrefsAtStart: { + type: "gpii.test.untrusted.pspIntegration.rawPrefsRequest" + }, + rawPrefsAtEnd: { + type: "gpii.test.untrusted.pspIntegration.rawPrefsRequest" + }, + rawPrefsAfterAutoSave: { + type: "gpii.test.untrusted.pspIntegration.rawPrefsRequest" + } + } +}); + +// The expected preferences change after running each test sequence. The array indexes maps the array indexes of test sequences. +gpii.test.untrusted.pspIntegration.expectedPrefsChange = [ + // 0 + { + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/magnification": 3 + } + } + } + } + }, + // 1 + { + }, + // 2 + { + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier": { + "mag-factor": 3 + } + } + } + } + } + }, + // 3 + { + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/magnification": 3, + "http://registry.gpii.net/common/volume": 0.75 + } + } + } + } + }, + // 4 + { + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/magnification": 1.5, + "http://registry.gpii.net/common/volume": 0.5, + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier": { + "mag-factor": 3 + } + } + } + } + } + }, + // 5 + { + }, + // 6 + { + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/magnification": 3 + } + } + } + } + } +]; + +// The sequence to be added to the start of each test sequence to verify the initial raw preferences +gpii.test.untrusted.pspIntegration.startSequence = fluid.freezeRecursive([ + { + func: "{rawPrefsAtStart}.send" + }, { + event: "{rawPrefsAtStart}.events.onComplete", + listener: "gpii.test.untrusted.pspIntegration.verifyRawPrefsAtStart", + args: ["{that}", "{arguments}.0"] + } +]); + +// The sequence to be appended to the end of each test sequence to verify the updated raw preferences +gpii.test.untrusted.pspIntegration.endSequence = [ + { + func: "{rawPrefsAtEnd}.send" + }, { + event: "{rawPrefsAtEnd}.events.onComplete", + listener: "gpii.test.untrusted.pspIntegration.verifyRawPrefsAtEnd" + // "i" needs to be dynamically provided at constructing testDefs with the current sequence number to + // match the expected prefs change for the current sequence from gpii.test.untrusted.pspIntegration.expectedPrefsChange[] + // args: ["{that}", "{arguments}.0", i] + } +]; + +gpii.test.untrusted.pspIntegration.verifyRawPrefsAtStart = function (that, preferences) { + that.options.initialPrefs = JSON.parse(preferences); + jqUnit.assertValue("The initial preferences has been received", preferences); +}; + +gpii.test.untrusted.pspIntegration.verifyRawPrefsAtEnd = function (that, preferences, sequenceNum, expectedChange) { + fluid.log(that.options.name, ", number ", sequenceNum, " in the sequence"); + var expectedPrefsChange = expectedChange ? expectedChange : gpii.test.untrusted.pspIntegration.expectedPrefsChange[sequenceNum]; + + var expected = fluid.extend(true, {}, that.options.initialPrefs, expectedPrefsChange); + jqUnit.assertDeepEq("The updated preferences have been saved to the cloud", expected, JSON.parse(preferences)); +}; + +gpii.test.untrusted.pspIntegration.verifyRawPrefsAfterAutoSave = function (that, preferences) { + jqUnit.assertDeepEq("The preferences not in \"autosave\" metadata are not auto saved", that.options.initialPrefs, JSON.parse(preferences)); +}; + +gpii.tests.pspIntegration.saveTestDefs = [ + { + name: "Auto save and explicit save", + expect: 13, + sequence: [ + { + func: "{rawPrefsAtStart}.send" + }, { + event: "{rawPrefsAtStart}.events.onComplete", + listener: "gpii.test.untrusted.pspIntegration.verifyRawPrefsAtStart", + args: ["{that}", "{arguments}.0"] + }, { + func: "gpii.test.expandSettings", + args: [ "{tests}", [ "contexts" ]] + }, { + func: "gpii.test.snapshotSettings", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{tests}.settingsStore", "{nameResolver}", "{tests}.events.onSnapshotComplete.fire"] + }, { + event: "{tests}.events.onSnapshotComplete", + listener: "fluid.identity" + }, { + func: "{loginRequest}.send" + }, { + event: "{loginRequest}.events.onComplete", + listener: "gpii.test.loginRequestListen" + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + func: "{pspClient}.connect" + }, { + event: "{pspClient}.events.onConnect", + listener: "gpii.tests.pspIntegration.connectionSucceeded" + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.settingsHandlers.afterConnect] + }, { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "modelChanged", { + settingControls: { + "http://registry\\.gpii\\.net/common/pitch": { + value: 0.8 + } + } + }] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.settingsHandlers.afterChangePitch] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "preferencesApplied"] + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + func: "{rawPrefsAfterAutoSave}.send" + }, { + event: "{rawPrefsAfterAutoSave}.events.onComplete", + listener: "gpii.test.untrusted.pspIntegration.verifyRawPrefsAfterAutoSave", + args: ["{that}", "{arguments}.0"] + }, { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "modelChanged", { + saveButtonClickCount: 1 + }] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "preferencesApplied"] + }, { + func: "{rawPrefsAtEnd}.send" + }, { + event: "{rawPrefsAtEnd}.events.onComplete", + listener: "gpii.test.untrusted.pspIntegration.verifyRawPrefsAtEnd", + args: ["{that}", "{arguments}.0", null, { + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/pitch": 0.8 + } + } + } + } + }] + }, { + func: "{logoutRequest}.send" + }, { + event: "{logoutRequest}.events.onComplete", + listener: "gpii.test.logoutRequestListen" + }, { + func: "gpii.test.checkRestoredConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{tests}.settingsStore", "{nameResolver}", "{tests}.events.onCheckRestoredConfigurationComplete.fire"] + }, { + event: "{tests}.events.onCheckRestoredConfigurationComplete", + listener: "fluid.identity" + } + ] + } +]; + +gpii.tests.untrusted.pspIntegration.addConfig = function (testDefIn) { + return fluid.extend(true, {}, testDefIn, { + config: { + configName: "gpii.tests.acceptance.untrusted.pspIntegration.config.json", + configPath: "%gpii-universal/tests/configs" + }, + gradeNames: [ + "gpii.tests.untrusted.pspIntegration.testCaseHolder", + "gpii.test.common.lifecycleManagerReceiver" + ], + distributeOptions: { + "acceptance.defaultSettings": { + "record": { + args: testDefIn.defaultSettings + }, + "target": "{that defaultSettingsLoader}.options.invokers.get" + } + } + }); +}; + +gpii.tests.untrusted.pspIntegration.testDefs = fluid.transform(gpii.tests.pspIntegration.applyPrefsTestDefs, function (testDefIn, i) { + var testDef = gpii.tests.untrusted.pspIntegration.addConfig(testDefIn); + + testDef.expect = testDef.expect + 2; + gpii.test.unshift(testDef.sequence, gpii.test.untrusted.pspIntegration.startSequence); + + var sequenceAtEnd = fluid.copy(gpii.test.untrusted.pspIntegration.endSequence); + fluid.set(sequenceAtEnd[1], ["args"], ["{that}", "{arguments}.0", i]); + gpii.test.push(testDef.sequence, sequenceAtEnd); + + return testDef; +}); + +// Test PSP integration with: +// 1. auto-save only saves preferences that are allowed to be autosaved to the cloud. +gpii.test.runCouchTestDefs(gpii.tests.untrusted.pspIntegration.testDefs); + +// Test PSP integration with: +// 1. preferences that are not allowed to be autosaved should not be autosaved to the cloud; +// 2. explicit save, such as when the save button is clicked, saves all updated preferences to the cloud. +gpii.test.runCouchTestDefs(fluid.transform(gpii.tests.pspIntegration.saveTestDefs, function (testDefIn) { + return gpii.tests.untrusted.pspIntegration.addConfig(testDefIn); +})); + +gpii.test.runCouchTestDefs(fluid.transform(gpii.tests.pspIntegration.readPrefsTestDefs, function (testDefIn) { + return gpii.tests.untrusted.pspIntegration.addConfig(testDefIn); +})); diff --git a/tests/UntrustedResetAtStartTests.js b/tests/UntrustedResetAtStartTests.js new file mode 100644 index 000000000..61ddc4bbc --- /dev/null +++ b/tests/UntrustedResetAtStartTests.js @@ -0,0 +1,23 @@ +/* + * Reset on System Startup Tests with Untrusted Config + * + * Copyright 2018 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +require("./shared/ResetDefaultSettingsTestDefs.js"); + +gpii.test.runCouchTestDefs(gpii.tests.resetDefaultSettings.buildTestDefs(gpii.tests.resetDefaultSettings.resetAtStartTestCases, { + configName: "gpii.tests.acceptance.untrusted.resetAtStart.config", + configPath: "%gpii-universal/tests/configs" +})); diff --git a/tests/UntrustedSuppressHttpEndpointsTests.js b/tests/UntrustedSuppressHttpEndpointsTests.js new file mode 100644 index 000000000..8404d47e1 --- /dev/null +++ b/tests/UntrustedSuppressHttpEndpointsTests.js @@ -0,0 +1,23 @@ +/* + * Suppress HTTP Endpoints Tests + * + * Copyright 2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +require("./shared/SuppressHttpEndpointsTestDefs.js"); + +gpii.test.runCouchTestDefs(gpii.tests.suppressHttpEndpoints.buildTestDefs({ + configName: "gpii.tests.acceptance.untrusted.suppressHttpEndpoints.config.json5", + configPath: "%gpii-universal/tests/configs" +})); diff --git a/tests/UntrustedUserLogonHandlersTests.js b/tests/UntrustedUserLogonHandlersTests.js new file mode 100644 index 000000000..2ace74b17 --- /dev/null +++ b/tests/UntrustedUserLogonHandlersTests.js @@ -0,0 +1,93 @@ +/* + * Untrusted User logon Handlers Tests + * + * Copyright 2015 OCAD University + * Copyright 2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +require("./shared/UserLogonHandlersTestDefs.js"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.untrusted.userLogonHandling"); + +gpii.tests.untrusted.userLogonHandling.buildTestDefs = function (testDefs) { + return fluid.transform(testDefs, function (testDefIn) { + var testDef = fluid.extend(true, {}, testDefIn, { + config: { + configName: "gpii.tests.acceptance.untrusted.userLogon.config", + configPath: "%gpii-universal/tests/configs" + }, + gradeNames: ["gpii.tests.userLogonHandlers.testCaseHolder", "gpii.test.integration.testCaseHolder.linux"], + gpiiKey: testDefIn.gpiiKey || gpii.tests.userLogonHandlers.gpiiKey + }); + + return testDef; + }); +}; + +// With untrusted config, nonexistent GPII keys can only key in with client credentials that have privilege +// to create nonexistent GPII keys and prefs safes. +// Note that with all-in-local config, nonexistent GPII keys are able to key in and key out. +gpii.tests.untrusted.userLogonHandling.nonexistentKeyInTestDefs = [{ + name: "Testing keyin and keyout with a nonexistent GPII key using a client credential that has privilege to access nonexistent GPII keys", + expect: 2, + distributeOptions: { + "test.clientCredentialFilePath": { + "record": "%gpii-universal/tests/data/clientCredentials/nova.json", + "target": "{that gpii.flowManager.untrusted settingsDataSource}.options.clientCredentialFilePath", + priority: "after:flowManager.clientCredentialFilePath" + } + }, + sequence: [{ + // keyin with a nonexistent GPII key + func: "{loginNonexistentRequest}.send" + }, { + event: "{loginNonexistentRequest}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testLoginResponse", + args: ["{arguments}.0", "nonexistent-gpii-key"] + }, { + // keyout with a nonexistent GPII key + func: "{logoutNonexistentRequest}.send" + }, { + event: "{logoutNonexistentRequest}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testLogoutResponse", + args: ["{arguments}.0", "nonexistent-gpii-key"] + }] +}, { + name: "Testing keyin with a nonexistent GPII key with a client that doesn't have the privilege to access nonexistent GPII keys", + expect: 3, + sequence: [{ + // login with a nonexistent GPII key + func: "{loginNonexistentRequest}.send" + }, { + event: "{loginNonexistentRequest}.events.onComplete", + listener: "kettle.test.assertErrorResponse", + args: { + errorTexts: "server_error while executing HTTP POST on url", + statusCode: 401, + string: "{arguments}.0", + request: "{loginNonexistentRequest}" + } + }] +}]; + +gpii.test.runCouchTestDefs(gpii.tests.untrusted.userLogonHandling.buildTestDefs(gpii.tests.userLogonHandlers.testDefs)); +gpii.test.runCouchTestDefs(gpii.tests.untrusted.userLogonHandling.buildTestDefs(gpii.tests.untrusted.userLogonHandling.nonexistentKeyInTestDefs)); diff --git a/tests/UntrustedUserLogonRequestTests.js b/tests/UntrustedUserLogonRequestTests.js new file mode 100644 index 000000000..2448312d2 --- /dev/null +++ b/tests/UntrustedUserLogonRequestTests.js @@ -0,0 +1,139 @@ +/* + * Untrusted User Logon Request Tests + * + * Copyright 2018-2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +require("./shared/UserLogonRequestTestDefs.js"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.untrusted.userLogonRequest"); + +// Specific tests for untrusted environment without database connection. +// Test the user report on NoConnection when the cloud cannot be accessed +gpii.tests.untrusted.userLogonRequest.buildTestDefs = function (testDefs) { + var config = { + configName: "gpii.config.untrusted.development", + configPath: "%gpii-universal/gpii/configs/shared" + }; + + return fluid.transform(testDefs, function (testDef) { + return fluid.extend(true, { + config: config, + gpiiKey: testDefs.gpiiKey || gpii.tests.userLogonRequest.gpiiKey + }, gpii.tests.userLogonRequest.commonTestConfig, testDef); + }); +}; + +gpii.tests.untrusted.userLogonRequest.untrustedWithoutDbConnection = [{ + name: "GPII-3529: report NoConnection user error when no cloud connection", + expect: 2, + sequence: [{ + // standard login without a cloud + task: "{lifecycleManager}.performLogin", + args: [gpii.tests.userLogonRequest.gpiiKey], + reject: "gpii.tests.userLogonRequest.testUserError", + rejectArgs: ["{arguments}.0", + { + "code": "ECONNREFUSED", + "errno": "ECONNREFUSED", + "syscall": "connect", + "address": "127.0.0.1", + "port": 8084, + "isError": true + }, + "{lifecycleManager}.userErrors.options.trackedUserErrors", + { + "isError": true, + "messageKey": "NoConnection", + "originalError": { + "code": "ECONNREFUSED", + "errno": "ECONNREFUSED", + "syscall": "connect", + "address": "127.0.0.1", + "port": 8084, + "isError": true + } + } + ] + }] +}]; + +gpii.tests.untrusted.userLogonRequest.nonexistentKeyInTestDefs = [{ + name: "Testing login and logout with a nonexistent GPII key using a client credential that has privilege to access nonexistent GPII keys", + expect: 2, + gpiiKey: "bogusToken", + distributeOptions: { + "test.clientCredentialFilePath": { + "record": "%gpii-universal/tests/data/clientCredentials/nova.json", + "target": "{that gpii.flowManager.untrusted settingsDataSource}.options.clientCredentialFilePath", + priority: "after:flowManager.clientCredentialFilePath" + } + }, + sequence: [ + { + // login with a non-existing GPII key + task: "{lifecycleManager}.performLogin", + args: ["{testCaseHolder}.options.gpiiKey"], + resolve: "gpii.tests.userLogonRequest.testLoginResponse", + resolveArgs: ["{arguments}.0", "{testCaseHolder}.options.gpiiKey"] + }, + { + event: "{lifecycleManager}.events.onQueueEmpty", + listener: "fluid.identity" + }, + { + // logout of different user + task: "{lifecycleManager}.performLogout", + args: ["{testCaseHolder}.options.gpiiKey"], + resolve: "gpii.tests.userLogonRequest.testLogoutResponse", + resolveArgs: ["{arguments}.0", "{testCaseHolder}.options.gpiiKey"] + } + ] +}, { + name: "Testing keyin with a nonexistent GPII key with a client that doesn't have the privilege to access nonexistent GPII keys", + expect: 2, + gpiiKey: "bogusToken", + sequence: [{ + task: "{lifecycleManager}.performLogin", + args: ["{testCaseHolder}.options.gpiiKey"], + reject: "gpii.tests.userLogonRequest.testUserError", + rejectArgs: ["{arguments}.0", + { + "error_description": "Unauthorized", + "message": "server_error while executing HTTP POST on url http://localhost:8084/access_token", + "isError": true, + "statusCode": 401 + }, + "{lifecycleManager}.userErrors.options.trackedUserErrors", + { + "isError": true, + "messageKey": "KeyInFail", + "originalError": "server_error while executing HTTP POST on url http://localhost:8084/access_token" + } + ] + }] +}]; + +// Tests for general user logon request tests +gpii.test.runCouchTestDefs(gpii.tests.userLogonRequest.buildTestDefs(gpii.tests.userLogonRequest.testDefs, "untrusted")); + +// Specific tests for untrusted environment without database connection. +gpii.test.runCouchTestDefs(gpii.tests.untrusted.userLogonRequest.buildTestDefs(gpii.tests.untrusted.userLogonRequest.untrustedWithoutDbConnection)); + +// Tests for key in and key out processes for nonexistent GPII keys +gpii.test.runCouchTestDefs(gpii.tests.userLogonRequest.buildTestDefs(gpii.tests.untrusted.userLogonRequest.nonexistentKeyInTestDefs, "untrusted")); diff --git a/tests/UserLogonHandlersEventsTests.js b/tests/UserLogonHandlersEventsTests.js new file mode 100644 index 000000000..957296371 --- /dev/null +++ b/tests/UserLogonHandlersEventsTests.js @@ -0,0 +1,89 @@ +/* + * User logon Handlers Events Tests + * + * Copyright 2013-2018 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ +"use strict"; + +var fluid = require("infusion"), + jqUnit = fluid.require("node-jqunit", require, "jqUnit"), + gpii = fluid.registerNamespace("gpii"); + + +fluid.require("%gpii-universal"); + +require("./shared/UserLogonHandlersTestDefs.js"); +fluid.registerNamespace("gpii.tests.userLogonEvents"); + +gpii.tests.userLogonEvents.modelChangeChecker = function (changePayload, type, inProgress, gpiiKey) { + jqUnit.assertEquals("Checking type of model change", type, changePayload.type); + jqUnit.assertEquals("Checking inProgress of model change", inProgress, changePayload.inProgress); + jqUnit.assertEquals("Checking gpiiKey of model change", gpiiKey, changePayload.gpiiKey); +}; + +gpii.tests.userLogonEvents.testDefs = [{ + name: "Testing events related to login and logout via proximityTriggered endpoint", + expect: 20, + sequence: [{ // standard login + func: "{proximityTriggeredRequest}.send" + }, { + changeEvent: "{lifecycleManager}.applier.modelChanged", + path: "logonChange", + listener: "gpii.tests.userLogonEvents.modelChangeChecker", + args: ["{arguments}.0", "logout", true, "noUser"] + }, { + changeEvent: "{lifecycleManager}.applier.modelChanged", + path: "logonChange", + listener: "gpii.tests.userLogonEvents.modelChangeChecker", + args: ["{arguments}.0", "logout", false, "noUser"] + }, { + changeEvent: "{lifecycleManager}.applier.modelChanged", + path: "logonChange", + listener: "gpii.tests.userLogonEvents.modelChangeChecker", + args: ["{arguments}.0", "login", true, gpii.tests.userLogonHandlers.gpiiKey] + }, { + changeEvent: "{lifecycleManager}.applier.modelChanged", + path: "logonChange", + listener: "gpii.tests.userLogonEvents.modelChangeChecker", + args: ["{arguments}.0", "login", false, gpii.tests.userLogonHandlers.gpiiKey] + }, { + event: "{proximityTriggeredRequest}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testLoginResponse", + args: ["{arguments}.0", gpii.tests.userLogonHandlers.gpiiKey] + }, { + // wait for debounce + func: "setTimeout", + args: ["{tests}.events.debounceTimeoutComplete.fire", 3500] + }, { + event: "{tests}.events.debounceTimeoutComplete", + listener: "fluid.identity" + }, { // standard logout + func: "{proximityTriggeredRequest2}.send" + }, { + changeEvent: "{lifecycleManager}.applier.modelChanged", + path: "logonChange", + listener: "gpii.tests.userLogonEvents.modelChangeChecker", + args: ["{arguments}.0", "logout", true, gpii.tests.userLogonHandlers.gpiiKey] + }, { + changeEvent: "{lifecycleManager}.applier.modelChanged", + path: "logonChange", + listener: "gpii.tests.userLogonEvents.modelChangeChecker", + args: ["{arguments}.0", "logout", false, gpii.tests.userLogonHandlers.gpiiKey] + }, { + event: "{proximityTriggeredRequest2}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testLogoutResponse", + args: ["{arguments}.0", gpii.tests.userLogonHandlers.gpiiKey] + }] +}]; + +gpii.test.runCouchTestDefs(gpii.tests.userLogonHandlers.buildTestDefs(gpii.tests.userLogonEvents.testDefs)); diff --git a/tests/UserLogonHandlersTests.js b/tests/UserLogonHandlersTests.js new file mode 100644 index 000000000..367a0a91a --- /dev/null +++ b/tests/UserLogonHandlersTests.js @@ -0,0 +1,49 @@ +/* + * User Logon Handlers Tests + * + * Copyright 2013 OCAD University + * Copyright 2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +require("./shared/UserLogonHandlersTestDefs.js"); + +// With all-in-local config, nonexistent GPII keys are able to key in and key out. +// Note that with untrusted config, nonexistent GPII keys can only key in with client credentials that have privilege +// to create nonexistent GPII keys and prefs safes. +gpii.tests.userLogonHandlers.nonexistentKeyInTestDefs = [{ + name: "Testing login and logout with a nonexistent GPII key", + expect: 2, + sequence: [{ + // login with a nonexistent GPII key + func: "{loginNonexistentRequest}.send" + }, { + event: "{loginNonexistentRequest}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testLoginResponse", + args: ["{arguments}.0", "nonexistent-gpii-key"] + }, { + // logout with a nonexistent GPII key + func: "{logoutNonexistentRequest}.send" + }, { + event: "{logoutNonexistentRequest}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testLogoutResponse", + args: ["{arguments}.0", "nonexistent-gpii-key"] + }] +}]; + +gpii.test.runCouchTestDefs(gpii.tests.userLogonHandlers.buildTestDefs(gpii.tests.userLogonHandlers.testDefs)); +gpii.test.runCouchTestDefs(gpii.tests.userLogonHandlers.buildTestDefs(gpii.tests.userLogonHandlers.nonexistentKeyInTestDefs)); diff --git a/tests/UserLogonRequestTests.js b/tests/UserLogonRequestTests.js new file mode 100644 index 000000000..4be66bd10 --- /dev/null +++ b/tests/UserLogonRequestTests.js @@ -0,0 +1,55 @@ +/* + * User Logon Request Tests + * + * Copyright 2013 OCAD University + * Copyright 2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +require("./shared/UserLogonRequestTestDefs.js"); + +// With all-in-local config, nonexistent GPII keys are able to key in and key out. +// Note that with untrusted config, nonexistent GPII keys can only key in with client credentials that have privilege +// to create nonexistent GPII keys and prefs safes. +gpii.tests.userLogonRequest.nonexistentKeyInTestDefs = [{ + name: "Testing login and logout with a nonexistent GPII key", + expect: 2, + gpiiKey: "bogusToken", + sequence: [ + { + // login with a non-existing GPII key + task: "{lifecycleManager}.performLogin", + args: ["{testCaseHolder}.options.gpiiKey"], + resolve: "gpii.tests.userLogonRequest.testLoginResponse", + resolveArgs: ["{arguments}.0", "{testCaseHolder}.options.gpiiKey"] + }, + { + event: "{lifecycleManager}.events.onQueueEmpty", + listener: "fluid.identity" + }, + { + // logout of different user + task: "{lifecycleManager}.performLogout", + args: ["{testCaseHolder}.options.gpiiKey"], + resolve: "gpii.tests.userLogonRequest.testLogoutResponse", + resolveArgs: ["{arguments}.0", "{testCaseHolder}.options.gpiiKey"] + } + ] +}]; + +gpii.test.runCouchTestDefs(gpii.tests.userLogonRequest.buildTestDefs(gpii.tests.userLogonRequest.testDefs)); +gpii.test.runCouchTestDefs(gpii.tests.userLogonRequest.buildTestDefs(gpii.tests.userLogonRequest.nonexistentKeyInTestDefs)); diff --git a/tests/all-tests.js b/tests/all-tests.js index ee7ef4018..f2bf8c474 100644 --- a/tests/all-tests.js +++ b/tests/all-tests.js @@ -2,23 +2,88 @@ GPII Universal Tests Copyright 2013 OCAD University +Copyright 2014 Emergya +Copyright 2014 Technosite +Copyright 2014 Raising the Floor - International +Copyright 2017-2018 OCAD University Licensed under the New BSD license. You may not use this file except in compliance with this License. +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + You may obtain a copy of the License at -https://github.com/gpii/universal/LICENSE.txt +https://github.com/GPII/universal/blob/master/LICENSE.txt */ +"use strict"; + +var fluid = require("infusion"), + kettle = fluid.require("kettle"); -/*global require, __dirname*/ -// This loads universal. -var fluid = fluid || require("infusion"); +// Ensure this happens first, to catch errors during code loading, especially before KETTLE-67 is fixed +kettle.loadTestingSupport(); -(function () { +// We must pass the current `require` to `fluid.require`, as nyc's instrumentation is hooked into it. +fluid.require("%gpii-universal", require); - fluid.require("../gpii/node_modules/settingsHandlers/test/SettingsHandlerUtilitiesTests.js", require); - fluid.require("../gpii/node_modules/settingsHandlers/test/JSONSettingsHandlerTests.js", require); - fluid.require("../gpii/node_modules/settingsHandlers/test/XMLSettingsHandlerTests.js", require); - fluid.require("../gpii/node_modules/settingsHandlers/test/INISettingsHandlerTests.js", require); +var testIncludes = [ + "./DevelopmentTests.js", + "./platform/cloud/CloudStatusTests.js", + "./platform/cloud/CloudRevisionGetTests.js", + "./platform/cloud/SettingsGetTests.js", + "./platform/cloud/SettingsPutTests.js", + "./CloseConflictingAppsTests.js", + "./DeviceReporterErrorTests.js", + "./ErrorTests.js", + "./IntegrationTests.js", + "./JournalIntegrationTests.js", + "./MultiSettingsHandlerTests.js", + "./PayloadSizeTest.js", + "./PSPIntegrationTests.js", + "./ResetAtStartTests.js", + "./SuppressHttpEndpointsTests.js", + "./UntrustedBrowserChannelTests.js", + "./UntrustedDevelopmentTests.js", + "./UntrustedPSPIntegrationTests.js", + "./UntrustedResetAtStartTests.js", + "./UntrustedSuppressHttpEndpointsTests.js", + "./UntrustedUserLogonHandlersTests.js", + "./UntrustedUserLogonRequestTests.js", + "./UserLogonHandlersEventsTests.js", + "./UserLogonHandlersTests.js", + "./UserLogonRequestTests.js", + "../gpii/node_modules/accessRequester/test/AccessRequesterTests.js", + "../gpii/node_modules/deviceReporter/test/StaticDeviceReporterTests.js", + "../gpii/node_modules/eventLog/test/all-tests.js", + "../gpii/node_modules/flatMatchMaker/test/FlatMatchMakerTests.js", + "../gpii/node_modules/flowManager/test/BrowserChannelTests.js", + "../gpii/node_modules/flowManager/test/CaptureTests.js", + "../gpii/node_modules/flowManager/test/DefaultSettingsLoaderTests.js", + "../gpii/node_modules/flowManager/test/PrefsServerDataSourceTests.js", + "../gpii/node_modules/flowManager/test/PSPChannelTests.js", + "../gpii/node_modules/flowManager/test/SettingsDataSourceTests.js", + "../gpii/node_modules/gpii-db-operation/test/DbDataStoreTests.js", + "../gpii/node_modules/gpii-ini-file/test/iniFileTests.js", + "../gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/authGrantFinderTests.js", + "../gpii/node_modules/gpii-oauth2/gpii-oauth2-authz-server/test/authorizationServiceTests.js", + "../gpii/node_modules/gpii-oauth2/gpii-oauth2-utilities/test/OAuth2UtilitiesTests.js", + "../gpii/node_modules/matchMakerFramework/test/MatchMakerFrameworkTests.js", + "../gpii/node_modules/ontologyHandler/test/node/OntologyHandlerTests.js", + "../gpii/node_modules/preferencesServer/test/preferencesServerTests.js", + "../gpii/node_modules/preferencesServer/test/preferencesServiceTests.js", + "../gpii/node_modules/settingsHandlers/test/INISettingsHandlerTests.js", + "../gpii/node_modules/settingsHandlers/test/JSONSettingsHandlerTests.js", + "../gpii/node_modules/settingsHandlers/test/NoSettingsHandlerTests.js", + "../gpii/node_modules/settingsHandlers/test/SettingsHandlerUtilitiesTests.js", + "../gpii/node_modules/settingsHandlers/test/WebSocketsSettingsHandlerTests.js", + "../gpii/node_modules/settingsHandlers/test/XMLSettingsHandlerTests.js", + "../gpii/node_modules/singleInstance/test/SingleInstanceTests.js", + "../gpii/node_modules/solutionsRegistry/test/all-tests.js", + "../gpii/node_modules/transformer/test/TransformerTests.js", + "../gpii/node_modules/userListeners/test/all-tests.js" +]; -}()); \ No newline at end of file +fluid.each(testIncludes, function (path) { + require(path); +}); diff --git a/tests/configs/gpii.tests.acceptance.localInstall.dynamicDeviceReporter.config.json5 b/tests/configs/gpii.tests.acceptance.localInstall.dynamicDeviceReporter.config.json5 new file mode 100644 index 000000000..68a2ac79f --- /dev/null +++ b/tests/configs/gpii.tests.acceptance.localInstall.dynamicDeviceReporter.config.json5 @@ -0,0 +1,4 @@ +{ + "type": "gpii.tests.acceptance.localInstall.dynamicDeviceReporter.config", + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.dynamicDR.local.json5" +} diff --git a/tests/configs/gpii.tests.acceptance.pspIntegration.config.json5 b/tests/configs/gpii.tests.acceptance.pspIntegration.config.json5 new file mode 100644 index 000000000..86701d8d0 --- /dev/null +++ b/tests/configs/gpii.tests.acceptance.pspIntegration.config.json5 @@ -0,0 +1,7 @@ +{ + "type": "gpii.tests.acceptance.pspIntegration.config", + "mergeConfigs": [ + "%gpii-universal/tests/platform/linux/configs/gpii.tests.acceptance.linux.builtIn.config", + "./shared/gpii.tests.resetDefaultSettings.config.json5" + ] +} diff --git a/tests/configs/gpii.tests.acceptance.resetAtStart.config.json5 b/tests/configs/gpii.tests.acceptance.resetAtStart.config.json5 new file mode 100644 index 000000000..62ed53162 --- /dev/null +++ b/tests/configs/gpii.tests.acceptance.resetAtStart.config.json5 @@ -0,0 +1,15 @@ +{ + "type": "gpii.tests.acceptance.resetAtStart.config", + "options": { + "distributeOptions": { + "acceptance.resetAtStart": { + "record": true, + "target": "{that gpii.flowManager.local}.options.resetAtStart" + } + } + }, + "mergeConfigs": [ + "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5", + "./shared/gpii.tests.resetDefaultSettings.config.json5" + ] +} diff --git a/tests/configs/gpii.tests.acceptance.suppressHttpEndpoints.config.json5 b/tests/configs/gpii.tests.acceptance.suppressHttpEndpoints.config.json5 new file mode 100644 index 000000000..4be8876fe --- /dev/null +++ b/tests/configs/gpii.tests.acceptance.suppressHttpEndpoints.config.json5 @@ -0,0 +1,14 @@ +{ + "type": "gpii.tests.acceptance.suppressHttpEndpoints.config", + "options": { + "distributeOptions": { + "acceptance.suppressHttpEndpoints": { + "record": true, + "target": "{that gpii.flowManager.local}.options.suppressHttpEndpoints" + } + } + }, + "mergeConfigs": [ + "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" + ] +} diff --git a/tests/configs/gpii.tests.acceptance.untrusted.browserChannel.config.json5 b/tests/configs/gpii.tests.acceptance.untrusted.browserChannel.config.json5 new file mode 100644 index 000000000..eaf5edf3e --- /dev/null +++ b/tests/configs/gpii.tests.acceptance.untrusted.browserChannel.config.json5 @@ -0,0 +1,25 @@ +{ + "type": "gpii.tests.acceptance.untrusted.browserChannel.config", + "options": { + "mainServerPort": 8081, + "distributeOptions": { + "browserChannel.tests.deviceReporter": { + "record": "%flowManager/test/data/browserChannel_deviceReporter.json", + "target": "{that localConfig deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + }, + "browserChannel.tests.solutions": { + "record": "%flowManager/test/data/browserChannel_solutionsRegistry.json", + "target": "{that flowManager solutionsRegistryDataSource}.options.path", + "priority": "after:flowManager.solutions" + }, + "browserChannel.tests.osReporting": { + "record": { + "funcName": "gpii.tests.flowManager.browserChannel.reportPlatform" + }, + "target": "{that localConfig deviceReporter platformReporter}.options.invokers.reportPlatform" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.local.json5" +} diff --git a/tests/configs/gpii.tests.acceptance.untrusted.pspIntegration.config.json b/tests/configs/gpii.tests.acceptance.untrusted.pspIntegration.config.json new file mode 100644 index 000000000..277d8b784 --- /dev/null +++ b/tests/configs/gpii.tests.acceptance.untrusted.pspIntegration.config.json @@ -0,0 +1,15 @@ +{ + "type": "gpii.tests.acceptance.linux.builtIn.UntrustedPSPIntegration.config", + "options": { + "distributeOptions": { + "acceptance.cloudBasedConfig.preferencesServer": { + "record": "gpii.tests.untrusted.pspIntegration.preferencesServer", + "target": "{that cloudBasedConfig preferencesServer}.type" + } + } + }, + "mergeConfigs": [ + "%gpii-universal/tests/platform/linux/configs/gpii.tests.acceptance.linux.builtIn.untrusted.config.json", + "./shared/gpii.tests.resetDefaultSettings.config.json5" + ] +} diff --git a/tests/configs/gpii.tests.acceptance.untrusted.resetAtStart.config.json5 b/tests/configs/gpii.tests.acceptance.untrusted.resetAtStart.config.json5 new file mode 100644 index 000000000..42f5e6fbb --- /dev/null +++ b/tests/configs/gpii.tests.acceptance.untrusted.resetAtStart.config.json5 @@ -0,0 +1,15 @@ +{ + "type": "gpii.tests.acceptance.untrusted.resetAtStart.config", + "options": { + "distributeOptions": { + "acceptance.resetAtStart": { + "record": true, + "target": "{that gpii.flowManager.local}.options.resetAtStart" + } + } + }, + "mergeConfigs": [ + "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.local.json5", + "./shared/gpii.tests.resetDefaultSettings.config.json5" + ] +} diff --git a/tests/configs/gpii.tests.acceptance.untrusted.suppressHttpEndpoints.config.json5 b/tests/configs/gpii.tests.acceptance.untrusted.suppressHttpEndpoints.config.json5 new file mode 100644 index 000000000..6f49db949 --- /dev/null +++ b/tests/configs/gpii.tests.acceptance.untrusted.suppressHttpEndpoints.config.json5 @@ -0,0 +1,14 @@ +{ + "type": "gpii.tests.acceptance.untrusted.suppressHttpEndpoints.config", + "options": { + "distributeOptions": { + "acceptance.suppressHttpEndpoints": { + "record": true, + "target": "{that gpii.flowManager.local}.options.suppressHttpEndpoints" + } + } + }, + "mergeConfigs": [ + "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.local.json5" + ] +} diff --git a/tests/configs/gpii.tests.acceptance.untrusted.userLogon.config.json5 b/tests/configs/gpii.tests.acceptance.untrusted.userLogon.config.json5 new file mode 100644 index 000000000..8cf6d6528 --- /dev/null +++ b/tests/configs/gpii.tests.acceptance.untrusted.userLogon.config.json5 @@ -0,0 +1,7 @@ +{ + "type": "gpii.tests.acceptance.untrusted.userLogon.config", + "mergeConfigs": [ + "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.local.json5", + "./shared/gpii.tests.userLogon.config.json5" + ] +} diff --git a/tests/configs/gpii.tests.acceptance.userLogon.config.json5 b/tests/configs/gpii.tests.acceptance.userLogon.config.json5 new file mode 100644 index 000000000..dfbc186b7 --- /dev/null +++ b/tests/configs/gpii.tests.acceptance.userLogon.config.json5 @@ -0,0 +1,7 @@ +{ + "type": "gpii.tests.acceptance.userLogon.config", + "mergeConfigs": [ + "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5", + "./shared/gpii.tests.userLogon.config.json5" + ] +} diff --git a/tests/configs/gpii.tests.cloudFlowManager.prefsServer.local.json5 b/tests/configs/gpii.tests.cloudFlowManager.prefsServer.local.json5 new file mode 100644 index 000000000..8860c55c3 --- /dev/null +++ b/tests/configs/gpii.tests.cloudFlowManager.prefsServer.local.json5 @@ -0,0 +1,33 @@ +/** + * This configuration starts the cloud based flow manager and the preferences server as separate kettle servers. + * + * 1. Cloud Based Flow Manager on port 8081 + * 2. Preferences Server on port 8084 + */ +{ + "type": "gpii.tests.cloudFlowManager.prefsServer.local", + "options": { + "gradeNames": ["kettle.multiConfig.config"], + "configs": { + "cloudBasedFlowManagerConfig": { + "configName": "gpii.flowManager.config.cloud.base", + "configPath": "%flowManager/configs" + }, + "prefsServerConfig": { + "configName": "gpii.preferencesServer.config.base", + "configPath": "%preferencesServer/configs" + } + }, + "distributeOptions": { + "prefsServer.port": { + "record": 8084, + "target": "{that prefsServerConfig}.options.mainServerPort" + }, + "CloudBasedFlowManager.prefs": { + "record": "http://localhost:8084", + "target": "{that cloudBasedFlowManagerConfig flowManager prefsServerDataSource}.options.prefsServerURL", + "priority": "after:flowManager.prefsServerDataSource.default" + } + } + } +} diff --git a/tests/configs/gpii.tests.multiSH.config.json5 b/tests/configs/gpii.tests.multiSH.config.json5 new file mode 100644 index 000000000..96d4a5bf1 --- /dev/null +++ b/tests/configs/gpii.tests.multiSH.config.json5 @@ -0,0 +1,18 @@ +{ + "type": "gpii.tests.multiSH.config", + "options": { + "distributeOptions": { + "multiSH.deviceReporter": { + "record": "%gpii-universal/tests/data/multiSHDeviceReporter.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + }, + "multiSH.solutionsRegistry": { + "record": "%gpii-universal/tests/data/multiSHsolutionsRegistry.json", + "target": "{that flowManager solutionsRegistryDataSource}.options.path", + "priority": "after:flowManager.solutions" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/tests/configs/gpii.tests.multiScreenreader.config.json5 b/tests/configs/gpii.tests.multiScreenreader.config.json5 new file mode 100644 index 000000000..150cbe28f --- /dev/null +++ b/tests/configs/gpii.tests.multiScreenreader.config.json5 @@ -0,0 +1,18 @@ +/** + * Used for the integration test CloseConflictingAppsTest. It uses the preference sets of + * the testData's acceptanceTests folder and a device reporter that reports only two screenreaders + * (Jaws and NVDA) installed. + */ +{ + "type": "gpii.tests.multiScreenreader.config", + "options": { + "distributeOptions": { + "multiSH.deviceReporter": { + "record": "%gpii-universal/tests/data/multiScreenreaderDeviceReporter.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/tests/configs/gpii.tests.untrusted.production.config.json5 b/tests/configs/gpii.tests.untrusted.production.config.json5 new file mode 100644 index 000000000..14e009b58 --- /dev/null +++ b/tests/configs/gpii.tests.untrusted.production.config.json5 @@ -0,0 +1,7 @@ +{ + "type": "gpii.tests.untrusted.production.config", + "options": { + "mainServerPort": "@expand:kettle.resolvers.env(GPII_FLOWMANAGER_LISTEN_PORT)" + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.json5" +} diff --git a/tests/configs/shared/gpii.tests.resetDefaultSettings.config.json5 b/tests/configs/shared/gpii.tests.resetDefaultSettings.config.json5 new file mode 100644 index 000000000..ff78e3ed4 --- /dev/null +++ b/tests/configs/shared/gpii.tests.resetDefaultSettings.config.json5 @@ -0,0 +1,27 @@ +{ + "type": "gpii.tests.resetDefaultSettings.config", + "options": { + "flowManagerOptions": { + // Do not disturb the defaultSettings.json5 file that has been copied to the settings dir on the computer + // that this test is running on. + "components": { + "defaultSettingsLoader": { + "type": "fluid.component", + "options": { + "invokers": { + "get": { + "funcName": "fluid.identity" + } + } + } + } + } + }, + "distributeOptions": { + "acceptance.flowManagerOptions": { + "source": "{that}.options.flowManagerOptions", + "target": "{that gpii.flowManager.local}.options" + } + } + } +} diff --git a/tests/configs/shared/gpii.tests.userLogon.config.json5 b/tests/configs/shared/gpii.tests.userLogon.config.json5 new file mode 100644 index 000000000..d74510076 --- /dev/null +++ b/tests/configs/shared/gpii.tests.userLogon.config.json5 @@ -0,0 +1,42 @@ +{ + "type": "gpii.tests.userLogon.config", + "options": { + "distributeOptions": { + "acceptance.debounceTimeMs": { + "record": 3000, + "target": "{that gpii.flowManager.local lifecycleManager}.options.debounceTimeMs" + }, + "acceptance.defaultSettings": { + "source": "{that}.options.flowManagerOptions", + "target": "{that gpii.flowManager.local}.options" + } + }, + "flowManagerOptions": { + // Do not disturb the defaultSettings.json5 file that has been copied to the settings dir on the computer + // that this test is running on. + "components": { + "defaultSettingsLoader": { + "type": "fluid.component", + "options": { + "invokers": { + "get": { + "funcName": "fluid.identity", + "args": [{ + "contexts": { + "gpii-default": { + "preferences": { + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/cursorSize": 0.5, + "http://registry.gpii.net/common/volume": 0.75 + } + } + } + }] + } + } + } + } + } + } + } +} diff --git a/tests/data/clientCredentials/nova.json b/tests/data/clientCredentials/nova.json new file mode 100644 index 000000000..c9ca51af3 --- /dev/null +++ b/tests/data/clientCredentials/nova.json @@ -0,0 +1,4 @@ +{ + "client_id": "nova-computer", + "client_secret": "nova-computer-secret" +} diff --git a/tests/data/dbData/README.txt b/tests/data/dbData/README.txt new file mode 100644 index 000000000..dbf53aec5 --- /dev/null +++ b/tests/data/dbData/README.txt @@ -0,0 +1,8 @@ +This folder contains the DB data that is used for acceptance testing. + +These data are dependent on by acceptance test, so they should not be deleted or modified, unless it is ensured that this will not affect the acceptance test(s) in question. + +The acceptance test definitions are located in the %gpii-universal/test/platform subdirectories. When running from universal, they +function as integration tests (with mocks for settings handlers and other environmental effects), but when running from the +platform-specific repositories (Linux, Windows and Android) they function as acceptance tests, having real effects on the +surrounding OS. diff --git a/tests/data/dbData/clientCredentials.json b/tests/data/dbData/clientCredentials.json new file mode 100644 index 000000000..eab0c69ae --- /dev/null +++ b/tests/data/dbData/clientCredentials.json @@ -0,0 +1,85 @@ +[ + { + "_id": "clientCredential-1", + "type": "clientCredential", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-1", + "oauth2ClientId": "pilot-computer", + "oauth2ClientSecret": "pilot-computer-secret", + "allowedIPBlocks": null, + "allowedPrefsToWrite": null, + "isCreateGpiiKeyAllowed": false, + "isCreatePrefsSafeAllowed": false, + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampRevoked": null + }, + { + "_id": "clientCredential-schemaV0.1", + "type": "clientCredential", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-schemaV0.1", + "oauth2ClientId": "pilot-computer-schemaV0.1", + "oauth2ClientSecret": "pilot-computer-secret-schemaV0.1", + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampRevoked": null + }, + { + "_id": "clientCredential-nova", + "type": "clientCredential", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-nova", + "oauth2ClientId": "nova-computer", + "oauth2ClientSecret": "nova-computer-secret", + "allowedIPBlocks": ["127.0.0.1/8", "172.0.0.0/8", "::1"], + "allowedPrefsToWrite": [ + "http://registry.gpii.net/common/language", + "http://registry.gpii.net/common/DPIScale", + "http://registry.gpii.net/common/highContrast/enabled", + "http://registry.gpii.net/common/highContrastTheme", + "http://registry.gpii.net/common/selfVoicing/enabled", + "http://registry.gpii.net/applications/com.microsoft.office" + ], + "isCreateGpiiKeyAllowed": true, + "isCreatePrefsSafeAllowed": true, + "revoked": false, + "revokedReason": null, + "timestampCreated": "2019-05-28T18:11:22.101Z", + "timestampRevoked": null + }, + { + "_id": "clientCredential-failInIpVerification", + "type": "clientCredential", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-nova-failInIpVerification", + "oauth2ClientId": "nova-computer-failInIpVerification", + "oauth2ClientSecret": "nova-computer-secret-failInIpVerification", + "allowedIPBlocks": ["192.168.1.1/128"], + "allowedPrefsToWrite": [], + "isCreateGpiiKeyAllowed": true, + "isCreatePrefsSafeAllowed": true, + "revoked": false, + "revokedReason": null, + "timestampCreated": "2019-05-28T18:11:22.101Z", + "timestampRevoked": null + }, + { + "_id": "clientCredential-failInAllowedPrefsToWrite", + "type": "clientCredential", + "schemaVersion": "0.2", + "clientId": "gpiiAppInstallationClient-nova-failInAllowedPrefsToWrite", + "oauth2ClientId": "nova-computer-failInAllowedPrefsToWrite", + "oauth2ClientSecret": "nova-computer-secret-failInAllowedPrefsToWrite", + "allowedIPBlocks": ["127.0.0.1/8", "172.0.0.0/8", "::1"], + "allowedPrefsToWrite": [], + "isCreateGpiiKeyAllowed": true, + "isCreatePrefsSafeAllowed": true, + "revoked": false, + "revokedReason": null, + "timestampCreated": "2019-05-28T18:11:22.101Z", + "timestampRevoked": null + } +] diff --git a/tests/data/dbData/gpiiAppInstallationClients.json b/tests/data/dbData/gpiiAppInstallationClients.json new file mode 100644 index 000000000..d7db029ca --- /dev/null +++ b/tests/data/dbData/gpiiAppInstallationClients.json @@ -0,0 +1,47 @@ +[ + { + "_id": "gpiiAppInstallationClient-1", + "type": "gpiiAppInstallationClient", + "schemaVersion": "0.2", + "name": "Pilot Computers", + "computerType": "public", + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + }, + { + "_id": "gpiiAppInstallationClient-schemaV0.1", + "type": "gpiiAppInstallationClient", + "schemaVersion": "0.2", + "name": "To match the client credential data structure that is in the old schema version 0.1", + "computerType": "public", + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + }, + { + "_id": "gpiiAppInstallationClient-nova", + "type": "gpiiAppInstallationClient", + "schemaVersion": "0.2", + "name": "NOVA - success workflow tests", + "computerType": "public", + "timestampCreated": "2019-05-28T18:11:22.101Z", + "timestampUpdated": null + }, + { + "_id": "gpiiAppInstallationClient-nova-failInIpVerification", + "type": "gpiiAppInstallationClient", + "schemaVersion": "0.2", + "name": "NOVA - erroneous workflow test for the ip verification", + "computerType": "public", + "timestampCreated": "2019-05-28T18:11:22.101Z", + "timestampUpdated": null + }, + { + "_id": "gpiiAppInstallationClient-nova-failInAllowedPrefsToWrite", + "type": "gpiiAppInstallationClient", + "schemaVersion": "0.2", + "name": "NOVA - erroneous workflow test for the allowed preferences to write", + "computerType": "public", + "timestampCreated": "2019-05-28T18:11:22.101Z", + "timestampUpdated": null + } +] diff --git a/tests/data/dbData/gpiiKeys.json b/tests/data/dbData/gpiiKeys.json new file mode 100644 index 000000000..7fe1d60ab --- /dev/null +++ b/tests/data/dbData/gpiiKeys.json @@ -0,0 +1,13 @@ +[ + { + "_id": "gpii_key_no_prefs_safe", + "type": "gpiiKey", + "schemaVersion": "0.2", + "prefsSafeId": null, + "prefsSetId": null, + "revoked": false, + "revokedReason": null, + "timestampCreated": "2017-11-21T18:11:22.101Z", + "timestampUpdated": null + } +] diff --git a/tests/data/faultyDeviceReport.jsonx b/tests/data/faultyDeviceReport.jsonx new file mode 100644 index 000000000..c83a93600 --- /dev/null +++ b/tests/data/faultyDeviceReport.jsonx @@ -0,0 +1,3 @@ +{ + "id": [a +} \ No newline at end of file diff --git a/tests/data/gpiiRevision/gpii-revision-invalid.json b/tests/data/gpiiRevision/gpii-revision-invalid.json new file mode 100644 index 000000000..444398374 --- /dev/null +++ b/tests/data/gpiiRevision/gpii-revision-invalid.json @@ -0,0 +1 @@ +{"sha256": ""} diff --git a/tests/data/gpiiRevision/gpii-revision-valid.json b/tests/data/gpiiRevision/gpii-revision-valid.json new file mode 100644 index 000000000..26befa7b4 --- /dev/null +++ b/tests/data/gpiiRevision/gpii-revision-valid.json @@ -0,0 +1 @@ +{"sha256": "2602bdf868aec49993d8780feec42d4e9f995e21"} diff --git a/tests/data/multiSHDeviceReporter.json b/tests/data/multiSHDeviceReporter.json new file mode 100644 index 000000000..9d79e3609 --- /dev/null +++ b/tests/data/multiSHDeviceReporter.json @@ -0,0 +1,5 @@ +[ + { + "id": "fakemag1" + } +] diff --git a/tests/data/multiSHSolutionsRegistry.json b/tests/data/multiSHSolutionsRegistry.json new file mode 100644 index 000000000..bea13f10f --- /dev/null +++ b/tests/data/multiSHSolutionsRegistry.json @@ -0,0 +1,63 @@ +{ + "win32": { + "fakemag1": { + "name": "Fake Magnifier 1", + "contexts": { + "OS": [ + { + "id": "win32", + "version": ">=5.0" + } + ] + }, + "capabilities": [ + "http://registry\\.gpii\\.net/common/magnification/enabled" + ], + "settingsHandlers": { + "configuration": { + "type": "gpii.settingsHandlers.JSONSettingsHandler", + "options": { + "filename": "/tmp/fakemag1.settings.json" + }, + "supportedSettings": { + "tilingMagnifiers": {}, + "magnification": {}, + "tracking": {} + }, + "capabilitiesTransformations": { + "magnification": "http://registry\\.gpii\\.net/common/magnification", + "tracking": "http://registry\\.gpii\\.net/common/tracking" + } + }, + "configuration1": { + "type": "gpii.settingsHandlers.JSONSettingsHandler", + "options": { + "filename": "/tmp/fakemag2.settings.json" + }, + "supportedSettings": { + "weirdSetting": {}, + "crazyColor": {} + }, + "capabilitiesTransformations": { + "crazyColor": "http://registry\\.gpii\\.net/common/invertColours" + } + } + }, + "configure": [ + "settings.configuration", + "settings.configuration1" + ], + "restore": [ + "settings.configuration", + "settings.configuration1" + ], + "start": [], + "stop": [], + "isInstalled": [ + { + "type": "gpii.deviceReporter.alwaysInstalled" + } + ] + } + } +} diff --git a/tests/data/multiScreenreaderDeviceReporter.json b/tests/data/multiScreenreaderDeviceReporter.json new file mode 100644 index 000000000..5e9147038 --- /dev/null +++ b/tests/data/multiScreenreaderDeviceReporter.json @@ -0,0 +1,9 @@ +[ + { + "id": "com.freedomscientific.jaws" + }, + + { + "id": "org.nvda-project" + } +] diff --git a/tests/data/preferences/README.txt b/tests/data/preferences/README.txt new file mode 100644 index 000000000..4567ae229 --- /dev/null +++ b/tests/data/preferences/README.txt @@ -0,0 +1,18 @@ +This directory contains the preference sets that are used for acceptance testing. + +Each preference set is depended on by an (or several) acceptance test, so they should not be deleted or modified, unless +it is ensured that this will not affect the acceptance test(s) in question. + +The acceptance test definitions are located in the %gpii-universal/tests/platform subdirectories. When running from +universal, they function as integration tests (with mocks for settings handlers and other environmental effects), but +when running from the platform-specific repositories (Linux, Windows and Android) they function as acceptance tests, +having real effects on the surrounding OS. + +Note that when any preferences file in this directory is modified, you will need to add the +data to the `gpiiKeys.json` and `prefsSafes.json` files used in standard tests. You can do so using the following +command: + +`npm run postinstall` + +These files are formatted so that they can be loaded into CouchDB when GPII runs using one of the development +configurations. diff --git a/tests/data/preferences/adjustCursor.json5 b/tests/data/preferences/adjustCursor.json5 new file mode 100644 index 000000000..389c05f77 --- /dev/null +++ b/tests/data/preferences/adjustCursor.json5 @@ -0,0 +1,12 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/cursorSize": 1 + } + } + } + } +} diff --git a/tests/data/preferences/chrome_high_contrast.json5 b/tests/data/preferences/chrome_high_contrast.json5 new file mode 100644 index 000000000..2d7a3452d --- /dev/null +++ b/tests/data/preferences/chrome_high_contrast.json5 @@ -0,0 +1,18 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 12, + "http://registry.gpii.net/common/screenReaderTTS/enabled": false, + "http://registry.gpii.net/common/magnification/enabled": false, + "http://registry.gpii.net/common/magnification": 1, + "http://registry.gpii.net/common/highContrast/enabled": true, + "http://registry.gpii.net/common/highContrastTheme": "white-black", + "http://registry.gpii.net/common/invertColours": false + } + } + } + } +} diff --git a/tests/data/preferences/context1.json5 b/tests/data/preferences/context1.json5 new file mode 100644 index 000000000..2cff28a7e --- /dev/null +++ b/tests/data/preferences/context1.json5 @@ -0,0 +1,46 @@ +{ + "flat": { + "name": "Multiple Contexts", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/magnification": 1.5, + "http://registry.gpii.net/common/volume": 0.5 + }, + "metadata": [ + { + "type": "autosave", + "scope": [ + "http://registry.gpii.net/common/magnification", + "http://registry.gpii.net/common/volume", + [ + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier", + "mag-factor" + ] + ] + } + ] + }, + "bright": { + "name": "bright", + "preferences": { + "http://registry.gpii.net/common/magnification": 2 + } + }, + "noise": { + "name": "noise", + "preferences": { + "http://registry.gpii.net/common/volume": 1 + } + }, + "brightandnoise": { + "name": "bright and noise", + "preferences": { + "http://registry.gpii.net/common/volume": 1, + "http://registry.gpii.net/common/magnification": 2 + } + } + } + } +} diff --git a/tests/data/preferences/giant.json5 b/tests/data/preferences/giant.json5 new file mode 100644 index 000000000..9999364e0 --- /dev/null +++ b/tests/data/preferences/giant.json5 @@ -0,0 +1,14 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/giantTesting": { + "key1": "fdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljkfdasljkadsfkljadsflkjadfslklkjdsfljkdfsakjlfdsajklfdsakljfdasljk" + } + } + } + } + } +} diff --git a/tests/data/preferences/jaws_application.json5 b/tests/data/preferences/jaws_application.json5 new file mode 100644 index 000000000..8468d354a --- /dev/null +++ b/tests/data/preferences/jaws_application.json5 @@ -0,0 +1,49 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.freedomscientific.jaws": { + "Voice Profiles.ActiveVoiceProfileName" : "GPII", + "Options.SayAllIndicateCaps" : 0, + "ENU-Global.Rate" : 100, + "ENU-Global.Pitch" : 70, + "ENU-Global.Volume" : 100, + "ENU-Global.Punctuation" : 2, + "ENU-JAWSCursor.Rate": 100, + "ENU-Keyboard.Rate": 100, + "ENU-MenuAndDialog.Rate": 100, + "ENU-Message.Rate": 100, + "ENU-PCCursor.Rate": 100, + "ENU-JAWSCursor.Pitch": 70, + "ENU-Keyboard.Pitch": 70, + "ENU-MenuAndDialog.Pitch": 70, + "ENU-Message.Pitch": 70, + "ENU-PCCursor.Pitch": 70, + "ENU-JAWSCursor.Volume": 100, + "ENU-Keyboard.Volume": 100, + "ENU-MenuAndDialog.Volume": 100, + "ENU-Message.Volume": 100, + "ENU-PCCursor.Volume": 100, + "ENU-JAWSCursor.Punctuation": 2, + "ENU-Keyboard.Punctuation": 2, + "ENU-MenuAndDialog.Punctuation": 2, + "ENU-Message.Punctuation": 2, + "ENU-PCCursor.Punctuation": 2, + "ENU-Global.SynthLangString": "Italian", + "ENU-JAWSCursor.SynthLangString": "Italian", + "ENU-Keyboard.SynthLangString": "Italian", + "ENU-MenuAndDialog.SynthLangString": "Italian", + "ENU-Message.SynthLangString": "Italian", + "ENU-PCCursor.SynthLangString": "Italian", + "Options.TypingEcho": 3, + "Options.SayAllMode": 0, + "Braille.BrailleMode": 0, + "Options.SayAllIgnoreShiftKeys": 1 + } + } + } + } + } +} diff --git a/tests/data/preferences/jaws_common.json5 b/tests/data/preferences/jaws_common.json5 new file mode 100644 index 000000000..019e35d68 --- /dev/null +++ b/tests/data/preferences/jaws_common.json5 @@ -0,0 +1,21 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/auditoryOutLanguage": "fr", + "http://registry.gpii.net/common/speechRate": 40, + "http://registry.gpii.net/common/pitch": 0.7, + "http://registry.gpii.net/common/volumeTTS": 0.5, + "http://registry.gpii.net/common/punctuationVerbosity": "most", + "http://registry.gpii.net/common/announceCapitals": false, + "http://registry.gpii.net/common/readingUnit": "sentence", + "http://registry.gpii.net/common/stickyKeys": false, + "http://registry.gpii.net/common/wordEcho": true, + "http://registry.gpii.net/common/keyEcho": true + } + } + } + } +} diff --git a/tests/data/preferences/jaws_common2.json5 b/tests/data/preferences/jaws_common2.json5 new file mode 100644 index 000000000..fab865bf2 --- /dev/null +++ b/tests/data/preferences/jaws_common2.json5 @@ -0,0 +1,21 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/auditoryOutLanguage": "it", + "http://registry.gpii.net/common/speechRate": 40, + "http://registry.gpii.net/common/pitch": 0.1, + "http://registry.gpii.net/common/volumeTTS": 0.1, + "http://registry.gpii.net/common/punctuationVerbosity": "none", + "http://registry.gpii.net/common/announceCapitals": false, + "http://registry.gpii.net/common/readingUnit": "line", + "http://registry.gpii.net/common/stickyKeys": false, + "http://registry.gpii.net/common/wordEcho": false, + "http://registry.gpii.net/common/keyEcho": false + } + } + } + } +} diff --git a/tests/data/preferences/jaws_common3.json5 b/tests/data/preferences/jaws_common3.json5 new file mode 100644 index 000000000..caec16d27 --- /dev/null +++ b/tests/data/preferences/jaws_common3.json5 @@ -0,0 +1,21 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/auditoryOutLanguage": "fr", + "http://registry.gpii.net/common/speechRate": 100, + "http://registry.gpii.net/common/pitch": 1, + "http://registry.gpii.net/common/volumeTTS": 1, + "http://registry.gpii.net/common/punctuationVerbosity": "most", + "http://registry.gpii.net/common/announceCapitals": true, + "http://registry.gpii.net/common/readingUnit": "paragraph", + "http://registry.gpii.net/common/stickyKeys": true, + "http://registry.gpii.net/common/wordEcho": false, + "http://registry.gpii.net/common/keyEcho": true + } + } + } + } +} diff --git a/tests/data/preferences/maavis_highcontrast.json5 b/tests/data/preferences/maavis_highcontrast.json5 new file mode 100644 index 000000000..45948b1c1 --- /dev/null +++ b/tests/data/preferences/maavis_highcontrast.json5 @@ -0,0 +1,13 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/highContrast/enabled": true, + "http://registry.gpii.net/common/highContrastTheme": "yellow-black" + } + } + } + } +} diff --git a/tests/data/preferences/maavis_selfvoicing.json5 b/tests/data/preferences/maavis_selfvoicing.json5 new file mode 100644 index 000000000..8fec9232a --- /dev/null +++ b/tests/data/preferences/maavis_selfvoicing.json5 @@ -0,0 +1,12 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/selfVoicing/enabled": true + } + } + } + } +} diff --git a/tests/data/preferences/magic_application.json5 b/tests/data/preferences/magic_application.json5 new file mode 100644 index 000000000..9cfe562b8 --- /dev/null +++ b/tests/data/preferences/magic_application.json5 @@ -0,0 +1,35 @@ +{ + "flat": { + "name": "MAGic application settings.", + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.freedomscientific.magic": { + "mag.caretbutton": 1, + "mag.colorbutton": 1, + "mag.cursorbutton": 1, + "mag.Focus Enhancement Enabled": 1, + // This may be problematic, as it is associated with a common term. + "mag.size": 2, + "mag.speechbutton": 0, + "mag.splitlocation": 2, + "mag.splitsize": 0, + "mag.splitbordercolor": 9, + "mag.splitframesize": 5, + "mag.splitwindowwidth": 20, + // This may be problematic, as it is associated with a common term. + "mag.startmagnified": 1, + "mag.typeechobutton": 1, + "mag.viewtype": 1, + "Options.SkipILM": 1, + "scheme.Color_Scheme": "Grayscale (Inverted)+Color", + // This may be problematic, as it is associated with a common term (crosshairs). + "scheme.Mouse_Scheme": "Scope When Moving+Mouse" + }, + "http://registry.gpii.net/applications/com.freedomscientific.magic/enabled": true + } + } + } + } +} diff --git a/tests/data/preferences/multiSHCommonOnly.json5 b/tests/data/preferences/multiSHCommonOnly.json5 new file mode 100644 index 000000000..131aa3301 --- /dev/null +++ b/tests/data/preferences/multiSHCommonOnly.json5 @@ -0,0 +1,14 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/magnification": 1.5, + "http://registry.gpii.net/common/tracking": [ "mouse", "caret" ], + "http://registry.gpii.net/common/invertColours": true + } + } + } + } +} diff --git a/tests/data/preferences/nvda_and_orca.json5 b/tests/data/preferences/nvda_and_orca.json5 new file mode 100644 index 000000000..7b048b1e9 --- /dev/null +++ b/tests/data/preferences/nvda_and_orca.json5 @@ -0,0 +1,43 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/org.nvda-project": { + "speech.synth": "espeak", + "speech.outputDevice": "Microsoft Sound Mapper", + "speech.symbolLevel": 300, + "speech.espeak.rate": 40, + "speech.espeak.voice": "en-us", + "speech.espeak.pitch": 60, + "speech.espeak.volume": 80, + "reviewCursor.followFocus": "False", + "reviewCursor.followCaret": "True", + "reviewCursor.followMouse": "True", + "keyboard.speakTypedWords": "True", + "speech.espeak.rateBoost": "True", + "keyboard.speakTypedCharacters": "False", + "presentation.reportHelpBalloons": "False", + "speech.espeak.sayCapForCapitals": "True", + "virtualBuffers.autoSayAllOnPageLoad": "False" + }, + "http://registry.gpii.net/applications/org.gnome.orca": { + "sayAllStyle": 1, + "enableEchoByWord": 1, + "enableEchoByCharacter": 0, + "voices.default.rate": 102.27272727272727, + "enableTutorialMessages": 0, + "voices.default.family": { + "locale": "en", + "name": "en-westindies" + }, + "voices.default.average-pitch": 1, + "voices.default.gain": 7, + "verbalizePunctuationStyle": 0 + } + } + } + } + } +} diff --git a/tests/data/preferences/onenote_learningstools_application.json5 b/tests/data/preferences/onenote_learningstools_application.json5 new file mode 100644 index 000000000..fb924682a --- /dev/null +++ b/tests/data/preferences/onenote_learningstools_application.json5 @@ -0,0 +1,20 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.office.windowsOneNoteLearningTools": { + "DictationLanguage": 1033, + "ReadingComprehensionDefaultFont": "Sitka Small", + "IsReadingComprehensionFontWide": 1, + "ReadingComprehensionTheme": 2, + "ReadingComprehensionFontSize": 52, + "SynthRate": 2, + "SynthVoices": "[{\"Lang\":\"en\",\"Voice\":\"Microsoft Zira Desktop\"}]" + } + } + } + } + } +} diff --git a/tests/data/preferences/os_common.json5 b/tests/data/preferences/os_common.json5 new file mode 100644 index 000000000..b21b05ba4 --- /dev/null +++ b/tests/data/preferences/os_common.json5 @@ -0,0 +1,23 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/magnifierPosition": "Lens", + "http://registry.gpii.net/common/magnification": 1.5, + "http://registry.gpii.net/common/tracking": [ "mouse", "caret" ], + "http://registry.gpii.net/common/invertColours": true, + "http://registry.gpii.net/common/cursorSize": 0.9, + "http://registry.gpii.net/common/fontSize": 9, + "http://registry.gpii.net/common/mouseTrailing": 10, + "http://registry.gpii.net/common/mouseEmulation/enabled": true, + "http://registry.gpii.net/common/cursorSpeed": 1, + "http://registry.gpii.net/common/stickyKeys": true, + "http://registry.gpii.net/common/debounce/enabled": true, + "http://registry.gpii.net/common/onScreenKeyboard/enabled": true + } + } + } + } +} diff --git a/tests/data/preferences/os_common2.json5 b/tests/data/preferences/os_common2.json5 new file mode 100644 index 000000000..ed77311da --- /dev/null +++ b/tests/data/preferences/os_common2.json5 @@ -0,0 +1,21 @@ +// This preference set is used to check the behaviour under GNU/Linux when highContrast/enabled is set to false. +// +// When logging in with this NP set, GNOME should use its default values for gtk-theme and icon-theme. +// So if you want to see this in action: +// 1. Manually turn HighContrast on +// 2. Log-in as os_common2 +// 3. GNOME will disable the HighContrast theme +// 4. Log-out +// 5. HighContrast is enabled again +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/highContrast/enabled": false + } + } + } + } +} diff --git a/tests/data/preferences/os_common_highContrast.json5 b/tests/data/preferences/os_common_highContrast.json5 new file mode 100644 index 000000000..3f9cce569 --- /dev/null +++ b/tests/data/preferences/os_common_highContrast.json5 @@ -0,0 +1,16 @@ +// A temporary preferences file created to work around a window acceptance test failure (GPII-3757) +// before the proper fix described in GPII-3759(https://issues.gpii.net/browse/GPII-3759) is implemented. +// The high contrast test is performed using this file to ensure it doesn't mess up the cursors test: +// high contrast applied after cursors will override cursors settings. +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/highContrast/enabled": true + } + } + } + } +} diff --git a/tests/data/preferences/os_gnome.json5 b/tests/data/preferences/os_gnome.json5 new file mode 100644 index 000000000..e368055d7 --- /dev/null +++ b/tests/data/preferences/os_gnome.json5 @@ -0,0 +1,22 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier": { + "mag-factor": 1.5, + "screen-position": "full-screen" + }, + "http://registry.gpii.net/applications/org.gnome.desktop.interface": { + "cursor-size": 90, + "text-scaling-factor": 0.75 + }, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 50 + } + } + } + } + } +} diff --git a/tests/data/preferences/os_gnome_display.json5 b/tests/data/preferences/os_gnome_display.json5 new file mode 100644 index 000000000..e16aedc2d --- /dev/null +++ b/tests/data/preferences/os_gnome_display.json5 @@ -0,0 +1,17 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/org.freedesktop.xrandr": { + "screen-resolution": [{ + "width": 800, + "height": 600 + }] + } + } + } + } + } +} diff --git a/tests/data/preferences/os_win.json5 b/tests/data/preferences/os_win.json5 new file mode 100644 index 000000000..2629d89e4 --- /dev/null +++ b/tests/data/preferences/os_win.json5 @@ -0,0 +1,278 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.mouseTrailing": { + "MouseTrails": { + "path": { + "get": "pvParam", + "set": "uiParam" + }, + "value": 10 + } + }, + "http://registry.gpii.net/applications/com.microsoft.windows.mouseKeys": { + "MouseKeysOn": { + "path": "pvParam.dwFlags.MKF_MOUSEKEYSON", + "value": true + }, + "MaxSpeed": { + "path": "pvParam.iMaxSpeed", + "value": 100 + }, + "Acceleration": { + "path": "pvParam.iTimeToMaxSpeed", + "value": 1000 + } + }, + "http://registry.gpii.net/applications/com.microsoft.windows.stickyKeys": { + "StickyKeysOn": { + "path": "pvParam.dwFlags.SKF_STICKYKEYSON", + "value": true + } + }, + "http://registry.gpii.net/applications/com.microsoft.windows.toggleKeys": { + "ToggleKeysOn": { + "path": "pvParam.dwFlags.TKF_TOGGLEKEYSON", + "value": true + } + }, + "http://registry.gpii.net/applications/com.microsoft.windows.underlineMenuShortcuts": { + "UnderlineMenuShortcutsOn": { + "value": 1, + "path": "pvParam" + }, + "KeyboardPreferenceOn": { + "value": 1, + "path": { + "get": "pvParam", + "set": "uiParam" + } + } + }, + "http://registry.gpii.net/applications/com.microsoft.windows.shortcutWarningMessage": { + "Warning Sounds": 1 + }, + "http://registry.gpii.net/applications/com.microsoft.windows.shortcutWarningSound": { + "Sound on Activation": 1 + }, + "http://registry.gpii.net/applications/com.microsoft.windows.filterKeys": { + "FilterKeysEnable": { + "path": "pvParam.dwFlags.FKF_FILTERKEYSON", + "value": true + } + // TODO: Figure out why the SPI settings handler kills the parent process if this is set to "1000" (number as string). + // TODO: Reenable this once we can confirm a safe range of values that work with the SPI settings handler. +// "BounceKeysInterval": { +// "path": "pvParam.iBounceMSec", +// "value": 1000 +// } + }, + "http://registry.gpii.net/applications/com.microsoft.windows.cursors": { + "No": "%SystemRoot%\\cursors\\no_l.cur", + "Hand": "%SystemRoot%\\cursors\\aero_link_l.cur", + "Help": "%SystemRoot%\\cursors\\help_l.cur", + "Wait": "%SystemRoot%\\cursors\\busy_l.cur", + "Arrow": "%SystemRoot%\\cursors\\arrow_l.cur", + "NWPen": "%SystemRoot%\\cursors\\pen_l.cur", + "SizeNS": "%SystemRoot%\\cursors\\size4_l.cur", + "SizeWE": "%SystemRoot%\\cursors\\size3_l.cur", + "SizeAll": "%SystemRoot%\\cursors\\move_l.cur", + "UpArrow": "%SystemRoot%\\cursors\\up_l.cur", + "SizeNESW": "%SystemRoot%\\cursors\\size1_l.cur", + "SizeNWSE": "%SystemRoot%\\cursors\\size2_l.cur", + "AppStarting": "%SystemRoot%\\cursors\\wait_l.cur" + }, + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "FollowFocus": 0, + "FollowCaret": 1, + "FollowMouse": 1, + "Invert": 1, + "Magnification": 150, + "MagnificationMode": 3, + "FollowNarrator": 0, + "FadeToMagIcon": 1, + "ZoomIncrement": 50, + "UseBitmapSmoothing": 1, + "LensHeight": 50, + "LensWidth": 50 + }, + "http://registry.gpii.net/applications/com.microsoft.windows.screenResolution": { + "screen-resolution": { + "width": 800, + "height": 600 + } + }, + "http://registry.gpii.net/applications/com.microsoft.windows.screenDPI": { + "screen-dpi": 1 + }, + "http://registry.gpii.net/applications/com.microsoft.windows.nightScreen": { + "SystemSettings_Display_BlueLight_ManualToggleQuickAction": { + "value": false + } + }, + "http://registry.gpii.net/applications/com.microsoft.windows.narrator": { + "SpeechSpeed": 11, + "SpeechPitch": 4, + "CoupleNarratorCursorKeyboard": 1, + "FollowInsertion": 0, + "InteractionMouse": 1, + "EchoWords": 1, + "EchoChars": 1, + "IntonationPause": 1, + "ReadHints": 1, + "PlayAudioCues": 1, + "NarratorCursorHighlight": 1, + "FastKeyEntryEnabled": 1, + "ReadingWithIntent": 1, + "ErrorNotificationType": 1, + "CoupleNarratorCursorMouse": 1, + "LockNarratorKeys": 1, + "SpeechVolume": 80, + "SpeechVoice": "Microsoft Zira - English (United States)", + "ContextVerbosityLevel": 4, + "RenderContextBeforeElement": 1, + "DuckAudio": 1, + "WinEnterLaunchEnabled": 1, + "VerbosityLevel": 3, + "DetailedFeedback": 1/*, +* These settings have been commented out for https://issues.gpii.net/browse/GPII-4462 + "SystemSettings_Accessibility_Narrator_IsAutoStartEnabled": { + "value": true + }, + "SystemSettings_Accessibility_Narrator_IsAutoStartOnLogonDesktopEnabled": { + "value": true + } +*/ + }, + "http://registry.gpii.net/applications/com.microsoft.windows.typingEnhancement": { + "EnableAutocorrection": true, + "EnableSpellchecking": true, + "EnableTextPrediction": true, + "EnablePredictionSpaceInsertion": true, + "EnableDoubleTapSpace": true, + "EnableKeyAudioFeedback": true, + "EnableAutoShiftEngage": true, + "EnableShiftLock": true, + "EnableCompatibilityKeyboard": true, + "EnableDesktopModeAutoInvoke": true + }, + "http://registry.gpii.net/applications/com.microsoft.windows.mouseSettings": { + "SwapMouseButtonsConfig": { + "path": { + "get": "pvParam", + "set": "uiParam" + }, + "value": 1 + }, + "ScrollWheelModeConfig": { + "path": { + "get": "pvParam", + "set": "uiParam" + }, + "value": 4294967294 + }, + "ScrollFocusRoutingConfig": { + "path": "pvParam", + "value": 1 + }, + "MouseCursorShadowEnable": { + "path": "pvParam", + "value": 1 + }, + "ScrollCharsConfig": { + "path": { + "get": "pvParam", + "set": "uiParam" + }, + "value": 10 + }, + "WindowsTrackingConfig": { + "path": "pvParam", + "value": 1 + }, + "ActiveZOrder": { + "path": "pvParam", + "value": 0 + }, + "WindowsArrangement": { + "path": { + "get": "pvParam", + "set": "uiParam" + }, + "value": 1 + }, + "DoubleClickTimeConfig": { + "value": 600 + }, + "PointerSpeedConfig": { + "path": "pvParam", + "value": 15 + }, + "EnhancePrecisionConfig": { + "path": "pvParam", + "value": [6, 10, 1] + }, + "SnapToDefaultButtonConfig": { + "path": { + "get": "pvParam", + "set": "uiParam" + }, + "value": 1 + }, + "HidePointerConfig": { + "path": "pvParam", + "value": 0 + }, + "MouseSonarConfig": { + "path": "pvParam", + "value": 1 + }, + "DoubleClickWidthConfig": { + "value": 4 + }, + "DoubleClickHeightConfig": { + "value": 4 + } + }, + "http://registry.gpii.net/applications/com.microsoft.windows.language": { + "MachinePreferredUILanguages": "en-US", + "PreferredUILanguages": "en-US" + }, + "http://registry.gpii.net/applications/com.microsoft.windows.touchPadSettings": { + "SystemSettings_Input_Touch_SetActivationTimeout": { + "value": "Low sensitivity" + } + }, + "http://registry.gpii.net/applications/com.microsoft.windows.onscreenKeyboard": { + "NavigationMode": 0, + "ClickSound": 0, + "ShowClearKeyboard": 0, + "ShowNumPad": 1, + "Mode": 2, + "HoverPeriod": 1500, + "ScanInterval": 1500, + "UseDevice": 0, + "UseKB": 0, + "ScanKey": 113, + "UseMouse": 1, + "UseTextPrediction": 0, + "InsertSpace": 0, + "Dock": 0 + }, + "http://registry.gpii.net/applications/com.microsoft.windows.soundSentry": { + "WindowsEffect": "2" + } + // TODO: Disabled due to GPII-4518 + //"http://registry.gpii.net/applications/com.microsoft.windows.volumeControl": { + // "Volume": { + // "value": 0.7 + // } + //} + } + } + } + } +} diff --git a/tests/data/preferences/os_win_2.json5 b/tests/data/preferences/os_win_2.json5 new file mode 100644 index 000000000..b732c15b2 --- /dev/null +++ b/tests/data/preferences/os_win_2.json5 @@ -0,0 +1,20 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.desktopBackgroundColor": { + "SolidColorConfig": { + "value": { "r": 67, "g": 187, "b": 19 } + }, + "ImageConfig": { + "path": "pvParam", + "value": "" + } + } + } + } + } + } +} diff --git a/tests/data/preferences/os_win_brightness.json5 b/tests/data/preferences/os_win_brightness.json5 new file mode 100644 index 000000000..5a964347f --- /dev/null +++ b/tests/data/preferences/os_win_brightness.json5 @@ -0,0 +1,12 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/screenBrightness": 70 + } + } + } + } +} diff --git a/tests/data/preferences/os_win_highContrast.json5 b/tests/data/preferences/os_win_highContrast.json5 new file mode 100644 index 000000000..a3faa6457 --- /dev/null +++ b/tests/data/preferences/os_win_highContrast.json5 @@ -0,0 +1,21 @@ +// A temporary preferences file created to work around a window acceptance test failure (GPII-3757) +// before the proper fix described in GPII-3759(https://issues.gpii.net/browse/GPII-3759) is implemented. +// The high contrast test is performed using this file to ensure it doesn't mess up the cursors test: +// high contrast applied after cursors will override cursors settings. +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.highContrast": { + "HighContrastOn": { + "path": "pvParam.dwFlags.HCF_HIGHCONTRASTON", + "value": true + } + } + } + } + } + } +} diff --git a/tests/data/preferences/readwritegold_check_dict_high.json5 b/tests/data/preferences/readwritegold_check_dict_high.json5 new file mode 100644 index 000000000..15709221e --- /dev/null +++ b/tests/data/preferences/readwritegold_check_dict_high.json5 @@ -0,0 +1,31 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.texthelp.readWriteGold": { + // Check It + "ApplicationSettings.Spelling.OrderByContext.$t": false, + "ApplicationSettings.Spelling.SpeakSpellingOnHover.$t": false, + "ApplicationSettings.Spelling.ShowCheckIt.$t": true, + "ApplicationSettings.Spelling.RightClickMSWord.$t": true, + // Dictionary + "ApplicationSettings.Dictionary.WebDictionary.$t": "Bing", + "ApplicationSettings.Dictionary.PopupDictionary.$t": true, + "ApplicationSettings.Dictionary.ToggleImages.$t": true, + // Highlights + "ApplicationSettings.StudySkills.CollectYellowHighlight.$t": false, + "ApplicationSettings.StudySkills.CollectBlueHighlight.$t": true, + "ApplicationSettings.StudySkills.CollectGreenHighlight.$t": false, + "ApplicationSettings.StudySkills.CollectPinkHighlight.$t": false, + "ApplicationSettings.StudySkills.CollectOrder.$t": "Position", + "ApplicationSettings.StudySkills.ColorSeparator.$t": "Page", + "ApplicationSettings.StudySkills.HighlightSeparator.$t": "Tab", + "ApplicationSettings.StudySkills.MultiDocHighlighting.$t": false + } + } + } + } + } +} diff --git a/tests/data/preferences/readwritegold_general.json5 b/tests/data/preferences/readwritegold_general.json5 new file mode 100644 index 000000000..a8b9c0932 --- /dev/null +++ b/tests/data/preferences/readwritegold_general.json5 @@ -0,0 +1,18 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.texthelp.readWriteGold": { + "ApplicationSettings.AppBar.IconSize.$t": "Small", + "ApplicationSettings.AppBar.ToolbarIconSet.$t": "Color", + "ApplicationSettings.AppBar.ShowText.$t": true, + "ApplicationSettings.AppBar.optToolbarBackColour.$t": "#008080", + "ApplicationSettings.AppBar.RunOnStartUp.$t": false + } + } + } + } + } +} diff --git a/tests/data/preferences/readwritegold_pred_scan_out.json5 b/tests/data/preferences/readwritegold_pred_scan_out.json5 new file mode 100644 index 000000000..796e0b7ad --- /dev/null +++ b/tests/data/preferences/readwritegold_pred_scan_out.json5 @@ -0,0 +1,48 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.texthelp.readWriteGold": { + // Prediction + "ApplicationSettings.Prediction.DisplayAlphabetic.$t": true, + "ApplicationSettings.Prediction.FollowCursor.$t": true, + "ApplicationSettings.Prediction.AutoHeightAdjust.$t": true, + "ApplicationSettings.Prediction.winPredictOneWordAhead.$t": true, + "ApplicationSettings.Prediction.InsertSpaceAfterPred.$t": false, + "ApplicationSettings.Prediction.UseSpellingForPredictions.$t": false, + // Prediction Speech + "ApplicationSettings.Prediction.SpeakPredictionOnMouseHover.$t": false, + // This two settings are excluyent, need to encode that + "ApplicationSettings.Prediction.SpeakOnWordClick.$t": true, + "ApplicationSettings.Prediction.InsertOnWordClick.$t": false, + // Prediction Learning + "ApplicationSettings.Prediction.SpellCheckBeforeLearn.$t": false, + "ApplicationSettings.Prediction.AutoLearnAsType.$t": false, + // Scanning + "ApplicationSettings.Scanning.dpi.$t": 600, + "ApplicationSettings.Scanning.colormode.$t": "GrayScale", + "ApplicationSettings.Scanning.HideInterface.$t": false, + // Device Setup (Advanced settings) + "ApplicationSettings.Scanning.Countdown.$t": true, + "ApplicationSettings.Scanning.CountdownInterval.$t": 10, + "ApplicationSettings.Scanning.ADF.$t": true, + "ApplicationSettings.Scanning.Duplex.$t": true, + // Save Options + "ApplicationSettings.Scanning.AutoSave.$t": true, + "ApplicationSettings.Scanning.DefaultFilePath.$t": "C:\\Users\\vagrant\\Download", + "ApplicationSettings.Scanning.DefaultFileName.$t": "ScannedDoc", + // PDF Output + "ApplicationSettings.Scanning.PdfTextOnly.$t": true, + "ApplicationSettings.Scanning.MSWordRetainFormat.$t": true, + "ApplicationSettings.Scanning.CoverFirstPage.$t": false, + // Output Types + "ApplicationSettings.Scanning.EnableEpub.$t": true, + "ApplicationSettings.Scanning.EnableWeb.$t": true + } + } + } + } + } +} diff --git a/tests/data/preferences/readwritegold_screen_mask_shot.json5 b/tests/data/preferences/readwritegold_screen_mask_shot.json5 new file mode 100644 index 000000000..c29692a81 --- /dev/null +++ b/tests/data/preferences/readwritegold_screen_mask_shot.json5 @@ -0,0 +1,39 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.texthelp.readWriteGold": { + // Screen Masking + "ApplicationSettings.ScreenMasking.ScreenMask.$t": "UnderlineTypingLine", + // Screen Masking -> Tint whole screen subsettings + "ApplicationSettings.ScreenMasking.TintScreenColour.$t": "#a7efce", + "ApplicationSettings.ScreenMasking.TintScreenOpacity.$t": 0.5, + "ApplicationSettings.ScreenMasking.ReadingLight.$t": true, + "ApplicationSettings.ScreenMasking.ReadingLightColour.$t": "#00FFFFFF", + "ApplicationSettings.ScreenMasking.ReadingLightOpacity.$t": 0.5, + "ApplicationSettings.ScreenMasking.ReadingLightHeight.$t": 150, + // Screen Masking -> Underline typing line + "ApplicationSettings.ScreenMasking.UnderlineTypingLineColour.$t": "#a7efce", + "ApplicationSettings.ScreenMasking.UnderlineTypingLineOpacity.$t": 0.3, + "ApplicationSettings.ScreenMasking.UnderlineTypingLineHeight.$t": 15, + // Screen Masking -> Tint typing line + "ApplicationSettings.ScreenMasking.TintTypingLineColour.$t": "#FF00FFFF", + "ApplicationSettings.ScreenMasking.TintTypingLineOpacity.$t": 0.6, + // Screen Masking -> Change page color + "ApplicationSettings.ScreenMasking.SystemBackgroundColour.$t": "#a7efce", + // Screen Masking -> Underline cursor + "ApplicationSettings.ScreenMasking.UnderlineCursorColour.$t": "#a7efce", + "ApplicationSettings.ScreenMasking.UnderlineCursorOpacity.$t": 0.4, + "ApplicationSettings.ScreenMasking.UnderlineCursorHeight.$t": 20, + // Screenshot Reader + "ApplicationSettings.Screenshot.CaptureBy.$t": "Drawing Freehand", + "ApplicationSettings.Screenshot.ScreenshotTo.$t": "Window", + "ApplicationSettings.Screenshot.AppendToMsWordDoc.$t": false + } + } + } + } + } +} diff --git a/tests/data/preferences/readwritegold_trans_voca_voice.json5 b/tests/data/preferences/readwritegold_trans_voca_voice.json5 new file mode 100644 index 000000000..80645d4e3 --- /dev/null +++ b/tests/data/preferences/readwritegold_trans_voca_voice.json5 @@ -0,0 +1,23 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.texthelp.readWriteGold": { + // Translation + "ApplicationSettings.Translation.FromLanguage.$t": "cs", + "ApplicationSettings.Translation.ToLanguage.$t": "da", + // Vocabulary List + // TODO: Verify with Microsoft word installation + "ApplicationSettings.Vocabulary.VocabListWindow.$t": true, + "ApplicationSettings.Vocabulary.VocabListImages.$t": false, + "ApplicationSettings.Vocabulary.VocabListDefinitions.$t": true, + // Voice Note + "ApplicationSettings.VoiceNote.InsertInDoc.$t": true + } + } + } + } + } +} diff --git a/tests/data/preferences/readwritegold_voice.json5 b/tests/data/preferences/readwritegold_voice.json5 new file mode 100644 index 000000000..7f6b5ce11 --- /dev/null +++ b/tests/data/preferences/readwritegold_voice.json5 @@ -0,0 +1,50 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.texthelp.readWriteGold": { + // Speech + "ApplicationSettings.Speech.CurrentVoice.$t": "Mexican Spanish Paulina - Vocalizer", + "ApplicationSettings.Speech.VoiceSpeed.$t": 70, + "ApplicationSettings.Speech.UseVoiceSpeedHotKey.$t": false, + "ApplicationSettings.Speech.WordPause.$t": 200, + "ApplicationSettings.Speech.VoicePitch.$t": 40, + // Speech -> Auto Read + "ApplicationSettings.Speech.SpeakAsIType.$t": true, + // Speech -> Auto Read -> Speak as I type + "ApplicationSettings.Speech.SpeakOnEachLetter.$t": true, + // Speech -> Auto Read -> Speak as I type + "ApplicationSettings.Speech.SpeakOnEachWord.$t": false, + // Speech -> Auto Read -> Speak as I type + "ApplicationSettings.Speech.SpeakOnEachSentence.$t": false, + // Speech -> Screen reading + "ApplicationSettings.Speech.ScreenReadingEnabled.$t": true, + // Speech -> Screen reading -> Items to read + // TODO: Setting is stored in an array format in XML, we need to check how + // XMLSettingsHandler handles this case. + // "ApplicationSettings.Speech.ScreenReadingItems.$t":, + // Speech -> Read by + "ApplicationSettings.Speech.SelectionType.$t": 0, + // Speech -> Continuous reading + "ApplicationSettings.Speech.ContinuousReading.$t": false, + // Speech -> Read the web + "ApplicationSettings.Speech.ReadTheWeb.$t": true, + // Speech -> Speech highlighting + "ApplicationSettings.Speech.HighlightingMethod.$t": 2, + // Speech -> Speech highlighting -> Font + "ApplicationSettings.Speech.TextReaderOneWordFontName.$t": "Times New Roman", + "ApplicationSettings.Speech.optSpeechHighTRFontName.$t": "Times New Roman", + // Speech -> Speech highlighting -> Font size + "ApplicationSettings.Speech.TextReaderOneWordFontSize.$t": 16, + "ApplicationSettings.Speech.optSpeechHighTRFontSize.$t": 16, + // Speech -> Speech highlighting -> Highlighting colors + "ApplicationSettings.Speech.HighlightingBackColour.$t": "Violet", + "ApplicationSettings.Speech.HighlightingForeColour.$t": "Chartreuse" + } + } + } + } + } +} diff --git a/tests/data/preferences/scoped_common_magnifier.json5 b/tests/data/preferences/scoped_common_magnifier.json5 new file mode 100644 index 000000000..53e3b0780 --- /dev/null +++ b/tests/data/preferences/scoped_common_magnifier.json5 @@ -0,0 +1,16 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.microsoft.windows.magnifier": { + "MagnificationMode": 3, + "http://registry.gpii.net/common/magnification": 1.5 + }, + "http://registry.gpii.net/common/tracking": [ "mouse", "caret" ] + } + } + } + } +} diff --git a/tests/data/preferences/screenreader_common.json5 b/tests/data/preferences/screenreader_common.json5 new file mode 100644 index 000000000..0e79a17cf --- /dev/null +++ b/tests/data/preferences/screenreader_common.json5 @@ -0,0 +1,23 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/speakTutorialMessages": false , + "http://registry.gpii.net/common/keyEcho": false , + "http://registry.gpii.net/common/wordEcho": true , + "http://registry.gpii.net/common/announceCapitals": true , + "http://registry.gpii.net/common/readingUnit": "word" , + "http://registry.gpii.net/common/punctuationVerbosity": "all" , + "http://registry.gpii.net/common/screenReaderTTS/enabled": true , + "http://registry.gpii.net/common/trackingTTS": ["mouse", "caret"], + "http://registry.gpii.net/common/speechRate": 400, + "http://registry.gpii.net/common/auditoryOutLanguage": "en-BS", + "http://registry.gpii.net/common/pitch": 0.1, + "http://registry.gpii.net/common/volumeTTS": 0.7 + } + } + } + } +} diff --git a/tests/data/preferences/screenreader_nvda.json5 b/tests/data/preferences/screenreader_nvda.json5 new file mode 100644 index 000000000..c774306c0 --- /dev/null +++ b/tests/data/preferences/screenreader_nvda.json5 @@ -0,0 +1,29 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/org.nvda-project": { + "speech.synth": "espeak", + "speech.outputDevice": "Microsoft Sound Mapper", + "speech.symbolLevel": 300, + "speech.espeak.rate": 40, + "speech.espeak.voice": "en-us", + "speech.espeak.pitch": 60, + "speech.espeak.volume": 80, + "reviewCursor.followFocus": "False", + "reviewCursor.followCaret": "True", + "reviewCursor.followMouse": "True", + "keyboard.speakTypedWords": "True", + "speech.espeak.rateBoost": "True", + "keyboard.speakTypedCharacters": "False", + "presentation.reportHelpBalloons": "False", + "speech.espeak.sayCapForCapitals": "True", + "virtualBuffers.autoSayAllOnPageLoad": "False" + } + } + } + } + } +} diff --git a/tests/data/preferences/screenreader_orca.json5 b/tests/data/preferences/screenreader_orca.json5 new file mode 100644 index 000000000..e8459aa14 --- /dev/null +++ b/tests/data/preferences/screenreader_orca.json5 @@ -0,0 +1,25 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/org.gnome.orca": { + "sayAllStyle": 1, + "enableEchoByWord": 1, + "enableEchoByCharacter": 0, + "voices.default.rate": 102.27272727272727, + "enableTutorialMessages": 0, + "voices.default.family": { + "locale": "en", + "name": "en-westindies" + }, + "voices.default.average-pitch": 1.0, + "voices.default.gain": 7, + "verbalizePunctuationStyle": 0 + } + } + } + } + } +} diff --git a/tests/data/preferences/settingsUser.json5 b/tests/data/preferences/settingsUser.json5 new file mode 100644 index 000000000..72cee4c73 --- /dev/null +++ b/tests/data/preferences/settingsUser.json5 @@ -0,0 +1,12 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/fontSize": 24 + } + } + } + } +} diff --git a/tests/data/preferences/wordhome_learningstools_application.json5 b/tests/data/preferences/wordhome_learningstools_application.json5 new file mode 100644 index 000000000..f9ac9ba27 --- /dev/null +++ b/tests/data/preferences/wordhome_learningstools_application.json5 @@ -0,0 +1,20 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.office.windowsWordHome365LearningTools": { + "ReadingModeColumnWidth": 2, + "ReadingModePageColor": 2, + "ReadingModePrintedPage": 0, + "ReadAloudVoiceId": "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech_OneCore\\Voices\\Tokens\\MSTTS_V110_enUS_ZiraM", + "ReadAloudVoiceRate": 2, + "ReadingModeSyllables": 1, + "ReadingModeTextSpacing": 1 + } + } + } + } + } +} diff --git a/tests/data/preferences/wordpro_learningstools_application.json5 b/tests/data/preferences/wordpro_learningstools_application.json5 new file mode 100644 index 000000000..72b08bb0d --- /dev/null +++ b/tests/data/preferences/wordpro_learningstools_application.json5 @@ -0,0 +1,20 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.office.windowsWordPro365LearningTools": { + "ReadingModeColumnWidth": 2, + "ReadingModePageColor": 2, + "ReadingModePrintedPage": 0, + "ReadAloudVoiceId": "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\TTS_MS_EN-US_ZIRA_11.0", + "ReadAloudVoiceRate": 2, + "ReadingModeSyllables": 1, + "ReadingModeTextSpacing": 1 + } + } + } + } + } +} diff --git a/tests/data/preferences/zoomtext_application.json5 b/tests/data/preferences/zoomtext_application.json5 new file mode 100644 index 000000000..5ce33be2d --- /dev/null +++ b/tests/data/preferences/zoomtext_application.json5 @@ -0,0 +1,55 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/com.aisquared.zoomtext": { + // [Preferences] + "Preferences.SmartInvert": 1, + + // [ZtSpeech] + "ZtSpeech.SapiProductName": "VocalizerExpressive-Dutch", + "ZtSpeech.Language": 12, + + // [VocalizerExpressive-Dutch Speech - Screen Reader] + // ================================================================== + "VocalizerExpressive-Dutch Speech - Screen Reader.Valid": 1, + "VocalizerExpressive-Dutch Speech - Screen Reader.SettingsValid": 1, + "VocalizerExpressive-Dutch Speech - Screen Reader.Muted": 0, + "VocalizerExpressive-Dutch Speech - Screen Reader.Voice": 0, + "VocalizerExpressive-Dutch Speech - Screen Reader.Rate": 120, + "VocalizerExpressive-Dutch Speech - Screen Reader.Volume": 70, + "VocalizerExpressive-Dutch Speech - Screen Reader.Pitch": 35, + + // [VocalizerExpressive-Dutch Speech - DocReader] + "VocalizerExpressive-Dutch Speech - DocReader.Valid": 1, + "VocalizerExpressive-Dutch Speech - DocReader.SettingsValid": 1, + "VocalizerExpressive-Dutch Speech - DocReader.Muted": 0, + "VocalizerExpressive-Dutch Speech - DocReader.Voice": 0, + "VocalizerExpressive-Dutch Speech - DocReader.Rate": 120, + "VocalizerExpressive-Dutch Speech - DocReader.Volume": 70, + "VocalizerExpressive-Dutch Speech - DocReader.Pitch": 35, + + // [Magnifier] + "PRIMARY.magPower": 140, + "STATIC 1.magPower": 140, + + // [EchoTyping] + "EchoTyping.Mode": 32769, + // [EchoMouse] + "EchoMouse.HoverMode": 1, + "EchoMouse.LineMode": 0, + + // [PointerScheme] + "PointerScheme.PtrEnhEnable": 1, + "PointerScheme.PtrEnhIndex": 2, + + // [Tracking] + "Tracking.RouteMouse": 0 + } + } + } + } + } +} diff --git a/tests/data/preferences/zoomtext_common.json5 b/tests/data/preferences/zoomtext_common.json5 new file mode 100644 index 000000000..4a164cb67 --- /dev/null +++ b/tests/data/preferences/zoomtext_common.json5 @@ -0,0 +1,12 @@ +{ + "flat": { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/magnification": 1.6 + } + } + } + } +} diff --git a/tests/platform/android/android-builtIn-testSpec.js b/tests/platform/android/android-builtIn-testSpec.js new file mode 100644 index 000000000..a85dd8a36 --- /dev/null +++ b/tests/platform/android/android-builtIn-testSpec.js @@ -0,0 +1,104 @@ +/* +GPII Integration and Acceptance Testing + +Copyright 2014 Emergya + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.android.builtIn"); + +gpii.tests.android.builtIn = [ + { + name: "Testing os_android using default matchmaker", + gpiiKey: "os_android", + settingsHandlers: { + "gpii.androidSettings": { + "some.app.id": [{ + "settings": { + "dim_screen": true, + "haptic_feedback_enabled": true, + "accelerometer_rotation": false, + "user_rotation": 2, + "screen_off_timeout": 10000 + }, + "options": { + "settingType": "System" + } + }] + }, + "gpii.androidAudioManager.volume": { + "some.app.id": [{ + "settings": { + "STREAM_SYSTEM": 0.5 + } + }] + }, + "gpii.androidPersistentConfiguration": { + "some.app.id": [{ + "settings": { + "fontScale": 1.5, + "locale": "en" + } + }] + } + } + }, + { + name: "Testing os_android_common using default matchmaker", + gpiiKey: "os_android_common", + settingsHandlers: { + "gpii.androidAudioManager.volume": { + "some.app.id": [{ + "settings": { + "STREAM_SYSTEM": 4 + } + }] + }, + "gpii.androidPersistentConfiguration": { + "some.app.id": [{ + "settings": { + "fontScale": 2, + "locale": "en_GB" + } + }] + } + } + }, + { + name: "Testing os_common using default matchmaker", + gpiiKey: "os_common", + settingsHandlers: { + "gpii.androidPersistentConfiguration": { + "some.app.id": [{ + "settings": { + "fontScale": 0.75 + } + }] + } + } + } +]; + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.android.builtIn", + configName: "gpii.tests.acceptance.android.builtIn.config", + configPath: "%gpii-universal/tests/platform/android/configs" +}, ["gpii.test.integration.testCaseHolder.android"], + module, require, __dirname); diff --git a/tests/platform/android/android-builtIn-testSpec.txt b/tests/platform/android/android-builtIn-testSpec.txt new file mode 100644 index 000000000..0f389c05c --- /dev/null +++ b/tests/platform/android/android-builtIn-testSpec.txt @@ -0,0 +1,10 @@ +AcceptanceTests_builtIn.js + +Description: + +This tests the built in functionality of Android settings. +It tests the system using four preference sets: + (1) os_android: application specific android settings + (2) os_android_common: android related common terms + (3) os_gnome: application specific gnome settings + (4) os_common: os related common terms diff --git a/tests/platform/android/android-talkback-testSpec.js b/tests/platform/android/android-talkback-testSpec.js new file mode 100644 index 000000000..0c79f075b --- /dev/null +++ b/tests/platform/android/android-talkback-testSpec.js @@ -0,0 +1,110 @@ +/* +GPII Integration and Acceptance Testing + +Copyright 2014 Emergya + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.android.talkback"); + +gpii.tests.android.talkback = [ + { + name: "Testing talkback1 using Flat matchmaker", + gpiiKey: "talkback1", + settingsHandlers: { + "gpii.androidAudioManager.volume": { + "some.app.id": [ + { + "settings": { + "STREAM_MUSIC": 14.5 + } + } + ] + }, + "gpii.androidSettings": { + "some.app.id": [ + { + "settings": { + "tts_default_rate": 450.99, + "tts_default_pitch": 450.99 + }, + "options": { + "settingType": "Secure" + } + } + ] + } + } + }, + { + name: "Testing talkback2 using Flat matchmaker", + gpiiKey: "talkback2", + settingsHandlers: { + "gpii.androidSettings": { + "some.app.id": [ + { + "settings": { + "tts_default_rate": 496, + "tts_default_pitch": 500 + }, + "options": { + "settingType": "Secure" + } + } + ] + } + } + }, + { + name: "Testing screenreader_nvda using Flat matchmaker", + gpiiKey: "screenreader_common", + settingsHandlers: { + "gpii.androidAudioManager.volume": { + "some.app.id": [ + { + "settings": { + "STREAM_MUSIC": 11 + } + } + ] + }, + "gpii.androidSettings": { + "some.app.id": [ + { + "settings": { + "tts_default_rate": 681, + "tts_default_pitch": 50 + }, + "options": { + "settingType": "Secure" + } + } + ] + } + } + } +]; + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.android.talkback", + configName: "gpii.tests.acceptance.android.talkback.config", + configPath: "%gpii-universal/tests/platform/android/configs" +}, ["gpii.test.integration.testCaseHolder.android"], + module, require, __dirname); diff --git a/tests/platform/android/android-talkback-testSpec.txt b/tests/platform/android/android-talkback-testSpec.txt new file mode 100644 index 000000000..9af10de40 --- /dev/null +++ b/tests/platform/android/android-talkback-testSpec.txt @@ -0,0 +1,11 @@ +AcceptanceTests_talkback.js + +Description: + +This will run the acceptance tests for Talkback screen reader. +It tests the system using four preference sets: + (1) talkback1: application specific talkback settings + (2) talkback2: screen reader common terms + (3) screenreader_common: screen reader common terms + (4) screenreader_orca: application specific orca settings + (5) screenreader_nvda: application specific nvda settings diff --git a/tests/platform/android/configs/gpii.tests.acceptance.android.builtIn.config.json b/tests/platform/android/configs/gpii.tests.acceptance.android.builtIn.config.json new file mode 100644 index 000000000..62bca1d77 --- /dev/null +++ b/tests/platform/android/configs/gpii.tests.acceptance.android.builtIn.config.json @@ -0,0 +1,13 @@ +{ + "type": "gpii.tests.acceptance.android.builtIn.config", + "options": { + "distributeOptions": { + "acceptance.installedSolutionsPath": { + "record": "%gpii-universal/testData/deviceReporter/acceptanceTests/android_builtIn.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/tests/platform/android/configs/gpii.tests.acceptance.android.builtIn.config.txt b/tests/platform/android/configs/gpii.tests.acceptance.android.builtIn.config.txt new file mode 100644 index 000000000..9b629914e --- /dev/null +++ b/tests/platform/android/configs/gpii.tests.acceptance.android.builtIn.config.txt @@ -0,0 +1,8 @@ +This config file is used for integration and acceptance tests on Android. + +It includes to the basic localInstall setup for acceptance tests (modifying the +preferences folder used), which in turn includes the standard development +config file (running GPII locally, using development setup). + +This config sets the device reporter file to be 'android_builtIn.json', which will +report only the standard built in solutions as installed. diff --git a/tests/platform/android/configs/gpii.tests.acceptance.android.talkback.config.json b/tests/platform/android/configs/gpii.tests.acceptance.android.talkback.config.json new file mode 100644 index 000000000..af9176392 --- /dev/null +++ b/tests/platform/android/configs/gpii.tests.acceptance.android.talkback.config.json @@ -0,0 +1,13 @@ +{ + "type": "gpii.tests.acceptance.android.talkback.config", + "options": { + "distributeOptions": { + "acceptance.installedSolutionsPath": { + "record": "%gpii-universal/testData/deviceReporter/acceptanceTests/talkback.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/tests/platform/android/configs/gpii.tests.acceptance.android.talkback.config.txt b/tests/platform/android/configs/gpii.tests.acceptance.android.talkback.config.txt new file mode 100644 index 000000000..640847d84 --- /dev/null +++ b/tests/platform/android/configs/gpii.tests.acceptance.android.talkback.config.txt @@ -0,0 +1,8 @@ +This configuration file is used by the for acceptance test: android-talkback-testSpec.js. + +It includes to the basic localInstall setup for acceptance tests (modifying the +preferences folder used), which in turn includes the standard development +config file (running GPII locally, using development setup). + +This config sets the device reporter file to be 'talkback.json', which will report +Talkback as being installed on the system. diff --git a/tests/platform/cloud/CloudRevisionGetTests.js b/tests/platform/cloud/CloudRevisionGetTests.js new file mode 100644 index 000000000..e9070768b --- /dev/null +++ b/tests/platform/cloud/CloudRevisionGetTests.js @@ -0,0 +1,120 @@ +/*! +Tests for CBFM's /revision endpoint + +Copyright 2020 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.registerNamespace("jqUnit"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.cloudRevision"); + +gpii.tests.cloudRevision.validGpiiRevision = require( + fluid.module.resolvePath( + "%gpii-universal/tests/data/gpiiRevision/gpii-revision-valid.json" + ) +); +gpii.tests.cloudRevision.invalidGpiiRevision = require( + fluid.module.resolvePath( + "%gpii-universal/tests/data/gpiiRevision/gpii-revision-invalid.json" + ) +); + +gpii.tests.cloudRevision.commonTestDefs = { + testUrl: "/revision", + expectedStatusCode: 200, // provided by each test run + expectedPayload: {}, // provided by each test run + distributeOptions: [{ + source: "{that}.options.testUrl", + target: "{that > serverRevisionRequest}.options.path" + }], + config: { + configName: "gpii.config.cloudBased.development", + configPath: "%gpii-universal/gpii/configs/shared" + }, + components: { + serverRevisionRequest: { + type: "kettle.test.request.http", + options: { + method: "GET", + port: 8081 + } + } + }, + sequence: [{ + func: "{serverRevisionRequest}.send" + }, { + event: "{serverRevisionRequest}.events.onComplete", + listener: "gpii.tests.cloudRevision.testResponse", + args: [ + "{that}.options.expectedStatusCode", + "{that}.options.expectedPayload", + "{serverRevisionRequest}.nativeResponse.statusCode", + "{arguments}.0" // response payload + ] + }] +}; + +gpii.tests.cloudRevision.testResponse = function (expectedStatus, expectedPayload, actualStatus, actualPayload) { + jqUnit.assertEquals("Checking http status code", expectedStatus, actualStatus); + jqUnit.assertDeepEq("Checking payload", expectedPayload, JSON.parse(actualPayload)); +}; + +gpii.tests.cloudRevision.testCases = { + testValidRevision: { + name: "Test retrieval of well-formed revision of the CBFM", + pathDistributeOptions: { + record: "%gpii-universal/tests/data/gpiiRevision/gpii-revision-valid.json", + target: "{that gpii.flowManager.cloudBased}.options.gpiiRevisionPath" + }, + expectedStatusCode: 200, + expectedPayload: gpii.tests.cloudRevision.validGpiiRevision + }, + testInvalidRevision: { + name: "Test failure to retrieve malformed revision of the CBFM", + pathDistributeOptions: { + record: "%gpii-universal/tests/data/gpiiRevision/gpii-revision-invalid.json", + target: "{that gpii.flowManager.cloudBased}.options.gpiiRevisionPath" + }, + expectedStatusCode: 404, + expectedPayload: { + "isError": true, + "message": "Error retrieving full git revision: Missing revision value" + } + }, + testMissingRevision: { + name: "Test missing revision of the CBFM", + pathDistributeOptions: { + record: "NoSuchFileZZzzz.json", + target: "{that gpii.flowManager.cloudBased}.options.gpiiRevisionPath" + }, + expectedStatusCode: 404, + expectedPayload: { + "isError": true, + "message": "Error retrieving full git revision: ENOENT: no such file or directory, open 'NoSuchFileZZzzz.json'" + } + } +}; + +fluid.each(gpii.tests.cloudRevision.testCases, function (aCase) { + var aTestDef = fluid.extend(true, {}, gpii.tests.cloudRevision.commonTestDefs, { + name: aCase.name, + expectedStatusCode: aCase.expectedStatusCode, + expectedPayload: aCase.expectedPayload + }); + aTestDef.distributeOptions.push(aCase.pathDistributeOptions); + gpii.test.runCouchTestDefs([aTestDef]); +}); diff --git a/tests/platform/cloud/CloudRevisionGetTests.txt b/tests/platform/cloud/CloudRevisionGetTests.txt new file mode 100644 index 000000000..c15d851d1 --- /dev/null +++ b/tests/platform/cloud/CloudRevisionGetTests.txt @@ -0,0 +1,7 @@ +CloudRevisionGetTests.js + +Description: +This is a series of tests of the cloud based flow manager's /revision endpoint and its GET handler using test (mock) revision files. + +Requirements: +GPII is installed and running a local "clouBased" flow manager. diff --git a/tests/platform/cloud/CloudStatusTests.js b/tests/platform/cloud/CloudStatusTests.js new file mode 100644 index 000000000..e702428b5 --- /dev/null +++ b/tests/platform/cloud/CloudStatusTests.js @@ -0,0 +1,137 @@ +/*! +Copyright 2018-2019 OCAD university + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.registerNamespace("jqUnit"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.cloudStatus"); + +gpii.tests.cloudStatus.configs = { + allLocalWithPrefsServer: { + configName: "gpii.config.cloudBased.development", + configPath: "%gpii-universal/gpii/configs/shared" + }, + allLocalWithoutPrefsServer: { + configName: "gpii.flowManager.config.cloud.base", + configPath: "%flowManager/configs" + }, + separateServersWithPrefsServer: { + configName: "gpii.tests.cloudFlowManager.prefsServer.local", + configPath: "%gpii-universal/tests/configs" + } +}; + +gpii.tests.cloudStatus.commonTestDefs = { + testUrl: null, // provided by each test run + expectedStatusCode: 200, // provided by each test run + expectedPayload: {}, // provided by each test run + distributeOptions: { + source: "{that}.options.testUrl", + target: "{that > serverStatusRequest}.options.path" + }, + components: { + serverStatusRequest: { + type: "kettle.test.request.http", + options: { + method: "GET", + port: 8081 + } + } + }, + sequence: [{ + func: "{serverStatusRequest}.send" + }, { + event: "{serverStatusRequest}.events.onComplete", + listener: "gpii.tests.cloudStatus.testResponse", + args: [ + "{that}.options.expectedStatusCode", + "{that}.options.expectedPayload", + "{serverStatusRequest}.nativeResponse.statusCode", + "{arguments}.0" // response payload + ] + }] +}; + +gpii.tests.cloudStatus.testResponse = function (expectedStatus, expectedPayload, actualStatus, actualPayload) { + jqUnit.assertEquals("Checking http status code", expectedStatus, actualStatus); + jqUnit.assertDeepEq("Checking payload", expectedPayload, JSON.parse(actualPayload)); +}; + +gpii.tests.cloudStatus.testCases = { + // Test cases for /health + testHealthWithAllUp: { + name: "Test the readiness of the cloud based flow manager: all local with both the cloud based flow manager and the prefs server up running", + url: "/health", + config: gpii.tests.cloudStatus.configs.allLocalWithPrefsServer, + expectedStatusCode: 200, + expectedPayload: {"isHealthy": true} + }, + testHealthWithoutPrefsServer: { + name: "Test the readiness of the cloud based flow manager: all local with only the cloud based flow manager running", + url: "/health", + config: gpii.tests.cloudStatus.configs.allLocalWithoutPrefsServer, + expectedStatusCode: 200, + expectedPayload: {"isHealthy": true} + }, + testHealthWithSeparateServers: { + name: "Test the readiness of the cloud based flow manager: the cloud based flow manager and the prefs server run as separate servers", + url: "/health", + config: gpii.tests.cloudStatus.configs.separateServersWithPrefsServer, + expectedStatusCode: 200, + expectedPayload: {"isHealthy": true} + }, + + // Test cases for /ready + testReadyWithAllUp: { + name: "Test the liveness of the cloud based flow manager: all local with both the cloud based flow manager and the prefs server up running", + url: "/ready", + config: gpii.tests.cloudStatus.configs.allLocalWithPrefsServer, + expectedStatusCode: 200, + expectedPayload: {"isReady": true} + }, + testReadyWithoutPrefsServer: { + name: "Test the liveness of the cloud based flow manager: all local with only the cloud based flow manager running", + url: "/ready", + config: gpii.tests.cloudStatus.configs.allLocalWithoutPrefsServer, + expectedStatusCode: 503, + expectedPayload: { + "isError": true, + "message": "Error connecting to Preferences Server" + } + }, + testReadyWithSeparateServers: { + name: "Test the liveness of the cloud based flow manager: the cloud based flow manager and the prefs server run as separate servers", + url: "/ready", + config: gpii.tests.cloudStatus.configs.separateServersWithPrefsServer, + expectedStatusCode: 200, + expectedPayload: {"isReady": true} + } +}; + +fluid.each(gpii.tests.cloudStatus.testCases, function (oneCase) { + var oneTestDef = fluid.extend(true, {}, gpii.tests.cloudStatus.commonTestDefs, { + name: oneCase.name, + testUrl: oneCase.url, + expectedStatusCode: oneCase.expectedStatusCode, + expectedPayload: oneCase.expectedPayload + }, { + config: oneCase.config + }); + + gpii.test.runCouchTestDefs([oneTestDef]); +}); diff --git a/tests/platform/cloud/CloudStatusTests.txt b/tests/platform/cloud/CloudStatusTests.txt new file mode 100644 index 000000000..f9bb215ef --- /dev/null +++ b/tests/platform/cloud/CloudStatusTests.txt @@ -0,0 +1,4 @@ +CloudStatusTests.js + +Description: +This is a series of tests using different configs to test the status endpoints of Cloud Based Flow Manager: /ready and /health. diff --git a/tests/platform/cloud/DisruptionSettingsSequenceGradeConfig.js b/tests/platform/cloud/DisruptionSettingsSequenceGradeConfig.js new file mode 100644 index 000000000..295b3bb03 --- /dev/null +++ b/tests/platform/cloud/DisruptionSettingsSequenceGradeConfig.js @@ -0,0 +1,25 @@ +/*! +Copyright 2019 OCAD university +Licensed under the New BSD license. You may not use this file except in +compliance with this License. +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); +fluid.registerNamespace("gpii.tests.cloud.oauth2.settings"); + +gpii.tests.cloud.oauth2.settings.config = { + configName: "gpii.config.cloudBased.development", + configPath: "%gpii-universal/gpii/configs/shared" +}; + +// Grade for "disruptions" that are also proper sequence grades. Use the grade +// for couch test harness for development config +fluid.defaults("gpii.test.disruption.settings.sequenceGrade", { + gradeNames: ["gpii.test.disruption", "gpii.test.couchSequenceGrade"] +}); diff --git a/tests/platform/cloud/SettingsGetTests.js b/tests/platform/cloud/SettingsGetTests.js new file mode 100644 index 000000000..4258c072d --- /dev/null +++ b/tests/platform/cloud/SettingsGetTests.js @@ -0,0 +1,32 @@ +/*! +Copyright 2017-2019 OCAD university + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); +require("./DisruptionSettingsSequenceGradeConfig.js"); +require("../../shared/FlowManagerSettingsGetTestDefs.js"); + +fluid.registerNamespace("gpii.tests.cloud.oauth2.settingsGet"); + +gpii.loadTestingSupport(); + +gpii.test.cloudBased.oauth2.runDisruptedTests( + gpii.tests.cloud.oauth2.settingsGet.disruptedTests, + gpii.tests.cloud.oauth2.settings.config, + "gpii.test.couchEnvironment" +); diff --git a/tests/platform/cloud/SettingsGetTests.txt b/tests/platform/cloud/SettingsGetTests.txt new file mode 100644 index 000000000..948ddad53 --- /dev/null +++ b/tests/platform/cloud/SettingsGetTests.txt @@ -0,0 +1,7 @@ +SettingsGetTests.js + +Description: +This is a series of tests using the cloud based flow manager to test the workflow that GPII app installations request settings in the ontology of preferences. Before requesting untrusted settings, the GPII app installation must request an access token from GPII Cloud using OAuth2 resource owner GPII key grant. This access token is then sent along with the untrusted setting request to represent the authorization of the GPII app installation to user settings. + +Requirements: +Since it's using the cloud based flow manager, there are NO REQUIREMENTS to the system other than the GPII being installed (and able to run in local 'cloudBased' flowmanager mode). diff --git a/tests/platform/cloud/SettingsPutTests.js b/tests/platform/cloud/SettingsPutTests.js new file mode 100644 index 000000000..997e9049e --- /dev/null +++ b/tests/platform/cloud/SettingsPutTests.js @@ -0,0 +1,34 @@ +/*! +Copyright 2017-2019 OCAD university + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); +require("./DisruptionSettingsSequenceGradeConfig.js"); +require("../../shared/FlowManagerSettingsPutTestDefs.js"); + +fluid.registerNamespace("gpii.tests.cloud.oauth2.settingsPut"); +gpii.loadTestingSupport(); + +gpii.test.cloudBased.oauth2.runDisruptedTests( + gpii.tests.cloud.oauth2.settingsPut.disruptedTests, + gpii.tests.cloud.oauth2.settings.config, + "gpii.test.couchEnvironment" +); + +gpii.test.cloudBased.oauth2.runDisruptedTests( + gpii.tests.cloud.oauth2.settingsPut.updateSnapsetTest, + gpii.tests.cloud.oauth2.settings.config, + "gpii.test.couchEnvironment" +); diff --git a/tests/platform/cloud/SettingsPutTests.txt b/tests/platform/cloud/SettingsPutTests.txt new file mode 100644 index 000000000..8c6e909a4 --- /dev/null +++ b/tests/platform/cloud/SettingsPutTests.txt @@ -0,0 +1,7 @@ +SettingsPutTests.js + +Description: +This is a series of tests using the cloud based flow manager to test the /settings PUT endpoint that GPII app installations use to update user settings. Before updating settings, the GPII app installation must request an access token from GPII Cloud using OAuth2 resource owner GPII key grant. This access token is then sent along with the update request to represent the authorization of the GPII app installation to user settings. + +Requirements: +Since it's using the cloud based flow manager, there are NO REQUIREMENTS to the system other than the GPII being installed (and able to run in local 'cloudBased' flowmanager mode). diff --git a/tests/platform/index-android.js b/tests/platform/index-android.js new file mode 100644 index 000000000..f0d8cd31f --- /dev/null +++ b/tests/platform/index-android.js @@ -0,0 +1,27 @@ +/* + +GPII Testing Android Architecture Configuration Index + +Copyright 2014 Lucendo Development Ltd. +Copyright 2014 Emergya + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; +// This file lists the test fixtures for the GPII system which run in the Android environment - +// this is used both for integration testing within the universal module and for acceptance +// testing within the windows module +// This is a useful site for manipulating which test fixtures will run during the development process + +module.exports = [ + "android/android-builtIn-testSpec.js", + "android/android-talkback-testSpec.js" +]; diff --git a/tests/platform/index-linux.js b/tests/platform/index-linux.js new file mode 100644 index 000000000..7e9e9788d --- /dev/null +++ b/tests/platform/index-linux.js @@ -0,0 +1,29 @@ +/* + +GPII Testing Linux Architecture Configuration Index + +Copyright 2014 Lucendo Development Ltd. + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; +// This file lists the test fixtures for the GPII system which run in the Linux environment - +// this is used both for integration testing within the universal module and for acceptance +// testing within the windows module +// This is a useful site for manipulating which test fixtures will run during the development process + +module.exports = [ + "linux/linux-builtIn-testSpec.js", + "linux/linux-orca-testSpec.js", + "linux/linux-uioPlus-testSpec.js", + "linux/linux-dynamicDeviceReporter-testSpec.js", + "linux/linux-xrandr-testSpec.js" +]; diff --git a/tests/platform/index-windows.js b/tests/platform/index-windows.js new file mode 100644 index 000000000..e2037c67c --- /dev/null +++ b/tests/platform/index-windows.js @@ -0,0 +1,36 @@ +/* + +GPII Testing Windows Architecture Configuration Index + +Copyright 2014 Lucendo Development Ltd. +Copyright 2016 RtF-US + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; +// This file lists the test fixtures for the GPII system which run in the Windows environment - +// this is used both for integration testing within the universal module and for acceptance +// testing within the windows module +// This is a useful site for manipulating which test fixtures will run during the development process + +module.exports = [ + "windows/windows-brightness-testSpec.js", + "windows/windows-builtIn-testSpec.js", + "windows/windows-jaws-testSpec.js", + "windows/windows-nvda-testSpec.js", + // TODO: Make the MAGic tests something other than a copy of the JAWS tests. + //"windows/windows-magic-testSpec.js", + "windows/windows-uioPlus-testSpec.js", + "windows/windows-zoomtext-testSpec.js", + "windows/windows-readWrite12-testSpec.js", + "windows/windows-dynamicDeviceReporter-testSpec.js", + "windows/windows-learningTools-testSpec.js" +]; diff --git a/tests/platform/linux/configs/gpii.tests.acceptance.linux.builtIn.config.json b/tests/platform/linux/configs/gpii.tests.acceptance.linux.builtIn.config.json new file mode 100644 index 000000000..790da246e --- /dev/null +++ b/tests/platform/linux/configs/gpii.tests.acceptance.linux.builtIn.config.json @@ -0,0 +1,13 @@ +{ + "type": "gpii.tests.acceptance.linux.builtIn", + "options": { + "distributeOptions": { + "acceptance.installedSolutionsPath": { + "record": "%gpii-universal/testData/deviceReporter/acceptanceTests/linux_builtIn.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/tests/platform/linux/configs/gpii.tests.acceptance.linux.builtIn.config.txt b/tests/platform/linux/configs/gpii.tests.acceptance.linux.builtIn.config.txt new file mode 100644 index 000000000..1880e0363 --- /dev/null +++ b/tests/platform/linux/configs/gpii.tests.acceptance.linux.builtIn.config.txt @@ -0,0 +1,8 @@ +This config file is used for integration and acceptance tests on Linux as well as the context integration tests + +It includes to the basic localInstall setup for acceptance tests (modifying the +preferences folder used), which in turn includes the standard development +config file (running GPII locally, using development setup). + +This config sets the device reporter file to be 'linux_builtIn.json', which will +report only the standard built in solutions as installed. diff --git a/tests/platform/linux/configs/gpii.tests.acceptance.linux.builtIn.untrusted.config.json b/tests/platform/linux/configs/gpii.tests.acceptance.linux.builtIn.untrusted.config.json new file mode 100644 index 000000000..e011cb832 --- /dev/null +++ b/tests/platform/linux/configs/gpii.tests.acceptance.linux.builtIn.untrusted.config.json @@ -0,0 +1,13 @@ +{ + "type": "gpii.tests.acceptance.linux.builtIn.untrusted.config", + "options": { + "distributeOptions": { + "acceptance.installedSolutionsPath": { + "record": "%gpii-universal/testData/deviceReporter/acceptanceTests/linux_builtIn.json", + "target": "{that localConfig deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.untrusted.development.local.json5" +} diff --git a/tests/platform/linux/configs/gpii.tests.acceptance.linux.dynamicDeviceReporter.config.json b/tests/platform/linux/configs/gpii.tests.acceptance.linux.dynamicDeviceReporter.config.json new file mode 100644 index 000000000..08e4405d7 --- /dev/null +++ b/tests/platform/linux/configs/gpii.tests.acceptance.linux.dynamicDeviceReporter.config.json @@ -0,0 +1,4 @@ +{ + "type": "gpii.tests.acceptance.linux.dynamicDeviceReporter.config", + "mergeConfigs": "%gpii-universal/tests/configs/gpii.tests.acceptance.localInstall.dynamicDeviceReporter.config.json5" +} diff --git a/tests/platform/linux/configs/gpii.tests.acceptance.linux.dynamicDeviceReporter.config.txt b/tests/platform/linux/configs/gpii.tests.acceptance.linux.dynamicDeviceReporter.config.txt new file mode 100644 index 000000000..c2d14dce0 --- /dev/null +++ b/tests/platform/linux/configs/gpii.tests.acceptance.linux.dynamicDeviceReporter.config.txt @@ -0,0 +1,4 @@ +This configuration file is used by the acceptance test: linux-dynamicDeviceReporter-config.js. + +This config makes use of dynamic device reporting, so it uses localInstall.dynamicDeviceReporter +as its base config, which uses the all.development.dr.production gpii's config. diff --git a/tests/platform/linux/configs/gpii.tests.acceptance.linux.orca.config.json b/tests/platform/linux/configs/gpii.tests.acceptance.linux.orca.config.json new file mode 100644 index 000000000..2fdc8de96 --- /dev/null +++ b/tests/platform/linux/configs/gpii.tests.acceptance.linux.orca.config.json @@ -0,0 +1,13 @@ +{ + "type": "gpii.tests.acceptance.linux.orca.config", + "options": { + "distributeOptions": { + "acceptance.installedSolutionsPath": { + "record": "%gpii-universal/testData/deviceReporter/acceptanceTests/orca.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/tests/platform/linux/configs/gpii.tests.acceptance.linux.orca.config.txt b/tests/platform/linux/configs/gpii.tests.acceptance.linux.orca.config.txt new file mode 100644 index 000000000..c8c82d5f0 --- /dev/null +++ b/tests/platform/linux/configs/gpii.tests.acceptance.linux.orca.config.txt @@ -0,0 +1,8 @@ +This configuration file is used by the for acceptance test: AcceptanceTests_orca.js. + +It includes to the basic localInstall setup for acceptance tests (modifying the +preferences folder used), which in turn includes the standard development +config file (running GPII locally, using development setup). + +This config sets the device reporter file to be 'orca.json', which will report +Orca as being installed on the system. diff --git a/tests/platform/linux/configs/gpii.tests.acceptance.linux.uioPlus.config.json b/tests/platform/linux/configs/gpii.tests.acceptance.linux.uioPlus.config.json new file mode 100644 index 000000000..9bf42a3b7 --- /dev/null +++ b/tests/platform/linux/configs/gpii.tests.acceptance.linux.uioPlus.config.json @@ -0,0 +1,13 @@ +{ + "type": "gpii.tests.acceptance.linux.uioPlus.config", + "options": { + "distributeOptions": { + "acceptance.installedSolutionsPath": { + "record": "%gpii-universal/testData/deviceReporter/acceptanceTests/uioPlus.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/tests/platform/linux/configs/gpii.tests.acceptance.linux.uioPlus.config.txt b/tests/platform/linux/configs/gpii.tests.acceptance.linux.uioPlus.config.txt new file mode 100644 index 000000000..bfc0b19f9 --- /dev/null +++ b/tests/platform/linux/configs/gpii.tests.acceptance.linux.uioPlus.config.txt @@ -0,0 +1,8 @@ +This configuration file is used for testing the browser extension in Linux + +It includes to the basic localInstall setup for acceptance tests (modifying the +preferences folder used), which in turn includes the standard development +config file (running GPII locally, using development setup). + +This config sets the device reporter file to be 'uioPlus.json', which will report +UIO+ as being installed on the system. diff --git a/tests/platform/linux/configs/gpii.tests.acceptance.linux.xrandr.config.json b/tests/platform/linux/configs/gpii.tests.acceptance.linux.xrandr.config.json new file mode 100644 index 000000000..402c36a1a --- /dev/null +++ b/tests/platform/linux/configs/gpii.tests.acceptance.linux.xrandr.config.json @@ -0,0 +1,13 @@ +{ + "type": "gpii.tests.acceptance.linux.xrandr", + "options": { + "distributeOptions": { + "acceptance.installedSolutionsPath": { + "record": "%gpii-universal/testData/deviceReporter/acceptanceTests/xrandr.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/tests/platform/linux/linux-builtIn-testSpec.js b/tests/platform/linux/linux-builtIn-testSpec.js new file mode 100644 index 000000000..60d921f29 --- /dev/null +++ b/tests/platform/linux/linux-builtIn-testSpec.js @@ -0,0 +1,441 @@ +/* +GPII Acceptance Testing + +Copyright 2014 Emergya + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. +*/ + +"use strict"; + + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.linux.builtIn"); + +gpii.tests.linux.builtIn = [ + { + name: "Testing os_common using default matchmaker", + gpiiKey: "os_common", + initialState: { + "gpii.gsettings.launch": { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "running": false + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-magnifier-enabled" + } + }], + "org.gnome.desktop.a11y.applications.onscreen-keyboard": [{ + "settings": { + "running": false + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-keyboard-enabled" + } + }] + } + }, + settingsHandlers: { + "gpii.gsettings": { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "mag-factor": 1.5, + "screen-position": "full-screen", + "mouse-tracking": "proportional", + "caret-tracking": "proportional", + "focus-tracking": "none" + }, + "options": { + "schema": "org.gnome.desktop.a11y.magnifier" + } + }], + "org.gnome.desktop.interface": [{ + "settings": { + "text-scaling-factor": 0.75, + "cursor-size": 41 + }, + "options": { + "schema": "org.gnome.desktop.interface" + } + }] + }, + "gpii.gsettings.launch": { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-magnifier-enabled" + } + }], + "org.gnome.desktop.a11y.applications.onscreen-keyboard": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-keyboard-enabled" + } + }] + } + } + }, { + name: "Testing os_common - magnifier running on startup", + gpiiKey: "os_common", + initialState: { + "gpii.gsettings.launch": { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-magnifier-enabled" + } + }], + "org.gnome.desktop.a11y.applications.onscreen-keyboard": [{ + "settings": { + "running": false + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-keyboard-enabled" + } + }] + } + }, + settingsHandlers: { + "gpii.gsettings": { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "mag-factor": 1.5, + "screen-position": "full-screen", + "mouse-tracking": "proportional", + "caret-tracking": "proportional", + "focus-tracking": "none" + }, + "options": { + "schema": "org.gnome.desktop.a11y.magnifier" + } + }], + "org.gnome.desktop.interface": [{ + "settings": { + "text-scaling-factor": 0.75, + "cursor-size": 41 + }, + "options": { + "schema": "org.gnome.desktop.interface" + } + }] + }, + "gpii.gsettings.launch": { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-magnifier-enabled" + } + }], + "org.gnome.desktop.a11y.applications.onscreen-keyboard": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-keyboard-enabled" + } + }] + } + } + }, { + name: "Testing os_common - magnifier and keyboard running on startup", + gpiiKey: "os_common", + initialState: { + "gpii.gsettings.launch": { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-magnifier-enabled" + } + }], + "org.gnome.desktop.a11y.applications.onscreen-keyboard": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-keyboard-enabled" + } + }] + } + }, + settingsHandlers: { + "gpii.gsettings": { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "mag-factor": 1.5, + "screen-position": "full-screen", + "mouse-tracking": "proportional", + "caret-tracking": "proportional", + "focus-tracking": "none" + }, + "options": { + "schema": "org.gnome.desktop.a11y.magnifier" + } + }], + "org.gnome.desktop.interface": [{ + "settings": { + "text-scaling-factor": 0.75, + "cursor-size": 41 + }, + "options": { + "schema": "org.gnome.desktop.interface" + } + }] + }, + "gpii.gsettings.launch": { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-magnifier-enabled" + } + }], + "org.gnome.desktop.a11y.applications.onscreen-keyboard": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-keyboard-enabled" + } + }] + } + } + }, + { + name: "Testing os_common2 using default matchmaker", + gpiiKey: "os_common2", + settingsHandlers: { + "gpii.gsettings": { + "some.app.id": [{ + "settings": { + "gtk-theme": "Adwaita", + "icon-theme": "gnome" + }, + "options": { + "schema": "org.gnome.desktop.interface" + } + }] + } + } + }, + { + name: "Testing os_gnome using default matchmaker", + gpiiKey: "os_gnome", + initialState: { + "gpii.gsettings.launch": { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "running": false + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-magnifier-enabled" + } + }] + } + }, + settingsHandlers: { + "gpii.gsettings": { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "mag-factor": 1.5, + "screen-position": "full-screen" + }, + "options": { + "schema": "org.gnome.desktop.a11y.magnifier" + } + }], + "org.gnome.desktop.interface": [{ + "settings": { + "text-scaling-factor": 0.75, + "cursor-size": 90 + }, + "options": { + "schema": "org.gnome.desktop.interface" + } + }] + }, + "gpii.alsa": { + "org.alsa-project": [{ + "settings": { + "masterVolume": 50 + } + }] + }, + "gpii.gsettings.launch": { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-magnifier-enabled" + } + }] + } + } + } +]; + +gpii.tests.linux.builtInHighContrast = [ + { + name: "Testing os_common_highContrast using default matchmaker", + gpiiKey: "os_common_highContrast", + initialState: { + "gpii.gsettings.launch": { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "running": false + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-magnifier-enabled" + } + }], + "org.gnome.desktop.a11y.applications.onscreen-keyboard": [{ + "settings": { + "running": false + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-keyboard-enabled" + } + }] + } + }, + settingsHandlers: { + "gpii.gsettings": { + "org.gnome.desktop.interface": [{ + "settings": { + "gtk-theme": "HighContrast", + "icon-theme": "HighContrast" + }, + "options": { + "schema": "org.gnome.desktop.interface" + } + }] + } + } + }, { + name: "Testing os_common_highContrast - magnifier running on startup", + gpiiKey: "os_common_highContrast", + initialState: { + "gpii.gsettings.launch": { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-magnifier-enabled" + } + }], + "org.gnome.desktop.a11y.applications.onscreen-keyboard": [{ + "settings": { + "running": false + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-keyboard-enabled" + } + }] + } + }, + settingsHandlers: { + "gpii.gsettings": { + "org.gnome.desktop.interface": [{ + "settings": { + "gtk-theme": "HighContrast", + "icon-theme": "HighContrast" + }, + "options": { + "schema": "org.gnome.desktop.interface" + } + }] + } + } + }, { + name: "Testing os_common_highContrast - magnifier and keyboard running on startup", + gpiiKey: "os_common_highContrast", + initialState: { + "gpii.gsettings.launch": { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-magnifier-enabled" + } + }], + "org.gnome.desktop.a11y.applications.onscreen-keyboard": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-keyboard-enabled" + } + }] + } + }, + settingsHandlers: { + "gpii.gsettings": { + "org.gnome.desktop.interface": [{ + "settings": { + "gtk-theme": "HighContrast", + "icon-theme": "HighContrast" + }, + "options": { + "schema": "org.gnome.desktop.interface" + } + }] + } + } + } +]; + +gpii.tests.linux.builtIn.testDefs = gpii.tests.linux.builtIn.concat(gpii.tests.linux.builtInHighContrast); + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.linux.builtIn.testDefs", + configName: "gpii.tests.acceptance.linux.builtIn.config", + configPath: "%gpii-universal/tests/platform/linux/configs" +}, ["gpii.test.integration.testCaseHolder.linux"], + module, require, __dirname); diff --git a/tests/platform/linux/linux-builtIn-testSpec.txt b/tests/platform/linux/linux-builtIn-testSpec.txt new file mode 100644 index 000000000..ef5e820b3 --- /dev/null +++ b/tests/platform/linux/linux-builtIn-testSpec.txt @@ -0,0 +1,11 @@ +AcceptanceTests_builtIn.js + +Description: + +This tests the built in functionality of GNU/Linux+GNOME, incl. magnifier, +onscreen keyboard, etc. It tests the system using four preference sets: + (1) os_common: common terms compatible with linux built in settings + (2) os_win: application specific windows settings + (3) os_gnome: application specific gnome settings + (4) os_common2: this is only used to check when highContrast/enabled=false + diff --git a/tests/platform/linux/linux-dynamicDeviceReporter-testSpec.js b/tests/platform/linux/linux-dynamicDeviceReporter-testSpec.js new file mode 100644 index 000000000..37130f4f9 --- /dev/null +++ b/tests/platform/linux/linux-dynamicDeviceReporter-testSpec.js @@ -0,0 +1,152 @@ +/* +GPII Integration and Acceptance Testing + +Copyright 2014 Emergya + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.deviceReporterAware.linux.orca"); + +gpii.tests.deviceReporterAware.linux.orca.testDefs = [ + { + name: "Testing screenreader_common using Flat matchmaker", + gpiiKey: "screenreader_common", + initialState: { + "gpii.gsettings.launch": { + "org.gnome.orca": [{ + "settings": { + "running": false + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-reader-enabled" + } + }] + } + }, + settingsHandlers: { + "gpii.orca": { + "data": [ + { + "settings": { + "sayAllStyle": 1, + "enableSpeech": 1, + "enableEchoByWord": 1, + "enableEchoByCharacter": 0, + "voices.default.rate": 102.27272727272727, + "voices.default.gain": 7, + "enableTutorialMessages": 0, + "voices.default.family": { + "locale": "en", + "name": "en-westindies" + }, + "verbalizePunctuationStyle": 0, + "voices.default.average-pitch": 1.0 + }, + "options": { + "user": "screenreader_common" + } + } + ] + }, + "gpii.gsettings.launch": { + "org.gnome.orca": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-reader-enabled" + } + }] + } + }, + deviceReporters: { + "gpii.packageKit.find": { + "expectInstalled": ["orca"] + } + } + }, + { + name: "Testing screenreader_orca using Flat matchmaker", + gpiiKey: "screenreader_orca", + initialState: { + "gpii.gsettings.launch": { + "org.gnome.orca": [{ + "settings": { + "running": false + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-reader-enabled" + } + }] + } + }, + settingsHandlers: { + "gpii.orca": { + "some.app.id": [ + { + "settings": { + "sayAllStyle": 1, + "enableEchoByWord": 1, + "enableEchoByCharacter": 0, + "voices.default.rate": 102.27272727272727, + "voices.default.gain": 7, + "enableTutorialMessages": 0, + "voices.default.family": { + "locale": "en", + "name": "en-westindies" + }, + "verbalizePunctuationStyle": 0, + "voices.default.average-pitch": 1.0 + }, + "options": { + "user": "screenreader_orca" + } + } + ] + }, + "gpii.gsettings.launch": { + "org.gnome.orca": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-reader-enabled" + } + }] + } + }, + deviceReporters: { + "gpii.packageKit.find": { + "expectInstalled": ["orca"] + } + } + } +]; + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.deviceReporterAware.linux.orca.testDefs", + configName: "gpii.tests.acceptance.linux.dynamicDeviceReporter.config", + configPath: "%gpii-universal/tests/platform/linux/configs" +}, ["gpii.test.integration.testCaseHolder.linux"], + module, require, __dirname); diff --git a/tests/platform/linux/linux-dynamicDeviceReporter-testSpec.txt b/tests/platform/linux/linux-dynamicDeviceReporter-testSpec.txt new file mode 100644 index 000000000..898de7aca --- /dev/null +++ b/tests/platform/linux/linux-dynamicDeviceReporter-testSpec.txt @@ -0,0 +1,6 @@ +linux-dynamicDeviceReporter-testSpec.js + +Descriptions: + +This will run the acceptance tests with dynamic device reporting. +It uses 3 preference sets: screenreader_common, screenreader_orca and screenreader_nvda diff --git a/tests/platform/linux/linux-orca-testSpec.js b/tests/platform/linux/linux-orca-testSpec.js new file mode 100644 index 000000000..503f0bf90 --- /dev/null +++ b/tests/platform/linux/linux-orca-testSpec.js @@ -0,0 +1,195 @@ +/* +GPII Integration and Acceptance Testing + +Copyright 2014 Emergya + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.linux.orca"); + +gpii.tests.linux.orca.testDefs = [ + { + name: "Testing screenreader_common", + gpiiKey: "screenreader_common", + initialState: { + "gpii.gsettings.launch": { + "org.gnome.orca": [{ + "settings": { + "running": false + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-reader-enabled" + } + }] + } + }, + settingsHandlers: { + "gpii.orca": { + "org.gnome.orca": [ + { + "settings": { + "sayAllStyle": 1, + "enableSpeech": 1, + "enableEchoByWord": 1, + "enableEchoByCharacter": 0, + "voices.default.rate": 102.27272727272727, + "voices.default.gain": 7, + "enableTutorialMessages": 0, + "voices.default.family": { + "locale": "en", + "name": "en-westindies" + }, + "verbalizePunctuationStyle": 0, + "voices.default.average-pitch": 1.0 + }, + "options": { + "user": "screenreader_common" + } + } + ] + }, + "gpii.gsettings.launch": { + "org.gnome.orca": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-reader-enabled" + } + }] + } + } + }, { + name: "Testing screenreader_common with orca running on login", + gpiiKey: "screenreader_common", + initialState: { + "gpii.gsettings.launch": { + "org.gnome.orca": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-reader-enabled" + } + }] + } + }, + settingsHandlers: { + "gpii.orca": { + "org.gnome.orca": [ + { + "settings": { + "sayAllStyle": 1, + "enableSpeech": 1, + "enableEchoByWord": 1, + "enableEchoByCharacter": 0, + "voices.default.rate": 102.27272727272727, + "voices.default.gain": 7, + "enableTutorialMessages": 0, + "voices.default.family": { + "locale": "en", + "name": "en-westindies" + }, + "verbalizePunctuationStyle": 0, + "voices.default.average-pitch": 1.0 + }, + "options": { + "user": "screenreader_common" + } + } + ] + }, + "gpii.gsettings.launch": { + "org.gnome.orca": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-reader-enabled" + } + }] + } + } + }, + { + name: "Testing screenreader_orca", + gpiiKey: "screenreader_orca", + initialState: { + "gpii.gsettings.launch": { + "org.gnome.orca": [{ + "settings": { + "running": false + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-reader-enabled" + } + }] + } + }, + settingsHandlers: { + "gpii.orca": { + "org.gnome.orca": [ + { + "settings": { + "sayAllStyle": 1, + "enableEchoByWord": 1, + "enableEchoByCharacter": 0, + "voices.default.rate": 102.27272727272727, + "voices.default.gain": 7, + "enableTutorialMessages": 0, + "voices.default.family": { + "locale": "en", + "name": "en-westindies" + }, + "verbalizePunctuationStyle": 0, + "voices.default.average-pitch": 1.0 + }, + "options": { + "user": "screenreader_orca" + } + } + ] + }, + "gpii.gsettings.launch": { + "org.gnome.orca": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-reader-enabled" + } + }] + } + } + } +]; + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.linux.orca.testDefs", + configName: "gpii.tests.acceptance.linux.orca.config", + configPath: "%gpii-universal/tests/platform/linux/configs" +}, ["gpii.test.integration.testCaseHolder.linux"], + module, require, __dirname); diff --git a/tests/platform/linux/linux-orca.testSpec.txt b/tests/platform/linux/linux-orca.testSpec.txt new file mode 100644 index 000000000..daa7ac485 --- /dev/null +++ b/tests/platform/linux/linux-orca.testSpec.txt @@ -0,0 +1,6 @@ +AcceptanceTests_orca.js + +Descriptions: + +This will run the acceptance tests for GNOME Orca screen reader. +It uses 3 preference sets: screenreader_orca, screenreader_nvda and screenreader_common. diff --git a/tests/platform/linux/linux-uioPlus-testSpec.js b/tests/platform/linux/linux-uioPlus-testSpec.js new file mode 100644 index 000000000..0d1330e6d --- /dev/null +++ b/tests/platform/linux/linux-uioPlus-testSpec.js @@ -0,0 +1,32 @@ +/* +GPII Integration and Acceptance Testing + +Copyright 2014 Emergya +Copyright 2017 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); +require("../shared/uioPlusTestDefs.js"); + +gpii.loadTestingSupport(); + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.uioPlus.testDefs", + configName: "gpii.tests.acceptance.linux.uioPlus.config", + configPath: "%gpii-universal/tests/platform/linux/configs" +}, ["gpii.test.integration.testCaseHolder.linux"], + module, require, __dirname); diff --git a/tests/platform/linux/linux-uioPlus-testSpec.txt b/tests/platform/linux/linux-uioPlus-testSpec.txt new file mode 100644 index 000000000..b341a7fc7 --- /dev/null +++ b/tests/platform/linux/linux-uioPlus-testSpec.txt @@ -0,0 +1,19 @@ +linux-uioPlus-testSpec.txt + +Descriptions: + +* Solution: UIO+ +* Device reporter file: uioPlus.json +* preference sets: + * uioPlus_character_space + * uioPlus_font_size + * uioPlus_high_contrast + * uioPlus_highlight_colour + * uioPlus_inputs_larger + * uioPlus_line_space + * uioPlus_multiple_settings + * uioPlus_self_voicing + * uioPlus_simplified + * uioPlus_support_tool + * uioPlus_syllabification + * uioPlus_toc diff --git a/tests/platform/linux/linux-xrandr-testSpec.js b/tests/platform/linux/linux-xrandr-testSpec.js new file mode 100644 index 000000000..205cbd146 --- /dev/null +++ b/tests/platform/linux/linux-xrandr-testSpec.js @@ -0,0 +1,55 @@ +/* +GPII Acceptance Testing + +Copyright 2017 Inclusive Design Research Centre, OCAD University. + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +The R&D leading to these results received funding from the +Department of Education - Grant H421A150005 (GPII-APCP). However, +these results do not necessarily represent the policy of the +Department of Education, and you should not assume endorsement by the +Federal Government. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.acceptance.linux.xrandr"); + +gpii.tests.acceptance.linux.xrandr.testDefs = fluid.freezeRecursive([ + { + name: "Testing os_linux_display using default matchmaker", + gpiiKey: "os_gnome_display", + settingsHandlers: { + "gpii.xrandr": { + "some.app.id": [{ + "settings": { + "screen-resolution": [{ + "width": 800, + "height": 600 + }] + } + }] + } + }, + processes: [] + } +]); + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.acceptance.linux.xrandr.testDefs", + configName: "gpii.tests.acceptance.linux.xrandr.config", + configPath: "%gpii-universal/tests/platform/linux/configs" +}, ["gpii.test.integration.testCaseHolder.linux"], + module, require, __dirname); diff --git a/tests/platform/linux/linux-xrandr-testSpec.txt b/tests/platform/linux/linux-xrandr-testSpec.txt new file mode 100644 index 000000000..bd5e45509 --- /dev/null +++ b/tests/platform/linux/linux-xrandr-testSpec.txt @@ -0,0 +1,7 @@ +AcceptanceTests_xrandr.js + +Description: + +This tests the functionality of GNOME/Linux XRandR bridge, or the linux display +settings handler. It tests the system using one preference set: + (1) os_gnome_display: application specific gnome settings. diff --git a/tests/platform/shared/uioPlusTestDefs.js b/tests/platform/shared/uioPlusTestDefs.js new file mode 100644 index 000000000..8480618c6 --- /dev/null +++ b/tests/platform/shared/uioPlusTestDefs.js @@ -0,0 +1,388 @@ +/* +GPII Integration and Acceptance Testing + +Copyright 2017-2018 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.tests.uioPlus"); + +gpii.tests.uioPlus.testDefs = [ + { + name: "Acceptance test for default preferences for UIO+", + gpiiKey: "uioPlus_defaults", + settingsHandlers: { + "gpii.settingsHandlers.webSockets": { + "data": [ + { + "settings": { + "characterSpace": 0, + "contrastTheme": "default", + "fontSize": 1, + "inputsLargerEnabled": false, + "lineSpace": 1, + "selectionTheme": "default", + "selfVoicingEnabled": false, + "simplifiedUiEnabled": false, + "syllabificationEnabled": false, + "tableOfContentsEnabled": false, + "wordSpace": 1 + }, + "options": { + "path": "net.gpii.uioPlus" + } + } + ] + } + } + }, + { + name: "Acceptance test for background color change for UIO+", + gpiiKey: "uioPlus_high_contrast", + settingsHandlers: { + "gpii.settingsHandlers.webSockets": { + "data": [ + { + "settings": { + "characterSpace": 0, + "contrastTheme": "wb", + "fontSize": 1, + "inputsLargerEnabled": false, + "lineSpace": 1, + "selectionTheme": "default", + "selfVoicingEnabled": false, + "simplifiedUiEnabled": false, + "syllabificationEnabled": false, + "tableOfContentsEnabled": false, + "wordSpace": 1 + }, + "options": { + "path": "net.gpii.uioPlus" + } + } + ] + } + } + }, + { + name: "Acceptance test for font size transformation for UIO+", + gpiiKey: "uioPlus_font_size", + settingsHandlers: { + "gpii.settingsHandlers.webSockets": { + "data": [ + { + "settings": { + "characterSpace": 0, + "contrastTheme": "default", + "fontSize": 2, + "inputsLargerEnabled": false, + "lineSpace": 1, + "selectionTheme": "default", + "selfVoicingEnabled": false, + "simplifiedUiEnabled": false, + "syllabificationEnabled": false, + "tableOfContentsEnabled": false, + "wordSpace": 1 + }, + "options": { + "path": "net.gpii.uioPlus" + } + } + ] + } + } + }, + { + name: "Acceptance test for line space transformation for UIO+", + gpiiKey: "uioPlus_line_space", + settingsHandlers: { + "gpii.settingsHandlers.webSockets": { + "data": [ + { + "settings": { + "characterSpace": 0, + "contrastTheme": "default", + "fontSize": 1, + "inputsLargerEnabled": false, + "lineSpace": 2, + "selectionTheme": "default", + "selfVoicingEnabled": false, + "simplifiedUiEnabled": false, + "syllabificationEnabled": false, + "tableOfContentsEnabled": false, + "wordSpace": 1 + }, + "options": { + "path": "net.gpii.uioPlus" + } + } + ] + } + } + }, + { + name: "Acceptance test for highlight color transformation for UIO+", + gpiiKey: "uioPlus_highlight_colour", + settingsHandlers: { + "gpii.settingsHandlers.webSockets": { + "data": [ + { + "settings": { + "characterSpace": 0, + "contrastTheme": "default", + "fontSize": 1, + "inputsLargerEnabled": false, + "lineSpace": 1, + "selectionTheme": "yellow", + "selfVoicingEnabled": false, + "simplifiedUiEnabled": false, + "syllabificationEnabled": false, + "tableOfContentsEnabled": false, + "wordSpace": 1 + }, + "options": { + "path": "net.gpii.uioPlus" + } + } + ] + } + } + }, + { + name: "Acceptance test for character space transformation for UIO+", + gpiiKey: "uioPlus_character_space", + settingsHandlers: { + "gpii.settingsHandlers.webSockets": { + "data": [ + { + "settings": { + "characterSpace": 0.2, + "contrastTheme": "default", + "fontSize": 1, + "inputsLargerEnabled": false, + "lineSpace": 1, + "selectionTheme": "default", + "selfVoicingEnabled": false, + "simplifiedUiEnabled": false, + "syllabificationEnabled": false, + "tableOfContentsEnabled": false, + "wordSpace": 1 + }, + "options": { + "path": "net.gpii.uioPlus" + } + } + ] + } + } + }, + { + name: "Acceptance test for inputs larger transformation for UIO+", + gpiiKey: "uioPlus_inputs_larger", + settingsHandlers: { + "gpii.settingsHandlers.webSockets": { + "data": [ + { + "settings": { + "characterSpace": 0, + "contrastTheme": "default", + "fontSize": 1, + "inputsLargerEnabled": true, + "lineSpace": 1, + "selectionTheme": "default", + "selfVoicingEnabled": false, + "simplifiedUiEnabled": false, + "syllabificationEnabled": false, + "tableOfContentsEnabled": false, + "wordSpace": 1 + }, + "options": { + "path": "net.gpii.uioPlus" + } + } + ] + } + } + }, + { + name: "Acceptance test for self voicing transformation for UIO+", + gpiiKey: "uioPlus_self_voicing", + settingsHandlers: { + "gpii.settingsHandlers.webSockets": { + "data": [ + { + "settings": { + "characterSpace": 0, + "contrastTheme": "default", + "fontSize": 1, + "inputsLargerEnabled": false, + "lineSpace": 1, + "selectionTheme": "default", + "selfVoicingEnabled": true, + "simplifiedUiEnabled": false, + "syllabificationEnabled": false, + "tableOfContentsEnabled": false, + "wordSpace": 1 + }, + "options": { + "path": "net.gpii.uioPlus" + } + } + ] + } + } + }, + { + name: "Acceptance test for table of contents transformation for UIO+", + gpiiKey: "uioPlus_toc", + settingsHandlers: { + "gpii.settingsHandlers.webSockets": { + "data": [ + { + "settings": { + "characterSpace": 0, + "contrastTheme": "default", + "fontSize": 1, + "inputsLargerEnabled": false, + "lineSpace": 1, + "selectionTheme": "default", + "selfVoicingEnabled": false, + "simplifiedUiEnabled": false, + "syllabificationEnabled": false, + "tableOfContentsEnabled": true, + "wordSpace": 1 + }, + "options": { + "path": "net.gpii.uioPlus" + } + } + ] + } + } + }, + { + name: "Acceptance test for simplified UI transformation for UIO+", + gpiiKey: "uioPlus_simplified", + settingsHandlers: { + "gpii.settingsHandlers.webSockets": { + "data": [ + { + "settings": { + "characterSpace": 0, + "contrastTheme": "default", + "fontSize": 1, + "inputsLargerEnabled": false, + "lineSpace": 1, + "selectionTheme": "default", + "selfVoicingEnabled": false, + "simplifiedUiEnabled": true, + "syllabificationEnabled": false, + "tableOfContentsEnabled": false, + "wordSpace": 1 + }, + "options": { + "path": "net.gpii.uioPlus" + } + } + ] + } + } + }, + { + name: "Acceptance test for syllabification transformation for UIO+", + gpiiKey: "uioPlus_syllabification", + settingsHandlers: { + "gpii.settingsHandlers.webSockets": { + "data": [ + { + "settings": { + "characterSpace": 0, + "contrastTheme": "default", + "fontSize": 1, + "inputsLargerEnabled": false, + "lineSpace": 1, + "selectionTheme": "default", + "selfVoicingEnabled": false, + "simplifiedUiEnabled": false, + "syllabificationEnabled": true, + "tableOfContentsEnabled": false, + "wordSpace": 1 + }, + "options": { + "path": "net.gpii.uioPlus" + } + } + ] + } + } + }, + { + name: "Acceptance test for word space transformation for UIO+", + gpiiKey: "uioPlus_word_space", + settingsHandlers: { + "gpii.settingsHandlers.webSockets": { + "data": [ + { + "settings": { + "characterSpace": 0, + "contrastTheme": "default", + "fontSize": 1, + "inputsLargerEnabled": false, + "lineSpace": 1, + "selectionTheme": "default", + "selfVoicingEnabled": false, + "simplifiedUiEnabled": false, + "syllabificationEnabled": false, + "tableOfContentsEnabled": false, + "wordSpace": 3 + }, + "options": { + "path": "net.gpii.uioPlus" + } + } + ] + } + } + }, + { + name: "Acceptance test for multiple transformations for UIO+", + gpiiKey: "uioPlus_multiple_settings", + settingsHandlers: { + "gpii.settingsHandlers.webSockets": { + "data": [ + { + "settings": { + "characterSpace": 0, + "contrastTheme": "yb", + "fontSize": 1.3, + "inputsLargerEnabled": true, + "lineSpace": 1.3, + "selectionTheme": "green", + "selfVoicingEnabled": true, + "simplifiedUiEnabled": true, + "syllabificationEnabled": true, + "tableOfContentsEnabled": true, + "wordSpace": 2 + }, + "options": { + "path": "net.gpii.uioPlus" + } + } + ] + } + } + } +]; diff --git a/tests/platform/windows/configs/gpii.tests.acceptance.windows.brightness.config.json b/tests/platform/windows/configs/gpii.tests.acceptance.windows.brightness.config.json new file mode 100644 index 000000000..cf17db958 --- /dev/null +++ b/tests/platform/windows/configs/gpii.tests.acceptance.windows.brightness.config.json @@ -0,0 +1,13 @@ +{ + "type": "gpii.tests.acceptance.windows.brightness.config", + "options": { + "distributeOptions": { + "acceptance.installedSolutionsPath": { + "record": "%gpii-universal/testData/deviceReporter/acceptanceTests/win_brightness.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/tests/platform/windows/configs/gpii.tests.acceptance.windows.brightness.config.txt b/tests/platform/windows/configs/gpii.tests.acceptance.windows.brightness.config.txt new file mode 100644 index 000000000..8522713c5 --- /dev/null +++ b/tests/platform/windows/configs/gpii.tests.acceptance.windows.brightness.config.txt @@ -0,0 +1 @@ +This configuration file is used by the for acceptance test: windows-brigthness-testSpec.js. diff --git a/tests/platform/windows/configs/gpii.tests.acceptance.windows.builtIn.config.json b/tests/platform/windows/configs/gpii.tests.acceptance.windows.builtIn.config.json new file mode 100644 index 000000000..a520cfa34 --- /dev/null +++ b/tests/platform/windows/configs/gpii.tests.acceptance.windows.builtIn.config.json @@ -0,0 +1,13 @@ +{ + "type": "gpii.tests.acceptance.windows.builtIn.config", + "options": { + "distributeOptions": { + "acceptance.installedSolutionsPath": { + "record": "%gpii-universal/testData/deviceReporter/acceptanceTests/win7_builtIn.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/tests/platform/windows/configs/gpii.tests.acceptance.windows.builtIn.config.txt b/tests/platform/windows/configs/gpii.tests.acceptance.windows.builtIn.config.txt new file mode 100644 index 000000000..91d923402 --- /dev/null +++ b/tests/platform/windows/configs/gpii.tests.acceptance.windows.builtIn.config.txt @@ -0,0 +1,5 @@ +This configuration file is used by the for acceptance test: AcceptanceTests_builtIn.js. + +It includes to the basic localInstall setup for acceptance tests (modifying the preferences folder used), which in turn includes the standard development config file (running GPII locally, using development setup). + +This config sets the device reporter file to be 'win7_builtIn.json', which will report only the standard built in solutions as installed. \ No newline at end of file diff --git a/tests/platform/windows/configs/gpii.tests.acceptance.windows.jaws.config.json b/tests/platform/windows/configs/gpii.tests.acceptance.windows.jaws.config.json new file mode 100644 index 000000000..976cafed1 --- /dev/null +++ b/tests/platform/windows/configs/gpii.tests.acceptance.windows.jaws.config.json @@ -0,0 +1,13 @@ +{ + "type": "gpii.tests.acceptance.windows.jaws.config", + "options": { + "distributeOptions": { + "acceptance.installedSolutionsPath": { + "record": "%gpii-universal/testData/deviceReporter/acceptanceTests/jaws.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/tests/platform/windows/configs/gpii.tests.acceptance.windows.magic.config.json b/tests/platform/windows/configs/gpii.tests.acceptance.windows.magic.config.json new file mode 100644 index 000000000..52e12e344 --- /dev/null +++ b/tests/platform/windows/configs/gpii.tests.acceptance.windows.magic.config.json @@ -0,0 +1,13 @@ +{ + "type": "gpii.tests.acceptance.windows.magic.config", + "options": { + "distributeOptions": { + "acceptance.installedSolutionsPath": { + "record": "%gpii-universal/testData/deviceReporter/acceptanceTests/magic.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/tests/platform/windows/configs/gpii.tests.acceptance.windows.nvda.config.json b/tests/platform/windows/configs/gpii.tests.acceptance.windows.nvda.config.json new file mode 100644 index 000000000..c19e3b964 --- /dev/null +++ b/tests/platform/windows/configs/gpii.tests.acceptance.windows.nvda.config.json @@ -0,0 +1,13 @@ +{ + "type": "gpii.tests.acceptance.windows.nvda.config", + "options": { + "distributeOptions": { + "acceptance.installedSolutionsPath": { + "record": "%gpii-universal/testData/deviceReporter/acceptanceTests/nvda.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/tests/platform/windows/configs/gpii.tests.acceptance.windows.nvda.config.txt b/tests/platform/windows/configs/gpii.tests.acceptance.windows.nvda.config.txt new file mode 100644 index 000000000..6029932aa --- /dev/null +++ b/tests/platform/windows/configs/gpii.tests.acceptance.windows.nvda.config.txt @@ -0,0 +1,5 @@ +This configuration file is used by the for acceptance test: AcceptanceTests_nvda.js. + +It includes to the basic localInstall setup for acceptance tests (modifying the preferences folder used), which in turn includes the standard development config file (running GPII locally, using development setup). + +This config sets the device reporter file to be 'nvda.json', which will report nvda as being installed on the system. \ No newline at end of file diff --git a/tests/platform/windows/configs/gpii.tests.acceptance.windows.oneNoteLearningTools.config.json b/tests/platform/windows/configs/gpii.tests.acceptance.windows.oneNoteLearningTools.config.json new file mode 100644 index 000000000..e540888ac --- /dev/null +++ b/tests/platform/windows/configs/gpii.tests.acceptance.windows.oneNoteLearningTools.config.json @@ -0,0 +1,13 @@ +{ + "type": "gpii.tests.acceptance.windows.oneNoteLearningTools.config", + "options": { + "distributeOptions": { + "acceptance.installedSolutionsPath": { + "record": "%gpii-universal/testData/deviceReporter/acceptanceTests/oneNoteLearningTools.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/tests/platform/windows/configs/gpii.tests.acceptance.windows.oneNoteLearningTools.config.txt b/tests/platform/windows/configs/gpii.tests.acceptance.windows.oneNoteLearningTools.config.txt new file mode 100644 index 000000000..33dff706c --- /dev/null +++ b/tests/platform/windows/configs/gpii.tests.acceptance.windows.oneNoteLearningTools.config.txt @@ -0,0 +1,12 @@ +This configuration file is used by the for acceptance test: windows-learningTools-testSpec.js. + +It includes to the basic localInstall setup for acceptance tests +(modifying the preferences folder used), which in turn includes +the standard development config file (running GPII locally, using development setup). + +This config sets the device reporter files to be: + - 'oneNoteLearningTools.json'. + - 'wordHome365LearningTools.json'. + - 'wordPro365LearningTools.json'. + +Which will report only the standard built in solutions as installed. \ No newline at end of file diff --git a/tests/platform/windows/configs/gpii.tests.acceptance.windows.readWrite.config.json b/tests/platform/windows/configs/gpii.tests.acceptance.windows.readWrite.config.json new file mode 100644 index 000000000..f27c95475 --- /dev/null +++ b/tests/platform/windows/configs/gpii.tests.acceptance.windows.readWrite.config.json @@ -0,0 +1,13 @@ +{ + "type": "gpii.tests.acceptance.windows.readWrite.config", + "options": { + "distributeOptions": { + "acceptance.installedSolutionsPath": { + "record": "%gpii-universal/testData/deviceReporter/acceptanceTests/readWrite.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/tests/platform/windows/configs/gpii.tests.acceptance.windows.readWrite.config.txt b/tests/platform/windows/configs/gpii.tests.acceptance.windows.readWrite.config.txt new file mode 100644 index 000000000..24f6bd073 --- /dev/null +++ b/tests/platform/windows/configs/gpii.tests.acceptance.windows.readWrite.config.txt @@ -0,0 +1,5 @@ +This configuration file is used by the acceptance tests for Read&Write Gold + +It includes the basic localInstall setup for acceptance tests (modifying the preferences folder used), which in turn includes the standard development config file (running GPII locally, using development setup). + +This config sets the device reporter file to be 'readWrite.json', which will report Read&Write Gold as being installed on the system. diff --git a/tests/platform/windows/configs/gpii.tests.acceptance.windows.uioPlus.config.json b/tests/platform/windows/configs/gpii.tests.acceptance.windows.uioPlus.config.json new file mode 100644 index 000000000..989121407 --- /dev/null +++ b/tests/platform/windows/configs/gpii.tests.acceptance.windows.uioPlus.config.json @@ -0,0 +1,13 @@ +{ + "type": "gpii.tests.acceptance.windows.uioPlus", + "options": { + "distributeOptions": { + "acceptance.installedSolutionsPath": { + "record": "%gpii-universal/testData/deviceReporter/acceptanceTests/uioPlus.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/tests/platform/windows/configs/gpii.tests.acceptance.windows.uioPlus.config.txt b/tests/platform/windows/configs/gpii.tests.acceptance.windows.uioPlus.config.txt new file mode 100644 index 000000000..eb0d1f265 --- /dev/null +++ b/tests/platform/windows/configs/gpii.tests.acceptance.windows.uioPlus.config.txt @@ -0,0 +1,8 @@ +This configuration file is used for testing the browser extension in Windows + +It includes to the basic localInstall setup for acceptance tests (modifying the +preferences folder used), which in turn includes the standard development +config file (running GPII locally, using development setup). + +This config sets the device reporter file to be 'uioPlus.json', which will report +UIO+ as being installed on the system. diff --git a/tests/platform/windows/configs/gpii.tests.acceptance.windows.wordHomeLearningTools.config.json b/tests/platform/windows/configs/gpii.tests.acceptance.windows.wordHomeLearningTools.config.json new file mode 100644 index 000000000..6500ccc1f --- /dev/null +++ b/tests/platform/windows/configs/gpii.tests.acceptance.windows.wordHomeLearningTools.config.json @@ -0,0 +1,13 @@ +{ + "type": "gpii.tests.acceptance.windows.wordHome365LearningTools.config", + "options": { + "distributeOptions": { + "acceptance.installedSolutionsPath": { + "record": "%gpii-universal/testData/deviceReporter/acceptanceTests/testData/wordHome365LearningTools.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/tests/platform/windows/configs/gpii.tests.acceptance.windows.wordHomeLearningTools.config.txt b/tests/platform/windows/configs/gpii.tests.acceptance.windows.wordHomeLearningTools.config.txt new file mode 100644 index 000000000..33dff706c --- /dev/null +++ b/tests/platform/windows/configs/gpii.tests.acceptance.windows.wordHomeLearningTools.config.txt @@ -0,0 +1,12 @@ +This configuration file is used by the for acceptance test: windows-learningTools-testSpec.js. + +It includes to the basic localInstall setup for acceptance tests +(modifying the preferences folder used), which in turn includes +the standard development config file (running GPII locally, using development setup). + +This config sets the device reporter files to be: + - 'oneNoteLearningTools.json'. + - 'wordHome365LearningTools.json'. + - 'wordPro365LearningTools.json'. + +Which will report only the standard built in solutions as installed. \ No newline at end of file diff --git a/tests/platform/windows/configs/gpii.tests.acceptance.windows.wordProLearningTools.config.json b/tests/platform/windows/configs/gpii.tests.acceptance.windows.wordProLearningTools.config.json new file mode 100644 index 000000000..2ac8cfeb0 --- /dev/null +++ b/tests/platform/windows/configs/gpii.tests.acceptance.windows.wordProLearningTools.config.json @@ -0,0 +1,13 @@ +{ + "type": "gpii.tests.acceptance.windows.wordPro365LearningTools.config", + "options": { + "distributeOptions": { + "acceptance.installedSolutionsPath": { + "record": "%gpii-universal/testData/deviceReporter/acceptanceTests/wordPro365LearningTools.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/tests/platform/windows/configs/gpii.tests.acceptance.windows.wordProLearningTools.config.txt b/tests/platform/windows/configs/gpii.tests.acceptance.windows.wordProLearningTools.config.txt new file mode 100644 index 000000000..33dff706c --- /dev/null +++ b/tests/platform/windows/configs/gpii.tests.acceptance.windows.wordProLearningTools.config.txt @@ -0,0 +1,12 @@ +This configuration file is used by the for acceptance test: windows-learningTools-testSpec.js. + +It includes to the basic localInstall setup for acceptance tests +(modifying the preferences folder used), which in turn includes +the standard development config file (running GPII locally, using development setup). + +This config sets the device reporter files to be: + - 'oneNoteLearningTools.json'. + - 'wordHome365LearningTools.json'. + - 'wordPro365LearningTools.json'. + +Which will report only the standard built in solutions as installed. \ No newline at end of file diff --git a/tests/platform/windows/configs/gpii.tests.acceptance.windows.zoomtext.config.json b/tests/platform/windows/configs/gpii.tests.acceptance.windows.zoomtext.config.json new file mode 100644 index 000000000..7875bba8c --- /dev/null +++ b/tests/platform/windows/configs/gpii.tests.acceptance.windows.zoomtext.config.json @@ -0,0 +1,13 @@ +{ + "type": "gpii.tests.acceptance.windows.zoomtext.config", + "options": { + "distributeOptions": { + "acceptance.installedSolutionsPath": { + "record": "%gpii-universal/testData/deviceReporter/acceptanceTests/zoomtext.json", + "target": "{that deviceReporter installedSolutionsDataSource}.options.path", + "priority": "after:development.installedSolutionsPath" + } + } + }, + "mergeConfigs": "%gpii-universal/gpii/configs/shared/gpii.config.development.local.json5" +} diff --git a/tests/platform/windows/configs/windows-dynamicDeviceReporter-config.json b/tests/platform/windows/configs/windows-dynamicDeviceReporter-config.json new file mode 100644 index 000000000..2d936645b --- /dev/null +++ b/tests/platform/windows/configs/windows-dynamicDeviceReporter-config.json @@ -0,0 +1,6 @@ +{ + "type": "acceptanceTests.windows.dynamicDeviceReporter", + "mergeConfigs": [ + "%gpii-universal/tests/configs/gpii.tests.acceptance.localInstall.dynamicDeviceReporter.config.json5" + ] +} diff --git a/tests/platform/windows/configs/windows-dynamicDeviceReporter-config.txt b/tests/platform/windows/configs/windows-dynamicDeviceReporter-config.txt new file mode 100644 index 000000000..4c8de6355 --- /dev/null +++ b/tests/platform/windows/configs/windows-dynamicDeviceReporter-config.txt @@ -0,0 +1,4 @@ +This configuration file is used by the acceptance test: windows-dynamicDeviceReporter-config.js. + +This config makes use of dynamic device reporting, so it uses localInstallWithDynamicDeviceReporter +as its base config, which uses the all.development.dr.production gpii's config. diff --git a/tests/platform/windows/windows-brightness-testSpec.js b/tests/platform/windows/windows-brightness-testSpec.js new file mode 100644 index 000000000..83d05a0f4 --- /dev/null +++ b/tests/platform/windows/windows-brightness-testSpec.js @@ -0,0 +1,61 @@ +/* +GPII Acceptance Testing + +Copyright 2018 Raising the Floor International + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.windows.brightness"); + +gpii.tests.windows.brightness.testDefs = [ + { + name: "Testing preference set \"os_win_brightness\"", + gpiiKey: "os_win_brightness", + settingsHandlers: { + "gpii.windows.wmiSettingsHandler": { + "com.microsoft.windows.brightness": [{ + "settings": { + "Brightness": { + "value": 70 + } + }, + "options": { + "Brightness": { + "namespace": "root\\WMI", + "get": { "query": "SELECT CurrentBrightness FROM WmiMonitorBrightness" }, + "set": { + "className": "WmiMonitorBrightnessMethods", + "method": "WmiSetBrightness", + "params": [0xFFFFFFFF, "$value"], + "returnVal": ["uint", 0] + }, + "settingType": "uint" + } + } + }] + } + } + } +]; + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.windows.brightness.testDefs", + configName: "gpii.tests.acceptance.windows.brightness.config", + configPath: "%gpii-universal/tests/platform/windows/configs" +}, ["gpii.test.integration.testCaseHolder.windows"], + module, require, __dirname); diff --git a/tests/platform/windows/windows-brightness-testSpec.txt b/tests/platform/windows/windows-brightness-testSpec.txt new file mode 100644 index 000000000..d71b64c2f --- /dev/null +++ b/tests/platform/windows/windows-brightness-testSpec.txt @@ -0,0 +1,14 @@ +windows-brightness-testSpec.js + +Description: +This tests the Windows 10 built in functionality brightness, this feature is hardware +dependent and isn't available in VMs, so it is isolated in its own file. + +Requirements: + +* Being on a windows 10 machine. +* Having a screen which allows changing its brightness by software. +* Having compatible graphics hardware. + +As long as the system is able to change brightness using the system interface, +this feature should be available in that system, and tests could be exercised there. diff --git a/tests/platform/windows/windows-builtIn-testSpec.js b/tests/platform/windows/windows-builtIn-testSpec.js new file mode 100644 index 000000000..82580c19c --- /dev/null +++ b/tests/platform/windows/windows-builtIn-testSpec.js @@ -0,0 +1,1729 @@ +/* +GPII Integration and Acceptance Testing + +Copyright 2014 Raising the Floor International + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.windows"); + +gpii.tests.windows.builtIn = [ + { + name: "Testing os_win using default matchmaker", + gpiiKey: "os_win", + initialState: { + "gpii.windows.enableRegisteredAT": { + "com.microsoft.windows.magnifier": [{ + "settings": { + "running": false + }, + "options": { + "verifySettings": true, + retryOptions: { + rewriteEvery: 0, + numRetries: 20, + retryInterval: 1000 + }, + "registryName": "magnifierpane", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "Magnify.exe" + } + ] + } + }], + "com.microsoft.windows.narrator": [{ + "settings": { + "running": false + }, + "options": { + "registryName": "Narrator", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "Narrator.exe" + } + ] + } + }] + } + }, + settingsHandlers: { + "gpii.windows.nativeSettingsHandler": { + "com.microsoft.windows.mouseSettings": [ + { + "settings": { + "DoubleClickTimeConfig": { + "value": 600 + } + }, + "options": { + "functionName": "DoubleClickTime" + } + }, + { + "settings": { + "DoubleClickWidthConfig": { + "value": 4 + } + }, + "options": { + "functionName": "DoubleClickWidth" + } + }, + { + "settings": { + "DoubleClickHeightConfig": { + "value": 4 + } + }, + "options": { + "functionName": "DoubleClickHeight" + } + } + // TODO: Disabled due to GPII-4518 + //{ + // "settings": { + // "Volume": { + // "value": 0.7 + // } + // }, + // "options": { + // "functionName": "Volume" + // } + //} + ] + }, + "gpii.windows.spiSettingsHandler": { + "com.microsoft.windows.mouseSettings": [ + { + "settings": { + "SwapMouseButtonsConfig": { + "path": { + "get": "pvParam", + "set": "uiParam" + }, + "value": 1 + } + }, + "options": { + "getAction": "SPI_GETMOUSEBUTTONSWAP", + "setAction": "SPI_SETMOUSEBUTTONSWAP", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + } + } + }, + { + "settings": { + "ScrollWheelModeConfig": { + "path": { + "get": "pvParam", + "set": "uiParam" + }, + "value": 4294967294 + } + }, + "options": { + "getAction": "SPI_GETWHEELSCROLLLINES", + "setAction": "SPI_SETWHEELSCROLLLINES", + "uiParam": 0, + "pvParam": { + "type": "UINT" + } + } + }, + { + "settings": { + "ScrollFocusRoutingConfig": { + "path": "pvParam", + "value": 1 + } + }, + "options": { + "getAction": "SPI_GETMOUSEWHEELROUTING", + "setAction": "SPI_SETMOUSEWHEELROUTING", + "uiParam": 0, + "pvParam": { + "type": "UINT" + } + } + }, + { + "settings": { + "MouseCursorShadowEnable": { + "path": "pvParam", + "value": 1 + } + }, + "options": { + "getAction": "SPI_GETCURSORSHADOW", + "setAction": "SPI_SETCURSORSHADOW", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + } + } + }, + { + "settings": { + "ScrollCharsConfig": { + "path": { + "get": "pvParam", + "set": "uiParam" + }, + "value": 10 + } + }, + "options": { + "getAction": "SPI_GETWHEELSCROLLCHARS", + "setAction": "SPI_SETWHEELSCROLLCHARS", + "uiParam": 0, + "pvParam": { + "type": "UINT" + } + } + }, + { + "settings": { + "WindowsTrackingConfig": { + "path": "pvParam", + "value": 1 + } + }, + "options": { + "getAction": "SPI_GETACTIVEWINDOWTRACKING", + "setAction": "SPI_SETACTIVEWINDOWTRACKING", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + } + } + }, + { + "settings": { + "ActiveZOrder": { + "path": "pvParam", + "value": 0 + } + }, + "options": { + "getAction": "SPI_GETACTIVEWNDTRKZORDER", + "setAction": "SPI_SETACTIVEWNDTRKZORDER", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + } + } + }, + { + "settings": { + "WindowsArrangement": { + "path": { + "get": "pvParam", + "set": "uiParam" + }, + "value": 1 + } + }, + "options": { + "getAction": "SPI_GETWINARRANGING", + "setAction": "SPI_SETWINARRANGING", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + } + } + }, + { + "settings": { + "PointerSpeedConfig": { + "path": "pvParam", + "value": 15 + } + }, + "options": { + "getAction": "SPI_GETMOUSESPEED", + "setAction": "SPI_SETMOUSESPEED", + "uiParam": 0, + "pvParam": { + "type": "UINT" + } + } + }, + { + "settings": { + "EnhancePrecisionConfig": { + "path": "pvParam", + "value": [6, 10, 1] + } + }, + "options": { + "getAction": "SPI_GETMOUSE", + "setAction": "SPI_SETMOUSE", + "uiParam": 0, + "pvParam": { + "type": "array", + "valueType": "INT", + "length": 3 + } + } + }, + { + "settings": { + "SnapToDefaultButtonConfig": { + "path": { + "get": "pvParam", + "set": "uiParam" + }, + "value": 1 + } + }, + "options": { + "getAction": "SPI_GETSNAPTODEFBUTTON", + "setAction": "SPI_SETSNAPTODEFBUTTON", + "uiParam": 0, + "pvParam": { + "type": "UINT" + } + } + }, + { + "settings": { + "HidePointerConfig": { + "path": "pvParam", + "value": 0 + } + }, + "options": { + "getAction": "SPI_GETMOUSEVANISH", + "setAction": "SPI_SETMOUSEVANISH", + "uiParam": 0, + "pvParam": { + "type": "UINT" + } + } + }, + { + "settings": { + "MouseSonarConfig": { + "path": "pvParam", + "value": 1 + } + }, + "options": { + "getAction": "SPI_GETMOUSESONAR", + "setAction": "SPI_SETMOUSESONAR", + "uiParam": 0, + "pvParam": { + "type": "UINT" + } + } + } + ], + "com.microsoft.windows.mouseTrailing": [ + { + "settings": { + "MouseTrails": { + "path": { + "get": "pvParam", + "set": "uiParam" + }, + "value": 10 + } + }, + "options": { + "getAction": "SPI_GETMOUSETRAILS", + "setAction": "SPI_SETMOUSETRAILS", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + } + } + } + ], + "com.microsoft.windows.mouseKeys": [ + { + "settings": { + "MouseKeysOn": { + "path": "pvParam.dwFlags.MKF_MOUSEKEYSON", + "value": true + }, + "MaxSpeed": { + "path": "pvParam.iMaxSpeed", + "value": 100 + } + // See solution spec in win32.json + // "Acceleration": { + // "path": "pvParam.iTimeToMaxSpeed", + // "value": 1000 + // } + }, + "options": { + "getAction": "SPI_GETMOUSEKEYS", + "setAction": "SPI_SETMOUSEKEYS", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "MOUSEKEYS" + } + } + } + ], + "com.microsoft.windows.stickyKeys": [ + { + "settings": { + "StickyKeysOn": { + "path": "pvParam.dwFlags.SKF_STICKYKEYSON", + "value": true + } + }, + "options": { + "getAction": "SPI_GETSTICKYKEYS", + "setAction": "SPI_SETSTICKYKEYS", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "STICKYKEYS" + } + } + } + ], + "com.microsoft.windows.filterKeys": [ + { + "settings": { + "FilterKeysEnable": { + "path": "pvParam.dwFlags.FKF_FILTERKEYSON", + "value": true + } + // TODO: Reenable this once we can confirm a safe range of values that work with the SPI settings handler. + // "BounceKeysInterval": { + // "path": "pvParam.iBounceMSec", + // "value": 0 + // } + }, + "options": { + "getAction": "SPI_GETFILTERKEYS", + "setAction": "SPI_SETFILTERKEYS", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "FILTERKEYS" + } + } + } + ], + "com.microsoft.windows.toggleKeys": [ + { + "settings": { + "ToggleKeysOn": { + "path": "pvParam.dwFlags.TKF_TOGGLEKEYSON", + "value": true + } + }, + "options": { + "getAction": "SPI_GETTOGGLEKEYS", + "setAction": "SPI_SETTOGGLEKEYS", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "TOGGLEKEYS" + } + } + } + ], + "com.microsoft.windows.underlineMenuShortcuts": [ + { + "settings": { + "UnderlineMenuShortcutsOn": { + "path": "pvParam", + "value": 1 + } + }, + "options": { + "getAction": "SPI_GETKEYBOARDCUES", + "setAction": "SPI_SETKEYBOARDCUES", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + } + } + }, + { + "settings": { + "KeyboardPreferenceOn": { + "path": { + "get": "pvParam", + "set": "uiParam" + }, + "value": 1 + } + }, + "options": { + "getAction": "SPI_GETKEYBOARDPREF", + "setAction": "SPI_SETKEYBOARDPREF", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + } + } + } + ] + }, + "gpii.windows.registrySettingsHandler": { + "com.microsoft.windows.magnifier": [{ // magnifier stuff + "settings": { + "Invert": 1, + "Magnification": 150, + "MagnificationMode": 3, + "FollowFocus": 0, + "FollowCaret": 1, + "FollowMouse": 1, + "FollowNarrator": 0, + "FadeToMagIcon": 1, + "ZoomIncrement": 50, + "UseBitmapSmoothing": 1, + "LensHeight": 50, + "LensWidth": 50 + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\ScreenMagnifier", + "dataTypes": { + "Magnification": "REG_DWORD", + "Invert": "REG_DWORD", + "FollowFocus": "REG_DWORD", + "FollowCaret": "REG_DWORD", + "FollowMouse": "REG_DWORD", + "FollowNarrator": "REG_DWORD", + "MagnificationMode": "REG_DWORD", + "FadeToMagIcon": "REG_DWORD", + "ZoomIncrement": "REG_DWORD", + "UseBitmapSmoothing": "REG_DWORD", + "LensHeight": "REG_DWORD", + "LensWidth": "REG_DWORD" + } + } + }], + "com.microsoft.windows.cursors": [{ // cursor size stuff + "settings": { + "No": "%SystemRoot%\\cursors\\no_l.cur", + "Hand": "%SystemRoot%\\cursors\\aero_link_l.cur", + "Help": "%SystemRoot%\\cursors\\help_l.cur", + "Wait": "%SystemRoot%\\cursors\\busy_l.cur", + "Arrow": "%SystemRoot%\\cursors\\arrow_l.cur", + "NWPen": "%SystemRoot%\\cursors\\pen_l.cur", + "SizeNS": "%SystemRoot%\\cursors\\size4_l.cur", + "SizeWE": "%SystemRoot%\\cursors\\size3_l.cur", + "SizeAll": "%SystemRoot%\\cursors\\move_l.cur", + "UpArrow": "%SystemRoot%\\cursors\\up_l.cur", + "SizeNESW": "%SystemRoot%\\cursors\\size1_l.cur", + "SizeNWSE": "%SystemRoot%\\cursors\\size2_l.cur", + "AppStarting": "%SystemRoot%\\cursors\\wait_l.cur" + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Control Panel\\Cursors", + "dataTypes": { + "Arrow": "REG_SZ", + "Hand": "REG_SZ", + "Help": "REG_SZ", + "AppStarting": "REG_SZ", + "No": "REG_SZ", + "NWPen": "REG_SZ", + "SizeAll": "REG_SZ", + "SizeNESW": "REG_SZ", + "SizeNS": "REG_SZ", + "SizeNWSE": "REG_SZ", + "SizeWE": "REG_SZ", + "UpArrow": "REG_SZ", + "Wait": "REG_SZ" + } + } + }, { // Narrator + "settings": { + "SpeechSpeed": 11, + "SpeechPitch": 4, + "InteractionMouse": 1, + "CoupleNarratorCursorKeyboard": 1, + "FollowInsertion": 0, + "EchoChars": 1, + "EchoWords": 1, + "IntonationPause": 1, + "ReadHints": 1, + "PlayAudioCues": 1, + "NarratorCursorHighlight": 1, + "FastKeyEntryEnabled": 1, + "ReadingWithIntent": 1, + "ErrorNotificationType": 1, + "CoupleNarratorCursorMouse": 1, + "LockNarratorKeys": 1 + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\Narrator", + "dataTypes": { + "SpeechSpeed": "REG_DWORD", + "SpeechPitch": "REG_DWORD", + "InteractionMouse": "REG_DWORD", + "CoupleNarratorCursorKeyboard": "REG_DWORD", + "FollowInsertion": "REG_DWORD", + "EchoChars": "REG_DWORD", + "EchoWords": "REG_DWORD", + "IntonationPause": "REG_DWORD", + "ReadHints": "REG_DWORD", + "PlayAudioCues": "REG_DWORD", + "NarratorCursorHighlight": "REG_DWORD", + "FastKeyEntryEnabled": "REG_DWORD", + "ReadingWithIntent": "REG_DWORD", + "ErrorNotificationType": "REG_DWORD", + "CoupleNarratorCursorMouse": "REG_DWORD", + "LockNarratorKeys": "REG_DWORD" + } + } + }, { // Narrator (NoRoam) + "settings": { + "SpeechVolume": 80, + "SpeechVoice": "Microsoft Zira - English (United States)", + "ContextVerbosityLevel": 4, + "RenderContextBeforeElement": 1, + "DuckAudio": 1, + "WinEnterLaunchEnabled": 1, + "VerbosityLevel": 3, + "DetailedFeedback": 1 + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\Narrator\\NoRoam", + "dataTypes": { + "SpeechVolume": "REG_DWORD", + "SpeechVoice": "REG_SZ", + "ContextVerbosityLevel": "REG_DWORD", + "RenderContextBeforeElement": "REG_DWORD", + "DuckAudio": "REG_DWORD", + "WinEnterLaunchEnabled": "REG_DWORD", + "VerbosityLevel": "REG_DWORD", + "DetailedFeedback": "REG_DWORD" + } + } + } + // TODO: Confirm working individually using programatic tests. + // { // TypingEnhancement + // "settings": { + // "EnableAutocorrection": true, + // "EnableSpellchecking": true, + // "EnableTextPrediction": true, + // "EnablePredictionSpaceInsertion": true, + // "EnableDoubleTapSpace": true, + // "EnableKeyAudioFeedback": true, + // "EnableAutoShiftEngage": true, + // "EnableShiftLock": true, + // "EnableCompatibilityKeyboard": true, + // "EnableDesktopModeAutoInvoke": true + // }, + // "options": { + // "hKey": "HKEY_CURRENT_USER", + // "path": "Software\\Microsoft\\TabletTip\\1.7", + // "dataTypes": { + // "EnableAutocorrection": "REG_DWORD", + // "EnableSpellchecking": "REG_DWORD", + // "EnableTextPrediction": "REG_DWORD", + // "EnablePredictionSpaceInsertion": "REG_DWORD", + // "EnableDoubleTapSpace": "REG_DWORD", + // "EnableKeyAudioFeedback": "REG_DWORD", + // "EnableAutoShiftEngage": "REG_DWORD", + // "EnableShiftLock": "REG_DWORD", + // "EnableCompatibilityKeyboard": "REG_DWORD", + // "EnableDesktopModeAutoInvoke": "REG_DWORD" + // } + // } + // } + ], + "com.microsoft.windows.onscreenKeyboard": [{ + "settings": { + "NavigationMode": 0, + "ClickSound": 0, + "ShowClearKeyboard": 0, + "ShowNumPad": 1, + "Mode": 2, + "HoverPeriod": 1500, + "ScanInterval": 1500, + "UseDevice": 0, + "UseKB": 0, + "ScanKey": 113, + "UseMouse": 1, + "UseTextPrediction": 0, + "InsertSpace": 0, + "Dock": 0 + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\Osk", + "dataTypes": { + "NavigationMode": "REG_DWORD", + "ClickSound": "REG_DWORD", + "ShowClearKeyboard": "REG_DWORD", + "ShowNumPad": "REG_DWORD", + "Mode": "REG_DWORD", + "HoverPeriod": "REG_DWORD", + "ScanInterval": "REG_DWORD", + "UseDevice": "REG_DWORD", + "UseKB": "REG_DWORD", + "ScanKey": "REG_DWORD", + "UseMouse": "REG_DWORD", + "UseTextPrediction": "REG_DWORD", + "InsertSpace": "REG_DWORD", + "Dock": "REG_DWORD" + } + } + }], + "com.microsoft.windows.language": [{ + "settings": { + "PreferredUILanguages": "en-US" + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Control Panel\\Desktop", + "dataTypes": { + "PreferredUILanguages": "REG_SZ" + } + } + }, { + "settings": { + "MachinePreferredUILanguages": "en-US" + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Control Panel\\Desktop\\MuiCached", + "dataTypes": { + "MachinePreferredUILanguages": "REG_SZ" + } + } + }], + "com.microsoft.windows.soundSentry": [{ + "settings": { + "WindowsEffect": "2" + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Control Panel\\Accessibility\\SoundSentry", + "dataTypes": { + "WindowsEffect": "REG_SZ" + } + } + }], + // TODO: Break this apart and test each setting individually + "com.microsoft.windows.shortcutWarningMessage": [{ + "settings": { + "Warning Sounds": 1 + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Control Panel\\Accessibility", + "dataTypes": { + "Warning Sounds": "REG_DWORD" + } + } + }], + "com.microsoft.windows.shortcutWarningSound": [{ + "settings": { + "Sound on Activation": 1 + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Control Panel\\Accessibility", + "dataTypes": { + "Sound on Activation": "REG_DWORD" + } + } + }] + }, + "gpii.windows.displaySettingsHandler": { + "com.microsoft.windows.screenResolution": [{ + "settings": { + "screen-resolution": { + "width": 800, + "height": 600 + }, + "screen-dpi": 1 + } + }] + }, + "gpii.windows.enableRegisteredAT": { + "com.microsoft.windows.magnifier": [{ + "settings": { + "running": true + }, + "options": { + "verifySettings": true, + retryOptions: { + rewriteEvery: 0, + numRetries: 20, + retryInterval: 1000 + }, + "registryName": "magnifierpane", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "Magnify.exe" + } + ] + } + }] + }, + "gpii.windows.systemSettingsHandler": { + "com.microsoft.windows.nightScreen": [{ + "settings": { + "SystemSettings_Display_BlueLight_ManualToggleQuickAction": { + "value": false + } + }, + "options": { + "Async": true, + "CheckResult": true + } + }], + "com.microsoft.windows.touchPadSettings": [{ + "settings": { + "SystemSettings_Input_Touch_SetActivationTimeout": { + "value": "Low sensitivity" + } + }, + "options": { + "Async": true, + "CheckResult": true + } + }]/*, + * These settings have been commented out for https://issues.gpii.net/browse/GPII-4462 + "com.microsoft.windows.narrator": [{ + "settings": { + "SystemSettings_Accessibility_Narrator_IsAutoStartEnabled": { + "value": true + }, + "SystemSettings_Accessibility_Narrator_IsAutoStartOnLogonDesktopEnabled": { + "value": true + } + }, + "options": { + "Async": true, + "CheckResult": true + } + }]*/ + } + }, + gradeNames: "gpii.test.integration.actionHandlersAware.windows" + }, { + name: "Testing os_win_2 using default matchmaker", + gpiiKey: "os_win_2", + initialState: {}, + settingsHandlers: { + "gpii.windows.spiSettingsHandler": { + "com.microsoft.windows.desktopBackgroundColor": [{ + "settings": { + "ImageConfig": { + "path": "pvParam", + "value": "" + } + }, + "options": { + "getAction": "SPI_GETDESKWALLPAPER", + "setAction": "SPI_SETDESKWALLPAPER", + "uiParam": 260, + "pvParam": { + "type": "array", + "valueType": "TCHAR", + "length": 260 + } + } + }] + }, + "gpii.windows.nativeSettingsHandler": { + "com.microsoft.windows.desktopBackgroundColor": [{ + "settings": { + "SolidColorConfig": { + "value": { + "r": 67, "g": 187, "b": 19 + } + } + }, + "options": { + "functionName": "SolidColor" + } + }] + } + } + }, { + name: "Testing os_common using default matchmaker", + gpiiKey: "os_common", + initialState: { + "gpii.windows.enableRegisteredAT": { + "com.microsoft.windows.magnifier": [{ + "settings": { + "running": false + }, + "options": { + "verifySettings": true, + retryOptions: { + rewriteEvery: 0, + numRetries: 20, + retryInterval: 1000 + }, + "registryName": "magnifierpane", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "Magnify.exe" + } + ] + } + }], + "com.microsoft.windows.onscreenKeyboard": [{ + "settings": { + "running": false + }, + "options": { + "registryName": "osk", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "osk.exe" + } + ] + } + }] + } + }, + settingsHandlers: { + "gpii.windows.spiSettingsHandler": { + "com.microsoft.windows.mouseTrailing": [ + { + "settings": { + "MouseTrails": { + "path": { + "get": "pvParam", + "set": "uiParam" + }, + "value": 10 + } + }, + "options": { + "getAction": "SPI_GETMOUSETRAILS", + "setAction": "SPI_SETMOUSETRAILS", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + } + } + } + ], + "com.microsoft.windows.mouseKeys": [ + { + "settings": { + "MouseKeysOn": { + "path": "pvParam.dwFlags.MKF_MOUSEKEYSON", + "value": true + } + }, + "options": { + "getAction": "SPI_GETMOUSEKEYS", + "setAction": "SPI_SETMOUSEKEYS", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "MOUSEKEYS" + } + } + } + ], + "com.microsoft.windows.stickyKeys": [ + { + "settings": { + "StickyKeysOn": { + "path": "pvParam.dwFlags.SKF_STICKYKEYSON", + "value": true + } + }, + "options": { + "getAction": "SPI_GETSTICKYKEYS", + "setAction": "SPI_SETSTICKYKEYS", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "STICKYKEYS" + } + } + } + ], + "com.microsoft.windows.filterKeys": [ + { + "settings": { + "FilterKeysEnable": { + "path": "pvParam.dwFlags.FKF_FILTERKEYSON", + "value": true + } + }, + "options": { + "getAction": "SPI_GETFILTERKEYS", + "setAction": "SPI_SETFILTERKEYS", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "FILTERKEYS" + } + } + } + ] + }, + "gpii.windows.registrySettingsHandler": { + "com.microsoft.windows.magnifier": [{ // magnifier stuff + "settings": { + "Invert": 1, + "Magnification": 150, + "MagnificationMode": 3, + "FollowFocus": 0, + "FollowCaret": 1, + "FollowMouse": 1 + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\ScreenMagnifier", + "dataTypes": { + "Magnification": "REG_DWORD", + "Invert": "REG_DWORD", + "FollowFocus": "REG_DWORD", + "FollowCaret": "REG_DWORD", + "FollowMouse": "REG_DWORD", + "MagnificationMode": "REG_DWORD" + } + } + }], + "com.microsoft.windows.cursors": [{ // cursor size stuff + "settings": { + "No": "%SystemRoot%\\cursors\\no_l.cur", + "Hand": "%SystemRoot%\\cursors\\aero_link_l.cur", + "Help": "%SystemRoot%\\cursors\\help_l.cur", + "Wait": "%SystemRoot%\\cursors\\busy_l.cur", + "Arrow": "%SystemRoot%\\cursors\\arrow_l.cur", + "NWPen": "%SystemRoot%\\cursors\\pen_l.cur", + "SizeNS": "%SystemRoot%\\cursors\\size4_l.cur", + "SizeWE": "%SystemRoot%\\cursors\\size3_l.cur", + "SizeAll": "%SystemRoot%\\cursors\\move_l.cur", + "UpArrow": "%SystemRoot%\\cursors\\up_l.cur", + "SizeNESW": "%SystemRoot%\\cursors\\size1_l.cur", + "SizeNWSE": "%SystemRoot%\\cursors\\size2_l.cur", + "AppStarting": "%SystemRoot%\\cursors\\wait_l.cur" + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Control Panel\\Cursors", + "dataTypes": { + "Arrow": "REG_SZ", + "Hand": "REG_SZ", + "Help": "REG_SZ", + "AppStarting": "REG_SZ", + "No": "REG_SZ", + "NWPen": "REG_SZ", + "SizeAll": "REG_SZ", + "SizeNESW": "REG_SZ", + "SizeNS": "REG_SZ", + "SizeNWSE": "REG_SZ", + "SizeWE": "REG_SZ", + "UpArrow": "REG_SZ", + "Wait": "REG_SZ" + } + } + }] + }, + "gpii.windows.enableRegisteredAT": { + "com.microsoft.windows.magnifier": [{ + "settings": { + "running": true + }, + "options": { + "verifySettings": true, + retryOptions: { + rewriteEvery: 0, + numRetries: 20, + retryInterval: 1000 + }, + "registryName": "magnifierpane", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "Magnify.exe" + } + ] + } + }], + "com.microsoft.windows.onscreenKeyboard": [{ + "settings": { + "running": true + }, + "options": { + "registryName": "osk", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "osk.exe" + } + ] + } + }] + } + } + }, { + name: "Testing os_common - magnifier running on startup", + gpiiKey: "os_common", + initialState: { + "gpii.windows.enableRegisteredAT": { + "com.microsoft.windows.magnifier": [{ + "settings": { + "running": true + }, + "options": { + "verifySettings": true, + retryOptions: { + rewriteEvery: 0, + numRetries: 20, + retryInterval: 1000 + }, + "registryName": "magnifierpane", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "Magnify.exe" + } + ] + } + }], + "com.microsoft.windows.onscreenKeyboard": [{ + "settings": { + "running": false + }, + "options": { + "registryName": "osk", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "osk.exe" + } + ] + } + }] + } + }, + settingsHandlers: { + "gpii.windows.spiSettingsHandler": { + "com.microsoft.windows.mouseTrailing": [ + { + "settings": { + "MouseTrails": { + "path": { + "get": "pvParam", + "set": "uiParam" + }, + "value": 10 + } + }, + "options": { + "getAction": "SPI_GETMOUSETRAILS", + "setAction": "SPI_SETMOUSETRAILS", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + } + } + } + ], + "com.microsoft.windows.mouseKeys": [ + { + "settings": { + "MouseKeysOn": { + "path": "pvParam.dwFlags.MKF_MOUSEKEYSON", + "value": true + } + }, + "options": { + "getAction": "SPI_GETMOUSEKEYS", + "setAction": "SPI_SETMOUSEKEYS", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "MOUSEKEYS" + } + } + } + ], + "com.microsoft.windows.stickyKeys": [ + { + "settings": { + "StickyKeysOn": { + "path": "pvParam.dwFlags.SKF_STICKYKEYSON", + "value": true + } + }, + "options": { + "getAction": "SPI_GETSTICKYKEYS", + "setAction": "SPI_SETSTICKYKEYS", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "STICKYKEYS" + } + } + } + ], + "com.microsoft.windows.filterKeys": [ + { + "settings": { + "FilterKeysEnable": { + "path": "pvParam.dwFlags.FKF_FILTERKEYSON", + "value": true + } + }, + "options": { + "getAction": "SPI_GETFILTERKEYS", + "setAction": "SPI_SETFILTERKEYS", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "FILTERKEYS" + } + } + } + ] + }, + "gpii.windows.registrySettingsHandler": { + "com.microsoft.windows.magnifier": [{ // magnifier stuff + "settings": { + "Invert": 1, + "Magnification": 150, + "MagnificationMode": 3, + "FollowFocus": 0, + "FollowCaret": 1, + "FollowMouse": 1 + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\ScreenMagnifier", + "dataTypes": { + "Magnification": "REG_DWORD", + "Invert": "REG_DWORD", + "FollowFocus": "REG_DWORD", + "FollowCaret": "REG_DWORD", + "FollowMouse": "REG_DWORD", + "MagnificationMode": "REG_DWORD" + } + } + }], + "com.microsoft.windows.cursors": [{ // cursor size stuff + "settings": { + "No": "%SystemRoot%\\cursors\\no_l.cur", + "Hand": "%SystemRoot%\\cursors\\aero_link_l.cur", + "Help": "%SystemRoot%\\cursors\\help_l.cur", + "Wait": "%SystemRoot%\\cursors\\busy_l.cur", + "Arrow": "%SystemRoot%\\cursors\\arrow_l.cur", + "NWPen": "%SystemRoot%\\cursors\\pen_l.cur", + "SizeNS": "%SystemRoot%\\cursors\\size4_l.cur", + "SizeWE": "%SystemRoot%\\cursors\\size3_l.cur", + "SizeAll": "%SystemRoot%\\cursors\\move_l.cur", + "UpArrow": "%SystemRoot%\\cursors\\up_l.cur", + "SizeNESW": "%SystemRoot%\\cursors\\size1_l.cur", + "SizeNWSE": "%SystemRoot%\\cursors\\size2_l.cur", + "AppStarting": "%SystemRoot%\\cursors\\wait_l.cur" + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Control Panel\\Cursors", + "dataTypes": { + "Arrow": "REG_SZ", + "Hand": "REG_SZ", + "Help": "REG_SZ", + "AppStarting": "REG_SZ", + "No": "REG_SZ", + "NWPen": "REG_SZ", + "SizeAll": "REG_SZ", + "SizeNESW": "REG_SZ", + "SizeNS": "REG_SZ", + "SizeNWSE": "REG_SZ", + "SizeWE": "REG_SZ", + "UpArrow": "REG_SZ", + "Wait": "REG_SZ" + } + } + }] + }, + "gpii.windows.enableRegisteredAT": { + "com.microsoft.windows.magnifier": [{ + "settings": { + "running": true + }, + "options": { + "verifySettings": true, + retryOptions: { + rewriteEvery: 0, + numRetries: 20, + retryInterval: 1000 + }, + "registryName": "magnifierpane", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "Magnify.exe" + } + ] + } + }], + "com.microsoft.windows.onscreenKeyboard": [{ + "settings": { + "running": true + }, + "options": { + "registryName": "osk", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "osk.exe" + } + ] + } + }] + } + } + }, { + name: "Testing os_common - magnifier and keyboard both running on startup", + gpiiKey: "os_common", + initialState: { + "gpii.windows.enableRegisteredAT": { + "com.microsoft.windows.magnifier": [{ + "settings": { + "running": true + }, + "options": { + "verifySettings": true, + retryOptions: { + rewriteEvery: 0, + numRetries: 20, + retryInterval: 1000 + }, + "registryName": "magnifierpane", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "Magnify.exe" + } + ] + } + }], + "com.microsoft.windows.onscreenKeyboard": [{ + "settings": { + "running": true + }, + "options": { + "registryName": "osk", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "osk.exe" + } + ] + } + }] + } + }, + settingsHandlers: { + "gpii.windows.spiSettingsHandler": { + "com.microsoft.windows.mouseTrailing": [ + { + "settings": { + "MouseTrails": { + "path": { + "get": "pvParam", + "set": "uiParam" + }, + "value": 10 + } + }, + "options": { + "getAction": "SPI_GETMOUSETRAILS", + "setAction": "SPI_SETMOUSETRAILS", + "uiParam": 0, + "pvParam": { + "type": "BOOL" + } + } + } + ], + "com.microsoft.windows.mouseKeys": [ + { + "settings": { + "MouseKeysOn": { + "path": "pvParam.dwFlags.MKF_MOUSEKEYSON", + "value": true + } + }, + "options": { + "getAction": "SPI_GETMOUSEKEYS", + "setAction": "SPI_SETMOUSEKEYS", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "MOUSEKEYS" + } + } + } + ], + "com.microsoft.windows.stickyKeys": [ + { + "settings": { + "StickyKeysOn": { + "path": "pvParam.dwFlags.SKF_STICKYKEYSON", + "value": true + } + }, + "options": { + "getAction": "SPI_GETSTICKYKEYS", + "setAction": "SPI_SETSTICKYKEYS", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "STICKYKEYS" + } + } + } + ], + "com.microsoft.windows.filterKeys": [ + { + "settings": { + "FilterKeysEnable": { + "path": "pvParam.dwFlags.FKF_FILTERKEYSON", + "value": true + } + }, + "options": { + "getAction": "SPI_GETFILTERKEYS", + "setAction": "SPI_SETFILTERKEYS", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "FILTERKEYS" + } + } + } + ] + }, + "gpii.windows.registrySettingsHandler": { + "com.microsoft.windows.magnifier": [{ // magnifier stuff + "settings": { + "Invert": 1, + "Magnification": 150, + "MagnificationMode": 3, + "FollowFocus": 0, + "FollowCaret": 1, + "FollowMouse": 1 + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Microsoft\\ScreenMagnifier", + "dataTypes": { + "Magnification": "REG_DWORD", + "Invert": "REG_DWORD", + "FollowFocus": "REG_DWORD", + "FollowCaret": "REG_DWORD", + "FollowMouse": "REG_DWORD", + "MagnificationMode": "REG_DWORD" + } + } + }], + "com.microsoft.windows.cursors": [{ // cursor size stuff + "settings": { + "No": "%SystemRoot%\\cursors\\no_l.cur", + "Hand": "%SystemRoot%\\cursors\\aero_link_l.cur", + "Help": "%SystemRoot%\\cursors\\help_l.cur", + "Wait": "%SystemRoot%\\cursors\\busy_l.cur", + "Arrow": "%SystemRoot%\\cursors\\arrow_l.cur", + "NWPen": "%SystemRoot%\\cursors\\pen_l.cur", + "SizeNS": "%SystemRoot%\\cursors\\size4_l.cur", + "SizeWE": "%SystemRoot%\\cursors\\size3_l.cur", + "SizeAll": "%SystemRoot%\\cursors\\move_l.cur", + "UpArrow": "%SystemRoot%\\cursors\\up_l.cur", + "SizeNESW": "%SystemRoot%\\cursors\\size1_l.cur", + "SizeNWSE": "%SystemRoot%\\cursors\\size2_l.cur", + "AppStarting": "%SystemRoot%\\cursors\\wait_l.cur" + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "Control Panel\\Cursors", + "dataTypes": { + "Arrow": "REG_SZ", + "Hand": "REG_SZ", + "Help": "REG_SZ", + "AppStarting": "REG_SZ", + "No": "REG_SZ", + "NWPen": "REG_SZ", + "SizeAll": "REG_SZ", + "SizeNESW": "REG_SZ", + "SizeNS": "REG_SZ", + "SizeNWSE": "REG_SZ", + "SizeWE": "REG_SZ", + "UpArrow": "REG_SZ", + "Wait": "REG_SZ" + } + } + }] + }, + "gpii.windows.enableRegisteredAT": { + "com.microsoft.windows.magnifier": [{ + "settings": { + "running": true + }, + "options": { + "verifySettings": true, + retryOptions: { + rewriteEvery: 0, + numRetries: 20, + retryInterval: 1000 + }, + "registryName": "magnifierpane", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "Magnify.exe" + } + ] + } + }], + "com.microsoft.windows.onscreenKeyboard": [{ + "settings": { + "running": true + }, + "options": { + "registryName": "osk", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "osk.exe" + } + ] + } + }] + } + } + } +]; + +gpii.tests.windows.builtInHighContrast = [ + { + name: "Testing os_win_highContrast using default matchmaker", + gpiiKey: "os_win_highContrast", + gradeNames: "gpii.test.integration.actionHandlersAware.windows", + settingsHandlers: { + "gpii.windows.spiSettingsHandler": { + "com.microsoft.windows.highContrast": [ + { // high contrast settings + "settings": { + "HighContrastOn": { + "path": "pvParam.dwFlags.HCF_HIGHCONTRASTON", + "value": true + } + }, + "options": { + "getAction": "SPI_GETHIGHCONTRAST", + "setAction": "SPI_SETHIGHCONTRAST", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "HIGHCONTRAST" + } + } + } + ] + } + } + }, { + name: "Testing os_common_highContrast using default matchmaker", + gpiiKey: "os_common_highContrast", + gradeNames: "gpii.test.integration.actionHandlersAware.windows", + settingsHandlers: { + "gpii.windows.spiSettingsHandler": { + "com.microsoft.windows.highContrast": [ + { // high contrast settings + "settings": { + "HighContrastOn": { + "path": "pvParam.dwFlags.HCF_HIGHCONTRASTON", + "value": true + } + }, + "options": { + "getAction": "SPI_GETHIGHCONTRAST", + "setAction": "SPI_SETHIGHCONTRAST", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "HIGHCONTRAST" + } + } + } + ] + } + } + }, { + name: "Testing os_common_highContrast - magnifier running on startup", + gpiiKey: "os_common_highContrast", + gradeNames: "gpii.test.integration.actionHandlersAware.windows", + initialState: { + "gpii.windows.enableRegisteredAT": { + "com.microsoft.windows.magnifier": [{ + "settings": { + "running": true + }, + "options": { + "verifySettings": true, + retryOptions: { + rewriteEvery: 0, + numRetries: 20, + retryInterval: 1000 + }, + "registryName": "magnifierpane", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "Magnify.exe" + } + ] + } + }], + "com.microsoft.windows.onscreenKeyboard": [{ + "settings": { + "running": false + }, + "options": { + "registryName": "osk", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "osk.exe" + } + ] + } + }] + } + }, + settingsHandlers: { + "gpii.windows.spiSettingsHandler": { + "com.microsoft.windows.highContrast": [ + { // high contrast settings + "settings": { + "HighContrastOn": { + "path": "pvParam.dwFlags.HCF_HIGHCONTRASTON", + "value": true + } + }, + "options": { + "getAction": "SPI_GETHIGHCONTRAST", + "setAction": "SPI_SETHIGHCONTRAST", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "HIGHCONTRAST" + } + } + } + ] + } + } + }, { + name: "Testing os_common_highContrast - magnifier and keyboard both running on startup", + gpiiKey: "os_common_highContrast", + gradeNames: "gpii.test.integration.actionHandlersAware.windows", + initialState: { + "gpii.windows.enableRegisteredAT": { + "com.microsoft.windows.magnifier": [{ + "settings": { + "running": true + }, + "options": { + "verifySettings": true, + retryOptions: { + rewriteEvery: 0, + numRetries: 20, + retryInterval: 1000 + }, + "registryName": "magnifierpane", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "Magnify.exe" + } + ] + } + }], + "com.microsoft.windows.onscreenKeyboard": [{ + "settings": { + "running": true + }, + "options": { + "registryName": "osk", + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "osk.exe" + } + ] + } + }] + } + }, + settingsHandlers: { + "gpii.windows.spiSettingsHandler": { + "com.microsoft.windows.highContrast": [ + { // high contrast settings + "settings": { + "HighContrastOn": { + "path": "pvParam.dwFlags.HCF_HIGHCONTRASTON", + "value": true + } + }, + "options": { + "getAction": "SPI_GETHIGHCONTRAST", + "setAction": "SPI_SETHIGHCONTRAST", + "uiParam": "struct_size", + "pvParam": { + "type": "struct", + "name": "HIGHCONTRAST" + } + } + } + ] + } + } + } +]; + +gpii.tests.windows.builtIn.testDefs = gpii.tests.windows.builtInHighContrast.concat(gpii.tests.windows.builtIn); + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.windows.builtIn.testDefs", + configName: "gpii.tests.acceptance.windows.builtIn.config", + configPath: "%gpii-universal/tests/platform/windows/configs" +}, ["gpii.test.integration.testCaseHolder.windows"], + module, require); diff --git a/tests/platform/windows/windows-builtIn-testSpec.txt b/tests/platform/windows/windows-builtIn-testSpec.txt new file mode 100644 index 000000000..dfb4cbcc5 --- /dev/null +++ b/tests/platform/windows/windows-builtIn-testSpec.txt @@ -0,0 +1,9 @@ +windows-builtIn-testSpec.js + +Description: +This tests the built in functionality of windows, incl. magnifier, onscreen keyboard, mouse trails, etc. It tests the system using three preference sets; (1) os_common: common terms compatible with windows built in settings, (2) os_win: application specific windows settings (3) os_gnome: application specific gnome settings + +Requirements: +* Basic Windows 10 install + +There are NO REQUIREMENTS to be able to run this as an acceptance test, except being on a windows 10 machine. It should just run on a fresh install. diff --git a/tests/platform/windows/windows-dynamicDeviceReporter-testSpec.js b/tests/platform/windows/windows-dynamicDeviceReporter-testSpec.js new file mode 100644 index 000000000..1cd849663 --- /dev/null +++ b/tests/platform/windows/windows-dynamicDeviceReporter-testSpec.js @@ -0,0 +1,244 @@ +/* +GPII Integration and Acceptance Testing + +Copyright 2015 Raising The Floor US + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.deviceReporterAware.windows"); + +gpii.tests.deviceReporterAware.windows.flexibleHandlerEntries = { + nvda: function (running) { + return { + "org.nvda-project": [{ + "settings": { + "running": running + }, + "options": { + "verifySettings": true, + retryOptions: { + rewriteEvery: 0, + numRetries: 20, + retryInterval: 1000 + }, + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "nvda" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe\\}\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "nvda_service.exe" + },{ + "type": "gpii.windows.closeProcessByName", + "filename": "nvda.exe" + } + ] + } + }] + }; + }, + readwrite: function (running) { + return { + "com.texthelp.readWriteGold": [{ + "settings": { + "running": running + }, + "options": { + "verifySettings": true, + retryOptions: { + rewriteEvery: 0, + numRetries: 40, + retryInterval: 20000 + }, + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{environment}.SystemDrive}\\Program Files (x86)\\Texthelp\\Read And Write 12\\ReadAndWrite.exe\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "ReadAndWrite.exe", + "options": { + "message": "WM_CLOSE" + } + } + ], + "getState": [ + { + "type": "gpii.tests.windows.readwrite.findProcess", + "command": "ReadAndWrite.exe", + "time": 10 + } + ] + } + }] + }; + }, + brightness: function (running) { + return { + "com.windows.brightness": [{ + "settings": { + "running": running + }, + "options": {} + }] + }; + } +}; + + +gpii.tests.deviceReporterAware.windows.testDefs = [ + { + name: "Testing screenreader_nvda using Flat matchmaker", + gpiiKey: "screenreader_nvda", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.deviceReporterAware.windows.flexibleHandlerEntries.nvda(false) + }, + settingsHandlers: { + "gpii.settingsHandlers.INISettingsHandler": { + "data": [ + { + "settings": { + "speech.espeak.rate": 40, + "speech.espeak.volume": 80, + "speech.espeak.pitch": 60, + "speech.espeak.rateBoost": "True", + "speech.synth": "espeak", + "speech.outputDevice": "Microsoft Sound Mapper", + "speech.symbolLevel": 300, + "speech.espeak.voice": "en-us", + "reviewCursor.followFocus": "False", + "reviewCursor.followCaret": "True", + "reviewCursor.followMouse": "True", + "keyboard.speakTypedWords": "True", + "keyboard.speakTypedCharacters": "False", + "presentation.reportHelpBalloons": "False", + "speech.espeak.sayCapForCapitals": "True" + }, + "options": { + "filename": "${{environment}.APPDATA}\\nvda\\nvda.ini" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.deviceReporterAware.windows.flexibleHandlerEntries.nvda(true) + }, + deviceReporters: { + "gpii.deviceReporter.registryKeyExists": { + "expectInstalled": [{ + "hKey": "HKEY_LOCAL_MACHINE", + "path": "Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe", + "subPath": "" + }] + }, + "gpii.deviceReporter.wmiSettingSupported": { + "expectInstalled": [{ + "namespace": "root\\WMI", + "query": "SELECT CurrentBrightness FROM WmiMonitorBrightness" + }] + } + } + }, + { + name: "Testing readwritegold_application1 using Flat matchmaker", + gpiiKey: "readwritegold_general", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.deviceReporterAware.windows.flexibleHandlerEntries.readwrite(false) + }, + settingsHandlers: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.deviceReporterAware.windows.flexibleHandlerEntries.readwrite(true) + }, + deviceReporters: { + "gpii.deviceReporter.registryKeyExists": { + "expectInstalled": [{ + "hKey": "HKEY_CURRENT_USER", + "path": "Software\\Texthelp\\Voices", + "subPath": "DefaultTokenId", + "dataType": "REG_SZ" + }] + }, + "gpii.deviceReporter.wmiSettingSupported": { + "expectInstalled": [{ + "namespace": "root\\WMI", + "query": "SELECT CurrentBrightness FROM WmiMonitorBrightness" + }] + } + } + }, + { + name: "Testing os_win_brightness using Flat matchmaker", + gpiiKey: "os_win_brightness", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.deviceReporterAware.windows.flexibleHandlerEntries.brightness(false) + }, + settingsHandlers: { + "gpii.windows.wmiSettingsHandler": { + "data": [ + { + "settings": { + "Brightness": { + "value": 70 + } + }, + "options": { + "Brightness": { + "namespace": "root\\WMI", + "get": { "query": "SELECT CurrentBrightness FROM WmiMonitorBrightness" }, + "set": { + "className": "WmiMonitorBrightnessMethods", + "method": "WmiSetBrightness", + "params": [0xFFFFFFFF, "$value"], + "returnVal": ["uint", 0] + } + } + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.deviceReporterAware.windows.flexibleHandlerEntries.brightness(false) + }, + deviceReporters: { + "gpii.deviceReporter.wmiSettingSupported": { + "expectInstalled": [{ + "namespace": "root\\WMI", + "query": "SELECT CurrentBrightness FROM WmiMonitorBrightness" + }] + } + } + } +]; + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.deviceReporterAware.windows.testDefs", + configName: "windows-dynamicDeviceReporter-config", + configPath: "%gpii-universal/tests/platform/windows/configs" +}, ["gpii.test.integration.testCaseHolder.windows"], + module, require); diff --git a/tests/platform/windows/windows-dynamicDeviceReporter-testSpec.txt b/tests/platform/windows/windows-dynamicDeviceReporter-testSpec.txt new file mode 100644 index 000000000..ce042f44d --- /dev/null +++ b/tests/platform/windows/windows-dynamicDeviceReporter-testSpec.txt @@ -0,0 +1,13 @@ +windows-dynamicDeviceReporter-testSpec.js + +Descriptions: + +This will run the acceptance tests with dynamic device reporting. +It uses 2 preference sets: screenreader_nvda and readwritegold_application1. + +Requirements: + +* NVDA installed +* Read and Write Gold not installed + +To run these tests, you need to have NVDA INSTALLED. NVDA is free and can be downloaded from: http://www.nvaccess.org/download/ diff --git a/tests/platform/windows/windows-jaws-testSpec.js b/tests/platform/windows/windows-jaws-testSpec.js new file mode 100644 index 000000000..89465dc0e --- /dev/null +++ b/tests/platform/windows/windows-jaws-testSpec.js @@ -0,0 +1,424 @@ +/* +GPII Acceptance Testing + +Copyright 2014 Raising the Floor International + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.windows.jaws"); + +// To avoid duplicating this entire piece in each test. Given a true or false value +// as input, this will return a settingshandler entry, containing all the options from +// the solutions registry entry for NVDAs launchHandler, with a settings block with +// running: X - where X is replaced with the input parameter +gpii.tests.windows.jaws.flexibleHandlerEntry = function (running) { + return { + "com.freedomscientific.jaws": [{ + "settings": { + "running": running + }, + "options": { + "verifySettings": true, + retryOptions: { + rewriteEvery: 0, + numRetries: 20, + retryInterval: 1000 + }, + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "jfw.exe" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\JAWS2019.exe\\}\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "jfw.exe", + "options": { + "closeWindow": true + } + } + ] + } + }] + }; +}; + +gpii.tests.windows.jaws.testDefs = [ + { + name: "Testing preference set \"jaws_application\"", + gpiiKey: "jaws_application", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.settingsHandlers.INISettingsHandler": { + "com.freedomscientific.jaws": [ + { + "settings": { + "Options.SayAllIndicateCaps" : 0, + "Options.TypingEcho": 3, + "Options.SayAllMode": 0, + "Braille.BrailleMode": 0, + "Options.SayAllIgnoreShiftKeys": 1 + }, + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF" + } + } + ] + }, + "gpii.settingsHandlers.JAWSSettingsHandler": { + "com.freedomscientific.jaws": [ + { + "settings": { + // NOTE: Commented until GPII-4336 is addressed + // ============================================ + // "ENU-Global.Rate": 100, + // "ENU-JAWSCursor.Rate": 100, + // "ENU-Keyboard.Rate": 100, + // "ENU-MenuAndDialog.Rate": 100, + // "ENU-Message.Rate": 100, + // "ENU-PCCursor.Rate": 100, + // "ENU-Global.Pitch": 70, + // "ENU-JAWSCursor.Pitch": 70, + // "ENU-Keyboard.Pitch": 70, + // "ENU-MenuAndDialog.Pitch": 70, + // "ENU-Message.Pitch": 70, + // "ENU-PCCursor.Pitch": 70, + // "ENU-Global.Volume": 100, + // "ENU-JAWSCursor.Volume": 100, + // "ENU-Keyboard.Volume": 100, + // "ENU-MenuAndDialog.Volume": 100, + // "ENU-Message.Volume": 100, + // "ENU-PCCursor.Volume": 100, + // "ENU-Global.Punctuation": 2, + // "ENU-JAWSCursor.Punctuation": 2, + // "ENU-Keyboard.Punctuation": 2, + // "ENU-MenuAndDialog.Punctuation": 2, + // "ENU-Message.Punctuation": 2, + // "ENU-PCCursor.Punctuation": 2, + // "ENU-Global.SynthLangString": "Italian", + // "ENU-JAWSCursor.SynthLangString": "Italian", + // "ENU-Keyboard.SynthLangString": "Italian", + // "ENU-MenuAndDialog.SynthLangString": "Italian", + // "ENU-Message.SynthLangString": "Italian", + // "ENU-PCCursor.SynthLangString": "Italian" + }, + "options": { + "defaultSettingsFilePath": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", + "voiceProfilesDirPath": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\VoiceProfiles" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(true) + } + }, + { + name: "Testing preference set \"jaws_application\" - where jaws is running on startup", + gpiiKey: "jaws_application", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(true) + }, + settingsHandlers: { + "gpii.settingsHandlers.INISettingsHandler": { + "com.freedomscientific.jaws": [ + { + "settings": { + "Options.SayAllIndicateCaps" : 0, + "Options.TypingEcho": 3, + "Options.SayAllMode": 0, + "Braille.BrailleMode": 0, + "Options.SayAllIgnoreShiftKeys": 1 + }, + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF" + } + } + ] + }, + "gpii.settingsHandlers.JAWSSettingsHandler": { + "com.freedomscientific.jaws": [ + { + "settings": { + // NOTE: Commented until GPII-4336 is addressed + // ============================================ + // "ENU-Global.Rate": 100, + // "ENU-JAWSCursor.Rate": 100, + // "ENU-Keyboard.Rate": 100, + // "ENU-MenuAndDialog.Rate": 100, + // "ENU-Message.Rate": 100, + // "ENU-PCCursor.Rate": 100, + // "ENU-Global.Pitch": 70, + // "ENU-JAWSCursor.Pitch": 70, + // "ENU-Keyboard.Pitch": 70, + // "ENU-MenuAndDialog.Pitch": 70, + // "ENU-Message.Pitch": 70, + // "ENU-PCCursor.Pitch": 70, + // "ENU-Global.Volume": 100, + // "ENU-JAWSCursor.Volume": 100, + // "ENU-Keyboard.Volume": 100, + // "ENU-MenuAndDialog.Volume": 100, + // "ENU-Message.Volume": 100, + // "ENU-PCCursor.Volume": 100, + // "ENU-Global.Punctuation": 2, + // "ENU-JAWSCursor.Punctuation": 2, + // "ENU-Keyboard.Punctuation": 2, + // "ENU-MenuAndDialog.Punctuation": 2, + // "ENU-Message.Punctuation": 2, + // "ENU-PCCursor.Punctuation": 2, + // "ENU-Global.SynthLangString": "Italian", + // "ENU-JAWSCursor.SynthLangString": "Italian", + // "ENU-Keyboard.SynthLangString": "Italian", + // "ENU-MenuAndDialog.SynthLangString": "Italian", + // "ENU-Message.SynthLangString": "Italian", + // "ENU-PCCursor.SynthLangString": "Italian" + }, + "options": { + "defaultSettingsFilePath": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", + "voiceProfilesDirPath": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\VoiceProfiles" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(true) + } + }, + { + name: "Testing preference set \"jaws_common\"", + gpiiKey: "jaws_common", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.settingsHandlers.INISettingsHandler": { + "com.freedomscientific.jaws": [ + { + "settings": { + "Options.SayAllMode": 1, + "Options.SayAllIndicateCaps": 0, + "Options.SayAllIgnoreShiftKeys": 0, + "Options.TypingEcho": 3 + }, + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF" + } + } + ] + }, + "gpii.settingsHandlers.JAWSSettingsHandler": { + "com.freedomscientific.jaws": [ + { + "settings": { + // NOTE: Commented until GPII-4336 is addressed + // ============================================ + // "ENU-Global.Rate": 40, + // "ENU-Global.Punctuation": 2, + // "ENU-Global.Pitch": 70, + // "ENU-Global.Volume": 50, + // "ENU-Global.SynthLangString": "French", + // "ENU-Message.Rate": 40, + // "ENU-Message.Punctuation": 2, + // "ENU-Message.Pitch": 70, + // "ENU-Message.Volume": 50, + // "ENU-Message.SynthLangString": "French", + // "ENU-Keyboard.Rate": 40, + // "ENU-Keyboard.Punctuation": 2, + // "ENU-Keyboard.Pitch": 70, + // "ENU-Keyboard.Volume": 50, + // "ENU-Keyboard.SynthLangString": "French", + // "ENU-PCCursor.Rate": 40, + // "ENU-PCCursor.Punctuation": 2, + // "ENU-PCCursor.Pitch": 70, + // "ENU-PCCursor.Volume": 50, + // "ENU-PCCursor.SynthLangString": "French", + // "ENU-JAWSCursor.Rate": 40, + // "ENU-JAWSCursor.Punctuation": 2, + // "ENU-JAWSCursor.Pitch": 70, + // "ENU-JAWSCursor.Volume": 50, + // "ENU-JAWSCursor.SynthLangString": "French", + // "ENU-MenuAndDialog.Rate": 40, + // "ENU-MenuAndDialog.Punctuation": 2, + // "ENU-MenuAndDialog.Pitch": 70, + // "ENU-MenuAndDialog.Volume": 50, + // "ENU-MenuAndDialog.SynthLangString": "French" + }, + "options": { + "defaultSettingsFilePath": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", + "voiceProfilesDirPath": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\VoiceProfiles" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(true) + } + }, + { + name: "Testing preference set \"jaws_common2\"", + gpiiKey: "jaws_common2", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.settingsHandlers.INISettingsHandler": { + "com.freedomscientific.jaws": [ + { + "settings": { + "Options.SayAllMode": 0, + "Options.SayAllIndicateCaps": 0, + "Options.SayAllIgnoreShiftKeys": 0 + }, + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF" + } + } + ] + }, + "gpii.settingsHandlers.JAWSSettingsHandler": { + "com.freedomscientific.jaws": [ + { + "settings": { + // NOTE: Commented until GPII-4336 is addressed + // ============================================ + // "ENU-Global.Rate": 40, + // "ENU-Global.Punctuation": 0, + // "ENU-Global.Pitch": 11, + // "ENU-Global.Volume": 10, + // "ENU-Global.SynthLangString": "Italian", + // "ENU-Message.Rate": 40, + // "ENU-Message.Punctuation": 0, + // "ENU-Message.Pitch": 11, + // "ENU-Message.Volume": 10, + // "ENU-Message.SynthLangString": "Italian", + // "ENU-Keyboard.Rate": 40, + // "ENU-Keyboard.Punctuation": 0, + // "ENU-Keyboard.Pitch": 11, + // "ENU-Keyboard.Volume": 10, + // "ENU-Keyboard.SynthLangString": "Italian", + // "ENU-PCCursor.Rate": 40, + // "ENU-PCCursor.Punctuation": 0, + // "ENU-PCCursor.Pitch": 11, + // "ENU-PCCursor.Volume": 10, + // "ENU-PCCursor.SynthLangString": "Italian", + // "ENU-JAWSCursor.Rate": 40, + // "ENU-JAWSCursor.Punctuation": 0, + // "ENU-JAWSCursor.Pitch": 11, + // "ENU-JAWSCursor.Volume": 10, + // "ENU-JAWSCursor.SynthLangString": "Italian", + // "ENU-MenuAndDialog.Rate": 40, + // "ENU-MenuAndDialog.Punctuation": 0, + // "ENU-MenuAndDialog.Pitch": 11, + // "ENU-MenuAndDialog.Volume": 10, + // "ENU-MenuAndDialog.SynthLangString": "Italian" + }, + "options": { + "defaultSettingsFilePath": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", + "voiceProfilesDirPath": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\VoiceProfiles" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(true) + } + }, + { + name: "Testing preference set \"jaws_common3\"", + gpiiKey: "jaws_common3", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.settingsHandlers.INISettingsHandler": { + "com.freedomscientific.jaws": [ + { + "settings": { + "Options.SayAllMode": 2, + "Options.SayAllIndicateCaps": 1, + "Options.SayAllIgnoreShiftKeys": 1, + "Options.TypingEcho": 1 + }, + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF" + } + } + ] + }, + "gpii.settingsHandlers.JAWSSettingsHandler": { + "com.freedomscientific.jaws": [ + { + "settings": { + // NOTE: Commented until GPII-4336 is addressed + // ============================================ + // "ENU-Global.Rate": 100, + // "ENU-Global.Punctuation": 2, + // "ENU-Global.Pitch": 100, + // "ENU-Global.Volume": 100, + // "ENU-Global.SynthLangString": "French Canadian", + // "ENU-Message.Rate": 100, + // "ENU-Message.Punctuation": 2, + // "ENU-Message.Pitch": 100, + // "ENU-Message.Volume": 100, + // "ENU-Message.SynthLangString": "French Canadian", + // "ENU-Keyboard.Rate": 100, + // "ENU-Keyboard.Punctuation": 2, + // "ENU-Keyboard.Pitch": 100, + // "ENU-Keyboard.Volume": 100, + // "ENU-Keyboard.SynthLangString": "French Canadian", + // "ENU-PCCursor.Rate": 100, + // "ENU-PCCursor.Punctuation": 2, + // "ENU-PCCursor.Pitch": 100, + // "ENU-PCCursor.Volume": 100, + // "ENU-PCCursor.SynthLangString": "French Canadian", + // "ENU-JAWSCursor.Rate": 100, + // "ENU-JAWSCursor.Punctuation": 2, + // "ENU-JAWSCursor.Pitch": 100, + // "ENU-JAWSCursor.Volume": 100, + // "ENU-JAWSCursor.SynthLangString": "French Canadian", + // "ENU-MenuAndDialog.Rate": 100, + // "ENU-MenuAndDialog.Punctuation": 2, + // "ENU-MenuAndDialog.Pitch": 100, + // "ENU-MenuAndDialog.Volume": 100, + // "ENU-MenuAndDialog.SynthLangString": "French Canadian" + }, + "options": { + "defaultSettingsFilePath": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF", + "voiceProfilesDirPath": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\VoiceProfiles" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(true) + } + } +]; + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.windows.jaws.testDefs", + configName: "gpii.tests.acceptance.windows.jaws.config", + configPath: "%gpii-universal/tests/platform/windows/configs" +}, ["gpii.test.integration.testCaseHolder.windows"], + module, require, __dirname); diff --git a/tests/platform/windows/windows-jaws-testSpec.txt b/tests/platform/windows/windows-jaws-testSpec.txt new file mode 100644 index 000000000..5536e18a4 --- /dev/null +++ b/tests/platform/windows/windows-jaws-testSpec.txt @@ -0,0 +1,9 @@ +windows-jaws-testSpec.js + +Descriptions: +This will run the acceptance tests for the JAWS screenreader (Freedom Scientific). It uses 4 preference sets: jaws_common.json5, jaws_common2.json5, jaws_common3.json5 and jaws_application.json5 + +Prerequisites: +* JAWS needs to be installed + +Only works with JAWS 2019 diff --git a/tests/platform/windows/windows-learningTools-testSpec.js b/tests/platform/windows/windows-learningTools-testSpec.js new file mode 100644 index 000000000..702a1ba17 --- /dev/null +++ b/tests/platform/windows/windows-learningTools-testSpec.js @@ -0,0 +1,390 @@ +/* +GPII Acceptance Testing + +Copyright 2018 Raising the Floor International + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.windows.oneNoteLearningTools"); +fluid.registerNamespace("gpii.tests.windows.wordHome365LearningTools"); +fluid.registerNamespace("gpii.tests.windows.wordPro365LearningTools"); + +gpii.tests.windows.oneNoteLearningTools.flexibleHandlerEntry = function (running) { + return { + "com.office.windowsOneNoteLearningTools": [{ + "settings": { + "running": running + }, + "options": { + "verifySettings": true, + "retryOptions": { + "rewriteEvery": 0, + "numRetries": 20 + }, + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "ONENOTE.exe" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\OneNote.exe\\}\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "ONENOTE.exe" + } + ] + } + }] + }; +}; + +gpii.tests.windows.oneNoteLearningTools.testDefs = [ + { + name: "Testing preference set \"onenote_learningstools_application\"", + gpiiKey: "onenote_learningstools_application", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.oneNoteLearningTools.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.windows.registrySettingsHandler": { + "com.office.windowsOneNoteLearningTools": [ + { + "settings": { + "DictationLanguage": 1033, + "ReadingComprehensionDefaultFont": "Sitka Small", + "IsReadingComprehensionFontWide": 1, + "ReadingComprehensionTheme": 2, + "ReadingComprehensionFontSize": 52, + "SynthRate": 2, + "SynthVoices": "[{\"Lang\":\"en\",\"Voice\":\"Microsoft Zira Desktop\"}]" + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "64:Software\\Microsoft\\OneNoteLearningTools", + "dataTypes": { + "DictationLanguage": "REG_DWORD", + "ReadingComprehensionDefaultFont": "REG_SZ", + "IsReadingComprehensionFontWide": "REG_DWORD", + "ReadingComprehensionTheme": "REG_DWORD", + "ReadingComprehensionFontSize": "REG_DWORD", + "SynthRate": "REG_DWORD", + "SynthVoices": "REG_SZ" + } + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.oneNoteLearningTools.flexibleHandlerEntry(true) + } + }, + { + name: "Testing preference set \"onenote_learningstools_application\" - where OneNote is running", + gpiiKey: "onenote_learningstools_application", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.oneNoteLearningTools.flexibleHandlerEntry(true) + }, + settingsHandlers: { + "gpii.windows.registrySettingsHandler": { + "com.office.windowsOneNoteLearningTools": [ + { + "settings": { + "DictationLanguage": 1033, + "ReadingComprehensionDefaultFont": "Sitka Small", + "IsReadingComprehensionFontWide": 1, + "ReadingComprehensionTheme": 2, + "ReadingComprehensionFontSize": 52, + "SynthRate": 2, + "SynthVoices": "[{\"Lang\":\"en\",\"Voice\":\"Microsoft Zira Desktop\"}]" + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "64:Software\\Microsoft\\OneNoteLearningTools", + "dataTypes": { + "DictationLanguage": "REG_DWORD", + "ReadingComprehensionDefaultFont": "REG_SZ", + "IsReadingComprehensionFontWide": "REG_DWORD", + "ReadingComprehensionTheme": "REG_DWORD", + "ReadingComprehensionFontSize": "REG_DWORD", + "SynthRate": "REG_DWORD", + "SynthVoices": "REG_SZ" + } + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.oneNoteLearningTools.flexibleHandlerEntry(true) + } + } +]; + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.windows.oneNoteLearningTools.testDefs", + configName: "gpii.tests.acceptance.windows.oneNoteLearningTools.config", + configPath: "%gpii-universal/tests/platform/windows/configs" +}, ["gpii.test.integration.testCaseHolder.windows"], + module, require, __dirname); + + +gpii.tests.windows.wordHome365LearningTools.flexibleHandlerEntry = function (running) { + return { + "com.office.windowsWordHome365LearningTools": [{ + "settings": { + "running": running + }, + "options": { + "verifySettings": true, + "retryOptions": { + "rewriteEvery": 0, + "numRetries": 20 + }, + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "WINWORD.exe" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\Winword.exe\\}\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "WINWORD.exe" + } + ] + } + }] + }; +}; + +gpii.tests.windows.wordHome365LearningTools.testDefs = [ + { + name: "Testing preference set \"wordhome_learningstools_application\"", + gpiiKey: "wordhome_learningstools_application", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.wordHome365LearningTools.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.windows.registrySettingsHandler": { + "com.office.windowsWordHome365LearningTools": [ + { + "settings": { + "ReadingModeColumnWidth": 2, + "ReadingModePageColor": 2, + "ReadingModePrintedPage": 0, + "ReadAloudVoiceId": "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech_OneCore\\Voices\\Tokens\\MSTTS_V110_enUS_ZiraM", + "ReadAloudVoiceRate": 2, + "ReadingModeSyllables": 1, + "ReadingModeTextSpacing": 1 + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "64:Software\\Microsoft\\Office\\16.0\\Word\\Options", + "dataTypes": { + "ReadingModeColumnWidth": "REG_DWORD", + "ReadingModePageColor": "REG_DWORD", + "ReadingModePrintedPage": "REG_DWORD", + "ReadAloudVoiceId": "REG_SZ", + "ReadAloudVoiceRate": "REG_DWORD", + "ReadingModeSyllables": "REG_DWORD", + "ReadingModeTextSpacing": "REG_DWORD" + } + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.wordHome365LearningTools.flexibleHandlerEntry(true) + } + }, + { + name: "Testing preference set \"wordhome_learningstools_application\" - where Word is running", + gpiiKey: "wordhome_learningstools_application", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.wordHome365LearningTools.flexibleHandlerEntry(true) + }, + settingsHandlers: { + "gpii.windows.registrySettingsHandler": { + "com.office.windowsWordHome365LearningTools": [ + { + "settings": { + "ReadingModeColumnWidth": 2, + "ReadingModePageColor": 2, + "ReadingModePrintedPage": 0, + "ReadAloudVoiceId": "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech_OneCore\\Voices\\Tokens\\MSTTS_V110_enUS_ZiraM", + "ReadAloudVoiceRate": 2, + "ReadingModeSyllables": 1, + "ReadingModeTextSpacing": 1 + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "64:Software\\Microsoft\\Office\\16.0\\Word\\Options", + "dataTypes": { + "ReadingModeColumnWidth": "REG_DWORD", + "ReadingModePageColor": "REG_DWORD", + "ReadingModePrintedPage": "REG_DWORD", + "ReadAloudVoiceId": "REG_SZ", + "ReadAloudVoiceRate": "REG_DWORD", + "ReadingModeSyllables": "REG_DWORD", + "ReadingModeTextSpacing": "REG_DWORD" + } + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.wordHome365LearningTools.flexibleHandlerEntry(true) + } + } +]; + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.windows.wordHome365LearningTools.testDefs", + configName: "gpii.tests.acceptance.windows.wordHomeLearningTools.config", + configPath: "%gpii-universal/tests/platform/windows/configs" +}, ["gpii.test.integration.testCaseHolder.windows"], + module, require, __dirname); + + +gpii.tests.windows.wordPro365LearningTools.flexibleHandlerEntry = function (running) { + return { + "com.office.windowsWordPro365LearningTools": [{ + "settings": { + "running": running + }, + "options": { + "verifySettings": true, + "retryOptions": { + "rewriteEvery": 0, + "numRetries": 20 + }, + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "WINWORD.exe" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\Winword.exe\\}\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "WINWORD.exe" + } + ] + } + }] + }; +}; + +gpii.tests.windows.wordPro365LearningTools.testDefs = [ + { + name: "Testing preference set \"wordpro_learningstools_application\"", + gpiiKey: "wordpro_learningstools_application", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.wordPro365LearningTools.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.windows.registrySettingsHandler": { + "com.office.windowsWordPro365LearningTools": [ + { + "settings": { + "ReadingModeColumnWidth": 2, + "ReadingModePageColor": 2, + "ReadingModePrintedPage": 0, + "ReadAloudVoiceId": "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\TTS_MS_EN-US_ZIRA_11.0", + "ReadAloudVoiceRate": 2, + "ReadingModeSyllables": 1, + "ReadingModeTextSpacing": 1 + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "64:Software\\Microsoft\\Office\\16.0\\Word\\Options", + "dataTypes": { + "ReadingModeColumnWidth": "REG_DWORD", + "ReadingModePageColor": "REG_DWORD", + "ReadingModePrintedPage": "REG_DWORD", + "ReadAloudVoiceId": "REG_SZ", + "ReadAloudVoiceRate": "REG_DWORD", + "ReadingModeSyllables": "REG_DWORD", + "ReadingModeTextSpacing": "REG_DWORD" + } + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.wordPro365LearningTools.flexibleHandlerEntry(true) + } + }, + { + name: "Testing preference set \"wordpro_learningstools_application\" - where Word is running", + gpiiKey: "wordpro_learningstools_application", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.wordPro365LearningTools.flexibleHandlerEntry(true) + }, + settingsHandlers: { + "gpii.windows.registrySettingsHandler": { + "com.office.windowsWordPro365LearningTools": [ + { + "settings": { + "ReadingModeColumnWidth": 2, + "ReadingModePageColor": 2, + "ReadingModePrintedPage": 0, + "ReadAloudVoiceId": "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\TTS_MS_EN-US_ZIRA_11.0", + "ReadAloudVoiceRate": 2, + "ReadingModeSyllables": 1, + "ReadingModeTextSpacing": 1 + }, + "options": { + "hKey": "HKEY_CURRENT_USER", + "path": "64:Software\\Microsoft\\Office\\16.0\\Word\\Options", + "dataTypes": { + "ReadingModeColumnWidth": "REG_DWORD", + "ReadingModePageColor": "REG_DWORD", + "ReadingModePrintedPage": "REG_DWORD", + "ReadAloudVoiceId": "REG_SZ", + "ReadAloudVoiceRate": "REG_DWORD", + "ReadingModeSyllables": "REG_DWORD", + "ReadingModeTextSpacing": "REG_DWORD" + } + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.wordPro365LearningTools.flexibleHandlerEntry(true) + } + } +]; + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.windows.wordPro365LearningTools.testDefs", + configName: "gpii.tests.acceptance.windows.wordProLearningTools.config", + configPath: "%gpii-universal/tests/platform/windows/configs" +}, ["gpii.test.integration.testCaseHolder.windows"], + module, require, __dirname); diff --git a/tests/platform/windows/windows-learningTools-testSpec.txt b/tests/platform/windows/windows-learningTools-testSpec.txt new file mode 100644 index 000000000..d551f55c4 --- /dev/null +++ b/tests/platform/windows/windows-learningTools-testSpec.txt @@ -0,0 +1,14 @@ +windows-learningTools-testSpec.js + +Descriptions: +This will run the acceptance tests for the LearningTools application for OneNote and Word365 Home and Pro versions. It uses 3 preferences set: + - onenote_learningstools_application.json5 + - wordhome_learningstools_application.json5 + - wordpro_learningstools_application.json5 + +Requirements: +* Word365 Pro installed. +* Word365 Home installed. +* OneNote 2016 + LearningTools for OneNote addin installed. + +To run these tests, you need to have all the previous solutions installed. \ No newline at end of file diff --git a/tests/platform/windows/windows-magic-testSpec.js b/tests/platform/windows/windows-magic-testSpec.js new file mode 100644 index 000000000..c3c5ae3ba --- /dev/null +++ b/tests/platform/windows/windows-magic-testSpec.js @@ -0,0 +1,406 @@ +/* +GPII Acceptance Testing + +Copyright 2014 Raising the Floor International + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.windows.jaws"); + +// TODO: This is just a copy of the JAWS tests, and not a real test of MAGic. + +// To avoid duplicating this entire piece in each test. Given a true or false value +// as input, this will return a settingshandler entry, containing all the options from +// the solutions registry entry for NVDAs launchHandler, with a settings block with +// running: X - where X is replaced with the input parameter +gpii.tests.windows.jaws.flexibleHandlerEntry = function (running) { + return { + "com.freedomscientific.jaws": [{ + "settings": { + "running": running + }, + "options": { + "verifySettings": true, + retryOptions: { + rewriteEvery: 0, + numRetries: 20, + retryInterval: 1000 + }, + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "jfw.exe" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\JAWS17.exe\\}\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "jfw.exe" + }, + { + "type": "gpii.windows.closeProcessByName", + "filename": "fsSynth32.exe" + }, + { + "type": "gpii.windows.closeProcessByName", + "filename": "jhookldr.exe" + } + ] + } + }] + }; +}; + +gpii.tests.windows.jaws.testDefs = [ + { + name: "Testing preference set \"jaws_application\"", + gpiiKey: "jaws_application", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.settingsHandlers.INISettingsHandler": { + "com.freedomscientific.jaws": [ + { + "settings": { + "Voice Profiles.ActiveVoiceProfileName" : "GPII", + "Options.SayAllIndicateCaps" : 0, + "Options.TypingEcho": 3, + "Options.SayAllMode": 0, + "Braille.BrailleMode": 0, + "Options.SayAllIgnoreShiftKeys": 1 + }, + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF" + } + }, + + { + "settings": { + "ENU-Global.Rate": 100, + "ENU-JAWSCursor.Rate": 100, + "ENU-Keyboard.Rate": 100, + "ENU-MenuAndDialog.Rate": 100, + "ENU-Message.Rate": 100, + "ENU-PCCursor.Rate": 100, + "ENU-Global.Pitch": 70, + "ENU-JAWSCursor.Pitch": 70, + "ENU-Keyboard.Pitch": 70, + "ENU-MenuAndDialog.Pitch": 70, + "ENU-Message.Pitch": 70, + "ENU-PCCursor.Pitch": 70, + "ENU-Global.Volume": 100, + "ENU-JAWSCursor.Volume": 100, + "ENU-Keyboard.Volume": 100, + "ENU-MenuAndDialog.Volume": 100, + "ENU-Message.Volume": 100, + "ENU-PCCursor.Volume": 100, + "ENU-Global.Punctuation": 2, + "ENU-JAWSCursor.Punctuation": 2, + "ENU-Keyboard.Punctuation": 2, + "ENU-MenuAndDialog.Punctuation": 2, + "ENU-Message.Punctuation": 2, + "ENU-PCCursor.Punctuation": 2, + "ENU-Global.SynthLangString": "Italian", + "ENU-JAWSCursor.SynthLangString": "Italian", + "ENU-Keyboard.SynthLangString": "Italian", + "ENU-MenuAndDialog.SynthLangString": "Italian", + "ENU-Message.SynthLangString": "Italian", + "ENU-PCCursor.SynthLangString": "Italian" + }, + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\VoiceProfiles\\GPII.VPF" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(true) + } + }, + { + name: "Testing preference set \"jaws_application\" - where jaws is running on startup", + gpiiKey: "jaws_application", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(true) + }, + settingsHandlers: { + "gpii.settingsHandlers.INISettingsHandler": { + "com.freedomscientific.jaws": [ + { + "settings": { + "Voice Profiles.ActiveVoiceProfileName" : "GPII", + "Options.SayAllIndicateCaps" : 0, + "Options.TypingEcho": 3, + "Options.SayAllMode": 0, + "Braille.BrailleMode": 0, + "Options.SayAllIgnoreShiftKeys": 1 + }, + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF" + } + }, + + { + "settings": { + "ENU-Global.Rate": 100, + "ENU-JAWSCursor.Rate": 100, + "ENU-Keyboard.Rate": 100, + "ENU-MenuAndDialog.Rate": 100, + "ENU-Message.Rate": 100, + "ENU-PCCursor.Rate": 100, + "ENU-Global.Pitch": 70, + "ENU-JAWSCursor.Pitch": 70, + "ENU-Keyboard.Pitch": 70, + "ENU-MenuAndDialog.Pitch": 70, + "ENU-Message.Pitch": 70, + "ENU-PCCursor.Pitch": 70, + "ENU-Global.Volume": 100, + "ENU-JAWSCursor.Volume": 100, + "ENU-Keyboard.Volume": 100, + "ENU-MenuAndDialog.Volume": 100, + "ENU-Message.Volume": 100, + "ENU-PCCursor.Volume": 100, + "ENU-Global.Punctuation": 2, + "ENU-JAWSCursor.Punctuation": 2, + "ENU-Keyboard.Punctuation": 2, + "ENU-MenuAndDialog.Punctuation": 2, + "ENU-Message.Punctuation": 2, + "ENU-PCCursor.Punctuation": 2, + "ENU-Global.SynthLangString": "Italian", + "ENU-JAWSCursor.SynthLangString": "Italian", + "ENU-Keyboard.SynthLangString": "Italian", + "ENU-MenuAndDialog.SynthLangString": "Italian", + "ENU-Message.SynthLangString": "Italian", + "ENU-PCCursor.SynthLangString": "Italian" + }, + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\VoiceProfiles\\GPII.VPF" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(true) + } + }, + { + name: "Testing preference set \"jaws_common\"", + gpiiKey: "jaws_common", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.settingsHandlers.INISettingsHandler": { + "com.freedomscientific.jaws": [ + { + "settings": { + "ENU-Global.Rate": 40, + "ENU-Global.Punctuation": 2, + "ENU-Global.Pitch": 70, + "ENU-Global.Volume": 55, + "ENU-Global.SynthLangString": "French", + "ENU-Message.Rate": 40, + "ENU-Message.Punctuation": 2, + "ENU-Message.Pitch": 70, + "ENU-Message.Volume": 55, + "ENU-Message.SynthLangString": "French", + "ENU-Keyboard.Rate": 40, + "ENU-Keyboard.Punctuation": 2, + "ENU-Keyboard.Pitch": 70, + "ENU-Keyboard.Volume": 55, + "ENU-Keyboard.SynthLangString": "French", + "ENU-PCCursor.Rate": 40, + "ENU-PCCursor.Punctuation": 2, + "ENU-PCCursor.Pitch": 70, + "ENU-PCCursor.Volume": 55, + "ENU-PCCursor.SynthLangString": "French", + "ENU-JAWSCursor.Rate": 40, + "ENU-JAWSCursor.Punctuation": 2, + "ENU-JAWSCursor.Pitch": 70, + "ENU-JAWSCursor.Volume": 55, + "ENU-JAWSCursor.SynthLangString": "French", + "ENU-MenuAndDialog.Rate": 40, + "ENU-MenuAndDialog.Punctuation": 2, + "ENU-MenuAndDialog.Pitch": 70, + "ENU-MenuAndDialog.Volume": 55, + "ENU-MenuAndDialog.SynthLangString": "French" + }, + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\VoiceProfiles\\GPII.VPF" + } + }, + + { + "settings": { + "Voice Profiles.ActiveVoiceProfileName": "GPII", + "Options.SayAllMode": 1, + "Options.SayAllIndicateCaps": 0, + "Options.SayAllIgnoreShiftKeys": 0, + "Options.TypingEcho": 3 + }, + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(true) + } + }, + { + name: "Testing preference set \"jaws_common2\"", + gpiiKey: "jaws_common2", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.settingsHandlers.INISettingsHandler": { + "com.freedomscientific.jaws": [ + { + "settings": { + "ENU-Global.Rate": 40, + "ENU-Global.Punctuation": 0, + "ENU-Global.Pitch": 11, + "ENU-Global.Volume": 19, + "ENU-Global.SynthLangString": "Italian", + "ENU-Message.Rate": 40, + "ENU-Message.Punctuation": 0, + "ENU-Message.Pitch": 11, + "ENU-Message.Volume": 19, + "ENU-Message.SynthLangString": "Italian", + "ENU-Keyboard.Rate": 40, + "ENU-Keyboard.Punctuation": 0, + "ENU-Keyboard.Pitch": 11, + "ENU-Keyboard.Volume": 19, + "ENU-Keyboard.SynthLangString": "Italian", + "ENU-PCCursor.Rate": 40, + "ENU-PCCursor.Punctuation": 0, + "ENU-PCCursor.Pitch": 11, + "ENU-PCCursor.Volume": 19, + "ENU-PCCursor.SynthLangString": "Italian", + "ENU-JAWSCursor.Rate": 40, + "ENU-JAWSCursor.Punctuation": 0, + "ENU-JAWSCursor.Pitch": 11, + "ENU-JAWSCursor.Volume": 19, + "ENU-JAWSCursor.SynthLangString": "Italian", + "ENU-MenuAndDialog.Rate": 40, + "ENU-MenuAndDialog.Punctuation": 0, + "ENU-MenuAndDialog.Pitch": 11, + "ENU-MenuAndDialog.Volume": 19, + "ENU-MenuAndDialog.SynthLangString": "Italian" + }, + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\VoiceProfiles\\GPII.VPF" + } + }, + + { + "settings": { + "Voice Profiles.ActiveVoiceProfileName": "GPII", + "Options.SayAllMode": 0, + "Options.SayAllIndicateCaps": 0, + "Options.SayAllIgnoreShiftKeys": 0 + }, + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(true) + } + }, + { + name: "Testing preference set \"jaws_common3\"", + gpiiKey: "jaws_common3", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.settingsHandlers.INISettingsHandler": { + "com.freedomscientific.jaws": [ + { + "settings": { + "ENU-Global.Rate": 100, + "ENU-Global.Punctuation": 2, + "ENU-Global.Pitch": 100, + "ENU-Global.Volume": 100, + "ENU-Global.SynthLangString": "French", + "ENU-Message.Rate": 100, + "ENU-Message.Punctuation": 2, + "ENU-Message.Pitch": 100, + "ENU-Message.Volume": 100, + "ENU-Message.SynthLangString": "French", + "ENU-Keyboard.Rate": 100, + "ENU-Keyboard.Punctuation": 2, + "ENU-Keyboard.Pitch": 100, + "ENU-Keyboard.Volume": 100, + "ENU-Keyboard.SynthLangString": "French", + "ENU-PCCursor.Rate": 100, + "ENU-PCCursor.Punctuation": 2, + "ENU-PCCursor.Pitch": 100, + "ENU-PCCursor.Volume": 100, + "ENU-PCCursor.SynthLangString": "French", + "ENU-JAWSCursor.Rate": 100, + "ENU-JAWSCursor.Punctuation": 2, + "ENU-JAWSCursor.Pitch": 100, + "ENU-JAWSCursor.Volume": 100, + "ENU-JAWSCursor.SynthLangString": "French", + "ENU-MenuAndDialog.Rate": 100, + "ENU-MenuAndDialog.Punctuation": 2, + "ENU-MenuAndDialog.Pitch": 100, + "ENU-MenuAndDialog.Volume": 100, + "ENU-MenuAndDialog.SynthLangString": "French" + }, + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\VoiceProfiles\\GPII.VPF" + } + }, + + { + "settings": { + "Voice Profiles.ActiveVoiceProfileName": "GPII", + "Options.SayAllMode": 2, + "Options.SayAllIndicateCaps": 1, + "Options.SayAllIgnoreShiftKeys": 1, + "Options.TypingEcho": 1 + }, + "options": { + "filename": "${{environment}.APPDATA}\\Freedom Scientific\\JAWS\\2019\\Settings\\enu\\DEFAULT.JCF" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.jaws.flexibleHandlerEntry(true) + } + } +]; + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.windows.jaws.testDefs", + configName: "gpii.tests.acceptance.windows.jaws.config", + configPath: "%gpii-universal/tests/platform/windows/configs" +}, ["gpii.test.integration.testCaseHolder.windows"], + module, require, __dirname); diff --git a/tests/platform/windows/windows-magic-testSpec.txt b/tests/platform/windows/windows-magic-testSpec.txt new file mode 100644 index 000000000..4b2d5ce22 --- /dev/null +++ b/tests/platform/windows/windows-magic-testSpec.txt @@ -0,0 +1,7 @@ +windows-magic-testSpec.js + +Descriptions: +This will run the acceptance tests for MAGic (Freedom Scientific). + +Prerequisites: +* MAGic 14.0 needs to be installed diff --git a/tests/platform/windows/windows-nvda-testSpec.js b/tests/platform/windows/windows-nvda-testSpec.js new file mode 100644 index 000000000..1e8722487 --- /dev/null +++ b/tests/platform/windows/windows-nvda-testSpec.js @@ -0,0 +1,182 @@ +/* +GPII Integration and Acceptance Testing + +Copyright 2014 Raising the Floor International + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.windows.nvda"); + +// To avoid duplicating this entire piece in each test. Given a true or false value +// as input, this will return a settingshandler entry, containing all the options from +// the solutions registry entry for NVDAs launchHandler, with a settings block with +// running: X - where X is replaced with the input parameter +gpii.tests.windows.nvda.flexibleHandlerEntry = function (running) { + return { + "org.nvda-project": [{ + "settings": { + "running": running + }, + "options": { + "verifySettings": true, + retryOptions: { + rewriteEvery: 0, + numRetries: 20, + retryInterval: 1000 + }, + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "nvda" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe\\}\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "nvda_service.exe" + },{ + "type": "gpii.windows.closeProcessByName", + "filename": "nvda.exe" + } + ] + } + }] + }; +}; + +gpii.tests.windows.nvda.testDef = [ + { + name: "Testing screenreader_nvda - When running on start", + gpiiKey: "screenreader_nvda", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.nvda.flexibleHandlerEntry(true) + }, + settingsHandlers: { + "gpii.settingsHandlers.INISettingsHandler": { + "org.nvda-project": [ + { + "settings": { + "speech.espeak.rate": 40, + "speech.espeak.volume": 80, + "speech.espeak.pitch": 60, + "speech.espeak.rateBoost": "True", + "speech.synth": "espeak", + "speech.outputDevice": "Microsoft Sound Mapper", + "speech.symbolLevel": 300, + "speech.espeak.voice": "en-us", + "reviewCursor.followFocus": "False", + "reviewCursor.followCaret": "True", + "reviewCursor.followMouse": "True", + "keyboard.speakTypedWords": "True", + "keyboard.speakTypedCharacters": "False", + "presentation.reportHelpBalloons": "False", + "speech.espeak.sayCapForCapitals": "True" + }, + "options": { + "filename": "${{environment}.APPDATA}\\nvda\\nvda.ini" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.nvda.flexibleHandlerEntry(true) + } + }, { + name: "Testing screenreader_nvda", + gpiiKey: "screenreader_nvda", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.nvda.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.settingsHandlers.INISettingsHandler": { + "org.nvda-project": [ + { + "settings": { + "speech.espeak.rate": 40, + "speech.espeak.volume": 80, + "speech.espeak.pitch": 60, + "speech.espeak.rateBoost": "True", + "speech.synth": "espeak", + "speech.outputDevice": "Microsoft Sound Mapper", + "speech.symbolLevel": 300, + "speech.espeak.voice": "en-us", + "reviewCursor.followFocus": "False", + "reviewCursor.followCaret": "True", + "reviewCursor.followMouse": "True", + "keyboard.speakTypedWords": "True", + "keyboard.speakTypedCharacters": "False", + "presentation.reportHelpBalloons": "False", + "speech.espeak.sayCapForCapitals": "True" + }, + "options": { + "filename": "${{environment}.APPDATA}\\nvda\\nvda.ini" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.nvda.flexibleHandlerEntry(true) + } + }, { + name: "Testing screenreader_common", + gpiiKey: "screenreader_common", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.nvda.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.settingsHandlers.INISettingsHandler": { + "org.nvda-project": [ + { + "settings": { + "speech.espeak.rate": 40, + "speech.espeak.volume": 70, + "speech.espeak.pitch": 10, + "speech.espeak.rateBoost": "True", + "speech.symbolLevel": 300, + "speech.espeak.voice": "en-us", + "reviewCursor.followFocus": "False", + "reviewCursor.followCaret": "True", + "reviewCursor.followMouse": "True", + "keyboard.speakTypedWords": "True", + "keyboard.speakTypedCharacters": "False", + "presentation.reportHelpBalloons": "False", + "speech.espeak.sayCapForCapitals": "True" + }, + "options": { + "filename": "${{environment}.APPDATA}\\nvda\\nvda.ini" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.nvda.flexibleHandlerEntry(true) + } + } +]; + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.windows.nvda.testDef", + configName: "gpii.tests.acceptance.windows.nvda.config", + configPath: "%gpii-universal/tests/platform/windows/configs" +}, ["gpii.test.integration.testCaseHolder.windows"], + module, require, __dirname); diff --git a/tests/platform/windows/windows-nvda-testSpec.txt b/tests/platform/windows/windows-nvda-testSpec.txt new file mode 100644 index 000000000..fd8d74153 --- /dev/null +++ b/tests/platform/windows/windows-nvda-testSpec.txt @@ -0,0 +1,9 @@ +windows-nvda-testSpec.js + +Descriptions: +This will run the acceptance tests for the free and open source screen reader NVDA. It uses 3 preference sets: screenreader_orca, screenreader_nvda and screenreader_common. + +Requirements: +* NVDA installed + +To run these tests, you need to have NVDA INSTALLED. NVDA is free and can be downloaded from: http://www.nvaccess.org/download/ \ No newline at end of file diff --git a/tests/platform/windows/windows-readWrite12-testSpec.js b/tests/platform/windows/windows-readWrite12-testSpec.js new file mode 100644 index 000000000..246ebe834 --- /dev/null +++ b/tests/platform/windows/windows-readWrite12-testSpec.js @@ -0,0 +1,638 @@ +/* +GPII Integration and Acceptance Testing + +Copyright 2016 RtF-US + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + child_process = require("child_process"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.windows.readwrite"); + +fluid.defaults("gpii.tests.windows.readwrite.findProcess", { + gradeNames: "fluid.function", + argumentMap: { + command: 0, + time: 1 + } +}); + +/** + * Test function used to introduce a delay in startup/shutdown cycles. + * + * @param {String} commandName The name of the process to be searched for. + * @param {Number} time The delay to wait before searching for the process. + * @return {Boolean} True if the process have been found, false otherwise. + */ +gpii.tests.windows.readwrite.findProcess = function (commandName, time) { + child_process.execSync("waitfor ghostEvent /t " + time + " 2>NUL || type nul>nul"); + + return gpii.processReporter.find(commandName); +}; + +// To avoid duplicating this entire piece in each test. Given a true or false value +// as input, this will return a settingshandler entry, containing all the options from +// the solutions registry entry for NVDAs launchHandler, with a settings block with +// running: X - where X is replaced with the input parameter +gpii.tests.windows.readwrite.flexibleHandlerEntry = function (running) { + return { + "com.texthelp.readWriteGold": [{ + "settings": { + "running": running + }, + "options": { + "verifySettings": true, + retryOptions: { + rewriteEvery: 0, + numRetries: 40, + retryInterval: 20000 + }, + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{environment}.SystemDrive}\\Program Files (x86)\\Texthelp\\Read And Write 12\\ReadAndWrite.exe\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "ReadAndWrite.exe", + "options": { + "message": "WM_CLOSE" + } + } + ], + "getState": [ + { + "type": "gpii.tests.windows.readwrite.findProcess", + "command": "ReadAndWrite.exe", + "time": 10 + } + ] + } + }] + }; +}; + + +gpii.tests.windows.readwrite.testDefs = [ + { + name: "Testing 'readwritegold_general' - running on login", + gpiiKey: "readwritegold_general", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(true) + }, + settingsHandlers: { + "gpii.settingsHandlers.XMLHandler": { + "com.texthelp.readWriteGold": [ + { + "settings": { + "ApplicationSettings.AppBar.IconSize.$t": "Small", + "ApplicationSettings.AppBar.ToolbarIconSet.$t": "Color", + "ApplicationSettings.AppBar.ShowText.$t": true, + "ApplicationSettings.AppBar.optToolbarBackColour.$t": "#008080", + "ApplicationSettings.AppBar.RunOnStartUp.$t": false + }, + "options": { + "filename": "${{environment}.APPDATA}\\Texthelp\\ReadAndWrite\\12\\RWSettings.xml", + "encoding": "utf-8", + "xml-tag": "" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(true) + } + }, + { + name: "Testing readwritegold_general", + gpiiKey: "readwritegold_general", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.settingsHandlers.XMLHandler": { + "com.texthelp.readWriteGold": [ + { + "settings": { + "ApplicationSettings.AppBar.IconSize.$t": "Small", + "ApplicationSettings.AppBar.ToolbarIconSet.$t": "Color", + "ApplicationSettings.AppBar.ShowText.$t": true, + "ApplicationSettings.AppBar.optToolbarBackColour.$t": "#008080", + "ApplicationSettings.AppBar.RunOnStartUp.$t": false + }, + "options": { + "filename": "${{environment}.APPDATA}\\Texthelp\\ReadAndWrite\\12\\RWSettings.xml", + "encoding": "utf-8", + "xml-tag": "" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(true) + } + }, + { + name: "Testing 'readwritegold_check_dict_high' - running on login", + gpiiKey: "readwritegold_check_dict_high", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(true) + }, + settingsHandlers: { + "gpii.settingsHandlers.XMLHandler": { + "com.texthelp.readWriteGold": [ + { + "settings": { + // Check It + "ApplicationSettings.Spelling.OrderByContext.$t": false, + "ApplicationSettings.Spelling.SpeakSpellingOnHover.$t": false, + "ApplicationSettings.Spelling.ShowCheckIt.$t": true, + "ApplicationSettings.Spelling.RightClickMSWord.$t": true, + // Dictionary + "ApplicationSettings.Dictionary.WebDictionary.$t": "Bing", + "ApplicationSettings.Dictionary.PopupDictionary.$t": true, + "ApplicationSettings.Dictionary.ToggleImages.$t": true, + // Highlights + "ApplicationSettings.StudySkills.CollectYellowHighlight.$t": false, + "ApplicationSettings.StudySkills.CollectBlueHighlight.$t": true, + "ApplicationSettings.StudySkills.CollectGreenHighlight.$t": false, + "ApplicationSettings.StudySkills.CollectPinkHighlight.$t": false, + "ApplicationSettings.StudySkills.CollectOrder.$t": "Position", + "ApplicationSettings.StudySkills.ColorSeparator.$t": "Page", + "ApplicationSettings.StudySkills.HighlightSeparator.$t": "Tab", + "ApplicationSettings.StudySkills.MultiDocHighlighting.$t": false + }, + "options": { + "filename": "${{environment}.APPDATA}\\Texthelp\\ReadAndWrite\\12\\RWSettings.xml", + "encoding": "utf-8", + "xml-tag": "" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(true) + } + }, + { + name: "Testing readwritegold_check_dict_high", + gpiiKey: "readwritegold_check_dict_high", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.settingsHandlers.XMLHandler": { + "com.texthelp.readWriteGold": [ + { + "settings": { + // Check It + "ApplicationSettings.Spelling.OrderByContext.$t": false, + "ApplicationSettings.Spelling.SpeakSpellingOnHover.$t": false, + "ApplicationSettings.Spelling.ShowCheckIt.$t": true, + "ApplicationSettings.Spelling.RightClickMSWord.$t": true, + // Dictionary + "ApplicationSettings.Dictionary.WebDictionary.$t": "Bing", + "ApplicationSettings.Dictionary.PopupDictionary.$t": true, + "ApplicationSettings.Dictionary.ToggleImages.$t": true, + // Highlights + "ApplicationSettings.StudySkills.CollectYellowHighlight.$t": false, + "ApplicationSettings.StudySkills.CollectBlueHighlight.$t": true, + "ApplicationSettings.StudySkills.CollectGreenHighlight.$t": false, + "ApplicationSettings.StudySkills.CollectPinkHighlight.$t": false, + "ApplicationSettings.StudySkills.CollectOrder.$t": "Position", + "ApplicationSettings.StudySkills.ColorSeparator.$t": "Page", + "ApplicationSettings.StudySkills.HighlightSeparator.$t": "Tab", + "ApplicationSettings.StudySkills.MultiDocHighlighting.$t": false + }, + "options": { + "filename": "${{environment}.APPDATA}\\Texthelp\\ReadAndWrite\\12\\RWSettings.xml", + "encoding": "utf-8", + "xml-tag": "" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(true) + } + }, + { + name: "Testing 'readwritegold_pred_scan_out' - running on login", + gpiiKey: "readwritegold_pred_scan_out", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(true) + }, + settingsHandlers: { + "gpii.settingsHandlers.XMLHandler": { + "com.texthelp.readWriteGold": [ + { + "settings": { + // Prediction + "ApplicationSettings.Prediction.DisplayAlphabetic.$t": true, + "ApplicationSettings.Prediction.FollowCursor.$t": true, + "ApplicationSettings.Prediction.AutoHeightAdjust.$t": true, + "ApplicationSettings.Prediction.winPredictOneWordAhead.$t": true, + "ApplicationSettings.Prediction.InsertSpaceAfterPred.$t": false, + "ApplicationSettings.Prediction.UseSpellingForPredictions.$t": false, + // Prediction Speech + "ApplicationSettings.Prediction.SpeakPredictionOnMouseHover.$t": false, + // This two settings are excluyent, need to encode that + "ApplicationSettings.Prediction.SpeakOnWordClick.$t": true, + "ApplicationSettings.Prediction.InsertOnWordClick.$t": false, + // Prediction Learning + "ApplicationSettings.Prediction.SpellCheckBeforeLearn.$t": false, + "ApplicationSettings.Prediction.AutoLearnAsType.$t": false, + // Scanning + "ApplicationSettings.Scanning.dpi.$t": 600, + "ApplicationSettings.Scanning.colormode.$t": "GrayScale", + "ApplicationSettings.Scanning.HideInterface.$t": false, + // Device Setup (Advanced settigns) + "ApplicationSettings.Scanning.Countdown.$t": true, + "ApplicationSettings.Scanning.CountdownInterval.$t": 10, + "ApplicationSettings.Scanning.ADF.$t": true, + "ApplicationSettings.Scanning.Duplex.$t": true, + // Save Options + "ApplicationSettings.Scanning.AutoSave.$t": true, + "ApplicationSettings.Scanning.DefaultFilePath.$t": "C:\\Users\\vagrant\\Download", + "ApplicationSettings.Scanning.DefaultFileName.$t": "ScannedDoc", + // PDF Output + "ApplicationSettings.Scanning.PdfTextOnly.$t": true, + "ApplicationSettings.Scanning.MSWordRetainFormat.$t": true, + "ApplicationSettings.Scanning.CoverFirstPage.$t": false, + // Output Types + "ApplicationSettings.Scanning.EnableEpub.$t": true, + "ApplicationSettings.Scanning.EnableWeb.$t": true + }, + "options": { + "filename": "${{environment}.APPDATA}\\Texthelp\\ReadAndWrite\\12\\RWSettings.xml", + "encoding": "utf-8", + "xml-tag": "" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(true) + } + }, + { + name: "Testing readwritegold_pred_scan_out", + gpiiKey: "readwritegold_pred_scan_out", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.settingsHandlers.XMLHandler": { + "com.texthelp.readWriteGold": [ + { + "settings": { + // Prediction + "ApplicationSettings.Prediction.DisplayAlphabetic.$t": true, + "ApplicationSettings.Prediction.FollowCursor.$t": true, + "ApplicationSettings.Prediction.AutoHeightAdjust.$t": true, + "ApplicationSettings.Prediction.winPredictOneWordAhead.$t": true, + "ApplicationSettings.Prediction.InsertSpaceAfterPred.$t": false, + "ApplicationSettings.Prediction.UseSpellingForPredictions.$t": false, + // Prediction Speech + "ApplicationSettings.Prediction.SpeakPredictionOnMouseHover.$t": false, + // This two settings are excluyent, need to encode that + "ApplicationSettings.Prediction.SpeakOnWordClick.$t": true, + "ApplicationSettings.Prediction.InsertOnWordClick.$t": false, + // Prediction Learning + "ApplicationSettings.Prediction.SpellCheckBeforeLearn.$t": false, + "ApplicationSettings.Prediction.AutoLearnAsType.$t": false, + // Scanning + "ApplicationSettings.Scanning.dpi.$t": 600, + "ApplicationSettings.Scanning.colormode.$t": "GrayScale", + "ApplicationSettings.Scanning.HideInterface.$t": false, + // Device Setup (Advanced settigns) + "ApplicationSettings.Scanning.Countdown.$t": true, + "ApplicationSettings.Scanning.CountdownInterval.$t": 10, + "ApplicationSettings.Scanning.ADF.$t": true, + "ApplicationSettings.Scanning.Duplex.$t": true, + // Save Options + "ApplicationSettings.Scanning.AutoSave.$t": true, + "ApplicationSettings.Scanning.DefaultFilePath.$t": "C:\\Users\\vagrant\\Download", + "ApplicationSettings.Scanning.DefaultFileName.$t": "ScannedDoc", + // PDF Output + "ApplicationSettings.Scanning.PdfTextOnly.$t": true, + "ApplicationSettings.Scanning.MSWordRetainFormat.$t": true, + "ApplicationSettings.Scanning.CoverFirstPage.$t": false, + // Output Types + "ApplicationSettings.Scanning.EnableEpub.$t": true, + "ApplicationSettings.Scanning.EnableWeb.$t": true + }, + "options": { + "filename": "${{environment}.APPDATA}\\Texthelp\\ReadAndWrite\\12\\RWSettings.xml", + "encoding": "utf-8", + "xml-tag": "" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(true) + } + }, + { + name: "Testing 'readwritegold_screen_mask_shot' - running on login", + gpiiKey: "readwritegold_screen_mask_shot", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(true) + }, + settingsHandlers: { + "gpii.settingsHandlers.XMLHandler": { + "com.texthelp.readWriteGold": [ + { + "settings": { + // Screen Masking + "ApplicationSettings.ScreenMasking.ScreenMask.$t": "UnderlineTypingLine", + // Screen Masking -> Tint whole screen subsettings + "ApplicationSettings.ScreenMasking.TintScreenColour.$t": "#a7efce", + "ApplicationSettings.ScreenMasking.TintScreenOpacity.$t": 0.5, + "ApplicationSettings.ScreenMasking.ReadingLight.$t": true, + "ApplicationSettings.ScreenMasking.ReadingLightColour.$t": "#00FFFFFF", + "ApplicationSettings.ScreenMasking.ReadingLightOpacity.$t": 0.5, + "ApplicationSettings.ScreenMasking.ReadingLightHeight.$t": 150, + // Screen Masking -> Underline typing line + "ApplicationSettings.ScreenMasking.UnderlineTypingLineColour.$t": "#a7efce", + "ApplicationSettings.ScreenMasking.UnderlineTypingLineOpacity.$t": 0.3, + "ApplicationSettings.ScreenMasking.UnderlineTypingLineHeight.$t": 15, + // Screen Masking -> Tint typing line + "ApplicationSettings.ScreenMasking.TintTypingLineColour.$t": "#FF00FFFF", + "ApplicationSettings.ScreenMasking.TintTypingLineOpacity.$t": 0.6, + // Screen Masking -> Change page color + "ApplicationSettings.ScreenMasking.SystemBackgroundColour.$t": "#a7efce", + // Screen Masking -> Underline cursor + "ApplicationSettings.ScreenMasking.UnderlineCursorColour.$t": "#a7efce", + "ApplicationSettings.ScreenMasking.UnderlineCursorOpacity.$t": 0.4, + "ApplicationSettings.ScreenMasking.UnderlineCursorHeight.$t": 20, + // Screenshot Reader + "ApplicationSettings.Screenshot.CaptureBy.$t": "Drawing Freehand", + "ApplicationSettings.Screenshot.ScreenshotTo.$t": "Window", + "ApplicationSettings.Screenshot.AppendToMsWordDoc.$t": false + }, + "options": { + "filename": "${{environment}.APPDATA}\\Texthelp\\ReadAndWrite\\12\\RWSettings.xml", + "encoding": "utf-8", + "xml-tag": "" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(true) + } + }, + { + name: "Testing 'readwritegold_screen_mask_shot'", + gpiiKey: "readwritegold_screen_mask_shot", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.settingsHandlers.XMLHandler": { + "com.texthelp.readWriteGold": [ + { + "settings": { + // Screen Masking + "ApplicationSettings.ScreenMasking.ScreenMask.$t": "UnderlineTypingLine", + // Screen Masking -> Tint whole screen subsettings + "ApplicationSettings.ScreenMasking.TintScreenColour.$t": "#a7efce", + "ApplicationSettings.ScreenMasking.TintScreenOpacity.$t": 0.5, + "ApplicationSettings.ScreenMasking.ReadingLight.$t": true, + "ApplicationSettings.ScreenMasking.ReadingLightColour.$t": "#00FFFFFF", + "ApplicationSettings.ScreenMasking.ReadingLightOpacity.$t": 0.5, + "ApplicationSettings.ScreenMasking.ReadingLightHeight.$t": 150, + // Screen Masking -> Underline typing line + "ApplicationSettings.ScreenMasking.UnderlineTypingLineColour.$t": "#a7efce", + "ApplicationSettings.ScreenMasking.UnderlineTypingLineOpacity.$t": 0.3, + "ApplicationSettings.ScreenMasking.UnderlineTypingLineHeight.$t": 15, + // Screen Masking -> Tint typing line + "ApplicationSettings.ScreenMasking.TintTypingLineColour.$t": "#FF00FFFF", + "ApplicationSettings.ScreenMasking.TintTypingLineOpacity.$t": 0.6, + // Screen Masking -> Change page color + "ApplicationSettings.ScreenMasking.SystemBackgroundColour.$t": "#a7efce", + // Screen Masking -> Underline cursor + "ApplicationSettings.ScreenMasking.UnderlineCursorColour.$t": "#a7efce", + "ApplicationSettings.ScreenMasking.UnderlineCursorOpacity.$t": 0.4, + "ApplicationSettings.ScreenMasking.UnderlineCursorHeight.$t": 20, + // Screenshot Reader + "ApplicationSettings.Screenshot.CaptureBy.$t": "Drawing Freehand", + "ApplicationSettings.Screenshot.ScreenshotTo.$t": "Window", + "ApplicationSettings.Screenshot.AppendToMsWordDoc.$t": false + }, + "options": { + "filename": "${{environment}.APPDATA}\\Texthelp\\ReadAndWrite\\12\\RWSettings.xml", + "encoding": "utf-8", + "xml-tag": "" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(true) + } + }, + { + name: "Testing 'readwritegold_voice' - running on login", + gpiiKey: "readwritegold_voice", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(true) + }, + settingsHandlers: { + "gpii.settingsHandlers.XMLHandler": { + "com.texthelp.readWriteGold": [ + { + "settings": { + // Speech + "ApplicationSettings.Speech.CurrentVoice.$t": "Mexican Spanish Paulina - Vocalizer", + "ApplicationSettings.Speech.VoiceSpeed.$t": 70, + "ApplicationSettings.Speech.UseVoiceSpeedHotKey.$t": false, + "ApplicationSettings.Speech.WordPause.$t": 200, + "ApplicationSettings.Speech.VoicePitch.$t": 40, + // Speech -> Auto Read + "ApplicationSettings.Speech.SpeakAsIType.$t": true, + // Speech -> Auto Read -> Speak as I type + "ApplicationSettings.Speech.SpeakOnEachLetter.$t": true, + // Speech -> Auto Read -> Speak as I type + "ApplicationSettings.Speech.SpeakOnEachWord.$t": false, + // Speech -> Auto Read -> Speak as I type + "ApplicationSettings.Speech.SpeakOnEachSentence.$t": false, + // Speech -> Screen reading + "ApplicationSettings.Speech.ScreenReadingEnabled.$t": true, + // Speech -> Screen reading -> Items to read + // TODO: Setting is stored in an array format in XML, we need to check how + // XMLSettingsHandler handles this case. + // "ApplicationSettings.Speech.ScreenReadingItems.$t":, + // Speech -> Read by + "ApplicationSettings.Speech.SelectionType.$t": 0, + // Speech -> Continuous reading + "ApplicationSettings.Speech.ContinuousReading.$t": false, + // Speech -> Read the web + "ApplicationSettings.Speech.ReadTheWeb.$t": true, + // Speech -> Speech highlighting + "ApplicationSettings.Speech.HighlightingMethod.$t": 2, + // Speech -> Speech highlighting -> Font + "ApplicationSettings.Speech.TextReaderOneWordFontName.$t": "Times New Roman", + "ApplicationSettings.Speech.optSpeechHighTRFontName.$t": "Times New Roman", + // Speech -> Speech highlighting -> Font size + "ApplicationSettings.Speech.TextReaderOneWordFontSize.$t": 16, + "ApplicationSettings.Speech.optSpeechHighTRFontSize.$t": 16, + // Speech -> Speech highlighting -> Highlighting colors + "ApplicationSettings.Speech.HighlightingBackColour.$t": "Violet", + "ApplicationSettings.Speech.HighlightingForeColour.$t": "Chartreuse" + }, + "options": { + "filename": "${{environment}.APPDATA}\\Texthelp\\ReadAndWrite\\12\\RWSettings.xml", + "encoding": "utf-8", + "xml-tag": "" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(true) + } + }, + { + name: "Testing 'readwritegold_voice'", + gpiiKey: "readwritegold_voice", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.settingsHandlers.XMLHandler": { + "com.texthelp.readWriteGold": [ + { + "settings": { + // Speech + "ApplicationSettings.Speech.CurrentVoice.$t": "Mexican Spanish Paulina - Vocalizer", + "ApplicationSettings.Speech.VoiceSpeed.$t": 70, + "ApplicationSettings.Speech.UseVoiceSpeedHotKey.$t": false, + "ApplicationSettings.Speech.WordPause.$t": 200, + "ApplicationSettings.Speech.VoicePitch.$t": 40, + // Speech -> Auto Read + "ApplicationSettings.Speech.SpeakAsIType.$t": true, + // Speech -> Auto Read -> Speak as I type + "ApplicationSettings.Speech.SpeakOnEachLetter.$t": true, + // Speech -> Auto Read -> Speak as I type + "ApplicationSettings.Speech.SpeakOnEachWord.$t": false, + // Speech -> Auto Read -> Speak as I type + "ApplicationSettings.Speech.SpeakOnEachSentence.$t": false, + // Speech -> Screen reading + "ApplicationSettings.Speech.ScreenReadingEnabled.$t": true, + // Speech -> Screen reading -> Items to read + // TODO: Setting is stored in an array format in XML, we need to check how + // XMLSettingsHandler handles this case. + // "ApplicationSettings.Speech.ScreenReadingItems.$t":, + // Speech -> Read by + "ApplicationSettings.Speech.SelectionType.$t": 0, + // Speech -> Continuous reading + "ApplicationSettings.Speech.ContinuousReading.$t": false, + // Speech -> Read the web + "ApplicationSettings.Speech.ReadTheWeb.$t": true, + // Speech -> Speech highlighting + "ApplicationSettings.Speech.HighlightingMethod.$t": 2, + // Speech -> Speech highlighting -> Font + "ApplicationSettings.Speech.TextReaderOneWordFontName.$t": "Times New Roman", + "ApplicationSettings.Speech.optSpeechHighTRFontName.$t": "Times New Roman", + // Speech -> Speech highlighting -> Font size + "ApplicationSettings.Speech.TextReaderOneWordFontSize.$t": 16, + "ApplicationSettings.Speech.optSpeechHighTRFontSize.$t": 16, + // Speech -> Speech highlighting -> Highlighting colors + "ApplicationSettings.Speech.HighlightingBackColour.$t": "Violet", + "ApplicationSettings.Speech.HighlightingForeColour.$t": "Chartreuse" + }, + "options": { + "filename": "${{environment}.APPDATA}\\Texthelp\\ReadAndWrite\\12\\RWSettings.xml", + "encoding": "utf-8", + "xml-tag": "" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(true) + } + }, + { + name: "Testing 'readwritegold_trans_voca_voice' - running on login", + gpiiKey: "readwritegold_trans_voca_voice", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(true) + }, + settingsHandlers: { + "gpii.settingsHandlers.XMLHandler": { + "com.texthelp.readWriteGold": [ + { + "settings": { + // Translation + "ApplicationSettings.Translation.FromLanguage.$t": "cs", + "ApplicationSettings.Translation.ToLanguage.$t": "da", + // Vocabulary List + // TODO: Verify with Microsoft word installation + "ApplicationSettings.Vocabulary.VocabListWindow.$t": true, + "ApplicationSettings.Vocabulary.VocabListImages.$t": false, + "ApplicationSettings.Vocabulary.VocabListDefinitions.$t": true, + // Voice Note + "ApplicationSettings.VoiceNote.InsertInDoc.$t": true + }, + "options": { + "filename": "${{environment}.APPDATA}\\Texthelp\\ReadAndWrite\\12\\RWSettings.xml", + "encoding": "utf-8", + "xml-tag": "" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(true) + } + }, + { + name: "Testing 'readwritegold_trans_voca_voice'", + gpiiKey: "readwritegold_trans_voca_voice", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.settingsHandlers.XMLHandler": { + "com.texthelp.readWriteGold": [ + { + "settings": { + // Translation + "ApplicationSettings.Translation.FromLanguage.$t": "cs", + "ApplicationSettings.Translation.ToLanguage.$t": "da", + // Vocabulary List + // TODO: Verify with Microsoft word installation + "ApplicationSettings.Vocabulary.VocabListWindow.$t": true, + "ApplicationSettings.Vocabulary.VocabListImages.$t": false, + "ApplicationSettings.Vocabulary.VocabListDefinitions.$t": true, + // Voice Note + "ApplicationSettings.VoiceNote.InsertInDoc.$t": true + }, + "options": { + "filename": "${{environment}.APPDATA}\\Texthelp\\ReadAndWrite\\12\\RWSettings.xml", + "encoding": "utf-8", + "xml-tag": "" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.readwrite.flexibleHandlerEntry(true) + } + } +]; + + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.windows.readwrite.testDefs", + configName: "gpii.tests.acceptance.windows.readWrite.config", + configPath: "%gpii-universal/tests/platform/windows/configs" +}, ["gpii.test.integration.testCaseHolder.windows"], + module, require, __dirname); diff --git a/tests/platform/windows/windows-readWrite12-testSpec.txt b/tests/platform/windows/windows-readWrite12-testSpec.txt new file mode 100644 index 000000000..acdd5291a --- /dev/null +++ b/tests/platform/windows/windows-readWrite12-testSpec.txt @@ -0,0 +1,15 @@ +windows-readWrite12-testSpec.js + +Descriptions: +This will run the acceptance tests for Read&Write Gold. It uses 4 preference sets: + - readwritegold_general. + - readwritegold_check_dict_high. + - readwritegold_pred_scan_out. + - readwritegold_screen_mask_shot. + - readwritegold_voice. + - readwritegold_trans_voca_voice. + +Requirements: +* Read&Write 12 Gold installed + +Has been tested with Read&Write Gold 12 on Windows 10. diff --git a/tests/platform/windows/windows-uioPlus-testSpec.js b/tests/platform/windows/windows-uioPlus-testSpec.js new file mode 100644 index 000000000..4966a6950 --- /dev/null +++ b/tests/platform/windows/windows-uioPlus-testSpec.js @@ -0,0 +1,32 @@ +/* +GPII Integration and Acceptance Testing + +Copyright 2014 Emergya +Copyright 2017 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); +require("../shared/uioPlusTestDefs.js"); + +gpii.loadTestingSupport(); + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.uioPlus.testDefs", + configName: "gpii.tests.acceptance.windows.uioPlus.config", + configPath: "%gpii-universal/tests/platform/windows/configs" +}, ["gpii.test.integration.testCaseHolder.windows"], + module, require, __dirname); diff --git a/tests/platform/windows/windows-uioPlus-testSpec.txt b/tests/platform/windows/windows-uioPlus-testSpec.txt new file mode 100644 index 000000000..f91d1115c --- /dev/null +++ b/tests/platform/windows/windows-uioPlus-testSpec.txt @@ -0,0 +1,19 @@ +windows-uioPlus-testSpec.txt + +Descriptions: + +* Solution: UIO+ +* Device reporter file: uioPlus.json +* preference sets: + * uioPlus_character_space + * uioPlus_font_size + * uioPlus_high_contrast + * uioPlus_highlight_colour + * uioPlus_inputs_larger + * uioPlus_line_space + * uioPlus_multiple_settings + * uioPlus_self_voicing + * uioPlus_simplified + * uioPlus_support_tool + * uioPlus_syllabification + * uioPlus_toc diff --git a/tests/platform/windows/windows-zoomtext-testSpec.js b/tests/platform/windows/windows-zoomtext-testSpec.js new file mode 100644 index 000000000..bb6803edf --- /dev/null +++ b/tests/platform/windows/windows-zoomtext-testSpec.js @@ -0,0 +1,161 @@ +/* +GPII Acceptance Testing + +Copyright 2014 Raising the Floor International + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.windows.zoomtext"); + +// To avoid duplicating this entire piece in each test. Given a true or false value +// as input, this will return a settingshandler entry, containing all the options from +// the solutions registry entry for NVDAs launchHandler, with a settings block with +// running: X - where X is replaced with the input parameter +gpii.tests.windows.zoomtext.flexibleHandlerEntry = function (running) { + return { + "com.aisquared.zoomtext": [{ + "settings": { + "running": running + }, + "options": { + "getState": [ + { + "type": "gpii.processReporter.find", + "command": "AiSquared.ZoomText.UI.exe" + } + ], + "setTrue": [ + { + "type": "gpii.launch.exec", + "command": "\"${{registry}.HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\ZoomText2019.exe\\}\"" + } + ], + "setFalse": [ + { + "type": "gpii.windows.closeProcessByName", + "filename": "Zt.exe", + "options": { + "message": "WM_CLOSE" + } + } + ] + } + }] + }; +}; + +gpii.tests.windows.zoomtext.testDefs = [ + { + name: "Testing NP set \"zoomtext_application\" using Flat matchmaker", + gpiiKey: "zoomtext_application", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.zoomtext.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.settingsHandlers.INISettingsHandler": { + "com.aisquared.zoomtext": [ + { + "settings": { + // [Preferences] + "Preferences.SmartInvert": 1, + + // [ZtSpeech] + "ZtSpeech.SapiProductName": "VocalizerExpressive-Dutch", + "ZtSpeech.Language": 12, + + // [VocalizerExpressive-Dutch Speech - Screen Reader] + // ================================================================== + "VocalizerExpressive-Dutch Speech - Screen Reader.Valid": 1, + "VocalizerExpressive-Dutch Speech - Screen Reader.SettingsValid": 1, + "VocalizerExpressive-Dutch Speech - Screen Reader.Muted": 0, + "VocalizerExpressive-Dutch Speech - Screen Reader.Voice": 0, + "VocalizerExpressive-Dutch Speech - Screen Reader.Rate": 120, + "VocalizerExpressive-Dutch Speech - Screen Reader.Volume": 70, + "VocalizerExpressive-Dutch Speech - Screen Reader.Pitch": 35, + + // [VocalizerExpressive-Dutch Speech - DocReader] + "VocalizerExpressive-Dutch Speech - DocReader.Valid": 1, + "VocalizerExpressive-Dutch Speech - DocReader.SettingsValid": 1, + "VocalizerExpressive-Dutch Speech - DocReader.Muted": 0, + "VocalizerExpressive-Dutch Speech - DocReader.Voice": 0, + "VocalizerExpressive-Dutch Speech - DocReader.Rate": 120, + "VocalizerExpressive-Dutch Speech - DocReader.Volume": 70, + "VocalizerExpressive-Dutch Speech - DocReader.Pitch": 35, + + // [Magnifier] + "PRIMARY.magPower": 140, + "STATIC 1.magPower": 140, + + // [EchoTyping] + "EchoTyping.Mode": 32769, + + // [EchoMouse] + "EchoMouse.HoverMode": 1, + "EchoMouse.LineMode": 0, + + // [PointerScheme] + "PointerScheme.PtrEnhEnable": 1, + "PointerScheme.PtrEnhIndex": 2, + + // [Tracking] + "Tracking.RouteMouse": 0 + }, + "options": { + "filename": "${{environment}.AppData}\\Freedom Scientific\\ZoomText\\2019\\Config\\zten-US.zxc", + "encoding": "utf16le" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.zoomtext.flexibleHandlerEntry(true) + } + }, + { + name: "Testing NP set \"zoomtext_common\" using Flat matchmaker", + gpiiKey: "zoomtext_common", + initialState: { + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.zoomtext.flexibleHandlerEntry(false) + }, + settingsHandlers: { + "gpii.settingsHandlers.INISettingsHandler": { + "com.aisquared.zoomtext": [ + { + "settings": { + "PRIMARY.magPower": 160, + "STATIC 1.magPower": 160 + }, + "options": { + "filename": "${{environment}.AppData}\\Freedom Scientific\\ZoomText\\2019\\Config\\zten-US.zxc", + "encoding": "utf16le" + } + } + ] + }, + "gpii.launchHandlers.flexibleHandler": gpii.tests.windows.zoomtext.flexibleHandlerEntry(true) + } + } +]; + +module.exports = gpii.test.bootstrap({ + testDefs: "gpii.tests.windows.zoomtext.testDefs", + configName: "gpii.tests.acceptance.windows.zoomtext.config", + configPath: "%gpii-universal/tests/platform/windows/configs" +}, ["gpii.test.integration.testCaseHolder.windows"], + module, require, __dirname); diff --git a/tests/platform/windows/windows-zoomtext-testSpec.txt b/tests/platform/windows/windows-zoomtext-testSpec.txt new file mode 100644 index 000000000..507f24f68 --- /dev/null +++ b/tests/platform/windows/windows-zoomtext-testSpec.txt @@ -0,0 +1,9 @@ +windows-zoomtext-testSpec.js + +Descriptions: +This will run the acceptance tests for the ZoomText 10.1 (AiSquared). It uses 4 NP sets: zoomtext_application.json, zoomtext_common.json, zoomtext_common2.json and zoomtext_common3.json + +Prerequisites: +* ZoomText needs to be installed + +Only works with ZoomText 10.1 \ No newline at end of file diff --git a/tests/production/AddUserSettingsToCouchTests.js b/tests/production/AddUserSettingsToCouchTests.js new file mode 100644 index 000000000..f0c256a2c --- /dev/null +++ b/tests/production/AddUserSettingsToCouchTests.js @@ -0,0 +1,119 @@ +/** +Tests for adding test gpii keys and 'user' prefs safe. + +Copyright 2019 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +WARNING: Do not run these tests directly. They are called from within the +"vagrantCloudBasedContainers.sh" after it has initialized the environment. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.productionConfigTesting"); + +require("./ProductionTestsUtils.js"); + +gpii.tests.productionConfigTesting.getDataFromFileById = function (locations) { + var resultDocs = []; + fluid.each(locations, function (oneFile) { + var dataArray = fluid.require(oneFile.filePath); + var ids = fluid.makeArray(oneFile.ids); + fluid.each(dataArray, function (anEntry) { + if (ids.includes(anEntry._id)) { + resultDocs.push(anEntry); + } + }); + }); + return { + "docs": resultDocs + }; +}; + +gpii.tests.productionConfigTesting.extraTestData = gpii.tests.productionConfigTesting.getDataFromFileById([ + { + filePath: "%gpii-universal/tests/data/dbData/gpiiKeys.json", + ids: "gpii_key_no_prefs_safe" + }, { + filePath: "%gpii-universal/build/tests/dbData/gpiiKeys.json", + ids: "os_gnome" + }, { + filePath: "%gpii-universal/build/tests/dbData/prefsSafes.json", + ids: "prefsSafe-os_gnome" + }, { + filePath: "%gpii-universal/tests/data/dbData/gpiiAppInstallationClients.json", + ids: [ + "gpiiAppInstallationClient-schemaV0.1", + "gpiiAppInstallationClient-nova", + "gpiiAppInstallationClient-nova-failInIpVerification", + "gpiiAppInstallationClient-nova-failInAllowedPrefsToWrite" + ] + }, { + filePath: "%gpii-universal/tests/data/dbData/clientCredentials.json", + ids: [ + "clientCredential-schemaV0.1", + "clientCredential-nova", + "clientCredential-failInIpVerification", + "clientCredential-failInAllowedPrefsToWrite" + ] + } +]); + +fluid.log(gpii.tests.productionConfigTesting.extraTestData); + +// Tests that add extra documents for tests to the database +fluid.defaults("gpii.tests.productionConfigTesting.addExtraDocs", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { funcName: "fluid.log", args: ["Adding extra test data..."]}, + { + func: "{addInBulk}.sendToDatabase", + args: [gpii.tests.productionConfigTesting.extraTestData] + }, { + event: "{addInBulk}.events.onComplete", + listener: "jqUnit.assertEquals", + args: ["Add extra test documents to database", "{addInBulk}.options.expectedStatusCode", "{addInBulk}.nativeResponse.statusCode"] + }, + { funcName: "fluid.log", args: ["Added extra test data."]} + ] +}); + +fluid.defaults("gpii.tests.productionConfigTesting.addRecordsSequence", { + gradeNames: ["fluid.test.sequence"], + sequenceElements: { + addRecords: { + gradeNames: "gpii.tests.productionConfigTesting.addExtraDocs" + } + } +}); + +gpii.tests.productionConfigTesting.addTestRecordsToDatabaseTests = [{ + name: "Flow manager production tests - add test GPII keys and PrefsSafe", + expect: 1, + config: gpii.tests.productionConfigTesting.config, + components: { + addInBulk: { + type: "gpii.tests.productionConfigTesting.dataBaseRequest", + options: { + path: "/gpii/_bulk_docs", + method: "POST", + expectedStatusCode: 201 + } + } + }, + sequenceGrade: "gpii.tests.productionConfigTesting.addRecordsSequence" +}]; + +gpii.test.runServerTestDefs(gpii.tests.productionConfigTesting.addTestRecordsToDatabaseTests); diff --git a/tests/production/CloudStatusProductionTests.js b/tests/production/CloudStatusProductionTests.js new file mode 100644 index 000000000..3ab0ca0a1 --- /dev/null +++ b/tests/production/CloudStatusProductionTests.js @@ -0,0 +1,105 @@ +/** +GPII Production Config tests + +Requirements: +* an internet connection +* a cloud based flow manager +* a preferences server +* a CouchDB server + +--- + +Copyright 2015 Raising the Floor - International +Copyright 2018, 2019 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +WARNING: Do not run these tests directly. They are called from within the +"vagrantCloudBasedContainers.sh" after it has initialized the environment. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.productionConfigTesting"); + +require("../shared/DevelopmentTestDefs.js"); +require("./ProductionTestsUtils.js"); + +// Flowmanager tests for: +// /user/%gpiiKey/login and /user/%gpiiKey/logout (as defined in gpii.tests.development.testDefs), +// /health, +// /ready, +gpii.tests.productionConfigTesting.testDefs = fluid.transform(gpii.tests.development.testDefs, function (testDefIn) { + var testDef = fluid.extend(true, {}, testDefIn, { + name: "Flow Manager production tests -- status, login, and logout", + config: gpii.tests.productionConfigTesting.config, + expect: 6, + components: { + healthRequest: { + type: "gpii.tests.productionConfigTesting.cloudStatusRequest", + options: { + path: "/health", + expectedPayload: {"isHealthy": true} + } + }, + readyRequest: { + type: "gpii.tests.productionConfigTesting.cloudStatusRequest", + options: { + path: "/ready", + expectedPayload: {"isReady": true} + } + } + }, + sequenceGrade: "gpii.tests.productionConfigTesting.cloudStatusSequence" + }); + return testDef; +}); + +fluid.defaults("gpii.tests.productionConfigTesting.cloudStatus", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { funcName: "fluid.log", args: ["Cloud status tests:"]}, + { + func: "{healthRequest}.sendToCBFM" + }, { + event: "{healthRequest}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testResponse" + }, { + func: "{readyRequest}.sendToCBFM" + }, { + event: "{readyRequest}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testResponse" + }, + { funcName: "fluid.log", args: ["Cloud status tests end"]} + ] +}); + +fluid.defaults("gpii.tests.productionConfigTesting.cloudStatusSequence", { + gradeNames: ["gpii.test.standardServerSequenceGrade"], + sequenceElements: { + cloudStatus: { + gradeNames: "gpii.tests.productionConfigTesting.cloudStatus", + priority: "after:startServer" + }, + loginLogout: { + gradeNames: "gpii.tests.development.loginLogout", + priority: "after:cloudStatus" + } + } +}); + +gpii.test.runServerTestDefs(gpii.tests.productionConfigTesting.testDefs); diff --git a/tests/production/DeleteUserSettingsFromCouchTests.js b/tests/production/DeleteUserSettingsFromCouchTests.js new file mode 100644 index 000000000..bb70856c4 --- /dev/null +++ b/tests/production/DeleteUserSettingsFromCouchTests.js @@ -0,0 +1,334 @@ +/** +Tests for deleting test gpii keys and 'user' prefs safe. + +Copyright 2019 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +WARNING: Do not run these tests directly. They are called from within the +"vagrantCloudBasedContainers.sh" after it has initialized the environment. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.productionConfigTesting"); + +require("./ProductionTestsUtils.js"); + +// Special case list of GPII Keys used by login/logout tests that generate +// extraneous access tokens that need to be removed after the tests are done. +gpii.tests.productionConfigTesting.loginGpiiKeys = [ + "testUser1", "nonexistent_gpii_key" +]; + +gpii.tests.productionConfigTesting.clientCredentials = [ + "clientCredential-1", + "clientCredential-schemaV0.1", + "clientCredential-nova", + "clientCredential-failInIpVerification", + "clientCredential-failInAllowedPrefsToWrite" +]; + +gpii.tests.productionConfigTesting.testGetThenSaveDocForDeletion = function (data, request, docsToRemove) { + gpii.tests.productionConfigTesting.testStatusCodes(request); + // Mark and store the to-be-deleted record + if (request.nativeResponse.statusCode === 201 || request.nativeResponse.statusCode === 200) { + var thisDoc = JSON.parse(data); + thisDoc._deleted = true; + // saved for the furthur use in getPrefsSafeDoc() for getting corresponding prefs safe of this GPII key + request.options.docToRemove = thisDoc; + fluid.log("Will remove ", thisDoc.type, " ", thisDoc._id); + docsToRemove.push(thisDoc); + } else { + fluid.log("Nothing to remove at ", request.options.path); + } +}; + +gpii.tests.productionConfigTesting.getPrefsSafeDoc = function (prefsSafeRequest, gpiiKeyRequest) { + var gpiiKeyToRemove = gpiiKeyRequest.options.docToRemove; + if (gpiiKeyToRemove && gpiiKeyToRemove.prefsSafeId) { + prefsSafeRequest.options.path = "/gpii/" + gpiiKeyToRemove.prefsSafeId; + } else { + // This path should force a 404 + prefsSafeRequest.options.path = "/gpii/prefsSafe-" + prefsSafeRequest.options.gpiiKey; + fluid.log("No Prefs Safe to retrieve for GPII key " + prefsSafeRequest.options.gpiiKey); + } + prefsSafeRequest.sendToDatabase(); +}; + +gpii.tests.productionConfigTesting.testGetExtraAccessTokens = function (data, accessTokensRequest, docsToRemove) { + gpii.tests.productionConfigTesting.testStatusCodes(accessTokensRequest); + var tokens = JSON.parse(data); + if (tokens.rows) { + fluid.each(tokens.rows, function (aRow) { + fluid.each(accessTokensRequest.options.extraGpiiKeys, function (extraGpiiKey) { + var aToken = aRow.value.authorization; + if (aToken.gpiiKey === extraGpiiKey && gpii.tests.productionConfigTesting.clientCredentials.includes(aToken.clientCredentialId)) { + aToken._deleted = true; + docsToRemove.push(aToken); + fluid.log("Will remove ", aToken.type, " for ", aToken.gpiiKey); + } + }); + }); + } else { + fluid.log("No access tokens to remove"); + } +}; + +fluid.defaults("gpii.tests.productionConfigTesting.deleteUserSettings", { + gradeNames: ["fluid.test.sequenceElement"], + members: { + docsToRemove: [] // collect documents to be removed at the end via /_bulk_docs + }, + sequence: [ + { funcName: "fluid.log", args: ["Deleting extra loaded data and access tokens generated during test runs:"]}, + { + // remove the loaded GPII key "os_gnome" and its prefs safe + func: "{getOsGnomeKey}.sendToDatabase" + }, { + event: "{getOsGnomeKey}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testGetThenSaveDocForDeletion", + args: ["{arguments}.0", "{arguments}.1", "{that}.docsToRemove"] + }, { + func: "gpii.tests.productionConfigTesting.getPrefsSafeDoc", + args: ["{getOsGnomeKeyPrefsSafe}", "{getOsGnomeKey}" ] + }, { + event: "{getOsGnomeKeyPrefsSafe}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testGetThenSaveDocForDeletion", + args: ["{arguments}.0", "{arguments}.1", "{that}.docsToRemove"] + }, { + // remove the GPII key "gpii_key_no_prefs_safe" and its generated prefs safe + func: "{getGpiiKeyNoPrefsSafe}.sendToDatabase" + }, { + event: "{getGpiiKeyNoPrefsSafe}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testGetThenSaveDocForDeletion", + args: ["{arguments}.0", "{arguments}.1", "{that}.docsToRemove"] + }, { + funcName: "gpii.tests.productionConfigTesting.getPrefsSafeDoc", + args: ["{getGpiiKeyNoPrefsSafePrefsSafe}", "{getGpiiKeyNoPrefsSafe}"] + }, { + event: "{getGpiiKeyNoPrefsSafePrefsSafe}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testGetThenSaveDocForDeletion", + args: ["{arguments}.0", "{arguments}.1", "{that}.docsToRemove"] + }, { + // remove loaded client credential data + func: "{getClientCredentialSchemaV01}.sendToDatabase" + }, { + event: "{getClientCredentialSchemaV01}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testGetThenSaveDocForDeletion", + args: ["{arguments}.0", "{arguments}.1", "{that}.docsToRemove"] + }, { + func: "{getClientCredentialNova}.sendToDatabase" + }, { + event: "{getClientCredentialNova}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testGetThenSaveDocForDeletion", + args: ["{arguments}.0", "{arguments}.1", "{that}.docsToRemove"] + }, { + func: "{getClientCredentialFailInIpVerification}.sendToDatabase" + }, { + event: "{getClientCredentialFailInIpVerification}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testGetThenSaveDocForDeletion", + args: ["{arguments}.0", "{arguments}.1", "{that}.docsToRemove"] + }, { + func: "{getClientCredentialFailInAllowedPrefsToWrite}.sendToDatabase" + }, { + event: "{getClientCredentialFailInAllowedPrefsToWrite}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testGetThenSaveDocForDeletion", + args: ["{arguments}.0", "{arguments}.1", "{that}.docsToRemove"] + }, { + // remove loaded client data + func: "{getClientOldSchema}.sendToDatabase" + }, { + event: "{getClientOldSchema}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testGetThenSaveDocForDeletion", + args: ["{arguments}.0", "{arguments}.1", "{that}.docsToRemove"] + }, { + func: "{getClientNova}.sendToDatabase" + }, { + event: "{getClientNova}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testGetThenSaveDocForDeletion", + args: ["{arguments}.0", "{arguments}.1", "{that}.docsToRemove"] + }, { + func: "{getClientFailInIpVerification}.sendToDatabase" + }, { + event: "{getClientFailInIpVerification}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testGetThenSaveDocForDeletion", + args: ["{arguments}.0", "{arguments}.1", "{that}.docsToRemove"] + }, { + func: "{getClientFailInAllowedPrefsToWrite}.sendToDatabase" + }, { + event: "{getClientFailInAllowedPrefsToWrite}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testGetThenSaveDocForDeletion", + args: ["{arguments}.0", "{arguments}.1", "{that}.docsToRemove"] + }, { + // remove the GPII key "nonexistent_gpii_key" and its prefs safe + func: "{getNonExistentGpiiKey}.sendToDatabase" + }, { + event: "{getNonExistentGpiiKey}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testGetThenSaveDocForDeletion", + args: ["{arguments}.0", "{arguments}.1", "{that}.docsToRemove"] + }, { + funcName: "gpii.tests.productionConfigTesting.getPrefsSafeDoc", + args: ["{getNonExistentGpiiKeyPrefsSafe}", "{getNonExistentGpiiKey}"] + }, { + event: "{getNonExistentGpiiKeyPrefsSafe}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testGetThenSaveDocForDeletion", + args: ["{arguments}.0", "{arguments}.1", "{that}.docsToRemove"] + }, { + // remove access tokens generated during all test runs + func: "{getExtraAccessTokens}.sendToDatabase" + }, { + event: "{getExtraAccessTokens}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testGetExtraAccessTokens", + args: ["{arguments}.0", "{arguments}.1", "{that}.docsToRemove"] + }, { + // delete all loaded test data and data generated during all test runs via /_bulk_docs + funcName: "gpii.tests.productionConfigTesting.bulkDelete", + args: ["{deleteInBulk}", "{that}.docsToRemove"] + }, { + event: "{deleteInBulk}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testStatusCode" + }, { + funcName: "fluid.log", + args: ["Deleted extra loaded data and access tokens generated during test runs"] + } + ] +}); + +fluid.defaults("gpii.tests.productionConfigTesting.deleteRecordsSequence", { + gradeNames: ["fluid.test.sequence"], + sequenceElements: { + deleteRecords: { + gradeNames: "gpii.tests.productionConfigTesting.deleteUserSettings" + } + } +}); + +// Tests for deleting test 'user' preferences from the database +gpii.tests.productionConfigTesting.deleteTestRecordsFromDatabaseTests = [{ + name: "Flow manager production tests - delete extra loaded data and access tokens generated during test runs", + expect: 16, + config: gpii.tests.productionConfigTesting.config, + gradeNames: ["gpii.test.common.testCaseHolder"], + components: { + getOsGnomeKey: { + type: "gpii.tests.productionConfigTesting.dataBaseRequest", + options: { + path: "/gpii/os_gnome" + } + }, + getOsGnomeKeyPrefsSafe: { + type: "gpii.tests.productionConfigTesting.dataBaseRequest", + options: { + // set at gpii.tests.productionConfigTesting.getPrefsSafeDoc() + path: null, + gpiiKey: "os_gnome" + } + }, + getGpiiKeyNoPrefsSafe: { + type: "gpii.tests.productionConfigTesting.dataBaseRequest", + options: { + path: "/gpii/gpii_key_no_prefs_safe" + } + }, + getGpiiKeyNoPrefsSafePrefsSafe: { + type: "gpii.tests.productionConfigTesting.dataBaseRequest", + options: { + // set at gpii.tests.productionConfigTesting.getPrefsSafeDoc() + path: null, + gpiiKey: "gpii_key_no_prefs_safe" + } + }, + getNonExistentGpiiKey: { + type: "gpii.tests.productionConfigTesting.dataBaseRequest", + options: { + path: "/gpii/nonexistent_gpii_key" + } + }, + getNonExistentGpiiKeyPrefsSafe: { + type: "gpii.tests.productionConfigTesting.dataBaseRequest", + options: { + // set at gpii.tests.productionConfigTesting.getPrefsSafeDoc() + path: null, + gpiiKey: "nonexistent_gpii_key" + } + }, + "getClientCredentialSchemaV01": { + type: "gpii.tests.productionConfigTesting.dataBaseRequest", + options: { + path: "/gpii/clientCredential-schemaV0.1" + } + }, + getClientCredentialNova: { + type: "gpii.tests.productionConfigTesting.dataBaseRequest", + options: { + path: "/gpii/clientCredential-nova" + } + }, + getClientCredentialFailInIpVerification: { + type: "gpii.tests.productionConfigTesting.dataBaseRequest", + options: { + path: "/gpii/clientCredential-failInIpVerification" + } + }, + getClientCredentialFailInAllowedPrefsToWrite: { + type: "gpii.tests.productionConfigTesting.dataBaseRequest", + options: { + path: "/gpii/clientCredential-failInAllowedPrefsToWrite" + } + }, + getClientOldSchema: { + type: "gpii.tests.productionConfigTesting.dataBaseRequest", + options: { + path: "/gpii/gpiiAppInstallationClient-schemaV0.1" + } + }, + getClientNova: { + type: "gpii.tests.productionConfigTesting.dataBaseRequest", + options: { + path: "/gpii/gpiiAppInstallationClient-nova" + } + }, + getClientFailInIpVerification: { + type: "gpii.tests.productionConfigTesting.dataBaseRequest", + options: { + path: "/gpii/gpiiAppInstallationClient-nova-failInIpVerification" + } + }, + getClientFailInAllowedPrefsToWrite: { + type: "gpii.tests.productionConfigTesting.dataBaseRequest", + options: { + path: "/gpii/gpiiAppInstallationClient-nova-failInAllowedPrefsToWrite" + } + }, + getExtraAccessTokens: { + type: "gpii.tests.productionConfigTesting.dataBaseRequest", + options: { + path: "/gpii/_design/views/_view/findInfoByAccessToken", + extraGpiiKeys: gpii.tests.productionConfigTesting.loginGpiiKeys + } + }, + deleteInBulk: { + type: "gpii.tests.productionConfigTesting.dataBaseRequest", + options: { + path: "/gpii/_bulk_docs", + method: "POST", + expectedStatusCode: 201 + } + } + }, + sequenceGrade: "gpii.tests.productionConfigTesting.deleteRecordsSequence" +}]; + +gpii.test.runServerTestDefs(gpii.tests.productionConfigTesting.deleteTestRecordsFromDatabaseTests); diff --git a/tests/production/ProductionTestsUtils.js b/tests/production/ProductionTestsUtils.js new file mode 100644 index 000000000..3d4d73bef --- /dev/null +++ b/tests/production/ProductionTestsUtils.js @@ -0,0 +1,187 @@ +/** +Common data and code shared among the production configuration tests. + +Copyright 2019 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ + +"use strict"; + +var fluid = require("infusion"), + url = fluid.require("url", require, "url"), + jqUnit = fluid.require("node-jqunit", require, "jqUnit"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); +fluid.registerNamespace("gpii.tests.productionConfigTesting"); + +gpii.tests.productionConfigTesting.config = { + configName: "gpii.tests.untrusted.production.config", + configPath: "%gpii-universal/tests/configs" +}; + +gpii.tests.productionConfigTesting.cloudUrl = url.parse( + process.env.GPII_CLOUD_URL +); + +gpii.tests.productionConfigTesting.couchdbUrl = url.parse( + process.env.GPII_COUCHDB_URL +); + +// Base grade for database requests +fluid.defaults("gpii.tests.productionConfigTesting.dataBaseRequest", { + gradeNames: ["kettle.test.request.http"], + host: gpii.tests.productionConfigTesting.couchdbUrl.hostname, + hostname: gpii.tests.productionConfigTesting.couchdbUrl.hostname, + auth: gpii.tests.productionConfigTesting.couchdbUrl.auth, + expectedStatusCodes: [200, 404], + invokers: { + sendToDatabase: { + funcName: "gpii.tests.productionConfigTesting.sendToRemoteHost", + args: [ + "{that}", + gpii.tests.productionConfigTesting.couchdbUrl.port, + "{arguments}.0" // payload (optional) + ] + } + } +}); + +// Cloud based flowmanager status requests +fluid.defaults("gpii.tests.productionConfigTesting.cloudStatusRequest", { + gradeNames: ["kettle.test.request.http"], + host: gpii.tests.productionConfigTesting.cloudUrl.hostname, + hostname: gpii.tests.productionConfigTesting.cloudUrl.hostname, + expectedStatusCode: 200, + invokers: { + sendToCBFM: { + funcName: "gpii.tests.productionConfigTesting.sendToRemoteHost", + args: ["{that}", gpii.tests.productionConfigTesting.cloudUrl.port] + } + } +}); + +// Access token deletion requests +fluid.defaults("gpii.tests.cloud.oauth2.accessTokensDeleteRequests", { + gradeNames: ["fluid.component"], + components: { + getAccessTokensRequest: { + type: "gpii.tests.productionConfigTesting.dataBaseRequest", + options: { + path: "/gpii/_design/views/_view/findInfoByAccessToken", + tokensToRemove: [] // set by successful request. + } + }, + bulkDeleteRequest: { + type: "gpii.tests.productionConfigTesting.dataBaseRequest", + options: { + path: "/gpii/_bulk_docs", + method: "POST", + expectedStatusCode: 201 + } + } + } +}); + +// Sequence elements for cleaning up extra access tokens +fluid.defaults("gpii.tests.productionConfigTesting.deleteAccessTokensSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { funcName: "fluid.log", args: ["Delete extra test access tokens"]}, + { + func: "{getAccessTokensRequest}.sendToDatabase" + }, { + event: "{getAccessTokensRequest}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.testGetAccessTokensForDeletion" + }, { + funcName: "gpii.tests.productionConfigTesting.bulkDelete", + args: ["{bulkDeleteRequest}", "{getAccessTokensRequest}.options.tokensToRemove"] + }, { + event: "{bulkDeleteRequest}.events.onComplete", + listener: "gpii.tests.productionConfigTesting.afterAccessTokensDeletion" + }, { + funcName: "fluid.log", + args: ["Deleted extra test access tokens"] + } + ] +}); + +// Grade for cleaning up access tokens +fluid.defaults("gpii.tests.productionConfigTesting.flushAccessTokens", { + gradeNames: ["gpii.test.standardServerSequenceGrade"], + testCaseGradeNames: "gpii.tests.cloud.oauth2.accessTokensDeleteRequests", + sequenceElements: { + deleteTokens: { + gradeNames: "gpii.tests.productionConfigTesting.deleteAccessTokensSequence", + priority: "before:stopServer" + } + } +}); + +// Grade for "disruptions" that are also proper sequence grades. Use the +// standard server sequence +fluid.defaults("gpii.test.disruption.settings.sequenceGrade", { + gradeNames: ["gpii.test.disruption", "gpii.tests.productionConfigTesting.flushAccessTokens"] +}); + +gpii.tests.productionConfigTesting.testStatusCode = function (data, request) { + jqUnit.assertEquals( + "Checking status of " + request.options.path, + request.options.expectedStatusCode, request.nativeResponse.statusCode + ); +}; + +gpii.tests.productionConfigTesting.testResponse = function (data, request) { + gpii.tests.productionConfigTesting.testStatusCode(data, request); + jqUnit.assertDeepEq( + "Checking paylod of " + request.options.path, + request.options.expectedPayload, JSON.parse(data) + ); +}; + +gpii.tests.productionConfigTesting.testStatusCodes = function (request) { + var expected = request.options.expectedStatusCodes; + var actual = request.nativeResponse.statusCode; + jqUnit.assertTrue( + "Database request " + request.options.path + " expected [" + expected + + "]; actual status: " + actual, + fluid.contains(expected, actual) + ); +}; + +gpii.tests.productionConfigTesting.testGetAccessTokensForDeletion = function (data, accessTokensRequest) { + gpii.tests.productionConfigTesting.testStatusCodes(accessTokensRequest); + var tokens = JSON.parse(data); + fluid.each(tokens.rows, function (aRow) { + var aToken = aRow.value.authorization; + if (fluid.contains(gpii.test.cloudBased.oauth2.accessTokenCache, aToken.accessToken)) { + aToken._deleted = true; + accessTokensRequest.options.tokensToRemove.push(aToken); + fluid.log("Will remove ", aToken.type, " for ", aToken.gpiiKey); + } + }); +}; + +gpii.tests.productionConfigTesting.bulkDelete = function (bulkDeleteRequest, docsToRemove) { + var bulkDocsArray = { "docs": [] }; + fluid.each(docsToRemove, function (aDocToRemove) { + if (aDocToRemove) { + bulkDocsArray.docs.push(aDocToRemove); + } + }); + bulkDeleteRequest.sendToDatabase(bulkDocsArray); +}; + +gpii.tests.productionConfigTesting.afterAccessTokensDeletion = function (data, request) { + gpii.tests.productionConfigTesting.testStatusCode(data, request); + gpii.test.cloudBased.oauth2.clearAccessTokenCache(); +}; + +gpii.tests.productionConfigTesting.sendToRemoteHost = function (request, remotePort, payload) { + request.send(payload, { port: remotePort }); +}; diff --git a/tests/production/SettingsGetProductionTests.js b/tests/production/SettingsGetProductionTests.js new file mode 100644 index 000000000..5a7efd1c1 --- /dev/null +++ b/tests/production/SettingsGetProductionTests.js @@ -0,0 +1,85 @@ +/** +GPII "GET /settings" tests using a production configuration where there are +separate docker containers running a cloud-based flowmanaer, preferences server +and couch data base + +Requirements: +* an internet connection +* a cloud based flow manager +* a preferences server +* a couchdb containing at least the standard snapset GPII keys and prefs safes + +--- + +Copyright 2015 Raising the Floor - International +Copyright 2018-2019 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +WARNING: Do not run these tests directly. They are called from within the +"vagrantCloudBasedContainers.sh" after it has initialized the environment. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +fluid.registerNamespace("gpii.tests.productionConfigTesting"); +fluid.registerNamespace("gpii.test.cloudBased.oauth2"); + +/* + * ======================================================================== + * Testing of untrusted local config with the live cloud based flow manager + * ======================================================================== + */ + +require("./ProductionTestsUtils.js"); +require("../shared/FlowManagerSettingsGetTestDefs.js"); + +gpii.loadTestingSupport(); + +// GET /settings tests + +fluid.defaults("gpii.tests.productionConfigTesting.settingsGet.testCaseHolder", { + gradeNames: [ + "gpii.test.cloudBased.oauth2.testCaseHolder", + "gpii.tests.cloud.oauth2.accessTokensDeleteRequests" + ], + productionHostConfig: { + hostname: gpii.tests.productionConfigTesting.cloudUrl.hostname, + port: gpii.tests.productionConfigTesting.cloudUrl.port + }, + distributeOptions: { + "accessTokenRequest.hostConfig": { + source: "{that}.options.productionHostConfig", + target: "{that accessTokenRequest}.options" + }, + "accessTokenRequest_settings.hostConfig": { + source: "{that}.options.productionHostConfig", + target: "{that accessTokenRequest_settings}.options" + }, + "settingsRequest.hostConfig": { + source: "{that}.options.productionHostConfig", + target: "{that settingsRequest}.options" + } + } +}); + +gpii.test.cloudBased.oauth2.clearAccessTokenCache(); + +gpii.test.cloudBased.oauth2.runDisruptedTests( + gpii.tests.cloud.oauth2.settingsGet.disruptedTests, + gpii.tests.productionConfigTesting.config, + "gpii.test.serverEnvironment", + "gpii.tests.productionConfigTesting.settingsGet.testCaseHolder" +); diff --git a/tests/production/SettingsPutProductionTests.js b/tests/production/SettingsPutProductionTests.js new file mode 100644 index 000000000..5bf12b608 --- /dev/null +++ b/tests/production/SettingsPutProductionTests.js @@ -0,0 +1,106 @@ +/** +GPII "PUT /settings" tests using a production configuration where there are +separate docker containers running a cloud-based flowmanaer, preferences server +and couch data base + +Requirements: +* an internet connection +* a cloud based flow manager +* a preferences server +* a couchdb containing at least the standard snapset GPII keys and prefs safes + +--- + +Copyright 2015 Raising the Floor - International +Copyright 2018-2019 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +WARNING: Do not run these tests directly. They are called from within the +"vagrantCloudBasedContainers.sh" after it has initialized the environment. +*/ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +fluid.registerNamespace("gpii.tests.productionConfigTesting"); +fluid.registerNamespace("gpii.test.cloudBased.oauth2"); + +/* + * ======================================================================== + * Testing of untrusted local config with the live cloud based flow manager + * ======================================================================== + */ + +require("./ProductionTestsUtils.js"); +require("../shared/FlowManagerSettingsPutTestDefs.js"); + +gpii.loadTestingSupport(); + +// PUT /settings tests + +fluid.defaults("gpii.tests.productionConfigTesting.settingsPut.testCaseHolder", { + gradeNames: [ + "gpii.test.cloudBased.oauth2.testCaseHolder", + "gpii.tests.cloud.oauth2.accessTokensDeleteRequests" + ], + productionHostConfig: { + hostname: gpii.tests.productionConfigTesting.cloudUrl.hostname, + port: gpii.tests.productionConfigTesting.cloudUrl.port + }, + distributeOptions: { + "accessTokenRequest.hostConfig": { + source: "{that}.options.productionHostConfig", + target: "{that accessTokenRequest}.options" + }, + "settingsPutRequest.hostConfig": { + source: "{that}.options.productionHostConfig", + target: "{that settingsPutRequest}.options" + }, + "accessTokenUpdateSnapsetRequest.hostConfig": { + source: "{that}.options.productionHostConfig", + target: "{that accessTokenUpdateSnapsetRequest}.options" + }, + "lifecycleRequest.hostConfig": { + source: "{that}.options.productionHostConfig", + target: "{that lifecycleRequest}.options" + }, + "putSettingsRequestFailure.hostConfig": { + source: "{that}.options.productionHostConfig", + target: "{that putSettingsRequestFailure}.options" + } + } +}); + +gpii.test.cloudBased.oauth2.clearAccessTokenCache(); +gpii.test.cloudBased.oauth2.runDisruptedTests( + gpii.tests.cloud.oauth2.settingsPut.disruptedTests, + gpii.tests.productionConfigTesting.config, + "gpii.test.serverEnvironment", + "gpii.tests.productionConfigTesting.settingsPut.testCaseHolder" +); + +/* + * ======================================================================== + * Testing inability to update a snapset + * ======================================================================== + */ + +gpii.test.cloudBased.oauth2.clearAccessTokenCache(); +gpii.test.cloudBased.oauth2.runDisruptedTests( + gpii.tests.cloud.oauth2.settingsPut.updateSnapsetTest, + gpii.tests.productionConfigTesting.config, + "gpii.test.serverEnvironment", + "gpii.tests.productionConfigTesting.settingsPut.testCaseHolder" +); diff --git a/tests/production/all-tests.js b/tests/production/all-tests.js new file mode 100644 index 000000000..a9fd06dd7 --- /dev/null +++ b/tests/production/all-tests.js @@ -0,0 +1,35 @@ +/*! +GPII Universal Production Tests + +Copyright 2019 OCAD University + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt + +WARNING: Do not run these tests directly. They are called from within the +"vagrantCloudBasedContainers.sh" after it has initialized the environment. +*/ + +"use strict"; + +var fluid = require("infusion"), + kettle = fluid.require("kettle"); + +// Ensure this happens first, to catch errors during code loading. +kettle.loadTestingSupport(); + +// Pass the current `require` to `fluid.require`, as nyc's instrumentation is hooked into it. +fluid.require("%gpii-universal", require); + +var testIncludes = [ + "./CloudStatusProductionTests.js", + "./SettingsGetProductionTests.js", + "./SettingsPutProductionTests.js" +]; + +fluid.each(testIncludes, function (path) { + require(path); +}); diff --git a/tests/shared/DevelopmentTestDefs.js b/tests/shared/DevelopmentTestDefs.js new file mode 100644 index 000000000..6b14e4922 --- /dev/null +++ b/tests/shared/DevelopmentTestDefs.js @@ -0,0 +1,75 @@ +/* + * GPII Flow Manager Development Test Definitions + * + * Copyright 2013 OCAD University + * Copyright 2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + jqUnit = fluid.require("node-jqunit", require, "jqUnit"), + gpii = fluid.registerNamespace("gpii"); + +// These tests simply execute the login and logout cycle for a user with some basic +// dummy preference settings that will not attempt to configure any solutions. We +// observe that the expected responses are received to login and logout and that no +// errors are triggered + +fluid.registerNamespace("gpii.tests.development"); + +gpii.tests.development.testLoginResponse = function (response, gpiiKey) { + jqUnit.assertEquals("Response is correct", "User with GPII key " + + gpiiKey + " was successfully logged in.", response); +}; + +gpii.tests.development.testLogoutResponse = function (response, gpiiKey) { + jqUnit.assertEquals("Response is correct", "User with GPII key " + + gpiiKey + " was successfully logged out.", response); +}; + +fluid.defaults ("gpii.tests.development.loginLogout", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { + func: "{loginRequest}.send" + }, { + event: "{loginRequest}.events.onComplete", + listener: "gpii.tests.development.testLoginResponse", + args: ["{arguments}.0", "{testCaseHolder}.options.gpiiKey"] + }, { + func: "{logoutRequest}.send" + }, { + event: "{logoutRequest}.events.onComplete", + listener: "gpii.tests.development.testLogoutResponse", + args: ["{arguments}.0", "{testCaseHolder}.options.gpiiKey"] + } + ] +}); + +fluid.defaults ("gpii.tests.development.commonTestSequence", { + gradeNames: ["gpii.test.couchSequenceGrade"], + sequenceElements: { + loginLogout: { + gradeNames: "gpii.tests.development.loginLogout", + priority: "after:startCouch" + } + } +}); + +gpii.tests.development.testDefs = [{ + name: "Flow Manager test: Key in and key out with an existing GPII key", + expect: 2, + gpiiKey: "testUser1", + sequenceGrade: "gpii.tests.development.commonTestSequence" +}]; diff --git a/tests/shared/FlowManagerSettingsGetTestDefs.js b/tests/shared/FlowManagerSettingsGetTestDefs.js new file mode 100644 index 000000000..3055db015 --- /dev/null +++ b/tests/shared/FlowManagerSettingsGetTestDefs.js @@ -0,0 +1,629 @@ +/* + * GPII Flow Manager Get/Put shared est Definitions + * + * Copyright 2018-2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.registerNamespace("jqUnit"); + +// These are test definitions for use with the cloud based flow manager in both +// development and production configurations. The definitions are for getting +// settings. + +// Define extra requests used for testing GET /settings endpoint +fluid.defaults("gpii.tests.cloud.oauth2.settingsGet.requests", { + gradeNames: ["fluid.component"], + components: { + accessTokenRequest_settings: { + type: "kettle.test.request.http", + options: { + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + path: "/access_token", + method: "POST" + } + }, + settingsRequest: { + type: "kettle.test.request.http", + options: { + path: "/%gpiiKey/settings/%device", + termMap: { + gpiiKey: "{testCaseHolder}.options.gpiiKey", + device: { + expander: { + func: "gpii.test.cloudBased.computeDevice", + args: [ + [ + "org.gnome.desktop.a11y.magnifier", + "org.gnome.desktop.interface", + "org.alsa-project" + ], + "linux" + ] + } + } + } + } + } + } +}); + +// For successful workflows that request user settings from /settings endpoint +// using access tokens granted by /access_token endpoint +fluid.defaults("gpii.tests.cloud.oauth2.settingsGet.mainSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { funcName: "fluid.log", args: ["Flowmanager get settings main sequence..."]}, + { + funcName: "gpii.test.cloudBased.oauth2.sendResourceOwnerGpiiKeyAccessTokenRequest", + args: ["{accessTokenRequest}", "{testCaseHolder}.options"] + }, + { + event: "{accessTokenRequest}.events.onComplete", + listener: "gpii.test.cloudBased.oauth2.verifyResourceOwnerGpiiKeyAccessTokenInResponse", + args: ["{arguments}.0", "{accessTokenRequest}"] + }, + { + funcName: "gpii.test.cloudBased.oauth2.sendResourceOwnerGpiiKeyAccessTokenRequest", + args: ["{accessTokenRequest_settings}", "{testCaseHolder}.options"] + }, + { + event: "{accessTokenRequest_settings}.events.onComplete", + listener: "gpii.tests.cloud.oauth2.settingsGet.verifyRefetchedAccessToken", + args: ["{arguments}.0", "{accessTokenRequest_settings}", "{accessTokenRequest}"] + }, + { + funcName: "gpii.test.cloudBased.oauth2.sendRequestWithAccessToken", + args: ["{settingsRequest}", "{accessTokenRequest_settings}.access_token"] + }, + { + event: "{settingsRequest}.events.onComplete", + listener: "gpii.tests.cloud.oauth2.settingsGet.verifyPayloadMatchMakerOutput", + args: ["{arguments}.0", "{testCaseHolder}.options.expectedPreferences", "{testCaseHolder}.options.expectedMatchMakerOutput"] + } + ] +}); + +fluid.defaults("gpii.tests.cloud.oauth2.settingsGet.disruption.mainSequence", { + gradeNames: ["gpii.test.disruption.settings.sequenceGrade"], + testCaseGradeNames: "gpii.tests.cloud.oauth2.settingsGet.requests", + sequenceElements: { + mainSequence: { + priority: "after:startServer", + gradeNames: "gpii.tests.cloud.oauth2.settingsGet.mainSequence" + } + } +}); + +// For failed test cases that are rejected by /access_token endpoint +fluid.defaults("gpii.tests.cloud.oauth2.settingsGet.statusCode", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { funcName: "fluid.log", args: ["Flowmanager rejected get settings access token sequence..."]}, + { + funcName: "gpii.test.cloudBased.oauth2.sendResourceOwnerGpiiKeyAccessTokenRequest", + args: ["{accessTokenRequest}", "{testCaseHolder}.options"] + }, + { + event: "{accessTokenRequest}.events.onComplete", + listener: "gpii.test.verifyStatusCodeResponse", + args: ["{arguments}.0", "{accessTokenRequest}", "{testCaseHolder}.options.expectedStatusCode"] + } + ] +}); + +fluid.defaults("gpii.tests.cloud.oauth2.settingsGet.disruption.statusCode", { + gradeNames: ["gpii.test.disruption.settings.sequenceGrade"], + recordName: "accessTokenForm", + testCaseGradeNames: "gpii.tests.cloud.oauth2.settingsGet.requests", + sequenceElements: { + mainSequence: { + priority: "after:startServer", + gradeNames: "gpii.tests.cloud.oauth2.settingsGet.statusCode" + } + } +}); + +// For successful test cases that are accepted by /access_token endpoint +fluid.defaults("gpii.tests.cloud.oauth2.settingsGet.accessTokenResponse", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { funcName: "fluid.log", args: ["Flowmanager accepted get settings access token sequence..."]}, + { + funcName: "gpii.test.cloudBased.oauth2.sendResourceOwnerGpiiKeyAccessTokenRequest", + args: ["{accessTokenRequest}", "{testCaseHolder}.options"] + }, + { + event: "{accessTokenRequest}.events.onComplete", + listener: "gpii.test.cloudBased.oauth2.verifyResourceOwnerGpiiKeyAccessTokenInResponse", + args: ["{arguments}.0", "{accessTokenRequest}"] + } + ] +}); + +fluid.defaults("gpii.tests.cloud.oauth2.settingsGet.disruption.accessTokenResponse", { + gradeNames: ["gpii.test.disruption.settings.sequenceGrade"], + recordName: "accessTokenForm", + testCaseGradeNames: "gpii.tests.cloud.oauth2.settingsGet.requests", + sequenceElements: { + mainSequence: { + priority: "after:startServer", + gradeNames: "gpii.tests.cloud.oauth2.settingsGet.accessTokenResponse" + } + } +}); + +fluid.defaults("gpii.tests.cloud.oauth2.settingsGet.disruptionsWithMissingGrantArgs", { + gradeNames: ["gpii.test.disruption.settings.sequenceGrade"], + disruptions: [ + { + name: "Attempt to get access token without sending client_id", + sequenceGrade: "gpii.tests.cloud.oauth2.settingsGet.disruption.statusCode", + changes: { + path: "client_id", + type: "DELETE" + }, + expectedStatusCode: 401 + }, + { + name: "Attempt to get access token without sending client_secret", + sequenceGrade: "gpii.tests.cloud.oauth2.settingsGet.disruption.statusCode", + changes: { + path: "client_secret", + type: "DELETE" + }, + expectedStatusCode: 401 + }, + { + name: "Attempt to get access token without sending username", + sequenceGrade: "gpii.tests.cloud.oauth2.settingsGet.disruption.statusCode", + changes: { + path: "username", + type: "DELETE" + }, + expectedStatusCode: 400 + }, + { + name: "Attempt to get access token without sending password", + sequenceGrade: "gpii.tests.cloud.oauth2.settingsGet.disruption.statusCode", + changes: { + path: "password", + type: "DELETE" + }, + expectedStatusCode: 400 + }, + { + name: "Attempt to get access token without sending grant_type", + sequenceGrade: "gpii.tests.cloud.oauth2.settingsGet.disruption.statusCode", + changes: { + path: "grant_type", + type: "DELETE" + }, + expectedStatusCode: 501 + } + ] +}); + +gpii.tests.cloud.oauth2.settingsGet.reject = [{ + sequenceGrade: "gpii.tests.cloud.oauth2.settingsGet.disruption.statusCode", + expectedStatusCode: 401 +}]; + +// For failed test case that are rejected by /settings endpoint +// 1. rejected when requesting /settings without providing an access token +fluid.defaults("gpii.tests.cloud.oauth2.settingsGet.settingsNoAccessTokenSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { funcName: "fluid.log", args: ["Flowmanager rejected get settings sequence -- no access token..."]}, + { + func: "{settingsRequest}.send" + }, + { + event: "{settingsRequest}.events.onComplete", + listener: "gpii.test.verifyStatusCodeResponse", + args: ["{arguments}.0", "{settingsRequest}", "{testCaseHolder}.options.expectedStatusCode"] + } + ] +}); + +fluid.defaults("gpii.tests.cloud.oauth2.settingsGet.disruption.settingsNoAccessTokenSequence", { + gradeNames: ["gpii.test.disruption.settings.sequenceGrade"], + testCaseGradeNames: "gpii.tests.cloud.oauth2.settingsGet.requests", + sequenceElements: { + mainSequence: { + priority: "after:startServer", + gradeNames: "gpii.tests.cloud.oauth2.settingsGet.settingsNoAccessTokenSequence" + } + } +}); + +// 2. rejected when requesting /settings by providing a wrong access token +fluid.defaults("gpii.tests.cloud.oauth2.settingsGet.settingsWrongAccessTokenSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { funcName: "fluid.log", args: ["Flowmanager rejected get settings sequence -- wrong access token..."]}, + { + funcName: "gpii.test.cloudBased.oauth2.sendRequestWithAccessToken", + args: ["{settingsRequest}", "a_wrong_access_token"] + }, + { + event: "{settingsRequest}.events.onComplete", + listener: "gpii.test.verifyStatusCodeResponse", + args: ["{arguments}.0", "{settingsRequest}", "{testCaseHolder}.options.expectedStatusCode"] + } + ] +}); + +fluid.defaults("gpii.tests.cloud.oauth2.settingsGet.disruption.settingsWrongAccessTokenSequence", { + gradeNames: ["gpii.test.disruption.settings.sequenceGrade"], + testCaseGradeNames: "gpii.tests.cloud.oauth2.settingsGet.requests", + sequenceElements: { + mainSequence: { + priority: "after:startServer", + gradeNames: "gpii.tests.cloud.oauth2.settingsGet.settingsWrongAccessTokenSequence" + } + } +}); + +// Main tests that contain all test cases +gpii.tests.cloud.oauth2.settingsGet.disruptedTests = [ + // Successful use cases that request settings for an existing GPII key. + // The client credential used in this test doesn't require the IP verification step (allowedIPBlocks === null). + { + testDef: { + name: "A successful workflow that retrieves settings for an existing GPII key using a client not requiring an ip range check", + + // The options below are for sending /access_token request + client_id: "pilot-computer", + client_secret: "pilot-computer-secret", + username: "os_gnome", + password: "dummy", + + // The options below are required for sending /settings + gpiiKey: "os_gnome", + expectedPreferences: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier": { + "mag-factor": 1.5, + "screen-position": "full-screen" + }, + "http://registry.gpii.net/applications/org.gnome.desktop.interface": { + "cursor-size": 90, + "text-scaling-factor": 0.75 + }, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 50 + } + } + } + } + }, + expectedMatchMakerOutput: { + "inferredConfiguration": { + "gpii-default": { + "applications": { + "org.gnome.desktop.a11y.magnifier": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier": { + "mag-factor": 1.5, + "screen-position": "full-screen" + } + } + }, + "org.gnome.desktop.interface": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/org.gnome.desktop.interface": { + "cursor-size": 90, + "text-scaling-factor": 0.75 + } + } + }, + "org.alsa-project": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 50 + } + } + } + } + } + } + } + }, + disruptions: [{ + sequenceGrade: "gpii.tests.cloud.oauth2.settingsGet.disruption.mainSequence" + }] + }, + + // Successful use cases that request settings for an existing GPII key. + // The client credential used in this test uses the hema version 0.1 that doesn't have allowedIPBlocks field. + { + testDef: { + name: "A successful workflow that retrieves settings for an existing GPII key using a client in the old schema version 0.1", + + // The options below are for sending /access_token request + client_id: "pilot-computer-schemaV0.1", + client_secret: "pilot-computer-secret-schemaV0.1", + username: "os_gnome", + password: "dummy", + + // The options below are required for sending /settings + gpiiKey: "os_gnome", + expectedPreferences: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier": { + "mag-factor": 1.5, + "screen-position": "full-screen" + }, + "http://registry.gpii.net/applications/org.gnome.desktop.interface": { + "cursor-size": 90, + "text-scaling-factor": 0.75 + }, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 50 + } + } + } + } + }, + expectedMatchMakerOutput: { + "inferredConfiguration": { + "gpii-default": { + "applications": { + "org.gnome.desktop.a11y.magnifier": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier": { + "mag-factor": 1.5, + "screen-position": "full-screen" + } + } + }, + "org.gnome.desktop.interface": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/org.gnome.desktop.interface": { + "cursor-size": 90, + "text-scaling-factor": 0.75 + } + } + }, + "org.alsa-project": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 50 + } + } + } + } + } + } + } + }, + disruptions: [{ + sequenceGrade: "gpii.tests.cloud.oauth2.settingsGet.disruption.mainSequence" + }] + }, + + // Successful use cases that request settings for an existing GPII key. + // The client credential used in this test requires the IP verification step (allowedIPBlocks is given a range + // that the "localhost" IP is within). + { + testDef: { + name: "A successful workflow that retrieves settings for an existing GPII key using a client with an ip range accepting the ip of the incoming request", + + // The options below are for sending /access_token request + client_id: "nova-computer", + client_secret: "nova-computer-secret", + username: "os_gnome", + password: "dummy", + + // The options below are required for sending /settings + gpiiKey: "os_gnome", + expectedPreferences: { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier": { + "mag-factor": 1.5, + "screen-position": "full-screen" + }, + "http://registry.gpii.net/applications/org.gnome.desktop.interface": { + "cursor-size": 90, + "text-scaling-factor": 0.75 + }, + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 50 + } + } + } + } + }, + expectedMatchMakerOutput: { + "inferredConfiguration": { + "gpii-default": { + "applications": { + "org.gnome.desktop.a11y.magnifier": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/org.gnome.desktop.a11y.magnifier": { + "mag-factor": 1.5, + "screen-position": "full-screen" + } + } + }, + "org.gnome.desktop.interface": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/org.gnome.desktop.interface": { + "cursor-size": 90, + "text-scaling-factor": 0.75 + } + } + }, + "org.alsa-project": { + "active": true, + "settings": { + "http://registry.gpii.net/applications/org.alsa-project": { + "masterVolume": 50 + } + } + } + } + } + } + } + }, + disruptions: [{ + sequenceGrade: "gpii.tests.cloud.oauth2.settingsGet.disruption.mainSequence" + }] + }, + + // Successful use cases that request settings for a non-existent GPII key with proper access tokens granted via Resource Owner GPII key grant + { + testDef: { + name: "A successful workflow that retrieves settings for a non-existent GPII key using a client with the privilege to create GPII keys and prefs safes", + + // The options below are for sending /access_token request + client_id: "nova-computer", + client_secret: "nova-computer-secret", + username: "nonexistent_gpii_key", + password: "dummy", + + // The options below are required for sending /settings + gpiiKey: "nonexistent_gpii_key", + expectedPreferences: { + }, + expectedMatchMakerOutput: { + "inferredConfiguration": { + } + } + }, + disruptions: [{ + sequenceGrade: "gpii.tests.cloud.oauth2.settingsGet.disruption.mainSequence" + }] + }, + + // Rejected by /access_token endpoint + { + testDef: { + name: "Attempt to get access token by requesting an access token for a nonexistent GPII key using a client without the privilege to create GPII keys and prefs safes", + client_id: "pilot-computer", + client_secret: "pilot-computer-secret", + username: "nonexistent_gpii_key", + password: "dummy" + }, + disruptions: gpii.tests.cloud.oauth2.settingsGet.reject + }, + { + testDef: { + name: "Attempt to get access token by requesting an access token for a nonexistent GPII key using a client in old schema version 0.1", + client_id: "pilot-computer-schemaV0.1", + client_secret: "pilot-computer-secret-schemaV0.1", + username: "nonexistent_gpii_key", + password: "dummy" + }, + disruptions: gpii.tests.cloud.oauth2.settingsGet.reject + }, + + // Rejected by /access_token endpoint + { + testDef: { + name: "Acceptance test for suppporting resource owner GPII key grant type (missing arguments at sending requests to /access_token)", + client_id: "pilot-computer", + client_secret: "pilot-computer-secret", + username: "os_gnome", + password: "dummy" + }, + disruptions: gpii.tests.cloud.oauth2.settingsGet.disruptionsWithMissingGrantArgs + }, + { + testDef: { + name: "Attempt to get access token by sending a client whose allowedIPBlocks value doesn't match the IP that the request is sent from", + client_id: "nova-computer-failInIpVerification", + client_secret: "nova-computer-secret-failInIpVerification", + username: "gpii_key_no_prefs_safe", + password: "dummy" + }, + disruptions: gpii.tests.cloud.oauth2.settingsGet.reject + }, + { + testDef: { + name: "Attempt to get access token by sending a wrong client (oauth2 client type is not \"gpiiAppInstallationClient\")", + client_id: "com.bdigital.easit4all", + client_secret: "client_secret_easit4all", + username: "gpii_key_no_prefs_safe", + password: "dummy" + }, + disruptions: gpii.tests.cloud.oauth2.settingsGet.reject + }, + { + testDef: { + name: "Attempt to get access token by sending an nonexistent client", + client_id: "nonexistent-client", + client_secret: "client_secret_easit4all", + username: "gpii_key_no_prefs_safe", + password: "dummy" + }, + disruptions: gpii.tests.cloud.oauth2.settingsGet.reject + }, + + // Rejected by /settings endpoint + { + testDef: { + name: "Attempt to retrieve user settings without providing an access token", + gpiiKey: "os_gnome" + }, + disruptions: [{ + sequenceGrade: "gpii.tests.cloud.oauth2.settingsGet.disruption.settingsNoAccessTokenSequence", + expectedStatusCode: 401 + }] + }, + { + testDef: { + name: "Attempt to retrieve user settings by providing a wrong access token", + gpiiKey: "os_gnome" + }, + disruptions: [{ + sequenceGrade: "gpii.tests.cloud.oauth2.settingsGet.disruption.settingsWrongAccessTokenSequence", + expectedStatusCode: 401 + }] + } +]; + +gpii.tests.cloud.oauth2.settingsGet.verifyRefetchedAccessToken = function (body, refetchAccessTokenRequest, initialAccessTokenRequest) { + gpii.test.cloudBased.oauth2.verifyResourceOwnerGpiiKeyAccessTokenInResponse(body, refetchAccessTokenRequest); + jqUnit.assertNotEquals("A new access token is issued at the refetch", initialAccessTokenRequest.access_token, refetchAccessTokenRequest.access_token); +}; + +gpii.tests.cloud.oauth2.settingsGet.verifyPayloadMatchMakerOutput = function (body, expectedPreferences, expectedMatchMakerOutput) { + var payload = JSON.parse(body); + jqUnit.assertDeepEq("Verify expected preferences", expectedPreferences, payload.preferences); + jqUnit.assertDeepEq("Verify expected matchMakerOutput", expectedMatchMakerOutput, payload.matchMakerOutput); +}; diff --git a/tests/shared/FlowManagerSettingsPutTestDefs.js b/tests/shared/FlowManagerSettingsPutTestDefs.js new file mode 100644 index 000000000..080f7bdfd --- /dev/null +++ b/tests/shared/FlowManagerSettingsPutTestDefs.js @@ -0,0 +1,622 @@ +/* + * GPII Flow Manager Get/Put shared est Definitions + * + * Copyright 2018-2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"), + jqUnit = fluid.require("node-jqunit", require, "jqUnit"); + +// These are test definitions for use with the cloud based flow manager in both +// development and production configurations. The definitions are for updating +// settings. + +fluid.registerNamespace("gpii.tests.cloud.oauth2.settingsPut"); + +// Unit tests +gpii.tests.cloud.oauth2.settingsPut.prefsAllowedTests = [{ + name: "All preferences are in the allowed list", + preferences: { + "http://registry.gpii.net/common/language": "en-US", + "http://registry.gpii.net/common/highContrast/enabled": true + }, + allowedPrefs: [ + "http://registry.gpii.net/common/language", + "http://registry.gpii.net/common/DPIScale", + "http://registry.gpii.net/common/highContrast/enabled", + "http://registry.gpii.net/common/selfVoicing/enabled" + ], + expected: true +}, { + name: "One or more preferences are not in the allowed list", + preferences: { + "http://registry.gpii.net/common/language": "en-US", + "http://registry.gpii.net/applications/com.microsoft.windows.mouseSettings": true, + "http://registry.gpii.net/common/highContrast/enabled": true + }, + allowedPrefs: [ + "http://registry.gpii.net/common/language", + "http://registry.gpii.net/common/DPIScale", + "http://registry.gpii.net/common/highContrast/enabled", + "http://registry.gpii.net/common/selfVoicing/enabled" + ], + expected: false +}, { + name: "One or more preferences are not in the allowed list", + preferences: { + "http://registry.gpii.net/common/language": "en-US", + "http://registry.gpii.net/applications/com.microsoft.windows.mouseSettings": true, + "http://registry.gpii.net/common/highContrast/enabled": true + }, + allowedPrefs: [], + expected: false +}, { + name: "When the given preferences is an empty object, return true as all allowed", + preferences: {}, + allowedPrefs: [ + "http://registry.gpii.net/common/language", + "http://registry.gpii.net/common/DPIScale", + "http://registry.gpii.net/common/highContrast/enabled", + "http://registry.gpii.net/common/selfVoicing/enabled" + ], + expected: true +}]; + +jqUnit.test("gpii.flowManager.cloudBased.settings.isPrefsAllowedToWrite() tests", function () { + fluid.each(gpii.tests.cloud.oauth2.settingsPut.prefsAllowedTests, function (oneTest) { + var result = gpii.flowManager.cloudBased.settings.isPrefsAllowedToWrite(oneTest.preferences, oneTest.allowedPrefs); + jqUnit[oneTest.expected ? "assertTrue" : "assertFalse"]("gpii.flowManager.cloudBased.settings.isPrefsAllowedToWrite() test - " + oneTest.name, result); + }); +}); + +// Integration tests +gpii.tests.cloud.oauth2.settingsPut.updatedPrefsSet = { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/language": "en-US", + "http://registry.gpii.net/common/highContrast/enabled": true, + "http://registry.gpii.net/common/highContrastTheme": "black-white", + "http://registry.gpii.net/applications/com.microsoft.office": { + "word-ribbon": "StandardSet" + } + } + } + } +}; + +gpii.tests.cloud.oauth2.settingsPut.invalidPrefsSet = { + "contexts": { + "gpii-default": { + "name": "Default preferences", + "preferences": { + "http://registry.gpii.net/common/highContrast/enabled": "So true, funny how it seems." + } + } + } +}; + +gpii.tests.cloud.oauth2.settingsPut.verifyUpdateResponse = function (responseText, request, expectedStatusCode, expectedGpiiKey, expectedMsg) { + var response = JSON.parse(responseText); + jqUnit.assertEquals("The returned message in the response is expected", expectedStatusCode, request.nativeResponse.statusCode); + jqUnit.assertEquals("The returned message in the response is expected", expectedMsg, response.message); + if (expectedGpiiKey) { + jqUnit.assertEquals("The returned GPII key in the response is expected", expectedGpiiKey, response.gpiiKey); + } else { + jqUnit.assertTrue("The returned is an error message", response.isError); + } +}; + +// For successful workflows that update user settings from /settings endpoint +// using access tokens granted by /access_token endpoint +fluid.defaults("gpii.tests.cloud.oauth2.settingsPut.mainSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { funcName: "fluid.log", args: ["Flowmanager put settings main sequence..."]}, + { + funcName: "gpii.test.cloudBased.oauth2.sendResourceOwnerGpiiKeyAccessTokenRequest", + args: ["{accessTokenRequest}", "{testCaseHolder}.options"] + }, + { + event: "{accessTokenRequest}.events.onComplete", + listener: "gpii.test.cloudBased.oauth2.verifyResourceOwnerGpiiKeyAccessTokenInResponse", + args: ["{arguments}.0", "{accessTokenRequest}"] + }, + { + funcName: "gpii.test.cloudBased.oauth2.sendRequestWithAccessToken", + args: ["{settingsPutRequest}", "{accessTokenRequest}.access_token", "{testCaseHolder}.options.updatedPrefsSet"] + }, + { + event: "{settingsPutRequest}.events.onComplete", + listener: "gpii.tests.cloud.oauth2.settingsPut.verifyUpdateResponse", + args: ["{arguments}.0", "{settingsPutRequest}", "{testCaseHolder}.options.expectedStatusCode", "{testCaseHolder}.options.expectedGpiiKey", "{testCaseHolder}.options.expectedMsg"] + } + ] +}); + +// Define extra requests used for testing PUT /settings endpoint +fluid.defaults("gpii.tests.cloud.oauth2.settingsPut.requests", { + gradeNames: ["fluid.component"], + components: { + settingsPutRequest: { + type: "kettle.test.request.http", + options: { + path: "/%gpiiKey/settings", + port: 8081, + method: "PUT", + termMap: { + gpiiKey: "{testCaseHolder}.options.gpiiKey" + } + } + } + } +}); + +fluid.defaults("gpii.tests.cloud.oauth2.settingsPut.disruption.mainSequence", { + gradeNames: ["gpii.test.disruption.settings.sequenceGrade"], + testCaseGradeNames: "gpii.tests.cloud.oauth2.settingsPut.requests", + sequenceElements: { + mainSequence: { + priority: "after:startServer", + gradeNames: "gpii.tests.cloud.oauth2.settingsPut.mainSequence" + } + } +}); + +// For failed test case that are rejected by /settings endpoint +// 1. rejected when requesting /settings without providing an access token +fluid.defaults("gpii.tests.cloud.oauth2.settingsPut.settingsPutNoAccessTokenSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { funcName: "fluid.log", args: ["Flowmanager rejected put settings sequence -- no access token..."]}, + { + func: "{settingsPutRequest}.send", + args: ["{testCaseHolder}.options.updatedPrefsSet"] + }, + { + event: "{settingsPutRequest}.events.onComplete", + listener: "gpii.test.verifyStatusCodeResponse", + args: ["{arguments}.0", "{settingsPutRequest}", "{testCaseHolder}.options.expectedStatusCode"] + } + ] +}); + +fluid.defaults("gpii.tests.cloud.oauth2.settingsPut.disruption.settingsPutNoAccessTokenSequence", { + gradeNames: ["gpii.test.disruption.settings.sequenceGrade"], + testCaseGradeNames: "gpii.tests.cloud.oauth2.settingsPut.requests", + sequenceElements: { + settingsPutNoAccessTokenSequence: { + priority: "after:startServer", + gradeNames: "gpii.tests.cloud.oauth2.settingsPut.settingsPutNoAccessTokenSequence" + } + } +}); + +// 2. rejected when requesting /settings by providing a wrong access token +fluid.defaults("gpii.tests.cloud.oauth2.settingsPut.settingsPutWrongAccessTokenSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { funcName: "fluid.log", args: ["Flowmanager rejected put settings sequence -- wrong access token..."]}, + { + funcName: "gpii.test.cloudBased.oauth2.sendRequestWithAccessToken", + args: ["{settingsPutRequest}", "a_wrong_access_token", "{testCaseHolder}.options.updatedPrefsSet"] + }, + { + event: "{settingsPutRequest}.events.onComplete", + listener: "gpii.test.verifyStatusCodeResponse", + args: ["{arguments}.0", "{settingsPutRequest}", "{testCaseHolder}.options.expectedStatusCode"] + } + ] +}); + +fluid.defaults("gpii.tests.cloud.oauth2.settingsPut.disruption.settingsPutWrongAccessTokenSequence", { + gradeNames: ["gpii.test.disruption.settings.sequenceGrade"], + testCaseGradeNames: "gpii.tests.cloud.oauth2.settingsPut.requests", + sequenceElements: { + settingsPutWrongAccessTokenSequence: { + priority: "after:startServer", + gradeNames: "gpii.tests.cloud.oauth2.settingsPut.settingsPutWrongAccessTokenSequence" + } + } +}); + +// Invalid payload, should be rejected. +fluid.defaults("gpii.tests.cloud.oauth2.settingsPut.settingsPutInvalidSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { + funcName: "gpii.test.cloudBased.oauth2.sendResourceOwnerGpiiKeyAccessTokenRequest", + args: ["{accessTokenRequest}", "{testCaseHolder}.options"] + }, + { + event: "{accessTokenRequest}.events.onComplete", + listener: "gpii.test.cloudBased.oauth2.verifyResourceOwnerGpiiKeyAccessTokenInResponse", + args: ["{arguments}.0", "{accessTokenRequest}"] + }, + { + funcName: "gpii.test.cloudBased.oauth2.sendRequestWithAccessToken", + args: ["{settingsPutRequest}", "{accessTokenRequest}.access_token", "{testCaseHolder}.options.updatedPrefsSet"] + }, + { + event: "{settingsPutRequest}.events.onComplete", + listener: "gpii.tests.cloud.oauth2.settingsPut.verifyUpdateResponse", + args: ["{arguments}.0", "{settingsPutRequest}", 400, false, "Your request was invalid. See the errors for details."] + } + ] +}); + +fluid.defaults("gpii.tests.cloud.oauth2.settingsPut.disruption.settingsPutInvalidSequence", { + gradeNames: ["gpii.test.disruption.settings.sequenceGrade"], + testCaseGradeNames: "gpii.tests.cloud.oauth2.settingsPut.requests", + sequenceElements: { + settingsPutWrongAccessTokenSequence: { + priority: "after:startServer", + gradeNames: "gpii.tests.cloud.oauth2.settingsPut.settingsPutInvalidSequence" + } + } +}); + +// Main tests that contain all test cases +gpii.tests.cloud.oauth2.settingsPut.disruptedTests = [ + // Succesful use cases that update user preferences with proper access tokens granted via Resource Owner GPII key grant + { + testDef: { + name: "A successful workflow that updates a GPII key with an existing prefs safe", + + // The options below are for sending /access_token request + client_id: "pilot-computer", + client_secret: "pilot-computer-secret", + username: "os_gnome", + password: "dummy", + + // The options below are required for sending /settings + gpiiKey: "os_gnome", + updatedPrefsSet: gpii.tests.cloud.oauth2.settingsPut.updatedPrefsSet, + + // Expected info + expectedGpiiKey: "os_gnome", + expectedMsg: gpii.flowManager.cloudBased.settings.put.messages.success + }, + disruptions: [{ + sequenceGrade: "gpii.tests.cloud.oauth2.settingsPut.disruption.mainSequence", + expectedStatusCode: 200 + }] + }, + + { + testDef: { + name: "A successful workflow that updates a GPII key that has no associated prefs safe", + + // The options below are for sending /access_token request + client_id: "pilot-computer", + client_secret: "pilot-computer-secret", + username: "gpii_key_no_prefs_safe", + password: "dummy", + + // The options below are required for sending /settings + gpiiKey: "gpii_key_no_prefs_safe", + updatedPrefsSet: gpii.tests.cloud.oauth2.settingsPut.updatedPrefsSet, + + // Expected info + expectedGpiiKey: "gpii_key_no_prefs_safe", + expectedMsg: gpii.flowManager.cloudBased.settings.put.messages.success + }, + disruptions: [{ + sequenceGrade: "gpii.tests.cloud.oauth2.settingsPut.disruption.mainSequence", + expectedStatusCode: 200 + }] + }, + + { + testDef: { + name: "A successful workflow that creates a given nonexistent GPII key and its preferences", + + // The options below are for sending /access_token request + client_id: "nova-computer", + client_secret: "nova-computer-secret", + username: "nonexistent_gpii_key", + password: "dummy", + + // The options below are required for sending /settings + gpiiKey: "nonexistent_gpii_key", + updatedPrefsSet: gpii.tests.cloud.oauth2.settingsPut.updatedPrefsSet, + + // Expected info + expectedGpiiKey: "nonexistent_gpii_key", + expectedMsg: gpii.flowManager.cloudBased.settings.put.messages.success + }, + disruptions: [{ + sequenceGrade: "gpii.tests.cloud.oauth2.settingsPut.disruption.mainSequence", + expectedStatusCode: 200 + }] + }, + + // Rejected by /settings endpoint + { + testDef: { + name: "Attempt to update preferences without providing an access token", + gpiiKey: "os_gnome", + updatedPrefsSet: gpii.tests.cloud.oauth2.settingsPut.updatedPrefsSet + }, + disruptions: [{ + sequenceGrade: "gpii.tests.cloud.oauth2.settingsPut.disruption.settingsPutNoAccessTokenSequence", + expectedStatusCode: 401 + }] + }, + + { + testDef: { + name: "Attempt to update preferences by providing a wrong access token", + gpiiKey: "os_gnome", + updatedPrefsSet: gpii.tests.cloud.oauth2.settingsPut.updatedPrefsSet + }, + disruptions: [{ + sequenceGrade: "gpii.tests.cloud.oauth2.settingsPut.disruption.settingsPutWrongAccessTokenSequence", + expectedStatusCode: 401 + }] + }, + + { + testDef: { + name: "Attempt to create GPII key and preferences with an access token granted to a different GPII key", + + // The options below are for sending /access_token request + client_id: "pilot-computer", + client_secret: "pilot-computer-secret", + username: "os_gnome", + password: "dummy", + + // The options below are required for sending /settings + gpiiKey: "a_different_gpii_key", + updatedPrefsSet: gpii.tests.cloud.oauth2.settingsPut.updatedPrefsSet, + + // Expected info + expectedGpiiKey: undefined, + expectedMsg: "Unauthorized" + }, + disruptions: [{ + sequenceGrade: "gpii.tests.cloud.oauth2.settingsPut.disruption.mainSequence", + expectedStatusCode: 401 + }] + }, + + { + testDef: { + name: "Attempt to update preferences whose one or more keys are not in the allowed preferences list", + + // The options below are for sending /access_token request + client_id: "nova-computer-failInAllowedPrefsToWrite", + client_secret: "nova-computer-secret-failInAllowedPrefsToWrite", + username: "os_gnome", + password: "dummy", + + // The options below are required for sending /settings + gpiiKey: "os_gnome", + updatedPrefsSet: gpii.tests.cloud.oauth2.settingsPut.updatedPrefsSet, + + // Expected info + expectedGpiiKey: undefined, + expectedMsg: "Unauthorized" + }, + disruptions: [{ + sequenceGrade: "gpii.tests.cloud.oauth2.settingsPut.disruption.mainSequence", + expectedStatusCode: 401 + }] + }, + + // Attempt to PUT an invalid payload + { + testDef: { + name: "Attempt to update preferences with an invalid payload.", + + // The options below are for sending /access_token request + client_id: "nova-computer-failInAllowedPrefsToWrite", + client_secret: "nova-computer-secret-failInAllowedPrefsToWrite", + username: "os_gnome", + password: "dummy", + + // The options below are required for sending /settings + gpiiKey: "os_gnome", + updatedPrefsSet: gpii.tests.cloud.oauth2.settingsPut.invalidPrefsSet, + + // Expected info + expectedGpiiKey: undefined, + expectedMsg: "Invalid" + }, + disruptions: [{ + sequenceGrade: "gpii.tests.cloud.oauth2.settingsPut.disruption.settingsPutInvalidSequence", + expectedStatusCode: 400 + }] + } +]; + +// Flowmanager test sequence for inability to update a snapset ('carla'). +// 1. /access_token, +// 2. /%gpiiKey/settings/%device +// 3. /%gpiiKey/settings +fluid.registerNamespace("gpii.tests.cloud.oauth2.settingsPut.updateSnapset"); + +gpii.tests.cloud.oauth2.settingsPut.updateSnapset.carlaKey = "carla"; +gpii.tests.cloud.oauth2.settingsPut.updateSnapset.carlaTokenRequestPayload = { + "username": gpii.tests.cloud.oauth2.settingsPut.updateSnapset.carlaKey, + "gpiiKey": gpii.tests.cloud.oauth2.settingsPut.updateSnapset.carlaKey, + "password": "dummy", + "client_id": "pilot-computer", + "client_secret": "pilot-computer-secret", + "grant_type": "password" +}; + +gpii.tests.cloud.oauth2.settingsPut.updateSnapset.device = { + OS: { id: "linux" }, + solutions: [{ + "id": "org.gnome.desktop.a11y.magnifier" + }] +}; + +fluid.defaults("gpii.tests.cloud.oauth2.settingsPut.updateSnapsetSequence", { + gradeNames: ["fluid.test.sequenceElement"], + sequence: [ + { funcName: "fluid.log", args: ["Flowmanager rejection of 'update-snapset' sequence..."]}, + { + funcName: "gpii.test.cloudBased.oauth2.sendResourceOwnerGpiiKeyAccessTokenRequest", + args: ["{accessTokenUpdateSnapsetRequest}", gpii.tests.cloud.oauth2.settingsPut.updateSnapset.carlaTokenRequestPayload] + }, { + event: "{accessTokenUpdateSnapsetRequest}.events.onComplete", + listener: "gpii.tests.cloud.oauth2.settingsPut.updateSnapset.testAndStashAccessResponse", + args: ["{arguments}.0", "{accessTokenUpdateSnapsetRequest}"] + }, { + func: "{lifecycleRequest}.send", + args: [ + null, + { + "headers": { + "Authorization": "{accessTokenUpdateSnapsetRequest}.options.stashedAuth" + } + } + ] + }, { + event: "{lifecycleRequest}.events.onComplete", + listener: "gpii.tests.cloud.oauth2.settingsPut.updateSnapset.testLifecycleResponse" + }, { + func: "{putSettingsRequestFailure}.send", + args: [ + gpii.tests.cloud.oauth2.settingsPut.updatedPrefsSet, + { + "headers": { + "Authorization": "{accessTokenUpdateSnapsetRequest}.options.stashedAuth" + } + } + ] + }, { + event: "{putSettingsRequestFailure}.events.onComplete", + listener: "gpii.tests.cloud.oauth2.settingsPut.updateSnapset.testResponse" + } + ] +}); + +// Define extra requests used for testing PUT /settings on a snapset +fluid.defaults("gpii.tests.cloud.oauth2.settingsPut.updateSnapsetRequests", { + gradeNames: ["fluid.component"], + components: { + accessTokenUpdateSnapsetRequest: { + type: "kettle.test.request.http", + options: { + path: "/access_token", + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + expectedStatusCode: 200 + } + }, + lifecycleRequest: { + type: "kettle.test.request.http", + options: { + path: fluid.stringTemplate("/%gpiiKey/settings/%device", { + gpiiKey: gpii.tests.cloud.oauth2.settingsPut.updateSnapset.carlaKey, + device: encodeURIComponent(JSON.stringify( + gpii.tests.cloud.oauth2.settingsPut.updateSnapset.device + ))} + ), + headers: { + "Authorization": "Bearer token" // set at test run + }, + method: "GET", + expectedStatusCode: 200 + } + }, + putSettingsRequestFailure: { // can't update snapset (readonly) + type: "kettle.test.request.http", + options: { + path: "/%gpiiKey/settings", + termMap: { + "gpiiKey": gpii.tests.cloud.oauth2.settingsPut.updateSnapset.carlaKey + }, + headers: { + "Authorization": "Bearer token" // set at test run + }, + method: "PUT", + expectedStatusCode: 404, + expectedErrorMessageStart: + "Can't update preferences for readOnly GPII key \"" + + gpii.tests.cloud.oauth2.settingsPut.updateSnapset.carlaKey + + "\" (perhaps it is a snapset) while executing HTTP PUT" + } + } + } +}); + +fluid.defaults("gpii.tests.cloud.oauth2.settingsPut.disruption.updateSnapsetFailure", { + gradeNames: ["gpii.test.disruption.settings.sequenceGrade"], + testCaseGradeNames: "gpii.tests.cloud.oauth2.settingsPut.updateSnapsetRequests", + sequenceElements: { + updateSnapsetSequence: { + priority: "after:startServer", + gradeNames: "gpii.tests.cloud.oauth2.settingsPut.updateSnapsetSequence" + } + } +}); + +// Tests showing the inability to update a snapset +gpii.tests.cloud.oauth2.settingsPut.updateSnapsetTest = [{ + testDef: { + name: "Flow manager tests - attempt (and fail) to update a snapset" + }, + disruptions: [{ + sequenceGrade: "gpii.tests.cloud.oauth2.settingsPut.disruption.updateSnapsetFailure" + }] +}]; + +gpii.tests.cloud.oauth2.settingsPut.updateSnapset.testStatusCode = function (data, request) { + jqUnit.assertEquals( + "Checking status of " + request.options.path, + request.options.expectedStatusCode, request.nativeResponse.statusCode + ); +}; + +gpii.tests.cloud.oauth2.settingsPut.updateSnapset.testResponse = function (data, request) { + gpii.tests.cloud.oauth2.settingsPut.updateSnapset.testStatusCode(data, request); + var actual = JSON.parse(data); + jqUnit.assertTrue( + "Checking payload error for " + request.options.path, + actual.isError + ); + jqUnit.assertTrue( + "Checking payload error message for " + request.options.path, + actual.message.startsWith(request.options.expectedErrorMessageStart) + ); +}; + +gpii.tests.cloud.oauth2.settingsPut.updateSnapset.testAndStashAccessResponse = function (data, request) { + var token = gpii.test.cloudBased.oauth2.verifyResourceOwnerGpiiKeyAccessTokenInResponse(data, request); + var auth = "Bearer " + token.access_token; + request.options.stashedAuth = auth; +}; + +gpii.tests.cloud.oauth2.settingsPut.updateSnapset.testLifecycleResponse = function (data, request) { + gpii.tests.cloud.oauth2.settingsPut.updateSnapset.testStatusCode(data, request); + var lifecycle = JSON.parse(data); + jqUnit.assertEquals( + "Checking lifecycle user '" + gpii.tests.cloud.oauth2.settingsPut.updateSnapset.carlaKey + "'", + gpii.tests.cloud.oauth2.settingsPut.updateSnapset.carlaKey, + lifecycle.gpiiKey + ); + // These checks based on + // https://github.com/GPII/universal/blob/master/documentation/FlowManager.md#get-lifecycle-instructions-from-cloud-based-flow-manager-get-gpiikeysettingsdevice + jqUnit.assertNotNull("Checking 'solutionsRegistryEntries'", lifecycle.solutionsRegistryEntries); + jqUnit.assertNotNull("Checking 'matchMakerOutput'", lifecycle.matchMakerOutput); +}; diff --git a/tests/shared/PSPIntegrationTestDefs.js b/tests/shared/PSPIntegrationTestDefs.js new file mode 100644 index 000000000..f8b9c756d --- /dev/null +++ b/tests/shared/PSPIntegrationTestDefs.js @@ -0,0 +1,1050 @@ +/** +Shared GPII PSP Integration Test definitions + +Copyright 2014, 2017 Raising the Floor - International + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.registerNamespace("gpii.tests.pspIntegration"); +fluid.require("%gpii-universal"); +gpii.loadTestingSupport(); + +require("./PSPIntegrationTestUtils.js"); + +gpii.tests.pspIntegration.applyPrefsTestDefs = [ + { + name: "Simple settings change by PSP - change an existing user setting", + expect: 9, + sequence: [ + { + func: "gpii.test.expandSettings", + args: [ "{tests}", [ "contexts" ]] + }, { + func: "gpii.test.snapshotSettings", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{tests}.settingsStore", "{nameResolver}", "{testCaseHolder}.events.onSnapshotComplete.fire"] + }, { + event: "{testCaseHolder}.events.onSnapshotComplete", + listener: "fluid.identity" + }, { + func: "{loginRequest}.send" + }, { + event: "{loginRequest}.events.onComplete", + listener: "gpii.test.loginRequestListen" + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + func: "{pspClient}.connect" + }, { + event: "{pspClient}.events.onConnect", + listener: "gpii.tests.pspIntegration.connectionSucceeded" + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.afterConnect] + }, { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "modelChanged", { + settingControls: { + "http://registry\\.gpii\\.net/common/magnification": { + value: 3 + } + } + }] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "preferencesApplied"] + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.afterChangeMagnification, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + func: "{logoutRequest}.send" + }, { + event: "{logoutRequest}.events.onComplete", + listener: "gpii.test.logoutRequestListen" + }, { + func: "gpii.test.checkRestoredConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{tests}.settingsStore", "{nameResolver}", "{testCaseHolder}.events.onCheckRestoredConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckRestoredConfigurationComplete", + listener: "fluid.identity" + } + ] + }, { + name: "Simple settings change by PSP - sequentially change a new setting", + expect: 13, + sequence: [ + { + func: "gpii.test.expandSettings", + args: [ "{tests}", [ "contexts" ]] + }, { + func: "gpii.test.snapshotSettings", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{tests}.settingsStore", "{nameResolver}", "{testCaseHolder}.events.onSnapshotComplete.fire"] + }, { + event: "{testCaseHolder}.events.onSnapshotComplete", + listener: "fluid.identity" + }, { + func: "{loginRequest}.send" + }, { + event: "{loginRequest}.events.onComplete", + listener: "gpii.test.loginRequestListen" + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + func: "{pspClient}.connect" + }, { + event: "{pspClient}.events.onConnect", + listener: "gpii.tests.pspIntegration.connectionSucceeded" + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.afterConnect] + }, { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "modelChanged", { + settingControls: { + "http://registry\\.gpii\\.net/common/cursorSize": { + value: 0.9 + } + } + }] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.afterChangeCursorSizeCombined] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "preferencesApplied"] + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.afterChangeCursorSize, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "modelChanged", { + settingControls: { + "http://registry\\.gpii\\.net/common/cursorSize": { + value: 0.5 + } + } + }] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "preferencesApplied"] + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.afterDecreaseCursorSize, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + func: "{logoutRequest}.send" + }, { + event: "{logoutRequest}.events.onComplete", + listener: "gpii.test.logoutRequestListen" + }, { + func: "gpii.test.checkRestoredConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{tests}.settingsStore", "{nameResolver}", "{testCaseHolder}.events.onCheckRestoredConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckRestoredConfigurationComplete", + listener: "fluid.identity" + } + ] + }, { + name: "Settings change by PSP with a scoped setting", + expect: 11, + sequence: [ + { + func: "gpii.test.expandSettings", + args: [ "{tests}", [ "contexts" ]] + }, { + func: "gpii.test.snapshotSettings", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{tests}.settingsStore", "{nameResolver}", "{testCaseHolder}.events.onSnapshotComplete.fire"] + }, { + event: "{testCaseHolder}.events.onSnapshotComplete", + listener: "fluid.identity" + }, { + func: "{loginRequest}.send" + }, { + event: "{loginRequest}.events.onComplete", + listener: "gpii.test.loginRequestListen" + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + func: "{pspClient}.connect" + }, { + event: "{pspClient}.events.onConnect", + listener: "gpii.tests.pspIntegration.connectionSucceeded" + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.afterConnect] + }, { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "modelChanged", { + settingControls: { + "http://registry\\.gpii\\.net/applications/org\\.gnome\\.desktop\\.a11y\\.magnifier.mag-factor": { + value: 3 + } + } + }] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.afterChangeMagnificationByAppTerm] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "preferencesApplied"] + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.afterChangeMagnification, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + func: "{logoutRequest}.send" + }, { + event: "{logoutRequest}.events.onComplete", + listener: "gpii.test.logoutRequestListen" + }, { + func: "gpii.test.checkRestoredConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{tests}.settingsStore", "{nameResolver}", "{testCaseHolder}.events.onCheckRestoredConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckRestoredConfigurationComplete", + listener: "fluid.identity" + } + ] + }, { + name: "Sequential setting changes by the PSP", + expect: 11, + sequence: [ + { + func: "gpii.test.expandSettings", + args: [ "{tests}", [ "contexts" ]] + }, { + func: "gpii.test.snapshotSettings", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{tests}.settingsStore", "{nameResolver}", "{testCaseHolder}.events.onSnapshotComplete.fire"] + }, { + event: "{testCaseHolder}.events.onSnapshotComplete", + listener: "fluid.identity" + }, { + func: "{loginRequest}.send" + }, { + event: "{loginRequest}.events.onComplete", + listener: "gpii.test.loginRequestListen" + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + func: "{pspClient}.connect" + }, { + event: "{pspClient}.events.onConnect", + listener: "gpii.tests.pspIntegration.connectionSucceeded" + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.afterConnect] + }, { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "modelChanged", { + settingControls: { + "http://registry\\.gpii\\.net/common/magnification": { + value: 3 + } + } + }] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "preferencesApplied"] + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.afterChangeMagnification, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "modelChanged", { + settingControls: { + "http://registry\\.gpii\\.net/common/volume": { + value: 0.75 + } + } + }] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "preferencesApplied"] + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.afterChangeVolume, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + func: "{logoutRequest}.send" + }, { + event: "{logoutRequest}.events.onComplete", + listener: "gpii.test.logoutRequestListen" + }, { + func: "gpii.test.checkRestoredConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{tests}.settingsStore", "{nameResolver}", "{testCaseHolder}.events.onCheckRestoredConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckRestoredConfigurationComplete", + listener: "fluid.identity" + } + ] + }, { + name: "GPII-4136: Sequential setting changes with the same scoped term on different settings", + expect: 15, + sequence: [ + { + func: "gpii.test.expandSettings", + args: [ "{tests}", [ "contexts" ]] + }, { + func: "gpii.test.snapshotSettings", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{tests}.settingsStore", "{nameResolver}", "{testCaseHolder}.events.onSnapshotComplete.fire"] + }, { + event: "{testCaseHolder}.events.onSnapshotComplete", + listener: "fluid.identity" + }, { + func: "{loginRequest}.send" + }, { + event: "{loginRequest}.events.onComplete", + listener: "gpii.test.loginRequestListen" + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + func: "{pspClient}.connect" + }, { + event: "{pspClient}.events.onConnect", + listener: "gpii.tests.pspIntegration.connectionSucceeded" + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.afterConnect] + }, { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "modelChanged", { + settingControls: { + "http://registry\\.gpii\\.net/applications/org\\.gnome\\.desktop\\.a11y\\.magnifier.mag-factor": { + value: 3 + } + } + }] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.afterChangeMagnificationByAppTerm] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "preferencesApplied"] + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.afterChangeMagnification, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "modelChanged", { + settingControls: { + "http://registry\\.gpii\\.net/applications/org\\.gnome\\.desktop\\.a11y\\.magnifier.show-cross-hairs": { + value: 1 + } + } + }] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.afterChangeShowCrosshairsByAppTerm] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "preferencesApplied"] + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.afterChangeShowCrosshairs, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + func: "{logoutRequest}.send" + }, { + event: "{logoutRequest}.events.onComplete", + listener: "gpii.test.logoutRequestListen" + }, { + func: "gpii.test.checkRestoredConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{tests}.settingsStore", "{nameResolver}", "{testCaseHolder}.events.onCheckRestoredConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckRestoredConfigurationComplete", + listener: "fluid.identity" + } + ] + }, { + name: "Preferences set change via the PSP", + expect: 10, + sequence: [ + { + func: "gpii.test.expandSettings", + args: [ "{tests}", [ "contexts" ]] + }, { + func: "gpii.test.snapshotSettings", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{tests}.settingsStore", "{nameResolver}", "{testCaseHolder}.events.onSnapshotComplete.fire"] + }, { + event: "{testCaseHolder}.events.onSnapshotComplete", + listener: "fluid.identity" + }, { + func: "{loginRequest}.send" + }, { + event: "{loginRequest}.events.onComplete", + listener: "gpii.test.loginRequestListen" + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + func: "{pspClient}.connect" + }, { + event: "{pspClient}.events.onConnect", + listener: "gpii.tests.pspIntegration.connectionSucceeded" + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.afterConnect] + }, { + funcName: "gpii.tests.pspIntegration.sendPrefsSetNameChange", + args: ["{pspClient}", "bright"] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.afterChangeToBright] + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.bright, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + func: "{logoutRequest}.send" + }, { + event: "{logoutRequest}.events.onComplete", + listener: "gpii.test.logoutRequestListen" + }, { + func: "gpii.test.checkRestoredConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{tests}.settingsStore", "{nameResolver}", "{testCaseHolder}.events.onCheckRestoredConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckRestoredConfigurationComplete", + listener: "fluid.identity" + } + ] + }, { + name: "Settings change from PSP followed by a preferences set change via the PSP", + expect: 12, + sequence: [ + { + func: "gpii.test.expandSettings", + args: [ "{tests}", [ "contexts" ]] + }, { + func: "gpii.test.snapshotSettings", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{tests}.settingsStore", "{nameResolver}", "{testCaseHolder}.events.onSnapshotComplete.fire"] + }, { + event: "{testCaseHolder}.events.onSnapshotComplete", + listener: "fluid.identity" + }, { + func: "{loginRequest}.send" + }, { + event: "{loginRequest}.events.onComplete", + listener: "gpii.test.loginRequestListen" + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + func: "{pspClient}.connect" + }, { + event: "{pspClient}.events.onConnect", + listener: "gpii.tests.pspIntegration.connectionSucceeded" + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.afterConnect] + }, { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "modelChanged", { + settingControls: { + "http://registry\\.gpii\\.net/common/magnification": { + value: 3 + } + } + }] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "preferencesApplied"] + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.afterChangeMagnification, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + funcName: "gpii.tests.pspIntegration.sendPrefsSetNameChange", + args: ["{pspClient}", "bright"] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.afterChangeToBright] + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.bright, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + func: "{logoutRequest}.send" + }, { + event: "{logoutRequest}.events.onComplete", + listener: "gpii.test.logoutRequestListen" + }, { + func: "gpii.test.checkRestoredConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{tests}.settingsStore", "{nameResolver}", "{testCaseHolder}.events.onCheckRestoredConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckRestoredConfigurationComplete", + listener: "fluid.identity" + } + ] + }, { + name: "GPII-3437: reset does not throw errors after a long asynchronous reset process", + expect: 9, + sequence: [ + { + func: "gpii.test.expandSettings", + args: [ "{tests}", [ "contexts" ]] + }, { + func: "gpii.test.snapshotSettings", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{tests}.settingsStore", "{nameResolver}", "{testCaseHolder}.events.onSnapshotComplete.fire"] + }, { + event: "{testCaseHolder}.events.onSnapshotComplete", + listener: "fluid.identity" + }, { + func: "{loginRequest}.send" + }, { + event: "{loginRequest}.events.onComplete", + listener: "gpii.test.loginRequestListen" + }, { + func: "gpii.test.checkConfiguration", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{nameResolver}", "{testCaseHolder}.events.onCheckConfigurationComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckConfigurationComplete", + listener: "fluid.identity" + }, { + func: "{pspClient}.connect" + }, { + event: "{pspClient}.events.onConnect", + listener: "gpii.tests.pspIntegration.connectionSucceeded" + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.afterConnect] + }, { + // Issue a setting change that will be applied using the async mock settings handler + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "modelChanged", { + settingControls: { + "http://registry\\.gpii\\.net/applications/org\\.gnome\\.orca.enableSpeech": { + value: true + } + } + }] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.afterChangeScreenReaderByAppTerm] + }, { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "preferencesApplied"] + }, { + func: "{resetRequest}.send" + }, { + event: "{resetRequest}.events.onComplete", + listener: "gpii.tests.pspIntegration.checkResetResponse", + args: ["{arguments}.0"] + } + ] + }, { + name: "GPII-3693: \"reset all\" does not corrupt PSPChannel's model", + expect: 10, + sequence: [ + { + func: "{pspClient}.connect" + }, + { + event: "{pspClient}.events.onConnect", + listener: "gpii.tests.pspIntegration.connectionSucceeded" + }, + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.noUser] + }, + { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "modelChanged", { + settingControls: { + "http://registry\\.gpii\\.net/common/magnification": { + value: 3 + } + } + }] + }, + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.afterChangeMagnification] + }, + { + func: "{resetRequest}.send" + }, + { + event: "{resetRequest}.events.onComplete", + listener: "gpii.tests.pspIntegration.checkResetResponse", + args: ["{arguments}.0"] + }, + { + // When "noUser" keys back in, PSP client receives empty settingControls block. + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.noUser] + }, + { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "modelChanged", { + settingControls: { + "http://registry\\.gpii\\.net/common/cursorSize": { + value: 0.9 + } + } + }] + }, + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.afterChangeCursorSize] + } + ] + }, { + name: "GPII-3828: PSPChannel reports default settings from the reset to default file when settingControls block is empty", + expect: 10, + "defaultSettings": { + "contexts": { + "gpii-default": { + "preferences": { + "http://registry.gpii.net/common/cursorSize": 0.8, + "http://registry.gpii.net/common/volume": 1, + "http://registry.gpii.net/applications/com.microsoft.office": { + "word-ribbon": "Basics+StandardSet" + } + } + } + } + }, + sequence: [ + { + func: "{pspClient}.connect" + }, + { + event: "{pspClient}.events.onConnect", + listener: "gpii.tests.pspIntegration.connectionSucceeded" + }, + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.noUserWithDefaultSettings] + }, + { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "modelChanged", { + settingControls: { + "http://registry\\.gpii\\.net/common/magnification": { + "value": 3 + } + } + }] + }, + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.afterChangeMagnificationWithDefaultSettings] + }, + { + func: "{resetRequest}.send" + }, + { + event: "{resetRequest}.events.onComplete", + listener: "gpii.tests.pspIntegration.checkResetResponse", + args: ["{arguments}.0"] + }, + { + // When "noUser" keys back in, PSP client receives empty settingControls block. + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.noUserWithDefaultSettings] + }, + { + // change a setting that is in default settings from the reset to standard file. + // settingControls.settingKey.schema.default should be set to the value from the default setting. + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "modelChanged", { + settingControls: { + "http://registry\\.gpii\\.net/common/cursorSize": { + "value": 0.9 + } + } + }] + }, + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.afterChangeCursorSizeWitthDefaultSettings] + } + ] + } +]; + +gpii.tests.pspIntegration.readPrefsTestDefs = [ + { + name: "Read the same setting sequentially", + expect: 9, + sequence: [ + // Set the initial underlying magnification value + { + func: "gpii.test.setSettings", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{nameResolver}", "{testCaseHolder}.events.onInitialSettingsComplete.fire"] + }, + { + event: "{tests}.events.onInitialSettingsComplete", + listener: "fluid.identity" + }, + { + func: "{pspClient}.connect" + }, + { + event: "{pspClient}.events.onConnect", + listener: "gpii.tests.pspIntegration.connectionSucceeded" + }, + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", {}] + }, + // read the initial magnification value + { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "pullModel", { + settingControls: { + "http://registry\\.gpii\\.net/common/magnification": { + "value": 1 + } + } + }] + }, + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.readPrefsInitial] + }, + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "preferenceReadSuccess"] + }, + // Change the underlying magnification value + { + func: "gpii.test.setSettings", + args: [gpii.tests.pspIntegration.settingsHandlers.bright, "{nameResolver}", "{testCaseHolder}.events.onInitialSettingsComplete.fire"] + }, + { + event: "{tests}.events.onInitialSettingsComplete", + listener: "fluid.identity" + }, + // Re-read the magnification preference + { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "pullModel", { + settingControls: { + "http://registry\\.gpii\\.net/common/magnification": { + "value": 1 + } + } + }] + }, + // The changed magnification value is returned + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.readPrefsBright] + }, + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "preferenceReadSuccess"] + } + ] + }, + { + name: "Read different settings one by one sequentially", + expect: 9, + sequence: [ + // Set the initial underlying preference values for magnification and volume + { + func: "gpii.test.setSettings", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{nameResolver}", "{testCaseHolder}.events.onInitialSettingsComplete.fire"] + }, + { + event: "{tests}.events.onInitialSettingsComplete", + listener: "fluid.identity" + }, + { + func: "{pspClient}.connect" + }, + { + event: "{pspClient}.events.onConnect", + listener: "gpii.tests.pspIntegration.connectionSucceeded" + }, + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", {}] + }, + // read the magnification + { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "pullModel", { + settingControls: { + "http://registry\\.gpii\\.net/common/magnification": { + "value": 1 + } + } + }] + }, + // Only the magnification value is returned + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.readPrefsInitial] + }, + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "preferenceReadSuccess"] + }, + // Read a different preference - volume + { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "pullModel", { + settingControls: { + "http://registry\\.gpii\\.net/common/volume": { + "value": 0.1 + } + } + }] + }, + // Both magnification and volume values are returned + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.readPrefsMulitple] + }, + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "preferenceReadSuccess"] + } + ] + }, + { + name: "Read multiple settings in one request", + expect: 6, + sequence: [ + // Set the initial underlying preference values for magnification and volume + { + func: "gpii.test.setSettings", + args: [gpii.tests.pspIntegration.settingsHandlers.initial, "{nameResolver}", "{testCaseHolder}.events.onInitialSettingsComplete.fire"] + }, + { + event: "{tests}.events.onInitialSettingsComplete", + listener: "fluid.identity" + }, + { + func: "{pspClient}.connect" + }, + { + event: "{pspClient}.events.onConnect", + listener: "gpii.tests.pspIntegration.connectionSucceeded" + }, + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", {}] + }, + // read the magnification + { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "pullModel", { + settingControls: { + "http://registry\\.gpii\\.net/common/magnification": { + "value": 1 + }, + "http://registry\\.gpii\\.net/common/volume": { + "value": 0.1 + } + } + }] + }, + // Both magnification and volume values are returned + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.readPrefsMulitple] + }, + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "preferenceReadSuccess"] + } + ] + }, + { + name: "Read a setting with success - does NOT return settingControls block when settings stay unchanged", + expect: 5, + sequence: [ + { + func: "{pspClient}.connect" + }, + { + event: "{pspClient}.events.onConnect", + listener: "gpii.tests.pspIntegration.connectionSucceeded" + }, + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", {}] + }, + // set the initial magnification value to trigger the first "modelChanged" response + { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "modelChanged", { + settingControls: { + "http://registry\\.gpii\\.net/common/magnification": { + "value": 3 + } + } + }] + }, + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", gpii.tests.pspIntegration.expectedSettingControls.changeMagnification] + }, + // read a setting + { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "pullModel", { + settingControls: { + "http://registry\\.gpii\\.net/common/magnification": { + "value": 1 + } + } + }] + }, + // Only return "preferenceReadSuccess" message. "modelChanged" message is not returned as all settings + // including magnification value stays unchanged. + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "preferenceReadSuccess"] + } + ] + }, + { + name: "Read a setting with failure", + expect: 4, + sequence: [ + { + func: "{pspClient}.connect" + }, + { + event: "{pspClient}.events.onConnect", + listener: "gpii.tests.pspIntegration.connectionSucceeded" + }, + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "modelChanged", {}] + }, + // read an undefined setting + { + funcName: "gpii.tests.pspIntegration.sendMsg", + args: [ "{pspClient}", "pullModel", { + settingControls: { + "http://registry\\.gpii\\.net/common/magnification": { + "value": 1 + } + } + }] + }, + { + event: "{pspClient}.events.onReceiveMessage", + listener: "gpii.tests.pspIntegration.checkPayload", + args: ["{arguments}.0", "preferenceReadFail"] + } + ] + } +]; diff --git a/tests/shared/PSPIntegrationTestUtils.js b/tests/shared/PSPIntegrationTestUtils.js new file mode 100644 index 000000000..e4cb08a8a --- /dev/null +++ b/tests/shared/PSPIntegrationTestUtils.js @@ -0,0 +1,705 @@ +/** +Shared GPII PSP Integration Test definitions + +Copyright 2014, 2017 Raising the Floor - International + +Licensed under the New BSD license. You may not use this file except in +compliance with this License. + +The research leading to these results has received funding from the European Union's +Seventh Framework Programme (FP7/2007-2013) under grant agreement no. 289016. + +You may obtain a copy of the License at +https://github.com/GPII/universal/blob/master/LICENSE.txt +*/ +"use strict"; + +var fluid = require("infusion"), + jqUnit = fluid.registerNamespace("jqUnit"), + gpii = fluid.registerNamespace("gpii"); + +fluid.defaults("gpii.tests.pspIntegration.testCaseHolder.common.linux", { + gradeNames: [ + "gpii.test.integration.testCaseHolder.linux" + ], + components: { + pspClient: { + type: "gpii.tests.pspIntegration.client" + }, + resetRequest: { + type: "gpii.tests.pspIntegration.resetRequestType" + } + }, + gpiiKey: "context1", + events: { + onInitialSettingsComplete: null + } +}); + +fluid.defaults("gpii.tests.pspIntegration.client", { + gradeNames: "kettle.test.request.ws", + path: "/pspChannel", + port: 8081 +}); + +fluid.defaults("gpii.tests.pspIntegration.resetRequestType", { + gradeNames: "kettle.test.request.http", + path: "/user/reset/login" +}); + +gpii.tests.pspIntegration.sendMsg = function (client, type, value) { + client.send({ + type: type, + value: value + }); +}; + +gpii.tests.pspIntegration.sendPrefsSetNameChange = function (client, newContext) { + gpii.tests.pspIntegration.sendMsg(client, "modelChanged", { + activePrefsSetName: newContext + }); +}; + +gpii.tests.pspIntegration.checkPayload = function (data, expectedType, expectedSettingControls) { + jqUnit.assertEquals("Checking message from PSP: ", expectedType, data.type); + if (expectedSettingControls) { + var actualSettingControls = data.payload.value.settingControls; + jqUnit.assertDeepEq("Checking received settingControls from PSP: ", expectedSettingControls, actualSettingControls); + } +}; + +gpii.tests.pspIntegration.connectionSucceeded = function (data) { + jqUnit.assertValue("Connection between client and server can be established", data); +}; + +gpii.tests.pspIntegration.checkResetResponse = function (data) { + jqUnit.assertEquals("The reset request completes successfully", "Reset successfully.", data); +}; + +gpii.tests.pspIntegration.settingsHandlers = { + initial: { + "gpii.gsettings": { + "data": [{ + "settings": { + "mag-factor": 1.5 + }, + "options": { + "schema": "org.gnome.desktop.a11y.magnifier" + } + }] + }, + "gpii.alsa": { + "data": [{ + "settings": { + "masterVolume": 50 + } + }] + } + }, + afterChangeMagnification: { + "gpii.gsettings": { + "data": [{ + "settings": { + "mag-factor": 3 + }, + "options": { + "schema": "org.gnome.desktop.a11y.magnifier" + } + }] + }, + "gpii.alsa": { + "data": [{ + "settings": { + "masterVolume": 50 + } + }] + } + }, + afterChangeCursorSize: { + "gpii.gsettings": { + "data": [{ + "settings": { + "cursor-size": 41 + }, + "options": { + "schema": "org.gnome.desktop.interface" + } + }] + }, + "gpii.alsa": { + "data": [{ + "settings": { + "masterVolume": 50 + } + }] + } + }, + afterDecreaseCursorSize: { + "gpii.gsettings": { + "data": [{ + "settings": { + "cursor-size": 29 + }, + "options": { + "schema": "org.gnome.desktop.interface" + } + }] + }, + "gpii.alsa": { + "data": [{ + "settings": { + "masterVolume": 50 + } + }] + } + }, + afterChangeVolume: { + "gpii.gsettings": { + "data": [{ + "settings": { + "mag-factor": 3 + }, + "options": { + "schema": "org.gnome.desktop.a11y.magnifier" + } + }] + }, + "gpii.alsa": { + "data": [{ + "settings": { + "masterVolume": 75 + } + }] + } + }, + afterChangeShowCrosshairs: { + "gpii.gsettings": { + "data": [{ + "settings": { + "mag-factor": 3, + "show-cross-hairs": 1 + }, + "options": { + "schema": "org.gnome.desktop.a11y.magnifier" + } + }] + } + }, + bright: { + "gpii.gsettings": { + "data": [{ + "settings": { + "mag-factor": 2 + }, + "options": { + "schema": "org.gnome.desktop.a11y.magnifier" + } + }] + } + } +}; + +gpii.tests.pspIntegration.expectedSettingControls = { + noUser: {}, + afterConnect: { + "http://registry\\.gpii\\.net/common/magnification": { + "solutionName": "Magnification", + "value": 1.5, + "schema": { + "title": "Magnification", + "description": "Level of magnification", + "type": "number", + "default": 1, + "minimum": 1, + "multipleOf": 0.1 + }, + "liveness": "live" + }, + "http://registry\\.gpii\\.net/common/volume": { + "solutionName": "Volume", + "value": 0.5, + "schema": { + "title": "Volume", + "description": "General volume of the operating system", + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "liveness": "live" + } + }, + afterChangeMagnificationByAppTerm: { + "http://registry\\.gpii\\.net/common/magnification": { + "solutionName": "Magnification", + "schema": { + "title": "Magnification", + "description": "Level of magnification", + "type": "number", + "default": 1, + "minimum": 1, + "multipleOf": 0.1 + }, + "liveness": "live", + "value": 1.5 + }, + "http://registry\\.gpii\\.net/common/volume": { + "solutionName": "Volume", + "schema": { + "title": "Volume", + "description": "General volume of the operating system", + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "liveness": "live", + "value": 0.5 + }, + "http://registry\\.gpii\\.net/applications/org\\.gnome\\.desktop\\.a11y\\.magnifier.mag-factor": { + "value": 3, + "schema": { + "title": "Magnification", + "description": "Set up magnification level", + "type": "number", + "default": 1, + "minimum": 1, + "maximum": 5 + }, + "liveness": "live", + "solutionName": "GNOME Shell Magnifier" + } + }, + afterChangeShowCrosshairsByAppTerm: { + "http://registry\\.gpii\\.net/common/magnification": { + "solutionName": "Magnification", + "schema": { + "title": "Magnification", + "description": "Level of magnification", + "type": "number", + "default": 1, + "minimum": 1, + "multipleOf": 0.1 + }, + "liveness": "live", + "value": 1.5 + }, + "http://registry\\.gpii\\.net/common/volume": { + "solutionName": "Volume", + "schema": { + "title": "Volume", + "description": "General volume of the operating system", + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "liveness": "live", + "value": 0.5 + }, + "http://registry\\.gpii\\.net/applications/org\\.gnome\\.desktop\\.a11y\\.magnifier.mag-factor": { + "value": 3, + "schema": { + "title": "Magnification", + "description": "Set up magnification level", + "type": "number", + "default": 1, + "minimum": 1, + "maximum": 5 + }, + "liveness": "live", + "solutionName": "GNOME Shell Magnifier" + }, + "http://registry\\.gpii\\.net/applications/org\\.gnome\\.desktop\\.a11y\\.magnifier.show-cross-hairs": { + "value": 1, + "schema": { + "title": "Show crosshairs", + "description": "Whether to show crosshairs", + "type": "boolean", + "default": false + }, + "liveness": "live", + "solutionName": "GNOME Shell Magnifier" + } + }, + afterChangeScreenReaderByAppTerm: { + "http://registry\\.gpii\\.net/common/magnification": { + "solutionName": "Magnification", + "schema": { + "title": "Magnification", + "description": "Level of magnification", + "type": "number", + "default": 1, + "minimum": 1, + "multipleOf": 0.1 + }, + "liveness": "live", + "value": 1.5 + }, + "http://registry\\.gpii\\.net/common/volume": { + "solutionName": "Volume", + "schema": { + "title": "Volume", + "description": "General volume of the operating system", + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "liveness": "live", + "value": 0.5 + }, + "http://registry\\.gpii\\.net/applications/org\\.gnome\\.orca.enableSpeech": { + "value": true, + "schema": { + "title": "Enable Speech", + "description": "Enable speech", + "enum": [ + 0, + 1 + ], + "enumLabels": [ + "off", + "on" + ], + "default": 0 + }, + "liveness": "manualRestart", + "solutionName": "ORCA Screen Reader" + } + }, + afterChangeToBright: { + "http://registry\\.gpii\\.net/common/magnification": { + "solutionName": "Magnification", + "value": 2, + "schema": { + "title": "Magnification", + "description": "Level of magnification", + "type": "number", + "default": 1, + "minimum": 1, + "multipleOf": 0.1 + }, + "liveness": "live" + } + }, + afterChangeMagnification: { + "http://registry\\.gpii\\.net/common/magnification": { + "solutionName": "Magnification", + "value": 3, + "schema": { + "title": "Magnification", + "description": "Level of magnification", + "type": "number", + "default": 1, + "minimum": 1, + "multipleOf": 0.1 + }, + "liveness": "live" + } + }, + afterChangeCursorSize: { + "http://registry\\.gpii\\.net/common/cursorSize": { + "solutionName": "Cursor Size", + "value": 0.9, + "schema": { + "title": "Cursor Size", + "description": "Cursor size", + "type": "number", + "default": 0.5, + "minimum": 0, + "maximum": 1, + "multipleOf": 0.1 + }, + "liveness": "live" + } + }, + afterChangeCursorSizeCombined: { + "http://registry\\.gpii\\.net/common/magnification": { + "solutionName": "Magnification", + "schema": { + "title": "Magnification", + "description": "Level of magnification", + "type": "number", + "default": 1, + "minimum": 1, + "multipleOf": 0.1 + }, + "liveness": "live", + "value": 1.5 + }, + "http://registry\\.gpii\\.net/common/volume": { + "solutionName": "Volume", + "schema": { + "title": "Volume", + "description": "General volume of the operating system", + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "liveness": "live", + "value": 0.5 + }, + "http://registry\\.gpii\\.net/common/cursorSize": { + "solutionName": "Cursor Size", + "value": 0.9, + "schema": { + "title": "Cursor Size", + "description": "Cursor size", + "type": "number", + "default": 0.5, + "minimum": 0, + "maximum": 1, + "multipleOf": 0.1 + }, + "liveness": "live" + } + }, + afterChangePitch: { + "http://registry\\.gpii\\.net/common/magnification": { + "solutionName": "Magnification", + "schema": { + "title": "Magnification", + "description": "Level of magnification", + "type": "number", + "default": 1, + "minimum": 1, + "multipleOf": 0.1 + }, + "liveness": "live", + "value": 1.5 + }, + "http://registry\\.gpii\\.net/common/volume": { + "solutionName": "Volume", + "schema": { + "title": "Volume", + "description": "General volume of the operating system", + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "liveness": "live", + "value": 0.5 + }, + "http://registry\\.gpii\\.net/common/pitch": { + "value": 0.8, + "schema": { + "title": "Pitch rate", + "description": "Text to speech output pitch rate", + "type": "number", + "default": 0.5, + "minimum": 0, + "maximum": 1, + "multipleOf": 0.1 + }, + "liveness": "manualRestart" + } + }, + noUserWithDefaultSettings: { + "http://registry\\.gpii\\.net/common/cursorSize": { + "solutionName": "Cursor Size", + "schema": { + "title": "Cursor Size", + "description": "Cursor size", + "type": "number", + "default": 0.8, + "minimum": 0, + "maximum": 1, + "multipleOf": 0.1 + }, + "liveness": "live" + }, + "http://registry\\.gpii\\.net/common/volume": { + "solutionName": "Volume", + "schema": { + "title": "Volume", + "description": "General volume of the operating system", + "type": "number", + "minimum": 0, + "maximum": 1, + "default": 1 + }, + "liveness": "live" + }, + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.office.word-ribbon": { + "solutionName": "Microsoft Office", + "schema": { + "title": "Word Ribbon Layout", + "description": "Specifies the custom layout of the ribbon and quick access toolbar for Word", + "enum": [ + "StandardSet", + "Basics+StandardSet", + "Essentials+StandardSet", + "Basics+Essentials+StandardSet" + ], + "enumLabels": [ + "Standard Set", + "Basics and Standard Set", + "Essentials and Standard Set", + "Basic, Essentials, and Standard Set" + ], + "default": "Basics+StandardSet" + }, + "liveness": "live" + } + }, + afterChangeMagnificationWithDefaultSettings: { + "http://registry\\.gpii\\.net/common/cursorSize": { + "solutionName": "Cursor Size", + "schema": { + "title": "Cursor Size", + "description": "Cursor size", + "type": "number", + "default": 0.8, + "minimum": 0, + "maximum": 1, + "multipleOf": 0.1 + }, + "liveness": "live" + }, + "http://registry\\.gpii\\.net/common/volume": { + "solutionName": "Volume", + "schema": { + "title": "Volume", + "description": "General volume of the operating system", + "type": "number", + "minimum": 0, + "maximum": 1, + "default": 1 + }, + "liveness": "live" + }, + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.office.word-ribbon": { + "solutionName": "Microsoft Office", + "schema": { + "title": "Word Ribbon Layout", + "description": "Specifies the custom layout of the ribbon and quick access toolbar for Word", + "enum": [ + "StandardSet", + "Basics+StandardSet", + "Essentials+StandardSet", + "Basics+Essentials+StandardSet" + ], + "enumLabels": [ + "Standard Set", + "Basics and Standard Set", + "Essentials and Standard Set", + "Basic, Essentials, and Standard Set" + ], + "default": "Basics+StandardSet" + }, + "liveness": "live" + }, + "http://registry\\.gpii\\.net/common/magnification": { + "solutionName": "Magnification", + "value": 3, + "schema": { + "title": "Magnification", + "description": "Level of magnification", + "type": "number", + "default": 1, + "minimum": 1, + "multipleOf": 0.1 + }, + "liveness": "live" + } + }, + afterChangeCursorSizeWitthDefaultSettings: { + "http://registry\\.gpii\\.net/common/cursorSize": { + "solutionName": "Cursor Size", + "value": 0.9, + "schema": { + "title": "Cursor Size", + "description": "Cursor size", + "type": "number", + "default": 0.8, + "minimum": 0, + "maximum": 1, + "multipleOf": 0.1 + }, + "liveness": "live" + }, + "http://registry\\.gpii\\.net/common/volume": { + "solutionName": "Volume", + "schema": { + "title": "Volume", + "description": "General volume of the operating system", + "type": "number", + "minimum": 0, + "maximum": 1, + "default": 1 + }, + "liveness": "live" + }, + "http://registry\\.gpii\\.net/applications/com\\.microsoft\\.office.word-ribbon": { + "solutionName": "Microsoft Office", + "schema": { + "title": "Word Ribbon Layout", + "description": "Specifies the custom layout of the ribbon and quick access toolbar for Word", + "enum": [ + "StandardSet", + "Basics+StandardSet", + "Essentials+StandardSet", + "Basics+Essentials+StandardSet" + ], + "enumLabels": [ + "Standard Set", + "Basics and Standard Set", + "Essentials and Standard Set", + "Basic, Essentials, and Standard Set" + ], + "default": "Basics+StandardSet" + }, + "liveness": "live" + } + }, + readPrefsInitial: { + "http://registry\\.gpii\\.net/common/magnification": { + "solutionName": "Magnification", + "value": 1.5, + "schema": { + "title": "Magnification", + "description": "Level of magnification", + "type": "number", + "default": 1, + "minimum": 1, + "multipleOf": 0.1 + } + } + }, + readPrefsBright: { + "http://registry\\.gpii\\.net/common/magnification": { + "solutionName": "Magnification", + "value": 2, + "schema": { + "title": "Magnification", + "description": "Level of magnification", + "type": "number", + "default": 1, + "minimum": 1, + "multipleOf": 0.1 + } + } + }, + readPrefsMulitple: { + "http://registry\\.gpii\\.net/common/magnification": { + "solutionName": "Magnification", + "value": 1.5, + "schema": { + "title": "Magnification", + "description": "Level of magnification", + "type": "number", + "default": 1, + "minimum": 1, + "multipleOf": 0.1 + } + }, + "http://registry\\.gpii\\.net/common/volume": { + "solutionName": "Volume", + "value": 0.5, + "schema": { + "title": "Volume", + "description": "General volume of the operating system", + "type": "number", + "minimum": 0, + "maximum": 1 + } + } + } +}; diff --git a/tests/shared/ResetDefaultSettingsTestDefs.js b/tests/shared/ResetDefaultSettingsTestDefs.js new file mode 100644 index 000000000..0e5069763 --- /dev/null +++ b/tests/shared/ResetDefaultSettingsTestDefs.js @@ -0,0 +1,107 @@ +/* + * Reset on System Startup Test Definitions + * + * Copyright 2018 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.resetDefaultSettings"); + +// 1. The test sequence for testing reset all on system start +gpii.tests.resetDefaultSettings.testSequence = { + expect: 1, + sequence: [{ + func: "gpii.test.checkRestoredInitialState", + args: ["{tests}.options.expectedState", "{nameResolver}", "{testCaseHolder}.events.onCheckRestoredInitialStateComplete.fire"] + }, { + event: "{testCaseHolder}.events.onCheckRestoredInitialStateComplete", + listener: "fluid.identity" + }] +}; + +// Test cases +gpii.tests.resetDefaultSettings.resetAtStartTestCases = [{ + name: "Testing reset on system startup with a /enabled preference", + defaultSettings: { + "contexts": { + "gpii-default": { + "preferences": { + "http://registry.gpii.net/common/magnification/enabled": true + } + } + } + }, + expectedState: { + "gpii.gsettings.launch": { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-magnifier-enabled" + } + }] + } + }, + testSequence: gpii.tests.resetDefaultSettings.testSequence +}, { + name: "Testing reset on system startup with a preference for applying settings", + defaultSettings: { + "contexts": { + "gpii-default": { + "preferences": { + "http://registry.gpii.net/common/magnification": 3 + } + } + } + }, + expectedState: { + "gpii.gsettings": { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "mag-factor": 3, + "screen-position": "full-screen" + }, + "options": { + "schema": "org.gnome.desktop.a11y.magnifier" + } + }] + } + }, + testSequence: gpii.tests.resetDefaultSettings.testSequence +}]; + +gpii.tests.resetDefaultSettings.buildTestDefs = function (testCases, config) { + return fluid.transform(testCases, function (oneTestCase) { + var gradeNamesForTest = oneTestCase.gradeNames ? oneTestCase.gradeNames : ["gpii.test.testCaseHolder", "gpii.test.integration.testCaseHolder.linux"]; + var testSequence = oneTestCase.testSequence ? oneTestCase.testSequence : gpii.tests.resetDefaultSettings.testSequence; + var testCaseOptions = fluid.censorKeys(oneTestCase, ["gradeNames", "defaultSettings", "testSequence"]); + return fluid.extend(true, { + gradeNames: gradeNamesForTest, + config: config, + "distributeOptions": { + "acceptance.defaultSettings": { + "record": { + args: oneTestCase.defaultSettings + }, + "target": "{that defaultSettingsLoader}.options.invokers.get" + } + } + }, testCaseOptions, testSequence); + }); +}; diff --git a/tests/shared/SuppressHttpEndpointsTestDefs.js b/tests/shared/SuppressHttpEndpointsTestDefs.js new file mode 100644 index 000000000..809b9742e --- /dev/null +++ b/tests/shared/SuppressHttpEndpointsTestDefs.js @@ -0,0 +1,86 @@ +/* + * Suppress HTTP Endpoints Test Definitions + * + * Copyright 2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + jqUnit = fluid.require("node-jqunit", require, "jqUnit"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.suppressHttpEndpoints"); + +fluid.defaults("gpii.tests.suppressHttpEndpoints.testCaseHolder", { + gradeNames: ["gpii.test.common.testCaseHolder"], + components: { + proximityTriggeredRequest: { + type: "kettle.test.request.http", + options: { + path: "/user/%gpiiKey/proximityTriggered", + termMap: { + gpiiKey: "{tests}.options.gpiiKey" + } + } + } + } +}); + +gpii.tests.suppressHttpEndpoints.verifyNonExistentHttpEndpoint = function (response, errorMsg) { + jqUnit.assertTrue("The HTTP endpoint doesn't exist", response.indexOf(errorMsg) !== -1); +}; + +// Test cases +gpii.tests.suppressHttpEndpoints.testDefs = [{ + name: "/login endpoint has been removed when the suppressHttpEndpoints flag is turned on", + expect: 1, + sequence: [{ + func: "{loginRequest}.send" + }, { + event: "{loginRequest}.events.onComplete", + listener: "gpii.tests.suppressHttpEndpoints.verifyNonExistentHttpEndpoint", + args: ["{arguments}.0", "Cannot GET /user/carla/login"] + }] +}, { + name: "/logout endpoint has been removed when the suppressHttpEndpoints flag is turned on", + expect: 1, + sequence: [{ + func: "{logoutRequest}.send" + }, { + event: "{logoutRequest}.events.onComplete", + listener: "gpii.tests.suppressHttpEndpoints.verifyNonExistentHttpEndpoint", + args: ["{arguments}.0", "Cannot GET /user/carla/logout"] + }] +}, { + name: "/proximityTriggered endpoint has been removed when the suppressHttpEndpoints flag is turned on", + expect: 1, + sequence: [{ + func: "{proximityTriggeredRequest}.send" + }, { + event: "{proximityTriggeredRequest}.events.onComplete", + listener: "gpii.tests.suppressHttpEndpoints.verifyNonExistentHttpEndpoint", + args: ["{arguments}.0", "Cannot GET /user/carla/proximityTriggered"] + }] +}]; + +gpii.tests.suppressHttpEndpoints.buildTestDefs = function (config) { + return fluid.transform(gpii.tests.suppressHttpEndpoints.testDefs, function (testDefIn) { + var testDef = fluid.extend(true, {}, testDefIn, { + config: config, + gradeNames: ["gpii.tests.suppressHttpEndpoints.testCaseHolder"], + gpiiKey: "carla" + }); + return testDef; + }); +}; diff --git a/tests/shared/UserLogonHandlersTestDefs.js b/tests/shared/UserLogonHandlersTestDefs.js new file mode 100644 index 000000000..c88f73564 --- /dev/null +++ b/tests/shared/UserLogonHandlersTestDefs.js @@ -0,0 +1,389 @@ +/* + * User Logon State Change Test Definitions + * + * Copyright 2013-2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + jqUnit = fluid.require("node-jqunit", require, "jqUnit"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.userLogonHandlers"); + +fluid.defaults("gpii.tests.userLogonHandlers.proximityTriggered", { + gradeNames: "kettle.test.request.http", + path: "/user/%gpiiKey/proximityTriggered", + termMap: { + gpiiKey: "{tests}.options.gpiiKey" + } +}); + +fluid.defaults("gpii.tests.userLogonHandlers.testCaseHolder", { + gradeNames: [ "gpii.test.common.lifecycleManagerReceiver", "gpii.test.testCaseHolder" ], + components: { + resetRequest: { + type: "kettle.test.request.http", + options: { + path: "/user/reset/proximityTriggered" + } + }, + resetRequest2: { + type: "kettle.test.request.http", + options: { + path: "/user/reset/proximityTriggered" + } + }, + proximityTriggeredRequest: { + type: "gpii.tests.userLogonHandlers.proximityTriggered" + }, + proximityTriggeredRequest2: { + type: "gpii.tests.userLogonHandlers.proximityTriggered" + }, + proximityTriggeredRequest3: { + type: "gpii.tests.userLogonHandlers.proximityTriggered" + }, + proximityTriggeredRequestOther: { + type: "kettle.test.request.http", + options: { + path: "/user/sammy/proximityTriggered" + } + }, + logoutSammyRequest: { + type: "kettle.test.request.http", + options: { + path: "/user/sammy/logout" + } + }, + loginAdjustCursorRequest: { + type: "kettle.test.request.http", + options: { + path: "/user/adjustCursor/login" + } + }, + logoutAdjustCursorRequest: { + type: "kettle.test.request.http", + options: { + path: "/user/adjustCursor/logout" + } + }, + loginNonexistentRequest: { + type: "kettle.test.request.http", + options: { + path: "/user/nonexistent-gpii-key/login" + } + }, + logoutNonexistentRequest: { + type: "kettle.test.request.http", + options: { + path: "/user/nonexistent-gpii-key/logout" + } + }, + logoutNoUserRequest: { + type: "kettle.test.request.http", + options: { + path: "/user/noUser/logout" + } + } + }, + events: { + debounceTimeoutComplete: null + } +}); + +gpii.tests.userLogonHandlers.gpiiKey = "adjustCursor"; + +gpii.tests.userLogonHandlers.testLoginResponse = function (data, gpiiKey) { + jqUnit.assertEquals("Response is correct", "User with GPII key " + + gpiiKey + " was successfully logged in.", data); +}; + +gpii.tests.userLogonHandlers.testLogoutResponse = function (data, gpiiKey) { + jqUnit.assertEquals("Response is correct", "User with GPII key " + + gpiiKey + " was successfully logged out.", data); +}; + +gpii.tests.userLogonHandlers.testResetResponse = function (data) { + jqUnit.assertEquals("Response is correct", "Reset successfully.", data); +}; + +gpii.tests.userLogonHandlers.buildTestDefs = function (testDefs) { + return fluid.transform(testDefs, function (testDef) { + return fluid.extend(true, { + config: { + // The custom config file is to config the debounce time at /proximityTriggered endpoint to 3 seconds + // rather than using the default 1.5 seconds. This is to work around an issue with testing a following + // request that occurs < the debounce time. In this test, the following request was sent after waiting + // for 1 second. However in the reality with all other running processes, CPU processing power etc, + // this request is usually sent a bit more than after 1 second, occassionally even more than 1.5 seconds, + // which causes the test to fail. Setting the debounce time to 3 seconds provides more buffering time + // for the following request to send. + configName: "gpii.tests.acceptance.userLogon.config", + configPath: "%gpii-universal/tests/configs" + }, + gradeNames: ["gpii.tests.userLogonHandlers.testCaseHolder", "gpii.test.integration.testCaseHolder.linux"], + gpiiKey: testDefs.gpiiKey || gpii.tests.userLogonHandlers.gpiiKey + }, testDef); + }); +}; + + +gpii.tests.userLogonHandlers.testDefs = [{ + name: "Testing standard proximityTriggered login and logout", + expect: 2, + sequence: [{ + // 1st /proximityTriggered request: standard login + func: "{proximityTriggeredRequest}.send" + }, { + event: "{proximityTriggeredRequest}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testLoginResponse", + args: ["{arguments}.0", gpii.tests.userLogonHandlers.gpiiKey] + }, { + // wait for the debounce period to pass so that the following /proximityTriggered request is not rejected + func: "setTimeout", + args: [ "{tests}.events.debounceTimeoutComplete.fire", 3500 ] + }, { + event: "{tests}.events.debounceTimeoutComplete", + listener: "fluid.identity" + }, { + // 2nd /proximityTriggered request: standard logout + func: "{proximityTriggeredRequest2}.send" + }, { + event: "{proximityTriggeredRequest2}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testLogoutResponse", + args: ["{arguments}.0", gpii.tests.userLogonHandlers.gpiiKey] + }] +}, { + name: "Testing proximityTriggered login with debounce", + expect: 4, + sequence: [{ + // 1st /proximityTriggered request: standard login + func: "{proximityTriggeredRequest}.send" + }, { + event: "{proximityTriggeredRequest}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testLoginResponse", + args: ["{arguments}.0", gpii.tests.userLogonHandlers.gpiiKey] + }, { + // wait within the debounce period to trigger the debounce logic so that the following /proximityTriggered request will be rejected + func: "setTimeout", + args: [ "{tests}.events.debounceTimeoutComplete.fire", 100 ] + }, { + event: "{tests}.events.debounceTimeoutComplete", + listener: "fluid.identity" + + }, { + // 2nd /proximityTriggered request: will be rejected + func: "{proximityTriggeredRequest2}.send" + }, { + event: "{proximityTriggeredRequest2}.events.onComplete", + listener: "kettle.test.assertErrorResponse", + args: { + message: "Received 429 error due to debounce rules", + errorTexts: "Proximity trigger ignored due to bounce rules", + statusCode: 429, + string: "{arguments}.0", + request: "{proximityTriggeredRequest2}" + } + }] +}, { + name: "Testing proximityTriggered logout with debounce", + expect: 5, + sequence: [{ + // 1st /proximityTriggered request: standard login + func: "{proximityTriggeredRequest}.send" + }, { + event: "{proximityTriggeredRequest}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testLoginResponse", + args: ["{arguments}.0", gpii.tests.userLogonHandlers.gpiiKey] + }, { + // wait for the debounce period to pass so that the following /proximityTriggered request is not rejected + func: "setTimeout", + args: [ "{tests}.events.debounceTimeoutComplete.fire", 3500 ] + }, { + event: "{tests}.events.debounceTimeoutComplete", + listener: "fluid.identity" + }, { + // 2nd /proximityTriggered request: standard logout + func: "{proximityTriggeredRequest2}.send" + }, { + event: "{proximityTriggeredRequest2}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testLogoutResponse", + args: ["{arguments}.0", gpii.tests.userLogonHandlers.gpiiKey] + }, { + // wait within the debounce period to trigger the debounce logic so that the following /proximityTriggered request with the same GPII key will be rejected + func: "setTimeout", + args: [ "{tests}.events.debounceTimeoutComplete.fire", 100 ] + }, { + event: "{tests}.events.debounceTimeoutComplete", + listener: "fluid.identity" + }, { + // 3rd /proximityTriggered request with the same GPII key: will be rejected + func: "{proximityTriggeredRequest3}.send" + }, { + event: "{proximityTriggeredRequest3}.events.onComplete", + listener: "kettle.test.assertErrorResponse", + args: { + message: "Received 429 error due to debounce rules", + errorTexts: "Proximity trigger ignored due to bounce rules", + statusCode: 429, + string: "{arguments}.0", + request: "{proximityTriggeredRequest3}" + } + }] +}, { + name: "Login with a different user with proximity trigger should log previous user out", + expect: 2, + sequence: [{ + // 1st /proximityTriggered request: standard login + func: "{proximityTriggeredRequest}.send" + }, { + event: "{proximityTriggeredRequest}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testLoginResponse", + args: ["{arguments}.0", gpii.tests.userLogonHandlers.gpiiKey] + }, { + // wait for the debounce period to pass + func: "setTimeout", + args: [ "{tests}.events.debounceTimeoutComplete.fire", 3500 ] + }, { + event: "{tests}.events.debounceTimeoutComplete", + listener: "fluid.identity" + }, { + // 2nd /proximityTriggered request to login with a different user + func: "{proximityTriggeredRequestOther}.send" + }, { + event: "{proximityTriggeredRequestOther}.events.onComplete", + listener: "jqUnit.assertEquals", + args: [ "Response is correct", "User with GPII key sammy was successfully logged in.", "{arguments}.0" ] + }] +}, { + name: "Login with a different user with proximity trigger should ignore debounce", + expect: 2, + sequence: [{ + // 1st /proximityTriggered request: standard login + func: "{proximityTriggeredRequest}.send" + }, { + event: "{proximityTriggeredRequest}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testLoginResponse", + args: ["{arguments}.0", gpii.tests.userLogonHandlers.gpiiKey] + }, { + // wait within the debounce period + func: "setTimeout", + args: [ "{tests}.events.debounceTimeoutComplete.fire", 100 ] + }, { + event: "{tests}.events.debounceTimeoutComplete", + listener: "fluid.identity" + }, { + // 2nd /proximityTriggered request to login with a different user + func: "{proximityTriggeredRequestOther}.send" + }, { + event: "{proximityTriggeredRequestOther}.events.onComplete", + listener: "jqUnit.assertEquals", + args: [ "Response is correct", "User with GPII key sammy was successfully logged in.", "{arguments}.0" ] + }] +}, { + name: "Testing proximityTriggered with 'reset' GPII key", + expect: 2, + sequence: [{ + // log in a user (part 1: login) + func: "{proximityTriggeredRequest}.send" + }, { + event: "{proximityTriggeredRequest}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testLoginResponse", + args: ["{arguments}.0", gpii.tests.userLogonHandlers.gpiiKey] + }, { + // resetting with user logged in (part 2: reset and check that user is logged out) + func: "{resetRequest2}.send" + }, { + event: "{resetRequest2}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testResetResponse", + args: ["{arguments}.0"] + }] +}, { + name: "Testing proximityTriggered with 'reset' noUser", + expect: 1, + sequence: [{ + // resetting with no user logged in + func: "{resetRequest}.send" + }, { + event: "{resetRequest}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testResetResponse", + args: ["{arguments}.0"] + }] +}, { + name: "Testing standard user//login and /user//logout URLs", + expect: 9, + sequence: [{ + // standard login + func: "{loginRequest}.send" + }, { + event: "{loginRequest}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testLoginResponse", + args: ["{arguments}.0", gpii.tests.userLogonHandlers.gpiiKey] + }, { + // standard login with an already logged in user: + func: "{loginAdjustCursorRequest}.send" + }, { + event: "{loginAdjustCursorRequest}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testLoginResponse", + args: ["{arguments}.0", gpii.tests.userLogonHandlers.gpiiKey] + }, { + // logout of different user + func: "{logoutSammyRequest}.send" + }, { + event: "{logoutSammyRequest}.events.onComplete", + listener: "kettle.test.assertErrorResponse", + args: { + message: "Received 409 error when logging out user who is not logged in", + errorTexts: "Got logout request from user sammy, but the user adjustCursor is logged in. So ignoring the request.", + statusCode: 409, + string: "{arguments}.0", + request: "{logoutSammyRequest}" + } + }, { + // logout of the correct user + func: "{logoutAdjustCursorRequest}.send" + }, { + event: "{logoutAdjustCursorRequest}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testLogoutResponse", + args: ["{arguments}.0", "adjustCursor"] + }, { + // logout of user when none is logged in + func: "{logoutRequest}.send" + }, { + event: "{logoutRequest}.events.onComplete", + listener: "kettle.test.assertErrorResponse", + args: { + message: "Received 409 error when logging out user when no user is logged in", + errorTexts: "Got logout request from user adjustCursor, but the user noUser is logged in. So ignoring the request.", + statusCode: 409, + string: "{arguments}.0", + request: "{logoutRequest}" + } + }] +}, { + name: "noUser logs back in after an explicit request to logout noUser", + expect: 1, + sequence: [{ + // 1st /proximityTriggered request: standard login + func: "{logoutNoUserRequest}.send" + }, { + event: "{logoutNoUserRequest}.events.onComplete", + listener: "gpii.tests.userLogonHandlers.testLogoutResponse", + args: ["{arguments}.0", "noUser"] + }] +}]; diff --git a/tests/shared/UserLogonRequestTestDefs.js b/tests/shared/UserLogonRequestTestDefs.js new file mode 100644 index 000000000..ac16d229b --- /dev/null +++ b/tests/shared/UserLogonRequestTestDefs.js @@ -0,0 +1,800 @@ +/* + * User Logon Request Test Definitions + * + * Copyright 2018-2019 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + */ + +"use strict"; + +var fluid = require("infusion"), + jqUnit = fluid.require("node-jqunit", require, "jqUnit"), + gpii = fluid.registerNamespace("gpii"); + +fluid.require("%gpii-universal"); + +gpii.loadTestingSupport(); + +fluid.registerNamespace("gpii.tests.userLogonRequest"); + +gpii.tests.userLogonRequest.gpiiKey = "adjustCursor"; +gpii.tests.userLogonRequest.anotherGpiiKey = "sammy"; + +fluid.defaults("gpii.tests.userLogonRequest.testCaseHolder", { + gradeNames: ["gpii.test.common.lifecycleManagerReceiver", "gpii.test.testCaseHolder"], + events: { + onNoUserLoggedIn: null, + debounceTimeoutComplete: null + }, + expectedModelChanges: { + proximityTriggeredLogon: [ + { + type: "logout", + inProgress: true, + gpiiKey: "noUser" + }, + { + type: "logout", + inProgress: false, + gpiiKey: "noUser" + }, + { + type: "login", + inProgress: true, + gpiiKey: gpii.tests.userLogonRequest.gpiiKey + }, + { + type: "login", + inProgress: false, + gpiiKey: gpii.tests.userLogonRequest.gpiiKey + } + ], + proximityTriggeredLogout: [ + { + type: "logout", + inProgress: true, + gpiiKey: gpii.tests.userLogonRequest.gpiiKey + }, + { + type: "logout", + inProgress: false, + gpiiKey: gpii.tests.userLogonRequest.gpiiKey + }, + { + type: "login", + inProgress: true, + gpiiKey: "noUser" + }, + { + type: "login", + inProgress: false, + gpiiKey: "noUser" + } + ], + logoutNoUser: [ + { + type: "logout", + inProgress: true, + gpiiKey: "noUser" + }, + { + type: "logout", + inProgress: false, + gpiiKey: "noUser" + }, + { + type: "login", + inProgress: true, + gpiiKey: "noUser" + }, + { + type: "login", + inProgress: false, + gpiiKey: "noUser" + } + ], + secondProximityTriggered: [ + { + type: "logout", + inProgress: true, + gpiiKey: gpii.tests.userLogonRequest.gpiiKey + }, + { + type: "logout", + inProgress: false, + gpiiKey: gpii.tests.userLogonRequest.gpiiKey + }, + { + type: "login", + inProgress: true, + gpiiKey: gpii.tests.userLogonRequest.anotherGpiiKey + }, + { + type: "login", + inProgress: false, + gpiiKey: gpii.tests.userLogonRequest.anotherGpiiKey + } + ], + resetLogout: [ + { + type: "logout", + inProgress: true, + gpiiKey: gpii.tests.userLogonRequest.gpiiKey + }, + { + type: "logout", + inProgress: false, + gpiiKey: gpii.tests.userLogonRequest.gpiiKey + }, + { + type: "login", + inProgress: true, + gpiiKey: "noUser" + }, + { + type: "login", + inProgress: false, + gpiiKey: "noUser" + } + ], + noUserLogin: [ + { + type: "login", + inProgress: true, + gpiiKey: "noUser" + }, + { + type: "login", + inProgress: false, + gpiiKey: "noUser" + } + ] + } +}); + +gpii.tests.userLogonRequest.verifyActiveGpiiKey = function (lifecycleManager, expected) { + jqUnit.assertDeepEq("The current active GPII key is as expected - " + expected, expected, lifecycleManager.getActiveSessionGpiiKey()); +}; + +gpii.tests.userLogonRequest.testLoginResponse = function (data, gpiiKey) { + jqUnit.assertEquals("Response is correct", "User with GPII key " + + gpiiKey + " was successfully logged in.", data); +}; + +gpii.tests.userLogonRequest.testLogoutResponse = function (data, gpiiKey) { + jqUnit.assertEquals("Response is correct", "User with GPII key " + + gpiiKey + " was successfully logged out.", data); +}; + +gpii.tests.userLogonRequest.testResetResponse = function (data) { + jqUnit.assertEquals("Response is correct", "Reset successfully.", data); +}; + +gpii.tests.userLogonRequest.checkLastModelChanges = function (trackedLogonChange, expectedModelChanges) { + var sliceToInspect = trackedLogonChange.slice(trackedLogonChange.length - expectedModelChanges.length); + fluid.each(expectedModelChanges, function (expectedModelChange, index) { + var actualChange = sliceToInspect[index]; + jqUnit.assertLeftHand("Checking model change " + index, expectedModelChange, actualChange); + }); +}; + +gpii.tests.userLogonRequest.testUserError = function (actualResponse, expectedResponse, userError, expectedUserError) { + jqUnit.assertDeepEq("onError fires with the expected message", expectedResponse, actualResponse); + jqUnit.assertDeepEq("User error has been fired with the expected content", expectedUserError, userError.error); +}; + +gpii.tests.userLogonRequest.testLogoutError = function (actualError, userError, expectedError) { + jqUnit.assertDeepEq("onError fires with the expected message", expectedError, actualError); + if (actualError.ignoreUserErrors) { + jqUnit.assertDeepEq("User error has not be fired", {}, userError); + } else { + var userErrorMessage = userError.error.originalError; + jqUnit.assertDeepEq("User error has been fired with the expected message", actualError.message, userErrorMessage); + } +}; + +gpii.tests.userLogonRequest.commonTestConfig = { + gradeNames: ["gpii.tests.userLogonRequest.testCaseHolder", "gpii.test.integration.testCaseHolder.linux"], + distributeOptions: { + "lifecycleManager.userErrorsListener": { + "record": { + trackedUserErrors: {}, + listeners: { + "userError.trackReportedError": { + listener: "fluid.set", + args: ["{that}.options.trackedUserErrors", "error", "{arguments}.0"] + } + } + }, + "target": "{that gpii.flowManager.local userErrors}.options" + } + } +}; + +gpii.tests.userLogonRequest.buildTestDefs = function (testDefs, testType) { + var config = { + // The custom config file is to config the debounce time at proximityTriggered endpoint to 3 seconds + // rather than using the default 1.5 seconds. This is to work around an issue with testing a following + // request that occurs < the debounce time. In this test, the following request was sent after waiting + // for 1 second. However in the reality with all other running processes, CPU processing power etc, + // this request is usually sent a bit more than after 1 second, occassionally even more than 1.5 seconds, + // which causes the test to fail. Setting the debounce time to 3 seconds provides more buffering time + // for the following request to send. + configName: testType === "untrusted" ? "gpii.tests.acceptance.untrusted.userLogon.config" : "gpii.tests.acceptance.userLogon.config", + configPath: "%gpii-universal/tests/configs" + }; + + return fluid.transform(testDefs, function (testDef) { + return fluid.extend(true, { + config: config, + gpiiKey: testDefs.gpiiKey || gpii.tests.userLogonRequest.gpiiKey, + distributeOptions: { + "lifecycleManager.logonChangeListener": { + "record": { + trackedLogonChange: [], + modelListeners: { + "logonChange": { + listener: "gpii.tests.userLogonRequest.trackLogonChange", + args: ["{that}.options.trackedLogonChange", "{change}.value", "{testCaseHolder}"] + } + } + }, + "target": "{that gpii.flowManager.local lifecycleManager}.options" + } + } + }, gpii.tests.userLogonRequest.commonTestConfig, testDef); + }); +}; + +gpii.tests.userLogonRequest.trackLogonChange = function (trackedLogonChange, logonChange) { + trackedLogonChange.push(logonChange); +}; + +// Note: There is an implementation risk with this fixture due to FLUID-5502 (https://issues.fluidproject.org/browse/FLUID-5502) +// but we believe that this is currently reliable because each of the model changes is triggered asynchronously. +gpii.tests.userLogonRequest.testDefs = [ + { + name: "Testing standard proximityTriggered login and logout", + expect: 13, + sequence: [ + { + // The initial active GPII key is "noUser" + func: "gpii.tests.userLogonRequest.verifyActiveGpiiKey", + args: ["{lifecycleManager}", "noUser"] + }, + { + // 1. First proximityTriggered request to key in adjustCursor + task: "{lifecycleManager}.performProximityTriggered", + args: [gpii.tests.userLogonRequest.gpiiKey], + resolve: "gpii.tests.userLogonRequest.testLoginResponse", + resolveArgs: ["{arguments}.0", gpii.tests.userLogonRequest.gpiiKey] + }, + { + funcName: "gpii.tests.userLogonRequest.checkLastModelChanges", + args: ["{lifecycleManager}.options.trackedLogonChange", "{that}.options.expectedModelChanges.proximityTriggeredLogon"] // trackedLogonChange, expectedModelChanges) + }, + { + func: "gpii.tests.userLogonRequest.verifyActiveGpiiKey", + args: ["{lifecycleManager}", gpii.tests.userLogonRequest.gpiiKey] + }, + { + // 2. wait for the debounce period to pass so that the following proximityTriggered request is not rejected + func: "setTimeout", + args: ["{tests}.events.debounceTimeoutComplete.fire", 3500] + }, + { + event: "{tests}.events.debounceTimeoutComplete", + listener: "fluid.identity" + }, + { + // 3. 2nd proximityTriggered request to key out adjustCursor + task: "{lifecycleManager}.performProximityTriggered", + args: [gpii.tests.userLogonRequest.gpiiKey], + resolve: "gpii.tests.userLogonRequest.testLogoutResponse", + resolveArgs: ["{arguments}.0", gpii.tests.userLogonRequest.gpiiKey] + }, + { + event: "{lifecycleManager}.events.onQueueEmpty", + listener: "gpii.tests.userLogonRequest.verifyActiveGpiiKey", + args: ["{lifecycleManager}", "noUser"] + }, + { + funcName: "gpii.tests.userLogonRequest.checkLastModelChanges", + args: ["{lifecycleManager}.options.trackedLogonChange", "{that}.options.expectedModelChanges.proximityTriggeredLogout"] // trackedLogonChange, expectedModelChanges) + } + ] + }, + { + name: "Login with a different user with proximity trigger should log previous user out and noUser does not login in between", + expect: 7, + sequence: [ + { + // 1. First proximityTriggered request to key in adjustCursor + task: "{lifecycleManager}.performProximityTriggered", + args: [gpii.tests.userLogonRequest.gpiiKey], + resolve: "gpii.tests.userLogonRequest.testLoginResponse", + resolveArgs: ["{arguments}.0", gpii.tests.userLogonRequest.gpiiKey] + }, + { + // 2. wait for the debounce period to pass + func: "setTimeout", + args: ["{tests}.events.debounceTimeoutComplete.fire", 3500] + }, + { + event: "{tests}.events.debounceTimeoutComplete", + listener: "fluid.identity" + }, + { + // 3. 2nd proximityTriggered request to key in another key "sammy". This should trigger: + // 1) key out the first key "adjustCursor"; + // 2) key in the 2nd key "sammy"; + // 3) "noUser" is not keyed in between step 1 and 2. + task: "{lifecycleManager}.performProximityTriggered", + args: [gpii.tests.userLogonRequest.anotherGpiiKey], + resolve: "gpii.tests.userLogonRequest.testLoginResponse", + resolveArgs: ["{arguments}.0", gpii.tests.userLogonRequest.anotherGpiiKey] + }, + { + event: "{lifecycleManager}.events.onQueueEmpty", + listener: "gpii.tests.userLogonRequest.verifyActiveGpiiKey", + args: ["{lifecycleManager}", gpii.tests.userLogonRequest.anotherGpiiKey] + }, + { + funcName: "gpii.tests.userLogonRequest.checkLastModelChanges", + args: ["{lifecycleManager}.options.trackedLogonChange", "{that}.options.expectedModelChanges.secondProximityTriggered"] // trackedLogonChange, expectedModelChanges) + } + ] + }, + { + name: "Login with a different user with proximity trigger should ignore debounce", + expect: 2, + sequence: [ + { + // 1. 1nd proximityTriggered request to key in adjustCursor + task: "{lifecycleManager}.performProximityTriggered", + args: [gpii.tests.userLogonRequest.gpiiKey], + resolve: "gpii.tests.userLogonRequest.testLoginResponse", + resolveArgs: ["{arguments}.0", gpii.tests.userLogonRequest.gpiiKey] + }, + { + // 2. wait within the debounce period + func: "setTimeout", + args: ["{tests}.events.debounceTimeoutComplete.fire", 10] + }, + { + event: "{tests}.events.debounceTimeoutComplete", + listener: "fluid.identity" + }, + { + // 3. 2nd proximityTriggered request to key in with a different user + task: "{lifecycleManager}.performProximityTriggered", + args: [gpii.tests.userLogonRequest.anotherGpiiKey], + resolve: "gpii.tests.userLogonRequest.testLoginResponse", + resolveArgs: ["{arguments}.0", gpii.tests.userLogonRequest.anotherGpiiKey] + } + ] + }, + { + name: "Testing proximityTriggered login with debounce", + expect: 2, + sequence: [ + { + // 1. 1nd proximityTriggered request to key in adjustCursor + task: "{lifecycleManager}.performProximityTriggered", + args: [gpii.tests.userLogonRequest.gpiiKey], + resolve: "gpii.tests.userLogonRequest.testLoginResponse", + resolveArgs: ["{arguments}.0", gpii.tests.userLogonRequest.gpiiKey] + }, + { + // 2. wait within the debounce period to trigger the debounce logic so that the following proximityTriggered request will be rejected + func: "setTimeout", + args: ["{tests}.events.debounceTimeoutComplete.fire", 10] + }, + { + event: "{tests}.events.debounceTimeoutComplete", + listener: "fluid.identity" + }, + { + // 1. 2nd proximityTriggered request to key out adjustCursor + task: "{lifecycleManager}.performProximityTriggered", + args: [gpii.tests.userLogonRequest.gpiiKey], + reject: "jqUnit.assertDeepEq", + rejectArgs: ["Proximity trigger ignored due to bounce rules", { + "statusCode": 429, + "message": "Proximity trigger ignored due to bounce rules. Please wait current logon change is complete", + "ignoreUserErrors": false + }, "{arguments}.0"] + } + ] + }, + { + name: "Testing proximityTriggered logout with debounce", + expect: 3, + sequence: [ + { + // 1. 1nd proximityTriggered request to key in adjustCursor + task: "{lifecycleManager}.performProximityTriggered", + args: [gpii.tests.userLogonRequest.gpiiKey], + resolve: "gpii.tests.userLogonRequest.testLoginResponse", + resolveArgs: ["{arguments}.0", gpii.tests.userLogonRequest.gpiiKey] + }, + { + // 2. wait for the debounce period to pass so that the following proximityTriggered request is not rejected + func: "setTimeout", + args: ["{tests}.events.debounceTimeoutComplete.fire", 3500] + }, + { + event: "{tests}.events.debounceTimeoutComplete", + listener: "fluid.identity" + }, + { + // 3. 2nd proximityTriggered request: standard logout + task: "{lifecycleManager}.performProximityTriggered", + args: [gpii.tests.userLogonRequest.gpiiKey], + resolve: "gpii.tests.userLogonRequest.testLogoutResponse", + resolveArgs: ["{arguments}.0", gpii.tests.userLogonRequest.gpiiKey] + }, + { + // 4. wait within the debounce period to trigger the debounce logic so that the following proximityTriggered request with the same GPII key will be rejected + // use 1000ms to allow a buffer time for "noUser" login after the logout above to complete. + func: "setTimeout", + args: ["{tests}.events.debounceTimeoutComplete.fire", 1000] + }, + { + event: "{tests}.events.debounceTimeoutComplete", + listener: "fluid.identity" + }, + { + // 5. 3rd proximityTriggered request with the same GPII key: will be rejected + task: "{lifecycleManager}.performProximityTriggered", + args: [gpii.tests.userLogonRequest.gpiiKey], + reject: "jqUnit.assertDeepEq", + rejectArgs: ["Proximity trigger ignored due to bounce rules", { + "statusCode": 429, + "message": "Proximity trigger ignored due to bounce rules. Please wait current logon change is complete", + "ignoreUserErrors": false + }, "{arguments}.0"] + } + ] + }, + { + name: "Testing 'reset' GPII key: resetting with noUser logs out noUser", + expect: 3, + sequence: [ + { + task: "{lifecycleManager}.performProximityTriggered", + args: ["reset"], + resolve: "gpii.tests.userLogonRequest.testResetResponse", + resolveArgs: ["{arguments}.0"] + }, + { + event: "{lifecycleManager}.events.onQueueEmpty", + listener: "gpii.tests.userLogonRequest.checkLastModelChanges", + args: ["{lifecycleManager}.options.trackedLogonChange", "{that}.options.expectedModelChanges.noUserLogin"] // trackedLogonChange, expectedModelChanges) + } + ] + }, + { + name: "Testing 'reset' GPII key: resetting with a user logged in", + expect: 9, + initialState: { + "gpii.gsettings.launch": { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "running": true + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-magnifier-enabled" + } + }] + } + }, + expectedStateAfterReset: { + "gpii.gsettings.launch": { + "org.gnome.desktop.a11y.magnifier": [{ + "settings": { + "running": false + }, + "options": { + "schema": "org.gnome.desktop.a11y.applications", + "key": "screen-magnifier-enabled" + } + }] + }, + "gpii.gsettings": { + "data": [{ + "settings": { + "cursor-size": 29 + }, + "options": { + "schema": "org.gnome.desktop.interface" + } + }] + }, + "gpii.alsa": { + "data": [{ + "settings": { + "masterVolume": 75 + } + }] + } + }, + sequence: [ + { + // 1. set the initial settings: start the magnifier + func: "gpii.test.expandSettings", + args: ["{tests}", "initialState"] + }, + { + func: "gpii.test.setInitialSettingsState", + args: ["{tests}.initialState", "{nameResolver}", "{testCaseHolder}.events.onInitialStateSet.fire"] + }, + { + event: "{testCaseHolder}.events.onInitialStateSet", + listener: "fluid.identity" + }, + { + func: "gpii.test.checkConfiguration", + args: ["{tests}.initialState", "{nameResolver}", "{testCaseHolder}.events.onInitialStateConfirmed.fire", "Confirming initial state"] + }, + { + event: "{testCaseHolder}.events.onInitialStateConfirmed", + listener: "fluid.identity" + }, + { + // 2. login + task: "{lifecycleManager}.performProximityTriggered", + args: [gpii.tests.userLogonRequest.gpiiKey], + resolve: "gpii.tests.userLogonRequest.testLoginResponse", + resolveArgs: ["{arguments}.0", gpii.tests.userLogonRequest.gpiiKey] + }, + { + // 3. reset and check that user is logged out) + task: "{lifecycleManager}.performProximityTriggered", + args: ["reset"], + resolve: "gpii.tests.userLogonRequest.testResetResponse", + resolveArgs: ["{arguments}.0"] + }, + { + event: "{lifecycleManager}.events.onQueueEmpty", + listener: "gpii.tests.userLogonRequest.verifyActiveGpiiKey", + args: ["{lifecycleManager}", "noUser"] + }, + { + funcName: "gpii.tests.userLogonRequest.checkLastModelChanges", + args: ["{lifecycleManager}.options.trackedLogonChange", "{that}.options.expectedModelChanges.resetLogout"] // trackedLogonChange, expectedModelChanges) + }, + { + // 6. Verify the default settings have been applied: stop the magnifier + func: "gpii.test.checkRestoredInitialState", + args: ["{tests}.options.expectedStateAfterReset", "{nameResolver}", "{testCaseHolder}.events.onCheckRestoredInitialStateComplete.fire"] + }, + { + event: "{testCaseHolder}.events.onCheckRestoredInitialStateComplete", + listener: "fluid.identity" + } + ] + }, + { + name: "Testing standard user//login and /user//logout URLs", + expect: 9, + initialState: { + "gpii.gsettings": { + "data": [{ + "settings": { + "cursor-size": 29 + }, + "options": { + "schema": "org.gnome.desktop.interface" + } + }] + }, + "gpii.alsa": { + "data": [{ + "settings": { + "masterVolume": 75 + } + }] + } + }, + expectedState: { + "gpii.gsettings": { + "data": [{ + "settings": { + "cursor-size": 41 + }, + "options": { + "schema": "org.gnome.desktop.interface" + } + }] + }, + "gpii.alsa": { + "data": [{ + "settings": { + "masterVolume": 75 + } + }] + } + }, + sequence: [ + { + // standard login + task: "{lifecycleManager}.performLogin", + args: [gpii.tests.userLogonRequest.gpiiKey], + resolve: "gpii.tests.userLogonRequest.testLoginResponse", + resolveArgs: ["{arguments}.0", gpii.tests.userLogonRequest.gpiiKey] + }, + { + // standard login completes: Verify both key-in settings and default settings for reset have been applied + event: "{lifecycleManager}.events.onQueueEmpty", + listener: "gpii.test.checkRestoredInitialState", + args: ["{tests}.options.expectedState", "{nameResolver}", "{testCaseHolder}.events.onCheckRestoredInitialStateComplete.fire"] + }, + { + event: "{testCaseHolder}.events.onCheckRestoredInitialStateComplete", + listener: "fluid.identity" + }, + { + // standard login with an already logged in user: + task: "{lifecycleManager}.performLogin", + args: [gpii.tests.userLogonRequest.anotherGpiiKey], + resolve: "gpii.tests.userLogonRequest.testLoginResponse", + resolveArgs: ["{arguments}.0", gpii.tests.userLogonRequest.anotherGpiiKey] + }, + { + event: "{lifecycleManager}.events.onQueueEmpty", + listener: "fluid.identity" + }, + { + // logout of different user + task: "{lifecycleManager}.performLogout", + args: [gpii.tests.userLogonRequest.gpiiKey], + reject: "gpii.tests.userLogonRequest.testLogoutError", + rejectArgs: ["{arguments}.0", "{lifecycleManager}.userErrors.options.trackedUserErrors", { + "statusCode": 409, + "message": "Got logout request from user adjustCursor, but the user sammy is logged in. So ignoring the request.", + "ignoreUserErrors": false + }] + }, + { + // logout of the correct user + task: "{lifecycleManager}.performLogout", + args: [gpii.tests.userLogonRequest.anotherGpiiKey], + resolve: "gpii.tests.userLogonRequest.testLogoutResponse", + resolveArgs: ["{arguments}.0", gpii.tests.userLogonRequest.anotherGpiiKey] + }, + { + event: "{lifecycleManager}.events.onQueueEmpty", + listener: "gpii.tests.userLogonRequest.verifyActiveGpiiKey", + args: ["{lifecycleManager}", "noUser"] + }, + { + funcName: "gpii.tests.userLogonRequest.checkLastModelChanges", + args: ["{lifecycleManager}.options.trackedLogonChange", "{that}.options.expectedModelChanges.noUserLogin"] // trackedLogonChange, expectedModelChanges) + } + ] + }, + { + name: "GPII-3481: /user//logout does not trigger the user error report when the current logged in user is noUser", + expect: 2, + sequence: [ + { + // logout of user when none is logged in + task: "{lifecycleManager}.performLogout", + args: [gpii.tests.userLogonRequest.gpiiKey], + reject: "gpii.tests.userLogonRequest.testLogoutError", + rejectArgs: ["{arguments}.0", "{lifecycleManager}.userErrors.options.trackedUserErrors", { + "statusCode": 409, + "message": "Got logout request from user adjustCursor, but the user noUser is logged in. So ignoring the request.", + "ignoreUserErrors": true + }, "{arguments}.0"] + } + ] + }, + { + name: "noUser logs back in after an explicit request to logout noUser", + expect: 7, + sequence: [ + { + // 1. "noUser" is keyed in initially + func: "gpii.tests.userLogonRequest.verifyActiveGpiiKey", + args: ["{lifecycleManager}", "noUser"] + }, + { + // 2. explicitly key out "noUser" + task: "{lifecycleManager}.performLogout", + args: ["noUser"], + resolve: "gpii.tests.userLogonRequest.testLogoutResponse", + resolveArgs: ["{arguments}.0", "noUser"] + }, + { + event: "{lifecycleManager}.events.onQueueEmpty", + listener: "gpii.tests.userLogonRequest.verifyActiveGpiiKey", + args: ["{lifecycleManager}", "noUser"] + }, + { + funcName: "gpii.tests.userLogonRequest.checkLastModelChanges", + args: ["{lifecycleManager}.options.trackedLogonChange", "{that}.options.expectedModelChanges.logoutNoUser"] // trackedLogonChange, expectedModelChanges) + } + ] + }, + { + name: "GPII-3988: Replay environmental login to support \"My Saved Settings\"", + expect: 10, + sequence: [ + { + // standard login + task: "{lifecycleManager}.performLogin", + args: [gpii.tests.userLogonRequest.gpiiKey, { + isEnvironmentalLogin: true + }], + resolve: "gpii.tests.userLogonRequest.testLoginResponse", + resolveArgs: ["{arguments}.0", gpii.tests.userLogonRequest.gpiiKey] + }, + { + funcName: "jqUnit.assertEquals", + args: ["The GPII key of the environmental login has been saved into the model", gpii.tests.userLogonRequest.gpiiKey, "{lifecycleManager}.model.lastEnvironmentalLoginGpiiKey"] + }, + { + // The replay of the last environmental login is accepted when the active GPII key is the same key + // stored for the replay + task: "{lifecycleManager}.replayEnvironmentalLogin", + resolve: "gpii.tests.userLogonRequest.testLoginResponse", + resolveArgs: ["{arguments}.0", gpii.tests.userLogonRequest.gpiiKey] + }, + { + // Logout of the current user + task: "{lifecycleManager}.performLogout", + args: [gpii.tests.userLogonRequest.gpiiKey], + resolve: "gpii.tests.userLogonRequest.testLogoutResponse", + resolveArgs: ["{arguments}.0", gpii.tests.userLogonRequest.gpiiKey] + }, + { + event: "{lifecycleManager}.events.onQueueEmpty", + listener: "gpii.tests.userLogonRequest.verifyActiveGpiiKey", + args: ["{lifecycleManager}", "noUser"] + }, + { + // The replay of the last environmental login is accepted when the active GPII key is "noUser" + task: "{lifecycleManager}.replayEnvironmentalLogin", + resolve: "gpii.tests.userLogonRequest.testLoginResponse", + resolveArgs: ["{arguments}.0", gpii.tests.userLogonRequest.gpiiKey] + }, + { + // Logout of the current user + task: "{lifecycleManager}.performLogout", + args: [gpii.tests.userLogonRequest.gpiiKey], + resolve: "gpii.tests.userLogonRequest.testLogoutResponse", + resolveArgs: ["{arguments}.0", gpii.tests.userLogonRequest.gpiiKey] + }, + { + // Login a different GPII key as a non-environmental login + task: "{lifecycleManager}.performLogin", + args: [gpii.tests.userLogonRequest.anotherGpiiKey], + resolve: "gpii.tests.userLogonRequest.testLoginResponse", + resolveArgs: ["{arguments}.0", gpii.tests.userLogonRequest.anotherGpiiKey] + }, + { + // Ensure the model value that saves the GPII key of the last environmental login stays unchanged + funcName: "jqUnit.assertEquals", + args: ["The GPII key of the last environmental login stays unchanged in the model", gpii.tests.userLogonRequest.gpiiKey, "{lifecycleManager}.model.lastEnvironmentalLoginGpiiKey"] + }, + { + // The replay of the last environmental login is rejected when the active GPII key is different from + // the GPII key saved for the last environmental login. + task: "{lifecycleManager}.replayEnvironmentalLogin", + reject: "jqUnit.assertDeepEq", + rejectArgs: ["Proximity trigger ignored due to bounce rules", { + "isError": true, + "message": "Replay environmental login ignored because the current active GPII key is not \"noUser\" or the last GPII key used for the environmental login" + }, "{arguments}.0"] + } + ] + } +]; diff --git a/tests/testem.js b/tests/testem.js new file mode 100644 index 000000000..7fb7de452 --- /dev/null +++ b/tests/testem.js @@ -0,0 +1,75 @@ +/* + + A Testem configuration file that extends gpii.testem. This is specific to this package, to work around limitations + in dealing with `node_modules` subdirectories with Istanbul, we have to: + + 1. Use our own (pretest) steps to instrument all required code. + 2. Handcraft our testemOptions.routes + 3. Avoid using the normal "generated options merging" from gpii.testem. + +*/ +"use strict"; +var fluid = require("infusion"); +var gpii = fluid.registerNamespace("gpii"); + +require("../"); + +fluid.require("%gpii-testem"); + +fluid.registerNamespace("gpii.tests.universal.testem"); + +gpii.tests.universal.testem.getTestemOptions = function (that) { + return that.options.testemOptions; +}; + +fluid.defaults("gpii.tests.universal.testem", { + gradeNames: ["gpii.testem.instrumentation"], + testPages: ["tests/web/html/all-tests.html"], + coverageDir: "coverage", + reportsDir: "reports", + instrumentationOptions: { + "excludes": [], + "sources": ["./**/*.js"], + "nonSources": [ + "./*.json", + "./**/*.json", + "./*.html", + "./**/*.html", + "./**/*Tests.js", + "./**/*TestEnv.js", + "./**/*TestsUtils.js", + "./scripts/*.js", + "./testData/**/*.js", + "./tests/**", + "./!(node_modules)/**/*.!(js)", + "./!(node_modules)/*.!(js)", + "./!(node_modules)/tests/**/*.js", + "./!(node_modules)/**/tests/**/*.js", + "./!(node_modules)/test/**/*.js", + "./!(node_modules)/**/test/**/*.js", + "./!(node_modules)/**/webTests/**/*.js", + "./!(node_modules)/**/public/lib/**/*" + ] + }, + sourceDirs: { + gpii: "%gpii-universal/gpii" + }, + contentDirs: { + browserify: "%gpii-universal/browserify", + node_modules: "%gpii-universal/node_modules", + testData: "%gpii-universal/testData" + }, + invokers: { + "getTestemOptions": { + funcName: "gpii.tests.universal.testem.getTestemOptions", + args: ["{that}"] + } + }, + testemOptions: { + tap_quiet_logs: true, + disable_watching: true, + skip: "PhantomJS,Opera,Safari,IE" + } +}); + +module.exports = gpii.tests.universal.testem().getTestemOptions(); diff --git a/tests/web/html/all-tests.html b/tests/web/html/all-tests.html index 12ef4a95c..9bf6ffaf6 100644 --- a/tests/web/html/all-tests.html +++ b/tests/web/html/all-tests.html @@ -4,19 +4,29 @@ GPII Universal framework complete tests runner - - - - + + + + + @@ -24,4 +34,4 @@

                            - \ No newline at end of file +