Skip to content

Commit

Permalink
Implement tests using webdriverio (MetaMask#1231)
Browse files Browse the repository at this point in the history
* Implement tests using webdriverio

* Add test for iframe sandboxing

* Run wdio in CI

* Patch mocha types

* Fix jest coverage

* Update snap shasums

* Test accessing iframe contentWindow.document

* Properly test iframe sandboxing

* Remove unnecessary timeout

* Run yarn lint:fix

* Move iframe URL to constant

* Improve test HTML

* Fix test

* Replace Ava with Webdriverio (MetaMask#1234)

* Set up webdriverio for snaps-execution-environments

* Convert BaseSnapExecutor test

* Run tests in CI

* Convert endowment hardening tests

* Remove ava and related files

* Update all snaps-execution-environments tests to use Webdriverio

* Fix Jest config

* Fix network tests

* Add IframeExecutionService tests

* Update wdio config

* Fix open handles

* Fix more open handles

* Fix one more open handle

* Add hack for atob, btoa, console

* Revert change to lockdown call

* Increase timeouts and change port

* Run test:browser in separate step

* Set maxInstances to 1

* Run tests in Firefox as well

* Update snaps-execution-environments tests to pass in Firefox

* Update snaps-controllers to run in Firefox too

* Use data-testid for getting snap iframe

* Update error message

* Add missing tests

* Use spy for testing error event handler

* Remove more unused dependencies

* Add comment to math endowment test

* Remove iframe-test bundle

* Move SILENT_LOGGER to execution environments package

* Revert logger

* Ignore test-utils for coverage

* Add comment to test

* Fix test

* Add realtimeReporting option

* Collect coverage

* Merge Jest coverage with Wdio coverage

* Update coverage reporters

* Fix webpack build

* Remove CI step

* Update snapshot

* Fix controllers

* Update more snapshots

* Add new line

* Update coverage

* Update coverage

* Try Codecov coverage merging

* Use Codecov for merging snaps-execution-environments too

* Add missing command

* Improve coverage merging

* Add comment to Mocha patch

* Use shared sleep funtion
  • Loading branch information
Mrtenz authored Mar 1, 2023
1 parent 67d71bd commit bd6b16d
Show file tree
Hide file tree
Showing 71 changed files with 6,314 additions and 2,764 deletions.
151 changes: 151 additions & 0 deletions .yarn/patches/@types-mocha-npm-10.0.1-7c94e9e170.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
`@types/mocha` and `@types/jest` both declare global `describe`, `it` and some other functions, which causes a conflict
in TypeScript. This patch removes the global declarations from `@types/mocha`, which are not needed in most cases. They
can still be imported from `mocha` if needed.

diff --git a/index.d.ts b/index.d.ts
index 8e5122397bd0b9418decc4db6c508e6a018abd33..702191271bebb2c8505c123420d2b704f5351425 100755
/**
* Execute before running tests.
@@ -2620,7 +2620,7 @@ declare var before: Mocha.HookFunction;
*
* @see https://mochajs.org/api/global.html#before
*/
-declare var suiteSetup: Mocha.HookFunction;
+// declare var suiteSetup: Mocha.HookFunction;

/**
* Execute after running tests.
@@ -2629,7 +2629,7 @@ declare var suiteSetup: Mocha.HookFunction;
*
* @see https://mochajs.org/api/global.html#after
*/
-declare var after: Mocha.HookFunction;
+// declare var after: Mocha.HookFunction;

/**
* Execute after running tests.
@@ -2638,7 +2638,7 @@ declare var after: Mocha.HookFunction;
*
* @see https://mochajs.org/api/global.html#after
*/
-declare var suiteTeardown: Mocha.HookFunction;
+// declare var suiteTeardown: Mocha.HookFunction;

/**
* Execute before each test case.
@@ -2647,7 +2647,7 @@ declare var suiteTeardown: Mocha.HookFunction;
*
* @see https://mochajs.org/api/global.html#beforeEach
*/
-declare var beforeEach: Mocha.HookFunction;
+// declare var beforeEach: Mocha.HookFunction;

/**
* Execute before each test case.
@@ -2656,7 +2656,7 @@ declare var beforeEach: Mocha.HookFunction;
*
* @see https://mochajs.org/api/global.html#beforeEach
*/
-declare var setup: Mocha.HookFunction;
+// declare var setup: Mocha.HookFunction;

/**
* Execute after each test case.
@@ -2665,7 +2665,7 @@ declare var setup: Mocha.HookFunction;
*
* @see https://mochajs.org/api/global.html#afterEach
*/
-declare var afterEach: Mocha.HookFunction;
+// declare var afterEach: Mocha.HookFunction;

/**
* Execute after each test case.
@@ -2674,77 +2674,77 @@ declare var afterEach: Mocha.HookFunction;
*
* @see https://mochajs.org/api/global.html#afterEach
*/
-declare var teardown: Mocha.HookFunction;
+// declare var teardown: Mocha.HookFunction;

/**
* Describe a "suite" containing nested suites and tests.
*
* - _Only available when invoked via the mocha CLI._
*/
-declare var describe: Mocha.SuiteFunction;
+// declare var describe: Mocha.SuiteFunction;

/**
* Describe a "suite" containing nested suites and tests.
*
* - _Only available when invoked via the mocha CLI._
*/
-declare var context: Mocha.SuiteFunction;
+// declare var context: Mocha.SuiteFunction;

/**
* Describe a "suite" containing nested suites and tests.
*
* - _Only available when invoked via the mocha CLI._
*/
-declare var suite: Mocha.SuiteFunction;
+// declare var suite: Mocha.SuiteFunction;

/**
* Pending suite.
*
* - _Only available when invoked via the mocha CLI._
*/
-declare var xdescribe: Mocha.PendingSuiteFunction;
+// declare var xdescribe: Mocha.PendingSuiteFunction;

/**
* Pending suite.
*
* - _Only available when invoked via the mocha CLI._
*/
-declare var xcontext: Mocha.PendingSuiteFunction;
+// declare var xcontext: Mocha.PendingSuiteFunction;

/**
* Describes a test case.
*
* - _Only available when invoked via the mocha CLI._
*/
-declare var it: Mocha.TestFunction;
+// declare var it: Mocha.TestFunction;

/**
* Describes a test case.
*
* - _Only available when invoked via the mocha CLI._
*/
-declare var specify: Mocha.TestFunction;
+// declare var specify: Mocha.TestFunction;

/**
* Describes a test case.
*
* - _Only available when invoked via the mocha CLI._
*/
-declare var test: Mocha.TestFunction;
+// declare var test: Mocha.TestFunction;

/**
* Describes a pending test case.
*
* - _Only available when invoked via the mocha CLI._
*/
-declare var xit: Mocha.PendingTestFunction;
+// declare var xit: Mocha.PendingTestFunction;

/**
* Describes a pending test case.
*
* - _Only available when invoked via the mocha CLI._
*/
-declare var xspecify: Mocha.PendingTestFunction;
+// declare var xspecify: Mocha.PendingTestFunction;

// #endregion Test interface augmentations

14 changes: 14 additions & 0 deletions .yarn/patches/jest-fetch-mock-npm-3.0.3-ac072ca8af.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
diff --git a/src/index.js b/src/index.js
index 42015c1815447aab80df4524d1af91ac33e5751d..003aa9a2f0ce3ac4a65053b0b29ca0fe9b5c7ff3 100644
--- a/src/index.js
+++ b/src/index.js
@@ -82,7 +82,8 @@ const isFn = unknown => typeof unknown === 'function'
const isMocking = jest.fn(staticMatches(true))

const abortError = () =>
- new DOMException('The operation was aborted. ', 'AbortError')
+ // `DOMException` is not available in Node.js.
+ new Error('The operation was aborted. ')

const abort = () => {
throw abortError()
1 change: 1 addition & 0 deletions jest.config.base.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module.exports = {
collectCoverageFrom: [
'./src/**/*.ts',
'!./src/**/*.test.ts',
'!./src/**/*.test.browser.ts',
'!./src/test-utils/**/*.ts',
'!./src/**/*.d.ts',
],
Expand Down
12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"build:post-tsc": "yarn workspaces foreach --parallel --topological --verbose run build:post-tsc",
"clean": "yarn workspaces foreach --parallel --verbose run clean",
"test": "yarn workspaces foreach --parallel --verbose run test",
"test:browser": "yarn workspaces foreach --verbose run test:browser",
"test:ci": "yarn workspace @metamask/snaps-execution-environments run build:test && yarn workspaces foreach --parallel --verbose run test:ci"
},
"simple-git-hooks": {
Expand All @@ -39,6 +40,10 @@
"prettier --write"
]
},
"resolutions": {
"@types/mocha@^10.0.1": "patch:@types/mocha@npm:10.0.1#.yarn/patches/@types-mocha-npm-10.0.1-7c94e9e170.patch",
"jest-fetch-mock@^3.0.3": "patch:jest-fetch-mock@npm:3.0.3#.yarn/patches/jest-fetch-mock-npm-3.0.3-ac072ca8af.patch"
},
"devDependencies": {
"@lavamoat/allow-scripts": "^2.0.3",
"@metamask/auto-changelog": "^3.1.0",
Expand All @@ -50,13 +55,15 @@
"@types/node": "^14.14.25",
"@typescript-eslint/eslint-plugin": "^5.42.1",
"@typescript-eslint/parser": "^5.42.1",
"chromedriver": "^110.0.0",
"eslint": "^8.27.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "^27.1.5",
"eslint-plugin-jsdoc": "^39.6.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.2.1",
"geckodriver": "^3.2.0",
"jest": "^29.0.2",
"lint-staged": "^12.4.1",
"prettier": "^2.7.1",
Expand All @@ -73,7 +80,10 @@
"allowScripts": {
"@lavamoat/preinstall-always-fail": false,
"simple-git-hooks": false,
"$root$": false
"$root$": false,
"chromedriver": true,
"jest>jest-cli>jest-config>ts-node>@swc/core": false,
"geckodriver": true
}
},
"packageManager": "[email protected]"
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/examples/bls-signer/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snaps-monorepo.git"
},
"source": {
"shasum": "oVAE5f7ztd6AFciQSLmxIhkWlrehFX4XQPrZzm3Ejm8=",
"shasum": "eTB8paFBd43sRlLm4UxmyYXHXHGNAuKrMDV+MOgTXpo=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/examples/insights/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snaps-monorepo.git"
},
"source": {
"shasum": "9wkEIIAbBKGnamMZANGKfMStc6PoXWupBxVOTbPrKgE=",
"shasum": "7qmJPX+kjEk75ZeWy3TieQivrpoIjQIevCSeBM9dZck=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/examples/ipfs/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snaps-monorepo.git"
},
"source": {
"shasum": "HDDbqHDwgbCY2cTqv3kTF+R2wKNpGh4LVKFaxkHrAUE=",
"shasum": "9Hli0/Lio8knvUT3gnvPCRjBQdhga3dvsHjNpi23tXg=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/examples/wasm/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snaps-monorepo.git"
},
"source": {
"shasum": "Vi77sDZum3489hjQS6q/n3C/+FTBhD7DJDXG74xfLIo=",
"shasum": "ueIVlX4nsgaYw7LJB4xS8UeekFpggFvHHE/uglPYtm0=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { InstallSnapsResult } from '@metamask/snaps-utils';
import { isObject } from '@metamask/utils';
import { ethErrors } from 'eth-rpc-errors';

export { InstallSnapsResult } from '@metamask/snaps-utils';
export type { InstallSnapsResult } from '@metamask/snaps-utils';

export type InstallSnapsHook = (
requestedSnaps: RequestedPermissions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ exports[`plugin generates a source map 1`] = `
};
}, {}]
}, {}, [1]);
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJtYXBwaW5ncyI6IkFBQUE7RUFBQTtJQUFBO01BQUE7UUFBQTtVQUFBO1VBQUE7VUFBQTtVQUFBO1VBQUE7UUFBQTtRQUFBO1VBQUFBO1FBQUE7UUFBQUM7VUFBQTtVQUFBO1FBQUE7TUFBQTtNQUFBO0lBQUE7SUFBQTtJQUFBO0VBQUE7RUFBQTtBQUFBO0VBQUE7SUNDQUM7TUFBQUM7SUFBQTtNQUNBQztNQUVBO1FBQUFDO1FBQUFDO01BQUE7TUFDQTtJQUNBIiwibmFtZXMiOlsiZXhwb3J0cyIsImUiLCJtb2R1bGUiLCJyZXF1ZXN0IiwiY29uc29sZSIsIm1ldGhvZCIsImlkIl0sInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL25vZGVfbW9kdWxlcy9icm93c2VyLXBhY2svX3ByZWx1ZGUuanMiLCJfc3RyZWFtXzAuanMiXSwic291cmNlc0NvbnRlbnQiOlsiKGZ1bmN0aW9uKCl7ZnVuY3Rpb24gcihlLG4sdCl7ZnVuY3Rpb24gbyhpLGYpe2lmKCFuW2ldKXtpZighZVtpXSl7dmFyIGM9XCJmdW5jdGlvblwiPT10eXBlb2YgcmVxdWlyZSYmcmVxdWlyZTtpZighZiYmYylyZXR1cm4gYyhpLCEwKTtpZih1KXJldHVybiB1KGksITApO3ZhciBhPW5ldyBFcnJvcihcIkNhbm5vdCBmaW5kIG1vZHVsZSAnXCIraStcIidcIik7dGhyb3cgYS5jb2RlPVwiTU9EVUxFX05PVF9GT1VORFwiLGF9dmFyIHA9bltpXT17ZXhwb3J0czp7fX07ZVtpXVswXS5jYWxsKHAuZXhwb3J0cyxmdW5jdGlvbihyKXt2YXIgbj1lW2ldWzFdW3JdO3JldHVybiBvKG58fHIpfSxwLHAuZXhwb3J0cyxyLGUsbix0KX1yZXR1cm4gbltpXS5leHBvcnRzfWZvcih2YXIgdT1cImZ1bmN0aW9uXCI9PXR5cGVvZiByZXF1aXJlJiZyZXF1aXJlLGk9MDtpPHQubGVuZ3RoO2krKylvKHRbaV0pO3JldHVybiBvfXJldHVybiByfSkoKSIsIlxuICBtb2R1bGUuZXhwb3J0cy5vblJwY1JlcXVlc3QgPSAoeyByZXF1ZXN0IH0pID0+IHtcbiAgICBjb25zb2xlLmxvZyhcIkhlbGxvLCB3b3JsZCFcIik7XG5cbiAgICBjb25zdCB7IG1ldGhvZCwgaWQgfSA9IHJlcXVlc3Q7XG4gICAgcmV0dXJuIG1ldGhvZCArIGlkO1xuICB9O1xuIl19"
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJyIiwiZSIsIm4iLCJ0IiwibyIsImkiLCJmIiwiYyIsInJlcXVpcmUiLCJ1IiwiYSIsIkVycm9yIiwiY29kZSIsInAiLCJleHBvcnRzIiwiY2FsbCIsImxlbmd0aCIsIm1vZHVsZSIsIm9uUnBjUmVxdWVzdCIsInJlcXVlc3QiLCJjb25zb2xlIiwibG9nIiwibWV0aG9kIiwiaWQiXSwic291cmNlcyI6WyIuLi8uLi9ub2RlX21vZHVsZXMvYnJvd3Nlci1wYWNrL19wcmVsdWRlLmpzIiwiX3N0cmVhbV8wLmpzIl0sInNvdXJjZXNDb250ZW50IjpbIihmdW5jdGlvbigpe2Z1bmN0aW9uIHIoZSxuLHQpe2Z1bmN0aW9uIG8oaSxmKXtpZighbltpXSl7aWYoIWVbaV0pe3ZhciBjPVwiZnVuY3Rpb25cIj09dHlwZW9mIHJlcXVpcmUmJnJlcXVpcmU7aWYoIWYmJmMpcmV0dXJuIGMoaSwhMCk7aWYodSlyZXR1cm4gdShpLCEwKTt2YXIgYT1uZXcgRXJyb3IoXCJDYW5ub3QgZmluZCBtb2R1bGUgJ1wiK2krXCInXCIpO3Rocm93IGEuY29kZT1cIk1PRFVMRV9OT1RfRk9VTkRcIixhfXZhciBwPW5baV09e2V4cG9ydHM6e319O2VbaV1bMF0uY2FsbChwLmV4cG9ydHMsZnVuY3Rpb24ocil7dmFyIG49ZVtpXVsxXVtyXTtyZXR1cm4gbyhufHxyKX0scCxwLmV4cG9ydHMscixlLG4sdCl9cmV0dXJuIG5baV0uZXhwb3J0c31mb3IodmFyIHU9XCJmdW5jdGlvblwiPT10eXBlb2YgcmVxdWlyZSYmcmVxdWlyZSxpPTA7aTx0Lmxlbmd0aDtpKyspbyh0W2ldKTtyZXR1cm4gb31yZXR1cm4gcn0pKCkiLCJcbiAgbW9kdWxlLmV4cG9ydHMub25ScGNSZXF1ZXN0ID0gKHsgcmVxdWVzdCB9KSA9PiB7XG4gICAgY29uc29sZS5sb2coXCJIZWxsbywgd29ybGQhXCIpO1xuXG4gICAgY29uc3QgeyBtZXRob2QsIGlkIH0gPSByZXF1ZXN0O1xuICAgIHJldHVybiBtZXRob2QgKyBpZDtcbiAgfTtcbiJdLCJtYXBwaW5ncyI6IkFBQUE7RUFBQSxTQUFBQSxFQUFBQyxDQUFBLEVBQUFDLENBQUEsRUFBQUMsQ0FBQTtJQUFBLFNBQUFDLEVBQUFDLENBQUEsRUFBQUMsQ0FBQTtNQUFBLEtBQUFKLENBQUEsQ0FBQUcsQ0FBQTtRQUFBLEtBQUFKLENBQUEsQ0FBQUksQ0FBQTtVQUFBLElBQUFFLENBQUEsd0JBQUFDLE9BQUEsSUFBQUEsT0FBQTtVQUFBLEtBQUFGLENBQUEsSUFBQUMsQ0FBQSxTQUFBQSxDQUFBLENBQUFGLENBQUE7VUFBQSxJQUFBSSxDQUFBLFNBQUFBLENBQUEsQ0FBQUosQ0FBQTtVQUFBLElBQUFLLENBQUEsT0FBQUMsS0FBQSwwQkFBQU4sQ0FBQTtVQUFBLE1BQUFLLENBQUEsQ0FBQUUsSUFBQSx1QkFBQUYsQ0FBQTtRQUFBO1FBQUEsSUFBQUcsQ0FBQSxHQUFBWCxDQUFBLENBQUFHLENBQUE7VUFBQVMsT0FBQTtRQUFBO1FBQUFiLENBQUEsQ0FBQUksQ0FBQSxLQUFBVSxJQUFBLENBQUFGLENBQUEsQ0FBQUMsT0FBQSxZQUFBZCxDQUFBO1VBQUEsSUFBQUUsQ0FBQSxHQUFBRCxDQUFBLENBQUFJLENBQUEsS0FBQUwsQ0FBQTtVQUFBLE9BQUFJLENBQUEsQ0FBQUYsQ0FBQSxJQUFBRixDQUFBO1FBQUEsR0FBQWEsQ0FBQSxFQUFBQSxDQUFBLENBQUFDLE9BQUEsRUFBQWQsQ0FBQSxFQUFBQyxDQUFBLEVBQUFDLENBQUEsRUFBQUMsQ0FBQTtNQUFBO01BQUEsT0FBQUQsQ0FBQSxDQUFBRyxDQUFBLEVBQUFTLE9BQUE7SUFBQTtJQUFBLFNBQUFMLENBQUEsd0JBQUFELE9BQUEsSUFBQUEsT0FBQSxFQUFBSCxDQUFBLE1BQUFBLENBQUEsR0FBQUYsQ0FBQSxDQUFBYSxNQUFBLEVBQUFYLENBQUEsSUFBQUQsQ0FBQSxDQUFBRCxDQUFBLENBQUFFLENBQUE7SUFBQSxPQUFBRCxDQUFBO0VBQUE7RUFBQSxPQUFBSixDQUFBO0FBQUE7RUFBQSxjQUFBUSxPQUFBLEVBQUFTLE1BQUEsRUFBQUgsT0FBQTtJQ0NBRyxNQUFBLENBQUFILE9BQUEsQ0FBQUksWUFBQTtNQUFBQztJQUFBO01BQ0FDLE9BQUEsQ0FBQUMsR0FBQTtNQUVBO1FBQUFDLE1BQUE7UUFBQUM7TUFBQSxJQUFBSixPQUFBO01BQ0EsT0FBQUcsTUFBQSxHQUFBQyxFQUFBO0lBQ0EifQ=="
`;

exports[`plugin processes files using Browserify 1`] = `
Expand Down
6 changes: 6 additions & 0 deletions packages/snaps-controllers/coverage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"branches": 87.43,
"functions": 94.21,
"lines": 95.25,
"statements": 95.17
}
37 changes: 6 additions & 31 deletions packages/snaps-controllers/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,12 @@ const deepmerge = require('deepmerge');

const baseConfig = require('../../jest.config.base');

delete baseConfig.coverageThreshold;

module.exports = deepmerge(baseConfig, {
coverageThreshold: {
global: {
branches: 88.94,
functions: 94.67,
lines: 96.26,
statements: 96.18,
},
},
projects: [
{
moduleNameMapper: baseConfig.moduleNameMapper,
preset: 'ts-jest',
testMatch: ['<rootDir>/src/services/iframe/*.test.ts'],
testEnvironment: '<rootDir>/jest.environment.js',
testEnvironmentOptions: {
resources: 'usable',
runScripts: 'dangerously',
customExportConditions: ['node', 'node-addons'],
},
},
{
moduleNameMapper: baseConfig.moduleNameMapper,
preset: 'ts-jest',
testPathIgnorePatterns: ['<rootDir>/src/services/iframe/*'],
testEnvironment: '<rootDir>/jest.environment.js',
testEnvironmentOptions: {
customExportConditions: ['node', 'node-addons'],
},
testRegex: ['\\.test\\.(ts|js)$'],
},
],
coverageDirectory: './coverage/jest',
testTimeout: 5000,

// This is required for `jest-fetch-mock` to work.
resetMocks: false,
});
18 changes: 0 additions & 18 deletions packages/snaps-controllers/jest.environment.js

This file was deleted.

Loading

0 comments on commit bd6b16d

Please sign in to comment.