From 7847b515e8d9b9e1b7903c85a5a519b2d5765c6b Mon Sep 17 00:00:00 2001 From: Kunal Nain Date: Tue, 17 May 2022 18:26:41 -0700 Subject: [PATCH] Transition audio test to mocha testing framework --- integration/mochaTests/README.md | 131 +++ integration/mochaTests/configs/BaseConfig.js | 40 + .../desktop/sample_test.config.json | 9 + integration/mochaTests/eslintrc.json | 44 + integration/mochaTests/jsconfig.json | 7 + integration/mochaTests/package-lock.json | 790 ++++++++++++++++++ integration/mochaTests/package.json | 21 + integration/mochaTests/pages/AudioTestPage.js | 217 +++++ integration/mochaTests/pages/BaseAppPage.js | 162 ++++ integration/mochaTests/script/run-test.js | 334 ++++++++ integration/mochaTests/tests/AudioTest.js | 282 +++++++ integration/mochaTests/tsconfig.json | 22 + .../mochaTests/utils/HelperFunctions.js | 81 ++ integration/mochaTests/utils/Logger.js | 85 ++ integration/mochaTests/utils/SdkBaseTest.js | 48 ++ .../mochaTests/utils/WebDriverFactory.js | 169 ++++ integration/mochaTests/utils/Window.js | 42 + 17 files changed, 2484 insertions(+) create mode 100644 integration/mochaTests/README.md create mode 100644 integration/mochaTests/configs/BaseConfig.js create mode 100644 integration/mochaTests/configs/browserCompatibilityTest/desktop/sample_test.config.json create mode 100644 integration/mochaTests/eslintrc.json create mode 100644 integration/mochaTests/jsconfig.json create mode 100644 integration/mochaTests/package-lock.json create mode 100644 integration/mochaTests/package.json create mode 100644 integration/mochaTests/pages/AudioTestPage.js create mode 100644 integration/mochaTests/pages/BaseAppPage.js create mode 100644 integration/mochaTests/script/run-test.js create mode 100644 integration/mochaTests/tests/AudioTest.js create mode 100644 integration/mochaTests/tsconfig.json create mode 100644 integration/mochaTests/utils/HelperFunctions.js create mode 100644 integration/mochaTests/utils/Logger.js create mode 100644 integration/mochaTests/utils/SdkBaseTest.js create mode 100644 integration/mochaTests/utils/WebDriverFactory.js create mode 100644 integration/mochaTests/utils/Window.js diff --git a/integration/mochaTests/README.md b/integration/mochaTests/README.md new file mode 100644 index 0000000000..e2c331daf0 --- /dev/null +++ b/integration/mochaTests/README.md @@ -0,0 +1,131 @@ +# Mocha Tests + +Currently, integration tests are being transitioned over from KITE to Mocha. `mochaTests` contains the tests that are being transitioned from KITE to Mocha. + +## Test Types + +There are two types of Mocha tests: integration and browser compatibility. + +### Integration tests +Integration tests are minimal tests that run on latest Chrome on MacOs. These tests are used to test the basic functionality across the more popular browsers. + +### Browser Compatibility Tests +Browser compatibility tests are used to test a feature across several different browser and OS combinations. This test is used to make sure that JS SDK features are compatible with a wide variety of browser and OS. There is a default set of browser and OS that are used for this test. In a case where a feature is not compatible with one of the browser or OS then the user can pass in a custom config. + +In the following section, we will go over the schema of the custom JSON config. +#### JSON Config Schema + +```json +{ + "clients": [ + { + /* + Browser name + @required + */ + "browserName": "chrome", + /* + Browser Version + @required + */ + "browserVersion": "latest", + /* + Platform + @required + */ + "platform": "MAC" + } + ] +} +``` +## Running tests + +### Running Integration Test Locally +You can run the integration tests locally. By default, integration tests will run on latest version of Chrome that is installed on your machine. +You will have to make sure that you have the required drivers installed on your machine that the selenium test uses. You can find more information about the different drivers available at the `selenium-webdriver` npm page: [https://www.npmjs.com/package/selenium-webdriver](https://www.npmjs.com/package/selenium-webdriver). + +If you have an older version of driver installed then you will need to have an older version of browser on your machine as well. Generally, it is recommended to do local testing on latest version. If you need to test on older version, you can run a browser compatibility test with saucelabs. + +Sample command to run an integration test locally: + +```bash +npm run test -- --test-name AudioTest --host local --test-type integration-test +``` + +Browser compatiblity tests will run across a variety of browser and OS combinations so it is recommended to use SauceLabs for them. + +### Running Browser Compatibility Tests on SauceLabs +To run the test on SauceLabs, the username and access key will be required for authentication. Update the following commands with the credentials and add the credentials to environment variables. +```bash +export SAUCE_USERNAME= +``` +```bash +export SAUCE_ACCESS_KEY= +``` + +SauceLabs will open a browser and load a test url. You need to have a serverless deployment of Chime JS SDK demo. You can set the demo url as an environment variable with the following command: +```bash +export TEST_URL= +``` + +Following command can be used to run a browser compatibility tests with default settings on SauceLabs: + +```bash +npm run test -- --test-name AudioTest --host saucelabs --test-type browser-compatibility +``` + +There are scenarios where a test might not be compatible with one of the browsers or OS. In that case, user can provide a custom config with updated clients array. `sample_test.config.json` is a sample test config already provided. +Following command can be used to run a browser compatibility test with custom config: + +```bash +npm run test -- --test-name AudioTest --host saucelabs --test-type browser-compatibility --config browserCompatibilityTest/desktop/sample_test.config.json +``` + +### Running Browser Compatibility Tests on DeviceFarm +To run the tests on DeviceFarm, you will need an AWS account. Once you have an AWS account, you will need to setup two environment variables that will allow the AWS SDK to authenticate to your AWS account. +```bash +export AWS_ACCESS_KEY_ID= +export AWS_SECRET_ACCESS_KEY= +``` + +You will need to create a DeviceFarm project in the AWS account that you are planning to use. There are two types of devicefarm projects that can be created: `Mobile Device Testing` and `Desktop Browser Testing`. For this section, we will focus mainly on desktop browser testing. +After the project is created you will need to setup a `PROJECT_ARN` as an environment variable. Project arn is used by the device farm API to identify the project to create test sessions inside. +```bash +export PROJECT_ARN= +``` + +Following command can be used to run a browser compatibility test with default settings on DeviceFarm: +```bash +npm run test -- --test-name AudioTest --host devicefarm --test-type browser-compatibility +``` + +Just like with SauceLabs, DeviceFarm can run browser compatibility tests with a custom config: +```bash +npm run test -- --test-name AudioTest --host devicefarm --test-type browser-compatibility --config browserCompatibilityTest/desktop/sample_test.config.json +``` + +## Logging + +In the integration tests, logging is implemented in a unique way. This section will go over the two distinct ways of logging: +1. Use `logger.log` for inline logging +2. Use `logger.pushLogs` for buffered logging + +Developers can use `logger.log` as a normal console log with some enhanced functionality. The main difference between `logger.log` and `console.log` is that logger log will take `LogLevel` as a parameter. Different log levels print to the console in different colors and have a minor difference in the logs which makes it easier to read the logs. There are four different types of log levels. +```ts +const LogLevel = { + INFO: 'INFO', + WARN: 'WARN', + ERROR: 'ERROR', + SUCCESS: 'SUCCESS', +}; +``` + +Integration tests use `mocha` as their test framework. Mocha prints the test results and some limited logs to the console automatically. Any console logs that are inside of `it` will print before the mocha logs. +This makes debugging hard as the logs seem out of sync and random. To get around this issue buffered logging was added. Inside of `it` blocks, `logger.pushLogs` can be used to add logs to the buffer and a call to `logger.printLogs` inside of the `afterEach` hook will print the logs in the desired order. +```ts +afterEach(async function () { + this.logger.printLogs(); +}); +``` + +Mocha provides hooks like `before`, `after`, `beforeEach`, `afterEach` to perform actions at specific time of the testing cycle. Any logs printed inside the hooks will print in line so inline logging can be used and there is no need for buffered logging. diff --git a/integration/mochaTests/configs/BaseConfig.js b/integration/mochaTests/configs/BaseConfig.js new file mode 100644 index 0000000000..d774c8cd9f --- /dev/null +++ b/integration/mochaTests/configs/BaseConfig.js @@ -0,0 +1,40 @@ +const config = { + firefoxOptions: { + browserName: 'firefox', + 'moz:firefoxOptions': { + args: ['-start-debugger-server', '9222'], + prefs: { + 'media.navigator.streams.fake': true, + 'media.navigator.permission.disabled': true, + 'media.peerconnection.video.h264_enabled': true, + 'media.webrtc.hw.h264.enabled': true, + 'media.webrtc.platformencoder': true, + 'devtools.chrome.enabled': true, + 'devtools.debugger.prompt-connection': false, + 'devtools.debugger.remote-enabled': true, + }, + }, + }, + chromeOptions: { + browserName: 'chrome', + 'goog:chromeOptions': { + args: ['--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream'], + }, + }, + safariOptions: { + browserName: 'safari', + }, + sauceOptions: { + platformName: process.env.PLATFORM_NAME || 'macOS 10.14', + browserVersion: process.env.BROWSER_VERSION || 'latest', + 'sauce:options': { + username: process.env.SAUCE_USERNAME, + accessKey: process.env.SAUCE_ACCESS_KEY, + noSSLBumpDomains: 'all', + extendedDebugging: true, + screenResolution: '1280x960', + }, + }, +}; + +module.exports = config; diff --git a/integration/mochaTests/configs/browserCompatibilityTest/desktop/sample_test.config.json b/integration/mochaTests/configs/browserCompatibilityTest/desktop/sample_test.config.json new file mode 100644 index 0000000000..9d5ad51b32 --- /dev/null +++ b/integration/mochaTests/configs/browserCompatibilityTest/desktop/sample_test.config.json @@ -0,0 +1,9 @@ +{ + "clients": [ + { + "browserName": "chrome", + "browserVersion": "latest", + "platform": "MAC" + } + ] +} diff --git a/integration/mochaTests/eslintrc.json b/integration/mochaTests/eslintrc.json new file mode 100644 index 0000000000..e76ac86f7d --- /dev/null +++ b/integration/mochaTests/eslintrc.json @@ -0,0 +1,44 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": ["./tsconfig.json"] + }, + "plugins": [ + "@typescript-eslint", + "simple-import-sort" + ], + "extends": [ + "plugin:@typescript-eslint/eslint-recommended", + "prettier/@typescript-eslint", + "plugin:prettier/recommended" + ], + "rules": { + "no-var": "error", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-inferrable-types": "off", + + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/explicit-member-accessibility": [ + "error", + { + "accessibility": "no-public", + "overrides": { + "parameterProperties": "explicit" + } + } + ], + "@typescript-eslint/explicit-function-return-type": [ + "error", + { "allowExpressions": true } + ], + "@typescript-eslint/no-parameter-properties": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" } + ], + "simple-import-sort/sort": "error", + "eqeqeq": ["error", "always"] + } +} diff --git a/integration/mochaTests/jsconfig.json b/integration/mochaTests/jsconfig.json new file mode 100644 index 0000000000..304f9800a3 --- /dev/null +++ b/integration/mochaTests/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6" + }, + "exclude": ["node_modules", "**/node_modules/*"] +} \ No newline at end of file diff --git a/integration/mochaTests/package-lock.json b/integration/mochaTests/package-lock.json new file mode 100644 index 0000000000..275bdd3047 --- /dev/null +++ b/integration/mochaTests/package-lock.json @@ -0,0 +1,790 @@ +{ + "name": "sdk-integration-tests", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "sdk-integration-tests", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "aws-sdk": "^2.1144.0", + "chalk": "^4.1.2", + "selenium-webdriver": "^4.1.2", + "uuid": "^8.3.2" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-sdk": { + "version": "2.1144.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1144.0.tgz", + "integrity": "sha512-0QZNPezu+MJOgvRRVeE1LyNPgZD4MaomHTAbZpRgCBca9fu2C7rJCm3eX/xwfNvvqVZ0RZfEpagcFXAxk9HITg==", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/jszip": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.9.1.tgz", + "integrity": "sha512-H9A60xPqJ1CuC4Ka6qxzXZeU8aNmgOeP5IFqwJbQQwtu2EUYxota3LdsiZWplF7Wgd9tkAd0mdu36nceSaPuYw==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "node_modules/selenium-webdriver": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.1.2.tgz", + "integrity": "sha512-e4Ap8vQvhipgBB8Ry9zBiKGkU6kHKyNnWiavGGLKkrdW81Zv7NVMtFOL/j3yX0G8QScM7XIXijKssNd4EUxSOw==", + "dependencies": { + "jszip": "^3.6.0", + "tmp": "^0.2.1", + "ws": ">=7.4.6" + }, + "engines": { + "node": ">= 10.15.0" + } + }, + "node_modules/set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "node_modules/xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "engines": { + "node": ">=4.0" + } + } + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "aws-sdk": { + "version": "2.1144.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1144.0.tgz", + "integrity": "sha512-0QZNPezu+MJOgvRRVeE1LyNPgZD4MaomHTAbZpRgCBca9fu2C7rJCm3eX/xwfNvvqVZ0RZfEpagcFXAxk9HITg==", + "requires": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + }, + "dependencies": { + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==" + }, + "jszip": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.9.1.tgz", + "integrity": "sha512-H9A60xPqJ1CuC4Ka6qxzXZeU8aNmgOeP5IFqwJbQQwtu2EUYxota3LdsiZWplF7Wgd9tkAd0mdu36nceSaPuYw==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + } + }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "selenium-webdriver": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.1.2.tgz", + "integrity": "sha512-e4Ap8vQvhipgBB8Ry9zBiKGkU6kHKyNnWiavGGLKkrdW81Zv7NVMtFOL/j3yX0G8QScM7XIXijKssNd4EUxSOw==", + "requires": { + "jszip": "^3.6.0", + "tmp": "^0.2.1", + "ws": ">=7.4.6" + } + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "requires": { + "rimraf": "^3.0.0" + } + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "requires": {} + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + } + } +} diff --git a/integration/mochaTests/package.json b/integration/mochaTests/package.json new file mode 100644 index 0000000000..251acc50d5 --- /dev/null +++ b/integration/mochaTests/package.json @@ -0,0 +1,21 @@ +{ + "name": "sdk-integration-tests", + "version": "1.0.0", + "description": "", + "scripts": { + "clean": "rm -rf temp && rm -rf ../kite-allure-reports && rm -rf logs", + "test": "npm install && node ./script/run-test", + "lint": "eslint --config eslintrc.json ./ --ext .ts,.tsx,.js --fix" + }, + "author": "", + "license": "ISC", + "dependencies": { + "aws-sdk": "^2.1144.0", + "chalk": "^4.1.2", + "selenium-webdriver": "^4.1.2", + "uuid": "^8.3.2" + }, + "mocha": { + "timeout": 300000 + } +} diff --git a/integration/mochaTests/pages/AudioTestPage.js b/integration/mochaTests/pages/AudioTestPage.js new file mode 100644 index 0000000000..fbaac20e02 --- /dev/null +++ b/integration/mochaTests/pages/AudioTestPage.js @@ -0,0 +1,217 @@ +const BaseAppPage = require('./BaseAppPage'); +const { LogLevel, Log } = require('../utils/Logger'); + +class AudioTestPage extends BaseAppPage { + constructor(webdriver, logger) { + super(webdriver, logger); + } + + async runAudioCheck(expectedState, checkStereoTones = false) { + let res = undefined; + try { + res = await this.driver.executeAsyncScript( + async (expectedState, checkStereoTones) => { + let logs = []; + let callback = arguments[arguments.length - 1]; + + const channelCount = checkStereoTones ? 2 : 1; + const successfulToneChecks = Array(channelCount).fill(0); + const totalToneChecks = Array(channelCount).fill(0); + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const minToneError = Array(channelCount).fill(Infinity); + const maxToneError = Array(channelCount).fill(-Infinity); + const percentages = Array(channelCount).fill(0); + const volumeTryCount = 5; + const frequencyTryCount = 5; + + const sleep = milliseconds => { + return new Promise(resolve => setTimeout(resolve, milliseconds)); + }; + + try { + const stream = document.getElementById('meeting-audio').srcObject; + const source = audioContext.createMediaStreamSource(stream); + let analyser = []; + for (let i = 0; i < channelCount; i++) { + analyser.push(audioContext.createAnalyser()); + } + // Byte frequency data is used to calculate the volume + let byteFrequencyData = []; + for (let i = 0; i < channelCount; i++) { + byteFrequencyData.push(new Uint8Array(analyser[i].frequencyBinCount)); + } + // Float frequency data is used to calculate the frequency of the audio stream + let floatFrequencyData = []; + for (let i = 0; i < channelCount; i++) { + floatFrequencyData.push(new Float32Array(analyser[i].frequencyBinCount)); + } + + if (checkStereoTones) { + const splitterNode = audioContext.createChannelSplitter(2); + source.connect(splitterNode); + splitterNode.connect(analyser[0], 0); + splitterNode.connect(analyser[1], 1); + } else { + source.connect(analyser[0]); + } + + await sleep(5000); + + const getAverageVolume = channelIndex => { + analyser[channelIndex].getByteFrequencyData(byteFrequencyData[channelIndex]); + let values = 0; + let average; + const length = byteFrequencyData[channelIndex].length; + // Get all the frequency amplitudes + for (let i = 0; i < length; i++) { + values += byteFrequencyData[channelIndex][i]; + } + average = values / length; + return average; + }; + + const checkVolumeFor = async (runCount, channelIndex) => { + for (let i = 0; i < runCount; i++) { + totalToneChecks[channelIndex]++; + const avgTestVolume = getAverageVolume(channelIndex); + logs.push(`[${i + 1}] Resulting volume of ${avgTestVolume}`); + if ( + (expectedState === 'AUDIO_ON' && avgTestVolume > 0) || + (expectedState === 'AUDIO_OFF' && avgTestVolume === 0) + ) { + successfulToneChecks[channelIndex]++; + } + await sleep(100); + } + }; + + const checkFrequency = (targetReceiveFrequency, channelIndex) => { + analyser[channelIndex].getFloatFrequencyData(floatFrequencyData[channelIndex]); + let maxBinDb = -Infinity; + let hotBinFrequency = 0; + const binSize = audioContext.sampleRate / analyser[channelIndex].fftSize; // default fftSize is 2048 + for (let i = 0; i < floatFrequencyData[channelIndex].length; i++) { + const v = floatFrequencyData[channelIndex][i]; + if (v > maxBinDb) { + maxBinDb = v; + hotBinFrequency = i * binSize; + } + } + const error = Math.abs(hotBinFrequency - targetReceiveFrequency); + if (maxBinDb > -Infinity) { + if (error < minToneError[channelIndex]) { + minToneError[channelIndex] = error; + } + if (error > maxToneError[channelIndex]) { + maxToneError[channelIndex] = error; + } + } + if (error <= 2 * binSize) { + successfulToneChecks[channelIndex]++; + } + totalToneChecks[channelIndex]++; + return hotBinFrequency; + }; + + const checkFrequencyFor = async (runCount, freq, channelIndex) => { + for (let i = 0; i < runCount; i++) { + const testFrequency = checkFrequency(freq, channelIndex); + logs.push( + `[${i + 1}] Resulting Frequency of ${testFrequency} for channel ${channelIndex}` + ); + await sleep(100); + } + }; + + if (expectedState === 'AUDIO_OFF') { + logs.push("Expected state is 'AUDIO_OFF'"); + logs.push('Checking whether the audio is off'); + logs.push('AUDIO_OFF checks are done by checking for volume'); + logs.push( + `------------------Checking volume ${volumeTryCount} times on channel index 0------------------` + ); + await checkVolumeFor(volumeTryCount, 0); + if (checkStereoTones) { + logs.push('Checking volume for stereo tones'); + logs.push( + `------------------Checking volume ${volumeTryCount} times on channel index 1------------------` + ); + await checkVolumeFor(volumeTryCount, 1); + } + } + + if (expectedState === 'AUDIO_ON') { + logs.push("Expected state is 'AUDIO_ON'"); + logs.push('Checking whether the audio is on'); + logs.push( + 'AUDIO_ON checks are done by checking the frequency of the output audio stream' + ); + if (checkStereoTones) { + // The test demo uses 500Hz on left stream and 1000Hz on right stream + logs.push( + `------------------Checking frequency ${frequencyTryCount} times of 500Hz on 0 channel index-----------------------` + ); + await checkFrequencyFor(frequencyTryCount, 500, 0); + logs.push( + `------------------Checking frequency ${frequencyTryCount} times of 1000Hz on 1 channel index------------------` + ); + await checkFrequencyFor(frequencyTryCount, 1000, 1); + } else { + // The test demo uses 440Hz frequency + logs.push( + `------------------Checking frequency ${frequencyTryCount} times of 440Hz------------------` + ); + await checkFrequencyFor(frequencyTryCount, 440, 0); + } + logs.push('Audio frequency check completed'); + } + + logs.push('Calculating success percentages'); + for (let i = 0; i < channelCount; i++) { + percentages[i] = successfulToneChecks[i] / totalToneChecks[i]; + } + } catch (error) { + logs.push(`Audio check failed with the following error: \n ${error}`); + } finally { + logs.push(`Audio check completed`); + await audioContext.close(); + callback({ + percentages, + logs, + }); + } + }, + expectedState, + checkStereoTones, + this.logger, + Log, + LogLevel + ); + } catch (e) { + this.logger.pushLogs(`Audio Check failed!! Error: \n ${e}`, LogLevel.ERROR); + } finally { + if (res) { + res.logs.forEach(l => { + this.logger.pushLogs(l); + }); + } + } + if (!res) { + throw new Error(`Audio check failed!!`); + } + + for (let i = 0; i < res.percentages.length; i++) { + this.logger.pushLogs( + `Audio check success rate for channel ${i}: ${res.percentages[i] * 100}%` + ); + if (res.percentages[i] < 0.75) { + throw new Error( + `Audio Check failed!! Success rate for channel ${i} is ${res.percentages[i] * 100}%` + ); + } + } + this.logger.pushLogs('Audio check passed!!', LogLevel.SUCCESS); + } +} + +module.exports = AudioTestPage; diff --git a/integration/mochaTests/pages/BaseAppPage.js b/integration/mochaTests/pages/BaseAppPage.js new file mode 100644 index 0000000000..7964c4e319 --- /dev/null +++ b/integration/mochaTests/pages/BaseAppPage.js @@ -0,0 +1,162 @@ +const { By, until } = require('selenium-webdriver'); +const { LogLevel } = require('../utils/Logger'); + +let elements; + +function findAllElements() { + // These will be stale after a reload. + elements = { + meetingIdInput: By.id('inputMeeting'), + attendeeNameInput: By.id('inputName'), + authenticateButton: By.id('authenticate'), + joinButton: By.id('joinButton'), + roster: By.id('roster'), + participants: By.css('li'), + + authenticationFlow: By.id('flow-authenticate'), + deviceFlow: By.id('flow-devices'), + meetingFlow: By.id('flow-meeting'), + + microphoneDropDownButton: By.id('button-microphone-drop'), + microphoneButton: By.id('button-microphone'), + microphoneDropDown: By.id('dropdown-menu-microphone'), + microphoneDropDown440HzButton: By.id('dropdown-menu-microphone-440-Hz'), + }; +} + +class BaseAppPage { + constructor(driver, logger) { + this.driver = driver; + this.logger = logger; + findAllElements(); + } + + async open(url) { + this.logger.pushLogs(`Opening demo at url: ${url}`); + await this.driver.get(url); + await this.waitForBrowserDemoToLoad(); + } + + async waitForBrowserDemoToLoad() { + await this.driver.wait( + until.elementIsVisible(this.driver.findElement(elements.authenticationFlow)) + ); + } + + async close(stepInfo) { + await stepInfo.driver.close(); + } + + async enterMeetingId(meetingId) { + let meetingIdInputBox = await this.driver.findElement(elements.meetingIdInput); + await meetingIdInputBox.clear(); + await meetingIdInputBox.sendKeys(meetingId); + } + + async enterAttendeeName(attendeeName) { + let attendeeNameInputBox = await this.driver.findElement(elements.attendeeNameInput); + await attendeeNameInputBox.clear(); + await attendeeNameInputBox.sendKeys(attendeeName); + } + + async selectRegion(region) { + await this.driver.findElement(By.css(`option[value=${region}]`)).click(); + } + + async authenticate() { + let authenticateButton = await this.driver.findElement(elements.authenticateButton); + await authenticateButton.click(); + await this.waitForUserAuthentication(); + } + + async waitForUserAuthentication() { + await this.driver.wait(until.elementIsVisible(this.driver.findElement(elements.joinButton))); + } + + async joinMeeting() { + let joinButton = await this.driver.findElement(elements.joinButton); + await joinButton.click(); + await this.waitForUserJoin(); + } + + async waitForUserJoin() { + await this.driver.wait(until.elementIsVisible(this.driver.findElement(elements.meetingFlow))); + } + + async clickMicrophoneButton() { + let microphoneButton = await this.driver.findElement(elements.microphoneButton); + await this.driver.wait(until.elementIsVisible(microphoneButton)); + await microphoneButton.click(); + } + + async getMicrophoneStatus() { + let microphoneButton = await this.driver.findElement(elements.microphoneButton); + await this.driver.wait(until.elementIsVisible(microphoneButton)); + let classNamesString = await microphoneButton.getAttribute('class'); + let classNames = classNamesString.split(' '); + return classNames; + } + + async muteMicrophone() { + let classNames = await this.getMicrophoneStatus(); + + if (classNames[1] === 'btn-success') { + this.logger.pushLogs('Microphone is currently unmuted; muting the microphone'); + await this.clickMicrophoneButton(); + } else if (classNames[1] === 'btn-outline-secondary') { + this.logger.pushLogs('Microphone button is already muted; no action taken'); + } else { + this.logger.pushLogs('Unkown microphone button state encountered!!', LogLevel.ERROR); + } + } + + async unmuteMicrophone() { + let classNames = await this.getMicrophoneStatus(); + + if (classNames[1] === 'btn-success') { + this.logger.pushLogs('Microphone is already unmuted; no action taken'); + } else if (classNames[1] === 'btn-outline-secondary') { + this.logger.pushLogs('Microphone button is currently muted; unmuting the microphone'); + await this.clickMicrophoneButton(); + } else { + this.logger.pushLogs('Unkown microphone button state encountered!!', LogLevel.ERROR); + } + } + + async getNumberOfParticipants() { + const participantElements = await this.driver.findElements(elements.participants); + this.logger.pushLogs(`Number of participants on roster: ${participantElements.length}`); + return participantElements.length; + } + + async rosterCheck(numberOfParticipant = 1) { + await this.driver.wait(async () => { + return (await this.getNumberOfParticipants()) == numberOfParticipant; + }, 5000); + } + + async clickOnMicrophoneDropdownButton() { + let microphoneDropDownButton = await this.driver.findElement(elements.microphoneDropDownButton); + await this.driver.wait(until.elementIsVisible(microphoneDropDownButton)); + await microphoneDropDownButton.click(); + await this.driver.wait( + until.elementIsVisible(this.driver.findElement(elements.microphoneDropDown)) + ); + } + + async playRandomTone() { + await this.unmuteMicrophone(); + await this.clickOnMicrophoneDropdownButton(); + let microphoneDropDown440HzButton = await this.driver.findElement( + elements.microphoneDropDown440HzButton + ); + await this.driver.wait(until.elementIsVisible(microphoneDropDown440HzButton)); + await microphoneDropDown440HzButton.click(); + } + + async stopPlayingRandomTone() { + await this.muteMicrophone(); + } +} + +module.exports = BaseAppPage; diff --git a/integration/mochaTests/script/run-test.js b/integration/mochaTests/script/run-test.js new file mode 100644 index 0000000000..c0ab78ad87 --- /dev/null +++ b/integration/mochaTests/script/run-test.js @@ -0,0 +1,334 @@ +#!/usr/bin/env node + +const { argv, kill, exit } = require('process'); +const { runSync, runAsync } = require('../utils/HelperFunctions'); +const args = require('minimist')(argv.slice(2)); +const path = require('path'); +const fs = require('fs'); +const { Logger, LogLevel, Log } = require('../utils/Logger'); +const crypto = require('crypto'); + +const pathToIntegrationFolder = path.resolve(__dirname, '../tests'); +const pathToTestDemoFolder = path.resolve(__dirname, '../../../demos/browser'); +const pathToConfigsFolder = path.resolve(__dirname, '../configs'); + +let testName = ''; +let testImplementation = ''; +let testType = 'integration-test'; +let testConfig = ''; +let logger; + +const setupLogger = () => { + logger = new Logger('Test Runner (run-test)'); + logger.log('Logger setup finished'); +} + +const usage = () => { + console.log(`Usage: run-test -- [-t test] [-h host] [-y test-type] [-c config]`); + console.log(` --test-name Target test name [Required]`); + console.log(` --host WebDriver server [Required] [default: local]`); + console.log(` --test-type Test type [Required] [default: integration-test]`); + console.log(` --test-implementation Name of mocha test file stored in the tests folder [Optional]`); + console.log(` --config Name of custom config stored in configs folder [Optional]`); + console.log(`Values:`); + console.log(` --test-name`); + console.log(` AudioTest: Test name\n`); + console.log(` --host`); + console.log(` local: Run tests locally`); + console.log(` saucelabs: Run tests on SauceLabs`); + console.log(` devicefarm: Run tests on DeviceFarm\n`); + console.log(` --test-type`); + console.log(` integration-test: Run integration test`); + console.log(` browser-compatibility: Run browser compatibility test\n`); + console.log(` --test-implementation`); + console.log(` By default, the test name will be used for test implementation file`); + console.log(` JS extension will be automatically appended to the test name`); + console.log(` If the test file has a different name then a file name can be passed`); + console.log(` DifferentAudioTest.js: Name of test file\n`); + console.log(` --config`); + console.log(` Custom config is passed when a test is incompatible with one of the browser / OS combination provided by default`) + console.log(` sample_test.config.json: Name of custom config stored in configs folder`); +}; + +const parseArgs = () => { + for (const [key, value] of Object.entries(args)) { + if (key === '_') continue; + switch (key) { + case 'help': + usage(); + exit(0); + + case 'test-name': + testName = value; + break; + + case 'host': + process.env.HOST = value.toLowerCase(); + break; + + case 'test-type': + process.env.TEST_TYPE = value.toLowerCase(); + testType = value.toLowerCase(); + break; + + case 'test-implementation': + testImplementation = value.concat('.js'); + break; + + case 'config': + testConfig = value; + break; + + default: + logger.log(`Invalid argument ${key}`, LogLevel.ERROR); + usage(); + exit(1); + } + } + + if(!testImplementation) { + logger.log('Using test name as the name for the test implementation file as per default settings', LogLevel.WARN); + testImplementation = testName.concat('.js'); + } + + // logger.log(`testName = ${testName}, host = ${process.env.HOST}, test-type = ${testType}, test-implementation = ${testImplementation}, config = ${testConfigs}`); + + return { + testSuite: testName, + testType + }; +}; + +const checkIfPortIsInUse = async port => + new Promise(resolve => { + const server = require('http') + .createServer() + .listen(port, 'localhost', () => { + server.close(); + resolve(false); + }) + .on('error', () => { + resolve(true); + }); + }); + +function startTestDemo() { + if(process.env.HOST !== 'local') { + logger.log('Local demo will be started only for local tests'); + logger.log('For SauceLabs or DeviceFarm test, please pass TEST_URL env variable'); + return; + } + + logger.log('Installing dependencies in test demo'); + runSync('npm', ['install'], { cwd: pathToTestDemoFolder }); + + logger.log('Starting the test demo'); + // The test demo will keep running until the process is terminated, + // so we should execute this command asynchronously without blocking other commands. + runAsync('npm', ['run', 'start'], { cwd: pathToTestDemoFolder }); +} + +const waitUntilTestDemoStarts = async () => { + if(process.env.HOST !== 'local') { + return; + } + + logger.log('Waiting for test demo to start'); + count = 0; + threshold = 60; + + while (count < 60) { + const isInUse = await checkIfPortIsInUse(8080); + if (isInUse === true) { + logger.log('Test demo has started successfully'); + return; + } + count += 1; + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + logger.log('Test demo did not start successfully', LogLevel.ERROR); + terminateTestDemo(pid); + exit(1); +}; + +const startTesting = async (testSuite, testType) => { + logger.log('Running test'); + const test = { + name: testName, + testImpl: testImplementation + }; + let clients; + + if(testType === 'browser-compatibility') { + logger.log('Setting clients for browser compatibility tests'); + + if(testConfig.length === 0) { + logger.log('Using default browser compatibility config'); + clients = [ + { + "browserName": "chrome", + "browserVersion": "latest-2", + "platform": "MAC" + }, + { + "browserName": "chrome", + "browserVersion": "latest-1", + "platform": "MAC" + }, + { + "browserName": "chrome", + "browserVersion": "latest", + "platform": "MAC" + }, + { + "browserName": "chrome", + "browserVersion": "latest-2", + "platform": "WINDOWS" + }, + { + "browserName": "chrome", + "browserVersion": "latest-1", + "platform": "WINDOWS" + }, + { + "browserName": "chrome", + "browserVersion": "latest", + "platform": "WINDOWS" + }, + { + "browserName": "firefox", + "browserVersion": "latest-2", + "platform": "MAC" + }, + { + "browserName": "firefox", + "browserVersion": "latest-1", + "platform": "MAC" + }, + { + "browserName": "firefox", + "browserVersion": "latest", + "platform": "MAC" + }, + { + "browserName": "firefox", + "browserVersion": "latest-2", + "platform": "WINDOWS" + }, + { + "browserName": "firefox", + "browserVersion": "latest-1", + "platform": "WINDOWS" + }, + { + "browserName": "firefox", + "browserVersion": "latest", + "platform": "WINDOWS" + }, + { + "browserName": "safari", + "browserVersion": "latest", + "platform": "MAC" + } + ]; + } + else { + logger.log('Using custom browser compatibility config'); + let testConfigRaw = fs.readFileSync(path.resolve(pathToConfigsFolder, testConfig)); + let testConfigJSON = JSON.parse(testConfigRaw); + clients = testConfigJSON.clients; + } + } + else { + logger.log('Setting clients for integration tests'); + clients = [ + { + browserName: "chrome", + browserVersion: "latest", + platform: "MAC" + } + ]; + } + + // logger.log(`test = ${JSON.stringify(test)}, clients = ${JSON.stringify(clients)}`); + await runTest(test, clients); +}; + +const runTest = async (test, clients) => { + process.env.FORCE_COLOR = '1'; + process.env.JOB_ID = crypto.randomUUID(); + process.env.TEST = JSON.stringify(test); + + const maxRetries = test.retry === undefined || test.retry < 1 ? 5 : test.retry; + let retryCount = 0; + let testResult; + let testSuiteResult = 0; + + for(let idx = 0; idx < clients.length; idx++) { + if(retryCount > 0) { + logger.log(`------------------ RETRY ATTEMPT ${retryCount} ------------------`, LogLevel.WARN); + } + + let client = clients[idx]; + process.env.CLIENT = JSON.stringify(client); + logger.log(`Running ${test.name} on \n browser name = ${client.browserName}, \n version = ${client.browserVersion}, and \n platform = ${client.platform}`); + + try { + testResult = await runAsync('mocha', [test.testImpl], { cwd: pathToIntegrationFolder, timeout: 300000, color: true, bail: true }); + } + catch (error) { + logger.log(`Mocha run command failed, failure status code: ${error}`, LogLevel.ERROR); + testResult = 1; + } + + if (testResult === 1) { + logger.log(`${test.name} failed on ${client.browserName}, ${client.browserVersion} on ${client.platform}`, LogLevel.ERROR); + + if(retryCount < maxRetries) { + logger.log(`${test.name} will rerun the test on the same browser and OS combination`, LogLevel.WARN); + logger.log(`Retrying ${test.name} on ${client.browserName}, ${client.browserVersion}, and ${client.platform} OS`); + idx--; + retryCount++; + continue; + } + if(retryCount === maxRetries) { + logger.log(`Test has retried on the same client ${retryCount} times`, LogLevel.WARN); + logger.log(`Setting test suite result to 1`, LogLevel.ERROR); + logger.log('Test will continue to run on other browser and OS combinations'); + testSuiteResult = 1; + break; + } + } + } + + // TODO: Any Cloudwatch metrics can be sent at this point, if the test fails after retrying 4 times than it should be logged as a failure. + // If a failure occurs and the consecutive retry runs successfully then only a success data point will be recorded. + if (testSuiteResult === 1) { + logger.log(`${test.name} suite failed :(`, LogLevel.ERROR); + } + if (testSuiteResult === 0) { + logger.log(`${test.name} suite ran successfully :)`, LogLevel.SUCCESS); + } + + logger.log('Test run completed'); +} + +const terminateTestDemo = () => { + if(process.env.HOST != 'local') { + return; + } + logger.log('Terminating the test demo'); + + const demoPid = runSync('lsof', ['-i', ':8080', '-t'], null, printOutput = false); + if (demoPid) kill(demoPid, 'SIGKILL'); +}; + +(async () => { + setupLogger(); + const { testSuite, testType } = parseArgs(); + startTestDemo(); + await waitUntilTestDemoStarts(); + await startTesting(testSuite, testType); + terminateTestDemo(); +})(); diff --git a/integration/mochaTests/tests/AudioTest.js b/integration/mochaTests/tests/AudioTest.js new file mode 100644 index 0000000000..bda89413c9 --- /dev/null +++ b/integration/mochaTests/tests/AudioTest.js @@ -0,0 +1,282 @@ +const { describe, before, after, it } = require('mocha'); +const { v4: uuidv4 } = require('uuid'); +const { Window } = require('../utils/Window'); +const WebDriverFactory = require('../utils/WebDriverFactory'); +const SdkBaseTest = require('../utils/SdkBaseTest'); +const AudioTestPage = require('../pages/AudioTestPage'); +const { Logger, LogLevel } = require('../utils/Logger'); + +/* + * 1. Starts a meeting + * 2. Adds 2 participants to the meeting + * 3. Turns on the audio tone for both + * 4. One attendee plays random tone + * 5. Checks if the other participant is able to hear the tone + * 6. Same attendee mutes the audio + * 7. Checks if the other participant is not able to hear the audio + * */ +describe('AudioTest', async function () { + describe('run test', async function () { + let driverFactoryOne; + let driverFactoryTwo; + let baseTestConfigOne; + let baseTestConfigTwo; + let pageOne; + let pageTwo; + let test_window; + let monitor_window; + let test_attendee_id; + let monitor_attendee_id; + let meetingId; + let numberOfSessions; + let failureCount; + + before(async function () { + this.logger = new Logger('AudioTest'); + this.logger.log('Retrieving the base test config'); + baseTestConfigOne = new SdkBaseTest(); + this.logger.log('Configuring the webdriver'); + driverFactoryOne = new WebDriverFactory( + baseTestConfigOne.testName, + baseTestConfigOne.host, + baseTestConfigOne.testType, + baseTestConfigOne.url + ); + await driverFactoryOne.build(); + this.logger.log('Using the webdriver, opening the browser window'); + numberOfSessions = driverFactoryOne.numberOfSessions; + driverFactoryOne.driver.manage().window().maximize(); + this.logger.log('Instantiating selenium helper class'); + pageOne = new AudioTestPage(driverFactoryOne.driver, this.logger); + failureCount = 0; + }); + + afterEach(async function () { + if (this.currentTest.state === 'failed') failureCount += 1; + }); + + after(async function () { + this.logger.log('Closing the webdriver'); + const passed = failureCount === 0; + + await driverFactoryOne.quit(passed); + + if (passed === true) { + this.logger.log('AudioTest passed!!!', LogLevel.SUCCESS); + process.exit(0); + } else { + this.logger.log('AudioTest failed!!!', LogLevel.ERROR); + process.exit(1); + } + }); + + describe('on single session', async function () { + before(async function () { + if (numberOfSessions !== 1) { + this.logger.log( + `Skipping single session test because number of sessions required is ${numberOfSessions}`, + LogLevel.WARN + ); + this.skip(); + } + }); + + afterEach(async function () { + this.logger.printLogs(); + }); + + describe('setup', async function () { + it('should open the meeting demo in two tabs', async function () { + test_window = await Window.existing(driverFactoryOne.driver, this.logger, 'TEST'); + monitor_window = await Window.openNew(driverFactoryOne.driver, this.logger, 'MONITOR'); + + await test_window.runCommands(async () => await pageOne.open(driverFactoryOne.url)); + await monitor_window.runCommands(async () => await pageOne.open(driverFactoryOne.url)); + }); + + it('should authenticate the user to the meeting', async function () { + meetingId = uuidv4(); + test_attendee_id = 'Test Attendee - ' + uuidv4().toString(); + monitor_attendee_id = 'Monitor Attendee - ' + uuidv4().toString(); + + await test_window.runCommands(async () => await pageOne.enterMeetingId(meetingId)); + await monitor_window.runCommands(async () => await pageOne.enterMeetingId(meetingId)); + + await test_window.runCommands( + async () => await pageOne.enterAttendeeName(test_attendee_id) + ); + await monitor_window.runCommands( + async () => await pageOne.enterAttendeeName(monitor_attendee_id) + ); + + // TODO: Add region selection option if needed + // await test_window.runCommands(async () => await page.selectRegion()); + // await monitor_window.runCommands(async () => await page.selectRegion()); + + await test_window.runCommands(async () => await pageOne.authenticate()); + await monitor_window.runCommands(async () => await pageOne.authenticate()); + }); + }); + + describe('user', async function () { + it('should join the meeting', async function () { + await test_window.runCommands(async () => await pageOne.joinMeeting()); + await monitor_window.runCommands(async () => await pageOne.joinMeeting()); + }); + }); + + describe('meeting', async function () { + it('should have two participants in the roster', async function () { + await test_window.runCommands(async () => await pageOne.rosterCheck(2)); + await monitor_window.runCommands(async () => await pageOne.rosterCheck(2)); + }); + }); + + describe('both attendee', async () => { + it('should be muted at the start of the test', async function () { + // TODO: Currently, the meeting demo does not have an option for the attendee to join muted. + // Using selenium to mute the attendees at this point. The demo should be updated to allow an option to join in the future. + await test_window.runCommands(async () => await pageOne.muteMicrophone()); + await monitor_window.runCommands(async () => await pageOne.muteMicrophone()); + }); + }); + + describe('test attendee', async function () { + it('should play random tone', async function () { + await test_window.runCommands(async () => await pageOne.playRandomTone()); + }); + }); + + describe('monitor attendee', async function () { + it('should check for random tone in the meeting', async function () { + await monitor_window.runCommands(async () => await pageOne.runAudioCheck('AUDIO_ON')); + }); + }); + + describe('test user', async function () { + it('should stop playing random tone', async function () { + await test_window.runCommands(async () => await pageOne.stopPlayingRandomTone()); + }); + }); + + describe('monitor user', async function () { + it('should check for no random tone in the meeting', async function () { + await test_window.runCommands(async () => await pageOne.runAudioCheck('AUDIO_OFF')); + }); + }); + }); + + describe('on two sessions', async function () { + before(async function () { + if (numberOfSessions !== 2) { + this.logger.log( + `Skipping two sessions test because number of sessions required is ${numberOfSessions}`, + LogLevel.WARN + ); + this.skip(); + } else { + baseTestConfigTwo = new SdkBaseTest(); + driverFactoryTwo = new WebDriverFactory( + baseTestConfigTwo.testName, + baseTestConfigTwo.host, + baseTestConfigTwo.testType, + baseTestConfigTwo.url + ); + await driverFactoryTwo.build(); + driverFactoryTwo.driver.manage().window().maximize(); + pageTwo = new AudioTestPage(driverFactoryTwo.driver, this.logger); + } + }); + + after(async function () { + if (numberOfSessions === 2) { + driverFactoryTwo.driver.quit(); + } + }); + + afterEach(async function () { + this.logger.printLogs(); + }); + + describe('setup', async function () { + it('should open the meeting demo in two windows', async function () { + test_window = await Window.existing(driverFactoryOne.driver, this.logger, 'TEST'); + monitor_window = await Window.existing(driverFactoryTwo.driver, this.logger, 'MONITOR'); + + await test_window.runCommands(async () => await pageOne.open(driverFactoryOne.url)); + await monitor_window.runCommands(async () => await pageTwo.open(driverFactoryOne.url)); + }); + + it('should authenticate the user to the meeting', async function () { + meetingId = uuidv4(); + test_attendee_id = 'Test Attendee - ' + uuidv4().toString(); + monitor_attendee_id = 'Monitor Attendee - ' + uuidv4().toString(); + + await test_window.runCommands(async () => await pageOne.enterMeetingId(meetingId)); + await monitor_window.runCommands(async () => await pageTwo.enterMeetingId(meetingId)); + + await test_window.runCommands( + async () => await pageOne.enterAttendeeName(test_attendee_id) + ); + await monitor_window.runCommands( + async () => await pageTwo.enterAttendeeName(monitor_attendee_id) + ); + + // TODO: Add region selection option if needed + // await test_window.runCommands(async () => await page.selectRegion()); + // await monitor_window.runCommands(async () => await page.selectRegion()); + + await test_window.runCommands(async () => await pageOne.authenticate()); + await monitor_window.runCommands(async () => await pageTwo.authenticate()); + }); + }); + + describe('user', async function () { + it('should join the meeting', async function () { + await test_window.runCommands(async () => await pageOne.joinMeeting()); + await monitor_window.runCommands(async () => await pageTwo.joinMeeting()); + }); + }); + + describe('meeting', async function () { + it('should have two participants in the roster', async function () { + await test_window.runCommands(async () => await pageOne.rosterCheck(2)); + await monitor_window.runCommands(async () => await pageTwo.rosterCheck(2)); + }); + }); + + describe('both attendee', async () => { + it('should be muted at the start of the test', async function () { + // TODO: Currently, the meeting demo does not have an option for the attendee to join muted. + // Using selenium to mute the attendees at this point. The demo should be updated to allow an option to join in the future. + await test_window.runCommands(async () => await pageOne.muteMicrophone()); + await monitor_window.runCommands(async () => await pageTwo.muteMicrophone()); + }); + }); + + describe('test attendee', async function () { + it('should play random tone', async function () { + await test_window.runCommands(async () => await pageOne.playRandomTone()); + }); + }); + + describe('monitor attendee', async function () { + it('should check for random tone in the meeting', async function () { + await monitor_window.runCommands(async () => await pageTwo.runAudioCheck('AUDIO_ON')); + }); + }); + + describe('test user', async function () { + it('should stop playing random tone', async function () { + await test_window.runCommands(async () => await pageOne.stopPlayingRandomTone()); + }); + }); + + describe('monitor user', async function () { + it('should check for no random tone in the meeting', async function () { + await test_window.runCommands(async () => await pageTwo.runAudioCheck('AUDIO_OFF')); + }); + }); + }); + }); +}); diff --git a/integration/mochaTests/tsconfig.json b/integration/mochaTests/tsconfig.json new file mode 100644 index 0000000000..24835be4f3 --- /dev/null +++ b/integration/mochaTests/tsconfig.json @@ -0,0 +1,22 @@ +{ + "include": [ + "./tests/*" + ], + "compilerOptions": { + "declaration": true, + "downlevelIteration": true, + "experimentalDecorators": true, + "lib": [ + "DOM", + "ES2015", + "ES2016.Array.Include", + "ES2017.SharedMemory" + ], + "module": "commonjs", + "noImplicitAny": true, + "noUnusedLocals": true, + "sourceMap": true, + "stripInternal": true, + "target": "ES2015", + }, +} diff --git a/integration/mochaTests/utils/HelperFunctions.js b/integration/mochaTests/utils/HelperFunctions.js new file mode 100644 index 0000000000..553ca633ab --- /dev/null +++ b/integration/mochaTests/utils/HelperFunctions.js @@ -0,0 +1,81 @@ +#!/usr/bin/env node +const { spawnSync, spawn } = require('child_process'); + +// Run the command asynchronously without blocking the Node.js event loop. +function runAsync(command, args, options) { + options = { + ...options, + shell: true + }; + const child = spawn(command, args, options); + + child.stdout.setEncoding('utf8'); + child.stderr.setEncoding('utf8'); + + child.stdout.on('data', (data) => { + process.stdout.write(data + "\r"); + }); + + const promise = new Promise((resolve, reject) => { + child.on('close', (code) => { + console.log(`Command ${command} closed all stdio streams with status code: ${code}`); + }); + + child.stderr.on('data', (error) => { + process.stdout.write(error); + reject(error); + }); + + child.on('exit', (code) => { + console.log(`Command ${command} exited with status code: ${code}`); + if(code === 0) { + resolve(code); + } + else if(code === 1) { + reject(code); + } + else { + console.log(`Command ${command} exited with an unknown status code`); + reject(code); + } + }); + }); + + return promise; +} + +// Run the command synchronously with blocking the Node.js event loop +// until the spawned process either exits or is terminated. +function runSync(command, args, options, printOutput = true) { + options = { + ...options, + shell: true + }; + const child = spawnSync(command, args, options); + + const output = child.stdout.toString(); + if (printOutput) { + process.stdout.write(output); + } + + if (child.error) { + process.stdout.write(`Command ${command} failed with ${child.error.code}`); + } + + if (child.status !== 0) { + process.stdout.write(`Command ${command} failed with exit code ${child.status} and signal ${child.signal}`); + process.stdout.write(child.stderr.toString()); + } + + return output; +} + +const sleep = milliseconds => { + return new Promise(resolve => setTimeout(resolve, milliseconds)); +}; + +module.exports = { + runAsync, + runSync, + sleep +} \ No newline at end of file diff --git a/integration/mochaTests/utils/Logger.js b/integration/mochaTests/utils/Logger.js new file mode 100644 index 0000000000..bf4d521fe4 --- /dev/null +++ b/integration/mochaTests/utils/Logger.js @@ -0,0 +1,85 @@ +// #!usr/bin/env node +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +const chalk = require('chalk'); + +const error = chalk.red; +const info = chalk.blue; +const warn = chalk.yellow; +const success = chalk.green; + +const LogLevel = { + INFO: 'INFO', + WARN: 'WARN', + ERROR: 'ERROR', + SUCCESS: 'SUCCESS', +}; + +class Log { + constructor(msg, logLevel) { + this.msg = msg; + if (!!logLevel) { + this.logLevel = logLevel; + } else { + this.logLevel = LogLevel.INFO; + } + } +} + +class Logger { + constructor(name, logLevel = LogLevel.INFO) { + this.logs = []; + this.name = name; + this.logLevel = logLevel; + } + + async printLogs() { + for (let i = 0; i < this.logs.length; i++) { + if (this.logs[i]) { + this.log(this.logs[i].msg, this.logs[i].logLevel); + } + } + this.logs = []; + } + + pushLogs(msg, logLevel = LogLevel.INFO) { + this.logs.push(new Log(msg, logLevel)); + } + + setLogLevel(logLevel) { + this.logLevel = logLevel; + } + + getLogLevel() { + return this.logLevel; + } + + log(msg, logLevel) { + if (!logLevel) { + logLevel = this.logLevel; + } + const timestamp = new Date(); + const logMessage = `${timestamp} [${logLevel}] ${this.name} - ${msg}`; + + switch (logLevel) { + case LogLevel.ERROR: + console.log(error(logMessage)); + break; + case LogLevel.WARN: + console.log(warn(logMessage)); + break; + case LogLevel.INFO: + console.log(info(logMessage)); + break; + case LogLevel.SUCCESS: + console.log(success(logMessage)); + break; + } + } +} + +module.exports = { + Log, + LogLevel, + Logger, +}; diff --git a/integration/mochaTests/utils/SdkBaseTest.js b/integration/mochaTests/utils/SdkBaseTest.js new file mode 100644 index 0000000000..813e586430 --- /dev/null +++ b/integration/mochaTests/utils/SdkBaseTest.js @@ -0,0 +1,48 @@ +class SdkBaseTest { + constructor() { + const test = JSON.parse(process.env.TEST); + const client = JSON.parse(process.env.CLIENT); + const host = process.env.HOST; + const testType = process.env.TEST_TYPE; + + this.testName = test.name; + if (client.platform == 'IOS' || client.platform == 'ANDROID') { + this.testName = this.testName.concat('Mobile'); + } + this.host = host; + this.testType = testType; + + if (host === 'local') { + this.url = 'http://127.0.0.1:8080/'; + } else if (host === 'saucelabs' || host === 'devicefarm') { + this.url = process.env.TEST_URL; + } + + if (client.browserName === 'safari') { + if (client.platform === 'MAC') { + client.platform = 'macOS 12'; + } else { + client.platform = 'macOS 10.13'; + } + process.env.PLATFORM_NAME = client.platform; + + client.browserVersion = '15'; + process.env.BROWSER_VERSION = client.browserVersion; + } + + let urlParams = []; + if (client.browserName === 'safari') { + urlParams.push('earlyConnect=1'); + } + urlParams.push('attendee-presence-timeout-ms=5000'); + urlParams.push('fatal=1'); + for (let i = 0; i < urlParams.length; i++) { + if (i == 0) { + this.url = this.url.concat(`?${urlParams[i]}`); + } + this.url = this.url.concat(`&${urlParams[i]}`); + } + } +} + +module.exports = SdkBaseTest; diff --git a/integration/mochaTests/utils/WebDriverFactory.js b/integration/mochaTests/utils/WebDriverFactory.js new file mode 100644 index 0000000000..141737e65a --- /dev/null +++ b/integration/mochaTests/utils/WebDriverFactory.js @@ -0,0 +1,169 @@ +const { Builder } = require('selenium-webdriver'); +const { Logger, LogLevel, Log } = require('../utils/Logger'); +const config = require('../configs/BaseConfig'); +var AWS = require('aws-sdk'); + +class WebDriverFactory { + constructor(testName, host, testType, url, logger) { + this.testName = testName; + this.host = host; + if (this.host === 'devicefarm') { + this.devicefarm = new AWS.DeviceFarm({ region: 'us-west-2' }); + } + this.testType = testType; + this.url = url; + if (!!logger) { + this.logger = logger; + } else { + this.logger = new Logger('WebDriverFactory'); + } + this.numberOfSessions = 1; + this.sauceLabsUrl = `https://${process.env.SAUCE_USERNAME}:${process.env.SAUCE_ACCESS_KEY}@ondemand.saucelabs.com/wd/hub`; + } + + async configure() { + let builder = new Builder(); + let client; + let capabilities = {}; + + switch (this.host) { + case 'local': + this.logger.log('No host configuration is required for local tests'); + this.logger.log( + 'Make sure the required selenium webdrivers are installed on the host machine', + LogLevel.WARN + ); + break; + + case 'saucelabs': + this.logger.log('Configuring the webdriver for SauceLabs'); + builder.usingServer(this.sauceLabsUrl); + + if (process.env.BROWSER_VERSION) { + this.logger.log('Using browser version stored as an environment variable'); + config.sauceOptions.browserVersion = process.env.BROWSER_VERSION; + } + if (process.env.PLATFORM_NAME) { + this.logger.log('Using platform name stored as an environment variable'); + config.sauceOptions.platformName = process.env.PLATFORM_NAME; + } + + Object.assign(capabilities, config.sauceOptions); + break; + + case 'devicefarm': + this.logger.log('Configuring the webdriver for DeviceFarm'); + + let testGridUrlResult = ''; + // Get the endpoint to create a new session + testGridUrlResult = await this.devicefarm + .createTestGridUrl({ + projectArn: process.env.PROJECT_ARN, + expiresInSeconds: 600, + }) + .promise(); + + builder.usingServer(testGridUrlResult.url); + break; + + default: + this.logger.log('Invalid host argument, using local host instead', LogLevel.WARN); + break; + } + + switch (this.testType) { + case 'integration-test': + this.logger.log('Using integration test default settings'); + builder.forBrowser('chrome'); + Object.assign(capabilities, config.chromeOptions); + break; + + case `browser-compatibility`: + this.logger.log('Using the provided browser compatibility config'); + client = JSON.parse(process.env.CLIENT); + + if (client.browserName === 'chrome') { + builder.forBrowser('chrome'); + Object.assign(capabilities, config.chromeOptions); + } else if (client.browserName === 'firefox') { + builder.forBrowser('firefox'); + Object.assign(capabilities, config.firefoxOptions); + } else if (client.browserName === 'safari') { + this.numberOfSessions = 2; + builder.forBrowser('safari'); + Object.assign(capabilities, config.safariOptions); + } else { + this.logger.log( + `browserName: ${client.browserName} defined in the test config is not valid`, + LogLevel.ERROR + ); + throw new Error(`browserName defined in the test config is not valid`); + } + + if (client.platform === 'android') { + this.numberOfSessions = 2; + } + process.env.PLATFORM_NAME = client.platform; + process.env.BROWSER_VERSION = client.browserVersion; + + break; + + default: + this.logger.log('Using default settings'); + this.logger.log('Running chrome latest on MAC'); + builder.forBrowser('chrome'); + Object.assign(capabilities, config.chromeOptions); + break; + } + + builder.withCapabilities({ + ...capabilities, + }); + + return builder; + } + + async build() { + try { + let builder = await this.configure(); + this.driver = builder.build(); + } catch (error) { + this.logger.log( + `Error occured while building a selenium webdriver, error: ${error}`, + LogLevel.ERROR + ); + } + + if (this.host === 'saucelabs') { + const { id_ } = await this.driver.getSession(); + this.sessionId = id_; + this.logger.log( + `Successfully created a SauceLabs session at https://saucelabs.com/tests/${this.sessionId}`, + LogLevel.SUCCESS + ); + this.driver.executeScript('sauce:job-name=' + this.testName); + } + + if (this.host === 'devicefarm') { + const { id_ } = await this.driver.getSession(); + this.sessionId = id_; + this.logger.log( + `Successfully created a DeviceFarm session with id: ${this.sessionId}`, + LogLevel.SUCCESS + ); + } + } + + async quit(testResult) { + if (this.host === 'saucelabs') { + this.driver.executeScript('sauce:job-result=' + testResult); + this.logger.log( + `See a video of the test run at https://saucelabs.com/tests/${this.sessionId}`, + LogLevel.SUCCESS + ); + } + await this.driver.quit(); + } +} + +module.exports = WebDriverFactory; diff --git a/integration/mochaTests/utils/Window.js b/integration/mochaTests/utils/Window.js new file mode 100644 index 0000000000..addb8c45a2 --- /dev/null +++ b/integration/mochaTests/utils/Window.js @@ -0,0 +1,42 @@ +const { LogLevel } = require('./Logger'); + +class Window { + constructor(webdriver, logger) { + this.driver = webdriver; + this.logger = logger; + } + + static async existing(webdriver, logger, name) { + const w = new Window(webdriver, logger); + const handles = await w.driver.getAllWindowHandles(); + w.handle = handles[handles.length - 1]; + w.name = name; + return w; + } + + static async openNew(webdriver, logger, name) { + const w = new Window(webdriver, logger); + await w.driver.executeScript('window.open()'); + const handles = await w.driver.getAllWindowHandles(); + w.handle = handles[handles.length - 1]; + w.name = name; + return w; + } + + async runCommands(commands) { + await this.driver.switchTo().window(this.handle); + try { + await commands(); + } catch (error) { + this.logger.pushLogs(`${error}`, LogLevel.ERROR); + throw new Error(error); + } + } + + async close() { + await this.driver.switchTo().window(this.handle); + await this.driver.close(); + } +} + +module.exports.Window = Window;