Emulator Launch, Commands and Extension Tech Debt Buydown (#1577)

* Tech debt buydown
This commit is contained in:
Justin Wilaby 2019-05-23 16:08:18 -07:00 коммит произвёл GitHub
Родитель a562689caf
Коммит a1fa135f1e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
213 изменённых файлов: 6083 добавлений и 18148 удалений

Просмотреть файл

@ -4,7 +4,7 @@
"@babel/preset-env",
{
"targets": {
"node": "8"
"node": "10"
}
}
],
@ -13,14 +13,14 @@
"ignore": ["**/*.d.ts"],
"sourceMaps": "inline",
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-react-jsx",
"@babel/plugin-transform-runtime",
[
"@babel/proposal-decorators",
"@babel/plugin-proposal-decorators",
{
"decoratorsBeforeExport": true
}
]
],
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-runtime"
]
}

Просмотреть файл

@ -7,14 +7,12 @@
"packages/emulator/cli",
"packages/emulator/core",
"packages/sdk/client",
"packages/sdk/main",
"packages/sdk/shared",
"packages/sdk/ui-react",
"packages/extensions/qnamaker",
"packages/extensions/qnamaker/client",
"packages/extensions/debug",
"packages/extensions/debug/client",
"packages/extensions/debug/main",
"packages/extensions/luis",
"packages/extensions/luis/client",
"packages/extensions/json",

321
package-lock.json сгенерированный
Просмотреть файл

@ -826,9 +826,9 @@
}
},
"@babel/plugin-transform-runtime": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.4.3.tgz",
"integrity": "sha512-7Q61bU+uEI7bCUFReT1NKn7/X6sDQsZ7wL1sJ9IYMAO7cI+eg6x9re1cEw2fCRMbbTVyoeUKWSV1M6azEfKCfg==",
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.4.4.tgz",
"integrity": "sha512-aMVojEjPszvau3NRg+TIH14ynZLvPewH4xhlCW1w6A3rkxTS1m4uwzRclYR9oS+rl/dr+kT+pzbfHuAWP/lc7Q==",
"requires": {
"@babel/helper-module-imports": "^7.0.0",
"@babel/helper-plugin-utils": "^7.0.0",
@ -3847,14 +3847,14 @@
}
},
"babel-loader": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.5.tgz",
"integrity": "sha512-NTnHnVRd2JnRqPC0vW+iOQWU5pchDbYXsG2E6DMXEpMfUcQKclF9gmf3G3ZMhzG7IG9ji4coL0cm+FxeWxDpnw==",
"version": "8.0.6",
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.6.tgz",
"integrity": "sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw==",
"requires": {
"find-cache-dir": "^2.0.0",
"loader-utils": "^1.0.2",
"mkdirp": "^0.5.1",
"util.promisify": "^1.0.0"
"pify": "^4.0.1"
}
},
"babel-messages": {
@ -4780,9 +4780,9 @@
}
},
"botframework-config": {
"version": "4.0.0-preview1.3.4",
"resolved": "https://registry.npmjs.org/botframework-config/-/botframework-config-4.0.0-preview1.3.4.tgz",
"integrity": "sha512-44X8ej+o1M43PGBjCsfs8fLA9L6WD74NAjqhvr52hxMrhMbaceFD+EsBBBdAdsaNmIjmtTTgkz5M7O6b3nZ9gA==",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/botframework-config/-/botframework-config-4.4.0.tgz",
"integrity": "sha512-5YyLkr3Zv60NJPZNTXZ/YEIj/yBRqfACw7+D90hPL3ueOtWi0Vc440a3cfggSq/RszSGDOVfX3VROVV85+Vm5g==",
"requires": {
"fs-extra": "^7.0.0",
"process": "^0.11.10",
@ -23274,6 +23274,23 @@
"execa": "^0.7.0"
}
},
"terser": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.0.0.tgz",
"integrity": "sha512-dOapGTU0hETFl1tCo4t56FN+2jffoKyER9qBGoUFyZ6y7WLoKT0bF+lAYi6B6YsILcGF3q1C2FBh8QcKSCgkgA==",
"requires": {
"commander": "^2.19.0",
"source-map": "~0.6.1",
"source-map-support": "~0.5.10"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"test-exclude": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.3.tgz",
@ -23623,18 +23640,6 @@
"utf8-byte-length": "^1.0.1"
}
},
"ts-loader": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-4.5.0.tgz",
"integrity": "sha512-ihgVaSmgrX4crGV4n7yuoHPoCHbDzj9aepCZR9TgIx4SgJ9gdnB6xLHgUBb7bsFM/f0K6x9iXa65KY/Fu1Klkw==",
"requires": {
"chalk": "^2.3.0",
"enhanced-resolve": "^4.0.0",
"loader-utils": "^1.0.2",
"micromatch": "^3.1.4",
"semver": "^5.0.1"
}
},
"tslib": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
@ -23799,92 +23804,84 @@
}
},
"uglifyjs-webpack-plugin": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz",
"integrity": "sha512-ovHIch0AMlxjD/97j9AYovZxG5wnHOPkL7T1GKochBADp/Zwc44pEWNqpKl1Loupp1WhFg7SlYmHZRUfdAacgw==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-2.1.3.tgz",
"integrity": "sha512-/lRkCaFbI6pT3CxsQHDhBcqB6tocOnqba0vJqJ2DzSWFLRgOIiip8q0nVFydyXk+n8UtF7ZuS6hvWopcYH5FuA==",
"requires": {
"cacache": "^10.0.4",
"find-cache-dir": "^1.0.0",
"schema-utils": "^0.4.5",
"serialize-javascript": "^1.4.0",
"cacache": "^11.3.2",
"find-cache-dir": "^2.1.0",
"is-wsl": "^1.1.0",
"schema-utils": "^1.0.0",
"serialize-javascript": "^1.7.0",
"source-map": "^0.6.1",
"uglify-es": "^3.3.4",
"webpack-sources": "^1.1.0",
"worker-farm": "^1.5.2"
"uglify-js": "^3.5.12",
"webpack-sources": "^1.3.0",
"worker-farm": "^1.7.0"
},
"dependencies": {
"commander": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz",
"integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA=="
},
"find-cache-dir": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz",
"integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=",
"cacache": {
"version": "11.3.2",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.2.tgz",
"integrity": "sha512-E0zP4EPGDOaT2chM08Als91eYnf8Z+eH1awwwVsngUmgppfM5jjJ8l3z5vO5p5w/I3LsiXawb1sW0VY65pQABg==",
"requires": {
"commondir": "^1.0.1",
"make-dir": "^1.0.0",
"pkg-dir": "^2.0.0"
"bluebird": "^3.5.3",
"chownr": "^1.1.1",
"figgy-pudding": "^3.5.1",
"glob": "^7.1.3",
"graceful-fs": "^4.1.15",
"lru-cache": "^5.1.1",
"mississippi": "^3.0.0",
"mkdirp": "^0.5.1",
"move-concurrently": "^1.0.1",
"promise-inflight": "^1.0.1",
"rimraf": "^2.6.2",
"ssri": "^6.0.1",
"unique-filename": "^1.1.1",
"y18n": "^4.0.0"
}
},
"find-up": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
"integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"requires": {
"locate-path": "^2.0.0"
"yallist": "^3.0.2"
}
},
"locate-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
"integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
"requires": {
"p-locate": "^2.0.0",
"path-exists": "^3.0.0"
}
},
"make-dir": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
"integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
"requires": {
"pify": "^3.0.0"
}
},
"p-limit": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
"integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
"requires": {
"p-try": "^1.0.0"
}
},
"p-locate": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
"requires": {
"p-limit": "^1.1.0"
}
},
"p-try": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
},
"pify": {
"mississippi": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
},
"pkg-dir": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
"integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=",
"resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
"integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==",
"requires": {
"find-up": "^2.1.0"
"concat-stream": "^1.5.0",
"duplexify": "^3.4.2",
"end-of-stream": "^1.1.0",
"flush-write-stream": "^1.0.0",
"from2": "^2.1.0",
"parallel-transform": "^1.1.0",
"pump": "^3.0.0",
"pumpify": "^1.3.3",
"stream-each": "^1.1.0",
"through2": "^2.0.0"
}
},
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"schema-utils": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
"integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
"requires": {
"ajv": "^6.1.0",
"ajv-errors": "^1.0.0",
"ajv-keywords": "^3.1.0"
}
},
"source-map": {
@ -23892,14 +23889,27 @@
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"uglify-es": {
"version": "3.3.9",
"resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",
"integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==",
"ssri": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
"requires": {
"commander": "~2.13.0",
"figgy-pudding": "^3.5.1"
}
},
"uglify-js": {
"version": "3.5.15",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.15.tgz",
"integrity": "sha512-fe7aYFotptIddkwcm6YuA0HmknBZ52ZzOsUxZEdhhkSsz7RfjHDX2QDxwKTiv4JQ5t5NhfmpgAK+J7LiDhKSqg==",
"requires": {
"commander": "~2.20.0",
"source-map": "~0.6.1"
}
},
"yallist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A=="
}
}
},
@ -24603,6 +24613,11 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
"integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw=="
},
"commander": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz",
"integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA=="
},
"eslint-scope": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
@ -24611,6 +24626,104 @@
"esrecurse": "^4.1.0",
"estraverse": "^4.1.1"
}
},
"find-cache-dir": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz",
"integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=",
"requires": {
"commondir": "^1.0.1",
"make-dir": "^1.0.0",
"pkg-dir": "^2.0.0"
}
},
"find-up": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
"integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
"requires": {
"locate-path": "^2.0.0"
}
},
"locate-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
"integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
"requires": {
"p-locate": "^2.0.0",
"path-exists": "^3.0.0"
}
},
"make-dir": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
"integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
"requires": {
"pify": "^3.0.0"
}
},
"p-limit": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
"integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
"requires": {
"p-try": "^1.0.0"
}
},
"p-locate": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
"requires": {
"p-limit": "^1.1.0"
}
},
"p-try": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
},
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
},
"pkg-dir": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
"integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=",
"requires": {
"find-up": "^2.1.0"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"uglify-es": {
"version": "3.3.9",
"resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",
"integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==",
"requires": {
"commander": "~2.13.0",
"source-map": "~0.6.1"
}
},
"uglifyjs-webpack-plugin": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz",
"integrity": "sha512-ovHIch0AMlxjD/97j9AYovZxG5wnHOPkL7T1GKochBADp/Zwc44pEWNqpKl1Loupp1WhFg7SlYmHZRUfdAacgw==",
"requires": {
"cacache": "^10.0.4",
"find-cache-dir": "^1.0.0",
"schema-utils": "^0.4.5",
"serialize-javascript": "^1.4.0",
"source-map": "^0.6.1",
"uglify-es": "^3.3.4",
"webpack-sources": "^1.1.0",
"worker-farm": "^1.5.2"
}
}
}
},
@ -24986,9 +25099,9 @@
"integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus="
},
"worker-farm": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz",
"integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz",
"integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==",
"requires": {
"errno": "~0.1.7"
}

Просмотреть файл

@ -4,8 +4,7 @@
"@babel/preset-env",
{
"targets": {
"chrome": "58",
"esmodules": true
"chrome": "68"
}
}
],
@ -16,8 +15,14 @@
],
"sourceMaps": "inline",
"plugins": [
"@babel/proposal-class-properties",
"@babel/plugin-transform-react-jsx",
[
"@babel/proposal-decorators",
{
"decoratorsBeforeExport": true
}
],
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-runtime"
]
}

Просмотреть файл

@ -46,9 +46,10 @@
"@babel/plugin-proposal-class-properties": "^7.1.0",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
"@babel/plugin-transform-react-jsx": "^7.0.0",
"@babel/plugin-transform-runtime": "^7.1.0",
"@babel/plugin-transform-runtime": "^7.4.4",
"@babel/preset-env": "^7.1.0",
"@babel/preset-typescript": "^7.1.0",
"@babel/plugin-proposal-decorators": "^7.4.0",
"@types/enzyme": "^3.1.10",
"@types/deep-diff": "^1.0.0",
"@types/jest": "^22.2.3",
@ -57,7 +58,7 @@
"@types/request": "^2.47.0",
"babel-eslint": "^10.0.1",
"babel-jest": "23.6.0",
"babel-loader": "^8.0.2",
"babel-loader": "^8.0.6",
"babel-preset-react-app": "^3.1.1",
"copy-webpack-plugin": "^4.5.1",
"coveralls": "^3.0.1",
@ -86,9 +87,10 @@
"rimraf": "^2.6.2",
"sass-loader": "^7.1.0",
"style-loader": "^0.21.0",
"ts-loader": "^4.4.2",
"terser": "^4.0.0",
"typescript": "3.1.1",
"typings-for-css-modules-loader": "^1.7.0",
"uglifyjs-webpack-plugin": "^2.1.3",
"url-loader": "^1.0.1",
"webpack": "4.16.4",
"webpack-cli": "^3.1.1",
@ -105,7 +107,7 @@
"@uifabric/merge-styles": "^6.2.0",
"@uifabric/styling": "^5.20.0",
"base64url": "2.0.0",
"botframework-config": "4.0.0-preview1.3.4",
"botframework-config": "4.4.0",
"botframework-schema": "^4.3.4",
"botframework-webchat": "^4.4.1",
"botframework-webchat-core": "^4.3.0",

Просмотреть файл

@ -31,16 +31,45 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { SharedConstants } from '@bfemulator/app-shared';
import { BotConfigWithPathImpl, CommandRegistryImpl } from '@bfemulator/sdk-shared';
import {
BotConfigWithPathImpl,
CommandRegistry,
CommandServiceImpl,
CommandServiceInstance,
} from '@bfemulator/sdk-shared';
import { combineReducers, createStore } from 'redux';
import * as BotActions from '../data/action/botActions';
import { bot } from '../data/reducer/bot';
import { resources } from '../data/reducer/resourcesReducer';
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
import { ActiveBotHelper } from '../ui/helpers/activeBotHelper';
import { registerCommands } from './botCommands';
import { BotCommands } from './botCommands';
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
const mockBotInfo = {
path: 'some/path.bot',
@ -74,24 +103,27 @@ jest.mock('../data/store', () => ({
return mockStore;
},
}));
jest.mock('../ui/dialogs/', () => ({}));
describe('The bot commands', () => {
let registry: CommandRegistryImpl;
let commandService: CommandServiceImpl;
let registry: CommandRegistry;
beforeAll(() => {
registry = new CommandRegistryImpl();
registerCommands(registry);
new BotCommands();
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
registry = commandService.registry;
});
it('should make the appropriate calls to switch bots', () => {
it('should make the appropriate calls to switch bots', async () => {
const remoteCallArgs = [];
CommandServiceImpl.remoteCall = async (...args: any[]) => {
commandService.remoteCall = async (...args: any[]) => {
remoteCallArgs.push(args);
return true;
return true as any;
};
const spy = jest.spyOn(ActiveBotHelper, 'confirmAndSwitchBots');
const { handler } = registry.getCommand(SharedConstants.Commands.Bot.Switch);
handler({});
const spy = jest.spyOn(ActiveBotHelper, 'confirmAndSwitchBots').mockResolvedValueOnce(true);
const handler = registry.getCommand(SharedConstants.Commands.Bot.Switch);
await handler({});
expect(spy).toHaveBeenCalledWith({});
expect(remoteCallArgs[0][0]).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
expect(remoteCallArgs[0][1]).toBe('bot_open');
@ -103,14 +135,14 @@ describe('The bot commands', () => {
it('should make the appropriate calls to close a bot', () => {
const spy = jest.spyOn(ActiveBotHelper, 'confirmAndCloseBot');
const { handler } = registry.getCommand(SharedConstants.Commands.Bot.Close);
const handler = registry.getCommand(SharedConstants.Commands.Bot.Close);
handler();
expect(spy).toHaveBeenCalled();
});
it('should make the appropriate calls to load a bot when the bot does not yet exist', () => {
const createSpy = jest.spyOn(ActiveBotHelper, 'confirmAndCreateBot');
const { handler } = registry.getCommand(SharedConstants.Commands.Bot.Load);
const handler = registry.getCommand(SharedConstants.Commands.Bot.Load);
handler({});
expect(createSpy).toHaveBeenCalledWith({}, '');
@ -118,7 +150,7 @@ describe('The bot commands', () => {
it('should make the appropriate calls to load a bot when the bot exists', () => {
const switchSpy = jest.spyOn(ActiveBotHelper, 'confirmAndSwitchBots');
const { handler } = registry.getCommand(SharedConstants.Commands.Bot.Load);
const handler = registry.getCommand(SharedConstants.Commands.Bot.Load);
handler({ path: 'some/path.bot' });
expect(switchSpy).toHaveBeenCalledWith({ path: 'some/path.bot' });
@ -126,8 +158,8 @@ describe('The bot commands', () => {
it('should make the appropriate calls to sync the bot list', () => {
const dispatchSpy = jest.spyOn(mockStore, 'dispatch');
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall');
const { handler } = registry.getCommand(SharedConstants.Commands.Bot.SyncBotList);
const remoteCallSpy = jest.spyOn(commandService, 'remoteCall');
const handler = registry.getCommand(SharedConstants.Commands.Bot.SyncBotList);
handler([{}]);
expect(dispatchSpy).toHaveBeenCalledWith(BotActions.loadBotInfos([{}]));
@ -136,11 +168,11 @@ describe('The bot commands', () => {
it('should make the appropriate call when setting the active bot', async () => {
const remoteCallArgs = [];
CommandServiceImpl.remoteCall = async (...args: any[]) => {
commandService.remoteCall = async (...args: any[]) => {
remoteCallArgs.push(args);
return true;
return true as any;
};
const { handler } = registry.getCommand(SharedConstants.Commands.Bot.SetActive);
const handler = registry.getCommand(SharedConstants.Commands.Bot.SetActive);
await handler(mockBot, mockBotInfo.path);
const state: any = mockStore.getState();
expect(state.bot.activeBot).toEqual(mockBot);
@ -149,10 +181,8 @@ describe('The bot commands', () => {
});
it('should dispatch the appropriate actions when updating the list of transcript files on disc', () => {
const { handler: transcriptFilesUpdated } = registry.getCommand(
SharedConstants.Commands.Bot.TranscriptFilesUpdated
);
const { handler: transcriptPathUpdated } = registry.getCommand(SharedConstants.Commands.Bot.TranscriptsPathUpdated);
const transcriptFilesUpdated = registry.getCommand(SharedConstants.Commands.Bot.TranscriptFilesUpdated);
const transcriptPathUpdated = registry.getCommand(SharedConstants.Commands.Bot.TranscriptsPathUpdated);
transcriptFilesUpdated([{ path: 'transcript/path.transcript' }]);
transcriptPathUpdated('transcript/');
const state: any = mockStore.getState();
@ -161,8 +191,8 @@ describe('The bot commands', () => {
});
it('should dispatch the appropriate actions when updating the list of chat files on disc', () => {
const { handler: chatFilesUpdated } = registry.getCommand(SharedConstants.Commands.Bot.ChatFilesUpdated);
const { handler: chatPathUpdated } = registry.getCommand(SharedConstants.Commands.Bot.ChatsPathUpdated);
const chatFilesUpdated = registry.getCommand(SharedConstants.Commands.Bot.ChatFilesUpdated);
const chatPathUpdated = registry.getCommand(SharedConstants.Commands.Bot.ChatsPathUpdated);
chatFilesUpdated([{ path: 'chat/path.chat' }]);
chatPathUpdated('chat/');
const state: any = mockStore.getState();

Просмотреть файл

@ -32,8 +32,9 @@
//
import { BotInfo, getBotDisplayName, SharedConstants } from '@bfemulator/app-shared';
import { BotConfigWithPath, CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { BotConfigWithPath, Command, CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { IFileService } from 'botframework-config/lib/schema';
import { newNotification } from '@bfemulator/app-shared';
import * as BotActions from '../data/action/botActions';
import * as FileActions from '../data/action/fileActions';
@ -45,82 +46,105 @@ import {
} from '../data/action/resourcesAction';
import { pathExistsInRecentBots } from '../data/botHelpers';
import { store } from '../data/store';
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
import { ActiveBotHelper } from '../ui/helpers/activeBotHelper';
import { beginAdd } from '../data/action/notificationActions';
const Commands = SharedConstants.Commands;
/** Registers bot commands */
export function registerCommands(commandRegistry: CommandRegistryImpl) {
const Commands = SharedConstants.Commands;
export class BotCommands {
@CommandServiceInstance()
private commandService: CommandServiceImpl;
// ---------------------------------------------------------------------------
// Switches the current active bot
commandRegistry.registerCommand(Commands.Bot.Switch, (bot: BotConfigWithPath | string) => {
@Command(Commands.Bot.Switch)
protected async switchBot(bot: BotConfigWithPath | string) {
let numOfServices;
if (typeof bot !== 'string') {
numOfServices = bot.services && bot.services.length;
}
CommandServiceImpl.remoteCall(Commands.Telemetry.TrackEvent, 'bot_open', {
method: 'bots_list',
numOfServices,
}).catch(_e => void 0);
return ActiveBotHelper.confirmAndSwitchBots(bot);
});
try {
await this.commandService.remoteCall(Commands.Telemetry.TrackEvent, 'bot_open', {
method: 'bots_list',
numOfServices,
});
return ActiveBotHelper.confirmAndSwitchBots(bot);
} catch (e) {
await beginAdd(newNotification(e));
}
}
// ---------------------------------------------------------------------------
// Closes the current active bot
commandRegistry.registerCommand(Commands.Bot.Close, () => ActiveBotHelper.confirmAndCloseBot());
@Command(Commands.Bot.Close)
protected async closeBot() {
try {
await ActiveBotHelper.confirmAndCloseBot();
} catch (e) {
await beginAdd(newNotification(e));
}
}
// ---------------------------------------------------------------------------
// Browse for a .bot file and open it
commandRegistry.registerCommand(Commands.Bot.OpenBrowse, () => ActiveBotHelper.confirmAndOpenBotFromFile());
@Command(Commands.Bot.OpenBrowse)
protected async browseForBotFile() {
try {
await ActiveBotHelper.confirmAndOpenBotFromFile();
} catch (e) {
await beginAdd(newNotification(e));
}
}
// ---------------------------------------------------------------------------
// Loads the bot on the client side using the activeBotHelper
commandRegistry.registerCommand(
Commands.Bot.Load,
(bot: BotConfigWithPath): Promise<any> => {
if (!pathExistsInRecentBots(bot.path)) {
// create and switch bots
return ActiveBotHelper.confirmAndCreateBot(bot, '');
}
return ActiveBotHelper.confirmAndSwitchBots(bot);
@Command(Commands.Bot.Load)
protected loadBot(bot: BotConfigWithPath): Promise<any> {
if (!pathExistsInRecentBots(bot.path)) {
// create and switch bots
return ActiveBotHelper.confirmAndCreateBot(bot, '');
}
);
return ActiveBotHelper.confirmAndSwitchBots(bot);
}
// ---------------------------------------------------------------------------
// Syncs the client side list of bots with bots arg (usually called from server side)
commandRegistry.registerCommand(
Commands.Bot.SyncBotList,
async (bots: BotInfo[]): Promise<void> => {
store.dispatch(BotActions.loadBotInfos(bots));
await CommandServiceImpl.remoteCall(Commands.Electron.UpdateFileMenu);
}
);
@Command(Commands.Bot.SyncBotList)
protected async syncBotList(bots: BotInfo[]): Promise<void> {
store.dispatch(BotActions.loadBotInfos(bots));
await this.commandService.remoteCall(Commands.Electron.UpdateFileMenu);
}
// ---------------------------------------------------------------------------
// Sets a bot as active (called from server-side)
commandRegistry.registerCommand(Commands.Bot.SetActive, async (bot: BotConfigWithPath, botDirectory: string) => {
@Command(Commands.Bot.SetActive)
protected async setActiveBot(bot: BotConfigWithPath, botDirectory: string) {
store.dispatch(BotActions.setActiveBot(bot));
store.dispatch(FileActions.setRoot(botDirectory));
await Promise.all([
CommandServiceImpl.remoteCall(Commands.Electron.UpdateFileMenu),
CommandServiceImpl.remoteCall(Commands.Electron.SetTitleBar, getBotDisplayName(bot)),
this.commandService.remoteCall(Commands.Electron.UpdateFileMenu),
this.commandService.remoteCall(Commands.Electron.SetTitleBar, getBotDisplayName(bot)),
]);
});
}
commandRegistry.registerCommand(Commands.Bot.TranscriptFilesUpdated, (transcripts: IFileService[]) => {
@Command(Commands.Bot.TranscriptFilesUpdated)
protected transcriptFilesUpdated(transcripts: IFileService[]) {
store.dispatch(transcriptsUpdated(transcripts));
});
}
commandRegistry.registerCommand(Commands.Bot.ChatFilesUpdated, (chatFiles: IFileService[]) => {
@Command(Commands.Bot.ChatFilesUpdated)
protected chatFilesUpdated(chatFiles: IFileService[]) {
store.dispatch(chatFilesUpdated(chatFiles));
});
}
commandRegistry.registerCommand(Commands.Bot.TranscriptsPathUpdated, (path: string) => {
@Command(Commands.Bot.TranscriptsPathUpdated)
protected transcriptsPathUpdated(path: string) {
store.dispatch(transcriptDirectoryUpdated(path));
});
}
commandRegistry.registerCommand(Commands.Bot.ChatsPathUpdated, (path: string) => {
@Command(Commands.Bot.ChatsPathUpdated)
protected chatsPathUpdated(path: string) {
store.dispatch(chatsDirectoryUpdated(path));
});
}
}

Просмотреть файл

@ -1,36 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
export const CommandRegistry = new CommandRegistryImpl();

Просмотреть файл

@ -32,36 +32,41 @@
//
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { Command } from '@bfemulator/sdk-shared';
const { Electron } = SharedConstants.Commands;
/** Registers electron commands */
export function registerCommands(commandRegistry: CommandRegistryImpl) {
const { Electron } = SharedConstants.Commands;
export class ElectronCommands {
// ---------------------------------------------------------------------------
// Toggle inspector dev tools for all open inspectors
commandRegistry.registerCommand(Electron.ToggleDevTools, () => {
@Command(Electron.ToggleDevTools)
protected toggleDevTools() {
window.dispatchEvent(new Event('toggle-inspector-devtools'));
});
}
// ---------------------------------------------------------------------------
// An update is ready to install
commandRegistry.registerCommand(Electron.UpdateAvailable, (...args: any[]) => {
@Command(Electron.UpdateAvailable)
protected emulatorUpdateAvailable(...args: any[]) {
// TODO: Show a notification
// eslint-disable-next-line no-console
console.log('Update available', ...args);
});
}
// ---------------------------------------------------------------------------
// Application is up to date
commandRegistry.registerCommand(Electron.UpdateNotAvailable, () => {
@Command(Electron.UpdateNotAvailable)
protected emulatorUpdateNotAvailable() {
// TODO: Show a notification
// eslint-disable-next-line no-console
console.log('Application is up to date');
});
}
// ---------------------------------------------------------------------------
// Open About dialog
commandRegistry.registerCommand(Electron.ShowAboutDialog, () => {
@Command(Electron.ShowAboutDialog)
protected showAboutDialog() {
// TODO: Show about dialog (native dialog box)
});
}
}

Просмотреть файл

@ -31,8 +31,8 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { newNotification, SharedConstants } from '@bfemulator/app-shared';
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { combineReducers, createStore } from 'redux';
import { CommandRegistry, CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { clientAwareSettingsChanged } from '../data/action/clientAwareSettingsActions';
import { beginAdd } from '../data/action/notificationActions';
@ -42,10 +42,9 @@ import { clientAwareSettings } from '../data/reducer/clientAwareSettingsReducer'
import { editor } from '../data/reducer/editor';
import { framework } from '../data/reducer/frameworkSettingsReducer';
import { RootState } from '../data/store';
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
import { frameworkSettingsChanged } from '../data/action/frameworkSettingsActions';
import { registerCommands } from './emulatorCommands';
import { EmulatorCommands } from './emulatorCommands';
const mockEndpoint = {
endpoint: 'https://localhost:8080/api/messages',
@ -57,13 +56,41 @@ jest.mock('../data/store', () => ({
return mockStore;
},
}));
jest.mock('../ui/dialogs/', () => ({}));
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
describe('The emulator commands', () => {
let registry: CommandRegistryImpl;
let commandService: CommandServiceImpl;
let registry: CommandRegistry;
beforeAll(() => {
registry = new CommandRegistryImpl();
registerCommands(registry);
new EmulatorCommands();
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
registry = commandService.registry;
});
beforeEach(() => {
@ -75,12 +102,14 @@ describe('The emulator commands', () => {
locale: 'en-us',
serverUrl: 'https://localhost',
debugMode: 1,
appPath: '',
savedBotUrls: [],
})
);
});
it('Should open a new emulator tabbed document for an endpoint', () => {
const { handler } = registry.getCommand(SharedConstants.Commands.Emulator.NewLiveChat);
const handler = registry.getCommand(SharedConstants.Commands.Emulator.NewLiveChat);
const documentId = handler(mockEndpoint, false);
const state: RootState = mockStore.getState();
const documentIds = Object.keys(state.chat.chats);
@ -93,7 +122,7 @@ describe('The emulator commands', () => {
it('should open a new emulator tabbed document for an endpoint and use the custom user id', () => {
let state: RootState = mockStore.getState();
mockStore.dispatch(frameworkSettingsChanged({ ...state.framework, userGUID: 'customUserId' }));
const { handler } = registry.getCommand(SharedConstants.Commands.Emulator.NewLiveChat);
const handler = registry.getCommand(SharedConstants.Commands.Emulator.NewLiveChat);
const documentId = handler(mockEndpoint, false);
state = mockStore.getState();
const document = state.chat.chats[documentId];
@ -101,7 +130,7 @@ describe('The emulator commands', () => {
});
it('should set the active tab of an existing chat', () => {
const { handler } = registry.getCommand(SharedConstants.Commands.Emulator.NewLiveChat);
const handler = registry.getCommand(SharedConstants.Commands.Emulator.NewLiveChat);
const documentId = handler(mockEndpoint, false);
const secondDocumentId = handler({
endpoint: 'https://localhost:8181/api/messages',
@ -114,7 +143,7 @@ describe('The emulator commands', () => {
});
it('should open a transcript', () => {
const { handler } = registry.getCommand(SharedConstants.Commands.Emulator.OpenTranscript);
const handler = registry.getCommand(SharedConstants.Commands.Emulator.OpenTranscript);
const filePath = 'transcript.transcript';
handler(filePath, filePath);
@ -124,9 +153,9 @@ describe('The emulator commands', () => {
});
it('Should prompt to open a transcript', async () => {
const { handler } = registry.getCommand(SharedConstants.Commands.Emulator.PromptToOpenTranscript);
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue('transcript.transcript');
const callSpy = jest.spyOn(CommandServiceImpl, 'call').mockResolvedValue(null);
const handler = registry.getCommand(SharedConstants.Commands.Emulator.PromptToOpenTranscript);
const remoteCallSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue('transcript.transcript');
const callSpy = jest.spyOn(commandService, 'call').mockResolvedValue(null);
await handler();
@ -144,9 +173,9 @@ describe('The emulator commands', () => {
});
it('should dispatch a notification when opening a transcript fails', async () => {
const { handler } = registry.getCommand(SharedConstants.Commands.Emulator.PromptToOpenTranscript);
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue('transcript.transcript');
const callSpy = jest.spyOn(CommandServiceImpl, 'call').mockImplementationOnce(() => {
const handler = registry.getCommand(SharedConstants.Commands.Emulator.PromptToOpenTranscript);
const remoteCallSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue('transcript.transcript');
const callSpy = jest.spyOn(commandService, 'call').mockImplementationOnce(() => {
throw new Error('Oh noes!');
});
const dispatchSpy = jest.spyOn(mockStore, 'dispatch');
@ -163,21 +192,21 @@ describe('The emulator commands', () => {
});
it('should reload a transcript', async () => {
const { handler: openTranscriptHandler } = registry.getCommand(SharedConstants.Commands.Emulator.OpenTranscript);
const openTranscriptHandler = registry.getCommand(SharedConstants.Commands.Emulator.OpenTranscript);
await openTranscriptHandler('transcript.transcript');
let state = mockStore.getState();
expect(state.chat.changeKey).toBe(1);
const { handler } = registry.getCommand(SharedConstants.Commands.Emulator.ReloadTranscript);
const handler = registry.getCommand(SharedConstants.Commands.Emulator.ReloadTranscript);
await handler('transcript.transcript');
state = mockStore.getState();
expect(state.chat.changeKey).toBe(3);
});
it('should open a chat file', async () => {
const callSpy = jest.spyOn(CommandServiceImpl, 'call').mockResolvedValue(true);
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue(true);
const callSpy = jest.spyOn(commandService, 'call').mockResolvedValue(true);
const remoteCallSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue(true);
const { handler: openChatFileHandler } = registry.getCommand(SharedConstants.Commands.Emulator.OpenChatFile);
const openChatFileHandler = registry.getCommand(SharedConstants.Commands.Emulator.OpenChatFile);
await openChatFileHandler('some/path.chat', true);
expect(remoteCallSpy).toHaveBeenCalledWith(SharedConstants.Commands.Emulator.OpenChatFile, 'some/path.chat');
expect(callSpy).toHaveBeenCalledWith(

Просмотреть файл

@ -33,9 +33,10 @@
// import base64Url from 'base64url';
// import { createDirectLine } from 'botframework-webchat';
import { DebugMode, newNotification, SharedConstants } from '@bfemulator/app-shared';
import { CommandRegistryImpl, isLocalHostUrl, uniqueId } from '@bfemulator/sdk-shared';
import { CommandServiceImpl, CommandServiceInstance, isLocalHostUrl, uniqueId } from '@bfemulator/sdk-shared';
import { IEndpointService } from 'botframework-config/lib/schema';
import { Activity } from 'botframework-schema';
import { Command } from '@bfemulator/sdk-shared';
import * as Constants from '../constants';
import * as ChatActions from '../data/action/chatActions';
@ -43,107 +44,105 @@ import * as EditorActions from '../data/action/editorActions';
import { beginAdd } from '../data/action/notificationActions';
import { getTabGroupForDocument } from '../data/editorHelpers';
import { store } from '../data/store';
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
/** Registers emulator (actual conversation emulation logic) commands */
export function registerCommands(commandRegistry: CommandRegistryImpl) {
const {
Emulator,
Telemetry: { TrackEvent },
} = SharedConstants.Commands;
const {
Emulator,
Telemetry: { TrackEvent },
} = SharedConstants.Commands;
export class EmulatorCommands {
@CommandServiceInstance()
private commandService: CommandServiceImpl;
// ---------------------------------------------------------------------------
// Open a new emulator tabbed document
commandRegistry.registerCommand(
Emulator.NewLiveChat,
(
endpoint: IEndpointService,
focusExistingChat: boolean = false,
conversationId: string,
mode: ChatActions.ChatMode = 'livechat'
) => {
const state = store.getState();
let documentId: string;
@Command(Emulator.NewLiveChat)
protected newLiveChat(
endpoint: IEndpointService,
focusExistingChat: boolean = false,
conversationId: string,
mode: ChatActions.ChatMode = 'livechat'
) {
const state = store.getState();
let documentId: string;
if (focusExistingChat && state.chat.chats) {
const { chats } = state.chat;
documentId = Object.keys(chats).find(docId => {
const { [docId]: chat } = chats;
// If we have a conversationId, the match must include it.
return chat.endpointUrl === endpoint.endpoint && (!conversationId || chat.conversationId === conversationId);
});
}
if (!documentId) {
documentId = uniqueId();
const { currentUserId } = state.clientAwareSettings.users;
const customUserId = state.framework.userGUID;
const action = ChatActions.newChat(documentId, mode, {
botId: 'bot',
endpointId: endpoint.id,
endpointUrl: endpoint.endpoint,
userId: customUserId || currentUserId,
conversationId,
// directLine: createDirectLine({
// secret: base64Url.encode(JSON.stringify({ conversationId, endpointId: endpoint.id })),
// domain: `${ state.clientAwareSettings.serverUrl }/v3/directline`,
// webSocket: false,
// })
});
if (state.clientAwareSettings.debugMode === DebugMode.Sidecar) {
action.payload.ui.horizontalSplitter[0].percentage = 75;
action.payload.ui.verticalSplitter[0].percentage = 25;
}
store.dispatch(action);
}
if (!isLocalHostUrl(endpoint.endpoint)) {
CommandServiceImpl.remoteCall(TrackEvent, 'livechat_openRemote').catch(_e => void 0);
}
store.dispatch(
EditorActions.open({
contentType: Constants.CONTENT_TYPE_LIVE_CHAT,
documentId,
isGlobal: false,
})
);
return documentId;
if (focusExistingChat && state.chat.chats) {
const { chats } = state.chat;
documentId = Object.keys(chats).find(docId => {
const { [docId]: chat } = chats;
// If we have a conversationId, the match must include it.
return chat.endpointUrl === endpoint.endpoint && (!conversationId || chat.conversationId === conversationId);
});
}
);
if (!documentId) {
documentId = uniqueId();
const { currentUserId } = state.clientAwareSettings.users;
const customUserId = state.framework.userGUID;
const action = ChatActions.newChat(documentId, mode, {
botId: 'bot',
endpointId: endpoint.id,
endpointUrl: endpoint.endpoint,
userId: customUserId || currentUserId,
conversationId,
// directLine: createDirectLine({
// secret: base64Url.encode(JSON.stringify({ conversationId, endpointId: endpoint.id })),
// domain: `${ state.clientAwareSettings.serverUrl }/v3/directline`,
// webSocket: false,
// })
});
if (state.clientAwareSettings.debugMode === DebugMode.Sidecar) {
action.payload.ui.horizontalSplitter[0].percentage = 75;
action.payload.ui.verticalSplitter[0].percentage = 25;
}
store.dispatch(action);
}
if (!isLocalHostUrl(endpoint.endpoint)) {
this.commandService.remoteCall(TrackEvent, 'livechat_openRemote').catch(_e => void 0);
}
store.dispatch(
EditorActions.open({
contentType: Constants.CONTENT_TYPE_LIVE_CHAT,
documentId,
isGlobal: false,
})
);
return documentId;
}
// ---------------------------------------------------------------------------
// Open the transcript file in a tabbed document
commandRegistry.registerCommand(
Emulator.OpenTranscript,
(filePath: string, fileName: string, additionalData?: object) => {
const tabGroup = getTabGroupForDocument(filePath);
const { currentUserId } = store.getState().clientAwareSettings.users;
if (!tabGroup) {
store.dispatch(
ChatActions.newChat(filePath, 'transcript', {
...additionalData,
botId: 'bot',
userId: currentUserId,
})
);
}
@Command(Emulator.OpenTranscript)
protected openTranscript(filePath: string, fileName: string, additionalData?: object) {
const tabGroup = getTabGroupForDocument(filePath);
const { currentUserId } = store.getState().clientAwareSettings.users;
if (!tabGroup) {
store.dispatch(
EditorActions.open({
contentType: Constants.CONTENT_TYPE_TRANSCRIPT,
documentId: filePath,
fileName,
filePath,
isGlobal: false,
ChatActions.newChat(filePath, 'transcript', {
...additionalData,
botId: 'bot',
userId: currentUserId,
})
);
}
);
store.dispatch(
EditorActions.open({
contentType: Constants.CONTENT_TYPE_TRANSCRIPT,
documentId: filePath,
fileName,
filePath,
isGlobal: false,
})
);
}
// ---------------------------------------------------------------------------
// Prompt to open a transcript file, then open it
commandRegistry.registerCommand(Emulator.PromptToOpenTranscript, async () => {
@Command(Emulator.PromptToOpenTranscript)
protected async promptToOpenTranscript() {
const dialogOptions = {
title: 'Open transcript file',
buttonLabel: 'Choose file',
@ -157,53 +156,54 @@ export function registerCommands(commandRegistry: CommandRegistryImpl) {
};
try {
const { ShowOpenDialog } = SharedConstants.Commands.Electron;
const filename = await CommandServiceImpl.remoteCall(ShowOpenDialog, dialogOptions);
const filename = await this.commandService.remoteCall(ShowOpenDialog, dialogOptions);
if (filename) {
await CommandServiceImpl.call(Emulator.OpenTranscript, filename);
CommandServiceImpl.remoteCall(TrackEvent, 'transcriptFile_open', {
method: 'file_menu',
}).catch(_e => void 0);
await this.commandService.call(Emulator.OpenTranscript, filename);
this.commandService
.remoteCall(TrackEvent, 'transcriptFile_open', {
method: 'file_menu',
})
.catch(_e => void 0);
}
} catch (e) {
const errMsg = `Error while opening transcript file: ${e}`;
const notification = newNotification(errMsg);
store.dispatch(beginAdd(notification));
}
});
}
// ---------------------------------------------------------------------------
// Same as open transcript, except that it closes the transcript first, before reopening it
commandRegistry.registerCommand(
Emulator.ReloadTranscript,
(filePath: string, fileName: string, additionalData?: object) => {
const tabGroup = getTabGroupForDocument(filePath);
const { currentUserId } = store.getState().clientAwareSettings.users;
if (tabGroup) {
store.dispatch(EditorActions.close(getTabGroupForDocument(filePath), filePath));
store.dispatch(ChatActions.closeDocument(filePath));
}
store.dispatch(
ChatActions.newChat(filePath, 'transcript', {
...additionalData,
botId: 'bot',
userId: currentUserId,
})
);
store.dispatch(
EditorActions.open({
contentType: Constants.CONTENT_TYPE_TRANSCRIPT,
documentId: filePath,
filePath,
fileName,
isGlobal: false,
})
);
@Command(Emulator.ReloadTranscript)
protected reloadTranscript(filePath: string, fileName: string, additionalData?: object) {
const tabGroup = getTabGroupForDocument(filePath);
const { currentUserId } = store.getState().clientAwareSettings.users;
if (tabGroup) {
store.dispatch(EditorActions.close(getTabGroupForDocument(filePath), filePath));
store.dispatch(ChatActions.closeDocument(filePath));
}
);
store.dispatch(
ChatActions.newChat(filePath, 'transcript', {
...additionalData,
botId: 'bot',
userId: currentUserId,
})
);
store.dispatch(
EditorActions.open({
contentType: Constants.CONTENT_TYPE_TRANSCRIPT,
documentId: filePath,
filePath,
fileName,
isGlobal: false,
})
);
}
// ---------------------------------------------------------------------------
// Open the chat file in a tabbed document as a transcript
commandRegistry.registerCommand(Emulator.OpenChatFile, async (filePath: string, reload?: boolean) => {
@Command(Emulator.OpenChatFile)
protected async openChatFile(filePath: string, reload?: boolean) {
try {
// wait for the main side to use the chatdown library to parse the activities (transcript) out of the .chat file
const {
@ -212,16 +212,16 @@ export function registerCommands(commandRegistry: CommandRegistryImpl) {
}: {
activities: Activity[];
fileName: string;
} = await CommandServiceImpl.remoteCall(Emulator.OpenChatFile, filePath);
} = await this.commandService.remoteCall<any>(Emulator.OpenChatFile, filePath);
// open or reload the transcript
if (reload) {
await CommandServiceImpl.call(Emulator.ReloadTranscript, filePath, fileName, { activities, inMemory: true });
await this.commandService.call(Emulator.ReloadTranscript, filePath, fileName, { activities, inMemory: true });
} else {
await CommandServiceImpl.call(Emulator.OpenTranscript, filePath, fileName, { activities, inMemory: true });
await this.commandService.call(Emulator.OpenTranscript, filePath, fileName, { activities, inMemory: true });
}
} catch (err) {
throw new Error(`Error while retrieving activities from main side: ${err}`);
}
});
}
}

Просмотреть файл

@ -32,39 +32,44 @@
//
import { isChatFile, isTranscriptFile, SharedConstants } from '@bfemulator/app-shared';
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { Command } from '@bfemulator/sdk-shared';
import * as EditorActions from '../data/action/editorActions';
import * as FileActions from '../data/action/fileActions';
import { store } from '../data/store';
const { File } = SharedConstants.Commands;
/** Registers file commands */
export function registerCommands(commandRegistry: CommandRegistryImpl) {
const { File } = SharedConstants.Commands;
export class FileCommands {
// ---------------------------------------------------------------------------
// Adds a file to the file store
commandRegistry.registerCommand(File.Add, payload => {
@Command(File.Add)
protected addFileToStore(payload) {
store.dispatch(FileActions.addFile(payload));
});
}
// ---------------------------------------------------------------------------
// Removes a file from the file store
commandRegistry.registerCommand(File.Remove, path => {
@Command(File.Remove)
protected removeFileFromStore(path) {
store.dispatch(FileActions.removeFile(path));
});
}
// ---------------------------------------------------------------------------
// Clears the file store
commandRegistry.registerCommand(File.Clear, () => {
@Command(File.Clear)
protected clearFileStore() {
store.dispatch(FileActions.clear());
});
}
// ---------------------------------------------------------------------------
// Called for files in the bot's directory whose contents have changed on disk
commandRegistry.registerCommand(File.Changed, (filename: string) => {
@Command(File.Changed)
protected fileChangedOnDisk(filename: string) {
// add the filename to pending updates and prompt the user once the document is focused again
if (isChatFile(filename) || isTranscriptFile(filename)) {
store.dispatch(EditorActions.addDocPendingChange(filename));
}
});
}
}

Просмотреть файл

@ -31,5 +31,22 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
export * from './commandRegistry';
export * from './registerAllCommands';
import { BotCommands } from './botCommands';
import { ElectronCommands } from './electronCommands';
import { EmulatorCommands } from './emulatorCommands';
import { FileCommands } from './fileCommands';
import { NotificationCommands } from './notificationCommands';
import { SettingsCommands } from './settingsCommands';
import { UiCommands } from './uiCommands';
import { MiscCommands } from './miscCommands';
export const commands = [
new BotCommands(),
new ElectronCommands(),
new EmulatorCommands(),
new FileCommands(),
new NotificationCommands(),
new SettingsCommands(),
new UiCommands(),
new MiscCommands(),
];

Просмотреть файл

@ -32,16 +32,18 @@
//
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { Command } from '@bfemulator/sdk-shared';
import { store } from '../data/store';
const Commands = SharedConstants.Commands.Misc;
/** Registers miscellaneous commands */
export function registerCommands(commandRegistry: CommandRegistryImpl) {
const Commands = SharedConstants.Commands.Misc;
export class MiscCommands {
// ---------------------------------------------------------------------------
// Returns the store's state
commandRegistry.registerCommand(Commands.GetStoreState, () => {
@Command(Commands.GetStoreState)
protected getStoreState() {
return store.getState();
});
}
}

Просмотреть файл

@ -32,28 +32,31 @@
//
import { Notification, SharedConstants } from '@bfemulator/app-shared';
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { Command } from '@bfemulator/sdk-shared';
import * as NotificationActions from '../data/action/notificationActions';
import { store } from '../data/store';
import { getGlobal } from '../utils';
const Commands = SharedConstants.Commands.Notifications;
/** Registers notification commands */
export function registerCommands(commandRegistry: CommandRegistryImpl) {
const Commands = SharedConstants.Commands.Notifications;
export class NotificationCommands {
// ---------------------------------------------------------------------------
// Adds a notification from the main side to the store / notification manager
commandRegistry.registerCommand(Commands.Add, (notification: Notification) => {
@Command(Commands.Add)
protected addNotificationFromMain(notification: Notification) {
if (!notification) {
notification = getGlobal(SharedConstants.NOTIFICATION_FROM_MAIN);
}
store.dispatch(NotificationActions.beginAdd(notification));
});
}
// ---------------------------------------------------------------------------
// Removes a notification from the store / notification manager
commandRegistry.registerCommand(Commands.Remove, (id: string) => {
@Command(Commands.Remove)
protected removeNotificationFromStore(id: string) {
store.dispatch(NotificationActions.beginRemove(id));
});
}
}

Просмотреть файл

@ -1,60 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { ExtensionManager } from '../extensions';
import * as LogService from '../platform/log/logService';
import { registerCommands as registerBotCommands } from './botCommands';
import { registerCommands as registerElectronCommands } from './electronCommands';
import { registerCommands as registerEmulatorCommands } from './emulatorCommands';
import { registerCommands as registerFileCommands } from './fileCommands';
import { registerCommands as registerMiscCommands } from './miscCommands';
import { registerCommands as registerNotificationCommands } from './notificationCommands';
import { registerCommands as registerSettingsCommand } from './settingsCommands';
import { registerCommands as registerUICommands } from './uiCommands';
/** Registers all commands */
export function registerAllCommands(commandRegistry: CommandRegistryImpl) {
LogService.registerCommands(commandRegistry);
ExtensionManager.registerCommands(commandRegistry);
registerBotCommands(commandRegistry);
registerElectronCommands(commandRegistry);
registerEmulatorCommands(commandRegistry);
registerFileCommands(commandRegistry);
registerMiscCommands(commandRegistry);
registerNotificationCommands(commandRegistry);
registerUICommands(commandRegistry);
registerSettingsCommand(commandRegistry);
}

Просмотреть файл

@ -31,16 +31,15 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandRegistry, CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { DebugMode, SharedConstants } from '@bfemulator/app-shared';
import { combineReducers, createStore } from 'redux';
import { DebugMode } from '@bfemulator/app-shared';
import { clientAwareSettings } from '../data/reducer/clientAwareSettingsReducer';
import { store } from '../data/store';
import { clientAwareSettingsChanged } from '../data/action/clientAwareSettingsActions';
import { registerCommands } from './settingsCommands';
import { SettingsCommands } from './settingsCommands';
const mockStore = createStore(combineReducers({ clientAwareSettings }));
jest.mock('../data/store', () => ({
@ -49,15 +48,44 @@ jest.mock('../data/store', () => ({
},
}));
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
describe('the settings commands', () => {
let registry: CommandRegistryImpl;
let commandService: CommandServiceImpl;
let registry: CommandRegistry;
beforeAll(() => {
registry = new CommandRegistryImpl();
registerCommands(registry);
new SettingsCommands();
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
registry = commandService.registry;
});
it('should dispatch to the store when settings are sent from the main side', () => {
const command = registry.getCommand(SharedConstants.Commands.Settings.ReceiveGlobalSettings).handler;
const command = registry.getCommand(SharedConstants.Commands.Settings.ReceiveGlobalSettings);
const dispatchSpy = jest.spyOn(store, 'dispatch');
command({ debugMode: DebugMode.Normal });
expect(dispatchSpy).toHaveBeenCalledWith(clientAwareSettingsChanged({ debugMode: DebugMode.Normal } as any));

Просмотреть файл

@ -32,20 +32,24 @@
//
import { ClientAwareSettings, SharedConstants } from '@bfemulator/app-shared';
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { Command, CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { clientAwareSettingsChanged } from '../data/action/clientAwareSettingsActions';
import { store } from '../data/store';
/** Registers settings commands */
export function registerCommands(commandRegistry: CommandRegistryImpl) {
const { Settings } = SharedConstants.Commands;
const { Settings } = SharedConstants.Commands;
commandRegistry.registerCommand(Settings.ReceiveGlobalSettings, async (settings: ClientAwareSettings) => {
/** Registers settings commands */
export class SettingsCommands {
@CommandServiceInstance()
private commandService: CommandServiceImpl;
@Command(Settings.ReceiveGlobalSettings)
protected async receiveGlobalSettings(settings: ClientAwareSettings) {
const state = store.getState();
store.dispatch(clientAwareSettingsChanged(settings));
if (state.clientAwareSettings.debugMode !== settings.debugMode) {
await commandRegistry.getCommand(SharedConstants.Commands.UI.SwitchDebugMode).handler(settings.debugMode);
await this.commandService.call(SharedConstants.Commands.UI.SwitchDebugMode, settings.debugMode);
}
});
}
}

Просмотреть файл

@ -31,7 +31,7 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { DebugMode, SharedConstants } from '@bfemulator/app-shared';
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { CommandRegistry, CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { CONTENT_TYPE_APP_SETTINGS, DOCUMENT_ID_APP_SETTINGS } from '../constants';
import { AzureAuthAction, AzureAuthWorkflow, invalidateArmToken } from '../data/action/azureAuthActions';
@ -47,53 +47,73 @@ import {
OpenBotDialogContainer,
SecretPromptDialogContainer,
} from '../ui/dialogs';
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
import { ExplorerActions } from '../data/action/explorerActions';
import { SWITCH_DEBUG_MODE } from '../data/action/debugModeAction';
import { ActiveBotHelper } from '../ui/helpers/activeBotHelper';
import { registerCommands } from './uiCommands';
import { UiCommands } from './uiCommands';
jest.mock('../ui/dialogs', () => ({
AzureLoginPromptDialogContainer: class {},
AzureLoginSuccessDialogContainer: class {},
BotCreationDialog: class {},
DialogService: { showDialog: () => Promise.resolve(true) },
SecretPromptDialog: class {},
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
const Commands = SharedConstants.Commands.UI;
describe('the uiCommands', () => {
let registry: CommandRegistryImpl;
let commandService: CommandServiceImpl;
let registry: CommandRegistry;
beforeAll(() => {
registry = new CommandRegistryImpl();
registerCommands(registry);
new UiCommands();
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
registry = commandService.registry;
});
it('should showExplorer the welcome page when the ShowWelcomePage command is dispatched', async () => {
const spy = jest.spyOn(editorHelpers, 'showWelcomePage');
await registry.getCommand(Commands.ShowWelcomePage).handler();
await registry.getCommand(Commands.ShowWelcomePage)();
expect(spy).toHaveBeenCalled();
});
it('should call DialogService.showDialog when the ShowBotCreationDialog command is dispatched', async () => {
const spy = jest.spyOn(DialogService, 'showDialog');
const result = await registry.getCommand(Commands.ShowBotCreationDialog).handler();
const spy = jest.spyOn(DialogService, 'showDialog').mockResolvedValueOnce(true);
const result = await registry.getCommand(Commands.ShowBotCreationDialog)();
expect(spy).toHaveBeenCalledWith(BotCreationDialog);
expect(result).toBe(true);
});
it('should call DialogService.showDialog when the ShowSecretPromptDialog command is dispatched', async () => {
const spy = jest.spyOn(DialogService, 'showDialog');
const result = await registry.getCommand(Commands.ShowSecretPromptDialog).handler();
const spy = jest.spyOn(DialogService, 'showDialog').mockResolvedValueOnce(true);
const result = await registry.getCommand(Commands.ShowSecretPromptDialog)();
expect(spy).toHaveBeenCalledWith(SecretPromptDialogContainer);
expect(result).toBe(true);
});
it('should call DialogService.showDialog when the ShowOpenBotDialog command is dispatched', async () => {
const spy = jest.spyOn(DialogService, 'showDialog');
const result = await registry.getCommand(Commands.ShowOpenBotDialog).handler();
const spy = jest.spyOn(DialogService, 'showDialog').mockResolvedValueOnce(true);
const result = await registry.getCommand(Commands.ShowOpenBotDialog)();
expect(spy).toHaveBeenCalledWith(OpenBotDialogContainer);
expect(result).toBe(true);
});
@ -103,7 +123,7 @@ describe('the uiCommands', () => {
// eslint-disable-next-line prefer-const
let arg: SelectNavBarAction = {} as SelectNavBarAction;
store.dispatch = action => ((arg as any) = action);
registry.getCommand(Commands.SwitchNavBarTab).handler('Do it Nauuuw!');
registry.getCommand(Commands.SwitchNavBarTab)('Do it Nauuuw!');
expect(arg.type).toBe(NavBarActions.select);
expect(arg.payload.selection).toBe('Do it Nauuuw!');
});
@ -112,7 +132,7 @@ describe('the uiCommands', () => {
// eslint-disable-next-line prefer-const
let arg: OpenEditorAction = {} as OpenEditorAction;
store.dispatch = action => ((arg as any) = action);
registry.getCommand(Commands.ShowAppSettings).handler();
registry.getCommand(Commands.ShowAppSettings)();
expect(arg.type).toBe(EditorActions.open);
expect(arg.payload.contentType).toBe(CONTENT_TYPE_APP_SETTINGS);
expect(arg.payload.documentId).toBe(DOCUMENT_ID_APP_SETTINGS);
@ -123,7 +143,7 @@ describe('the uiCommands', () => {
// eslint-disable-next-line prefer-const
let arg: AzureAuthAction<AzureAuthWorkflow> = {} as AzureAuthAction<AzureAuthWorkflow>;
store.dispatch = action => ((arg as any) = action);
registry.getCommand(Commands.SignInToAzure).handler();
registry.getCommand(Commands.SignInToAzure)();
expect(arg.payload.loginSuccessDialog).toBe(AzureLoginSuccessDialogContainer);
expect(arg.payload.promptDialog).toBe(AzureLoginPromptDialogContainer);
});
@ -132,17 +152,17 @@ describe('the uiCommands', () => {
// eslint-disable-next-line prefer-const
let arg: AzureAuthAction<void> = {} as AzureAuthAction<void>;
store.dispatch = action => ((arg as any) = action);
registry.getCommand(Commands.InvalidateAzureArmToken).handler();
registry.getCommand(Commands.InvalidateAzureArmToken)();
expect(arg).toEqual(invalidateArmToken());
});
});
it('should set the proper href on the theme tag when the SwitchTheme command is dispatched', () => {
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall');
const remoteCallSpy = jest.spyOn(commandService, 'remoteCall');
const link = document.createElement('link');
link.id = 'themeVars';
document.querySelector('head').appendChild(link);
registry.getCommand(Commands.SwitchTheme).handler('light', './light.css');
registry.getCommand(Commands.SwitchTheme)('light', './light.css');
expect(link.href).toBe('http://localhost/light.css');
expect(remoteCallSpy).toHaveBeenCalledWith(SharedConstants.Commands.Telemetry.TrackEvent, 'app_chooseTheme', {
themeName: 'light',
@ -156,7 +176,7 @@ describe('the uiCommands', () => {
return action;
};
const closeActiveBotSpy = jest.spyOn(ActiveBotHelper, 'closeActiveBot').mockResolvedValueOnce(true);
await registry.getCommand(Commands.SwitchDebugMode).handler(DebugMode.Sidecar);
await registry.getCommand(Commands.SwitchDebugMode)(DebugMode.Sidecar);
expect(dispatchedActions.length).toBe(2);
expect(closeActiveBotSpy).toHaveBeenCalled();
[ExplorerActions.Show, SWITCH_DEBUG_MODE].forEach((type, index) =>
@ -171,9 +191,7 @@ describe('the uiCommands', () => {
dispatchedActions.push(action);
return action;
};
await registry
.getCommand(Commands.ShowMarkdownPage)
.handler('http://localhost', 'Yo!', { navigator: { onLine: false } });
await registry.getCommand(Commands.ShowMarkdownPage)('http://localhost', 'Yo!', { navigator: { onLine: false } });
expect(dispatchedActions.length).toBe(1);
expect(dispatchedActions[0].payload.meta).toEqual({
markdown: '',
@ -188,10 +206,8 @@ describe('the uiCommands', () => {
dispatchedActions.push(action);
return action;
};
jest.spyOn(CommandServiceImpl, 'remoteCall').mockRejectedValueOnce('oh noes! ENOTFOUND');
await registry
.getCommand(Commands.ShowMarkdownPage)
.handler('http://localhost', 'Yo!', { navigator: { onLine: true } });
jest.spyOn(commandService, 'remoteCall').mockRejectedValueOnce('oh noes! ENOTFOUND');
await registry.getCommand(Commands.ShowMarkdownPage)('http://localhost', 'Yo!', { navigator: { onLine: true } });
expect(dispatchedActions.length).toBe(1);
expect(dispatchedActions[0].payload.meta).toEqual({
markdown: '',
@ -206,10 +222,8 @@ describe('the uiCommands', () => {
dispatchedActions.push(action);
return action;
};
jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValueOnce(true);
await registry
.getCommand(Commands.ShowMarkdownPage)
.handler('http://localhost', 'Yo!', { navigator: { onLine: true } });
jest.spyOn(commandService, 'remoteCall').mockResolvedValueOnce(true);
await registry.getCommand(Commands.ShowMarkdownPage)('http://localhost', 'Yo!', { navigator: { onLine: true } });
expect(dispatchedActions.length).toBe(1);
expect(dispatchedActions[0].payload).toEqual({
contentType: 'application/vnd.microsoft.bfemulator.document.markdown',

Просмотреть файл

@ -32,8 +32,9 @@
//
import { DebugMode, SharedConstants } from '@bfemulator/app-shared';
import { CommandRegistry } from '@bfemulator/sdk-shared';
import { Command, CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { ServiceTypes } from 'botframework-config/lib/schema';
import { newNotification } from '@bfemulator/app-shared';
import * as Constants from '../constants';
import { azureArmTokenDataChanged, beginAzureAuthWorkflow, invalidateArmToken } from '../data/action/azureAuthActions';
@ -46,7 +47,6 @@ import { switchTheme } from '../data/action/themeActions';
import { getTabGroupForDocument, showMarkdownPage, showWelcomePage } from '../data/editorHelpers';
import { AzureAuthState } from '../data/reducer/azureAuthReducer';
import { store } from '../data/store';
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
import {
AzureLoginFailedDialogContainer,
AzureLoginPromptDialogContainer,
@ -63,93 +63,96 @@ import {
import * as ExplorerActions from '../data/action/explorerActions';
import { closeConversation } from '../data/action/chatActions';
import { ActiveBotHelper } from '../ui/helpers/activeBotHelper';
import { beginAdd } from '../data/action/notificationActions';
const { UI, Telemetry } = SharedConstants.Commands;
/** Register UI commands (toggling UI) */
export function registerCommands(commandRegistry: CommandRegistry) {
const { UI, Telemetry } = SharedConstants.Commands;
export class UiCommands {
@CommandServiceInstance()
private commandService: CommandServiceImpl;
// ---------------------------------------------------------------------------
// Shows the welcome page
commandRegistry.registerCommand(UI.ShowWelcomePage, () => {
@Command(UI.ShowWelcomePage)
protected showWelcomePageDispatcher() {
return showWelcomePage();
});
}
// ---------------------------------------------------------------------------
// Shows the markdown page after retrieving the remote source
commandRegistry.registerCommand(
UI.ShowMarkdownPage,
async (urlOrMarkdown: string, label: string, windowRef = window) => {
let markdown = '';
let { onLine } = windowRef.navigator;
if (!onLine) {
return showMarkdownPage(markdown, label, onLine);
}
try {
new URL(urlOrMarkdown); // Is this a valid URL?
const bytes: ArrayBuffer = await CommandServiceImpl.remoteCall(
SharedConstants.Commands.Electron.FetchRemote,
urlOrMarkdown
);
markdown = new TextDecoder().decode(bytes);
} catch (e) {
if (typeof e === 'string' && ('' + e).includes('ENOTFOUND')) {
onLine = false;
} else {
// assume this is markdown text
markdown = urlOrMarkdown;
}
}
@Command(UI.ShowMarkdownPage)
protected async showMarkdownPage(urlOrMarkdown: string, label: string, windowRef = window) {
let markdown = '';
let { onLine } = windowRef.navigator;
if (!onLine) {
return showMarkdownPage(markdown, label, onLine);
}
);
try {
new URL(urlOrMarkdown); // Is this a valid URL?
const bytes = await this.commandService.remoteCall<ArrayBuffer>(
SharedConstants.Commands.Electron.FetchRemote,
urlOrMarkdown
);
markdown = new TextDecoder().decode(bytes);
} catch (e) {
if (typeof e === 'string' && ('' + e).includes('ENOTFOUND')) {
onLine = false;
} else {
// assume this is markdown text
markdown = urlOrMarkdown;
}
}
return showMarkdownPage(markdown, label, onLine);
}
// ---------------------------------------------------------------------------
// Shows a bot creation dialog
commandRegistry.registerCommand(UI.ShowBotCreationDialog, async () => {
@Command(UI.ShowBotCreationDialog)
protected async showBotCreationPage() {
return await DialogService.showDialog(BotCreationDialog);
});
}
// ---------------------------------------------------------------------------
// Shows a bot creation dialog
commandRegistry.registerCommand(UI.ShowOpenBotDialog, async () => {
@Command(UI.ShowOpenBotDialog)
protected async showOpenBotDialog() {
return await DialogService.showDialog(OpenBotDialogContainer);
});
}
// ---------------------------------------------------------------------------
// Shows a dialog prompting the user for a bot secret
commandRegistry.registerCommand(UI.ShowSecretPromptDialog, async () => {
@Command(UI.ShowSecretPromptDialog)
protected async showSecretePromptDialog() {
return await DialogService.showDialog(SecretPromptDialogContainer);
});
}
// ---------------------------------------------------------------------------
// Switches navbar tab selection
commandRegistry.registerCommand(
UI.SwitchNavBarTab,
(tabName: string): void => {
store.dispatch(NavBarActions.select(tabName));
}
);
@Command(UI.SwitchNavBarTab)
protected switchNavBar(tabName: string): void {
store.dispatch(NavBarActions.select(tabName));
}
// ---------------------------------------------------------------------------
// Open App Settings
commandRegistry.registerCommand(
UI.ShowAppSettings,
(): void => {
const { CONTENT_TYPE_APP_SETTINGS, DOCUMENT_ID_APP_SETTINGS } = Constants;
store.dispatch(
EditorActions.open({
contentType: CONTENT_TYPE_APP_SETTINGS,
documentId: DOCUMENT_ID_APP_SETTINGS,
isGlobal: true,
meta: null,
})
);
}
);
@Command(UI.ShowAppSettings)
protected showAppSettings(): void {
const { CONTENT_TYPE_APP_SETTINGS, DOCUMENT_ID_APP_SETTINGS } = Constants;
store.dispatch(
EditorActions.open({
contentType: CONTENT_TYPE_APP_SETTINGS,
documentId: DOCUMENT_ID_APP_SETTINGS,
isGlobal: true,
meta: null,
})
);
}
// ---------------------------------------------------------------------------
// Theme switching from main
commandRegistry.registerCommand(UI.SwitchTheme, (themeName: string, themeHref: string) => {
@Command(UI.SwitchTheme)
protected switchTheme(themeName: string, themeHref: string) {
const linkTags = document.querySelectorAll<HTMLLinkElement>('[data-theme-component="true"]');
const themeTag = document.getElementById('themeVars') as HTMLLinkElement;
if (themeTag) {
@ -157,14 +160,17 @@ export function registerCommands(commandRegistry: CommandRegistry) {
}
const themeComponents = Array.prototype.map.call(linkTags, link => link.href); // href is fully qualified
store.dispatch(switchTheme(themeName, themeComponents));
CommandServiceImpl.remoteCall(Telemetry.TrackEvent, 'app_chooseTheme', {
themeName,
}).catch();
});
this.commandService
.remoteCall(Telemetry.TrackEvent, 'app_chooseTheme', {
themeName,
})
.catch();
}
// ---------------------------------------------------------------------------
// Debug mode from main
commandRegistry.registerCommand(UI.SwitchDebugMode, async (debugMode: DebugMode) => {
@Command(UI.SwitchDebugMode)
protected async switchDebugMode(debugMode: DebugMode) {
const {
editor: { editors, activeEditor },
} = store.getState();
@ -180,11 +186,12 @@ export function registerCommands(commandRegistry: CommandRegistry) {
store.dispatch(closeConversation(documentId));
}
});
});
}
// ---------------------------------------------------------------------------
// Azure sign in
commandRegistry.registerCommand(UI.SignInToAzure, (serviceType: ServiceTypes) => {
@Command(UI.SignInToAzure)
protected signIntoAzure(serviceType: ServiceTypes) {
store.dispatch(
beginAzureAuthWorkflow(
AzureLoginPromptDialogContainer,
@ -193,53 +200,64 @@ export function registerCommands(commandRegistry: CommandRegistry) {
AzureLoginFailedDialogContainer
)
);
});
}
commandRegistry.registerCommand(UI.ArmTokenReceivedOnStartup, (azureAuth: AzureAuthState) => {
@Command(UI.ArmTokenReceivedOnStartup)
protected armTokenReceivedOnStartup(azureAuth: AzureAuthState) {
store.dispatch(azureArmTokenDataChanged(azureAuth.access_token));
});
}
commandRegistry.registerCommand(UI.InvalidateAzureArmToken, () => {
@Command(UI.InvalidateAzureArmToken)
protected invalidateAzureArmToken() {
store.dispatch(invalidateArmToken());
});
}
// ---------------------------------------------------------------------------
// Show post migration dialog on startup if the user has just been migrated
commandRegistry.registerCommand(UI.ShowPostMigrationDialog, () => {
@Command(UI.ShowPostMigrationDialog)
protected showPostMigrationDialog() {
return DialogService.showDialog(PostMigrationDialogContainer);
});
}
// ---------------------------------------------------------------------------
// Shows the progress indicator component
commandRegistry.registerCommand(UI.ShowProgressIndicator, async (props?: ProgressIndicatorPayload) => {
return await DialogService.showDialog(
ProgressIndicatorContainer,
props
// eslint-disable-next-line no-console
).catch(e => console.error(e));
});
@Command(UI.ShowProgressIndicator)
protected async showProgressIndicator(props?: ProgressIndicatorPayload) {
try {
return await DialogService.showDialog(ProgressIndicatorContainer, props);
} catch (e) {
beginAdd(newNotification(e));
}
}
// ---------------------------------------------------------------------------
// Updates the progress of the progress indicator component
commandRegistry.registerCommand(UI.UpdateProgressIndicator, (value: ProgressIndicatorPayload) => {
@Command(UI.UpdateProgressIndicator)
protected updateProgressIndicator(value: ProgressIndicatorPayload) {
store.dispatch(updateProgressIndicator(value));
});
}
// ---------------------------------------------------------------------------
// Shows the dialog telling the user that an update is available
commandRegistry.registerCommand(UI.ShowUpdateAvailableDialog, async (version: string = '') => {
return await DialogService.showDialog(UpdateAvailableDialogContainer, {
version,
// eslint-disable-next-line no-console
}).catch(e => console.error(e));
});
@Command(UI.ShowUpdateAvailableDialog)
protected async showUpdateAvailableDialog(version: string = '') {
try {
return await DialogService.showDialog(UpdateAvailableDialogContainer, {
version,
});
} catch (e) {
beginAdd(newNotification(e));
}
}
// ---------------------------------------------------------------------------
// Shows the dialog telling the user that an update is unavailable
commandRegistry.registerCommand(UI.ShowUpdateUnavailableDialog, async () => {
return await DialogService.showDialog(
UpdateUnavailableDialogContainer
// eslint-disable-next-line no-console
).catch(e => console.error(e));
});
@Command(UI.ShowUpdateUnavailableDialog)
protected async showUpdateUnavailableDialog() {
try {
return await DialogService.showDialog(UpdateUnavailableDialogContainer);
} catch (e) {
beginAdd(newNotification(e));
}
}
}

Просмотреть файл

@ -60,7 +60,7 @@ export interface ActiveInspectorChangedPayload {
inspectorWebView: HTMLWebViewElement;
}
export interface NewChatPayload {
export interface NewChatPayload extends ClearLogPayload {
[propName: string]: any;
documentId: string;
@ -97,6 +97,7 @@ export interface AppendLogPayload {
export interface ClearLogPayload {
documentId: string;
resolver?: Function;
}
export interface SetInspectorObjectsPayload {
@ -179,7 +180,12 @@ export function updatePendingSpeechTokenRetrieval(pending: boolean): ChatAction<
};
}
export function newChat(documentId: string, mode: ChatMode, additionalData?: object): ChatAction<NewChatPayload> {
export function newChat(
documentId: string,
mode: ChatMode,
additionalData?: object,
resolver?: Function
): ChatAction<NewChatPayload> {
return {
type: ChatActions.newChat,
payload: {
@ -256,11 +262,12 @@ export function appendToLog(documentId: string, entry: LogEntry): ChatAction<App
};
}
export function clearLog(documentId: string): ChatAction<ClearLogPayload> {
export function clearLog(documentId: string, resolver?: Function): ChatAction<ClearLogPayload> {
return {
type: ChatActions.clearLog,
payload: {
documentId,
resolver,
},
};
}

Просмотреть файл

@ -30,30 +30,38 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { Action } from 'redux';
import { Channel, Disposable, IPC } from '@bfemulator/sdk-shared';
const { ipcRenderer } = (window as any).require('electron');
export const EXECUTE_COMMAND = 'EXECUTE_COMMAND';
class ElectronIPCImpl extends IPC {
constructor() {
super();
ipcRenderer.on('ipc:message', (_sender: any, ...args: any[]) => {
const channelName = args.shift();
const channel = this._channels[channelName];
if (channel) {
channel.onMessage(...args);
}
});
}
public send(...args: any[]): void {
ipcRenderer.send('ipc:message', ...args);
}
public registerChannel(channel: Channel): Disposable {
return super.registerChannel(channel);
}
export interface CommandAction<T> extends Action {
payload: T;
}
export const ElectronIPC = new ElectronIPCImpl();
export interface CommandActionPayload {
isRemote: boolean;
commandName: string;
args: any[];
resolver?: Function;
}
/**
* Executes a command and calls the resolve function
* with the result when complete.
*
* @param isRemote
* @param commandName
* @param resolver
* @param args
*/
export function executeCommand(
isRemote: boolean,
commandName,
resolver: Function = null,
...args: any[]
): CommandAction<CommandActionPayload> {
return {
type: EXECUTE_COMMAND,
payload: { isRemote, commandName, resolver, args },
};
}

Просмотреть файл

@ -30,34 +30,25 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { Action } from 'redux';
export enum PresentationActions {
disable = 'PRESENTATION/DISABLE',
enable = 'PRESENTATION/ENABLE',
}
export interface EnablePresentationAction {
type: PresentationActions.enable;
payload: {};
export interface PresentationAction extends Action {
type: PresentationActions;
}
export interface DisablePresentationAction {
type: PresentationActions.disable;
payload: {};
}
export type PresentationAction = EnablePresentationAction | DisablePresentationAction;
export function enable(): EnablePresentationAction {
export function enable(): PresentationAction {
return {
type: PresentationActions.enable,
payload: {},
};
}
export function disable(): DisablePresentationAction {
export function disable(): PresentationAction {
return {
type: PresentationActions.disable,
payload: {},
};
}

Просмотреть файл

@ -31,7 +31,7 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { LogEntry } from '@bfemulator/sdk-shared';
import { LogEntry, LogItemType } from '@bfemulator/sdk-shared';
import {
addTranscript,
@ -40,8 +40,8 @@ import {
clearLog,
clearTranscripts,
closeDocument,
newConversation,
newChat,
newConversation,
removeTranscript,
setInspectorObjects,
updateChat,
@ -62,7 +62,7 @@ describe('Chat reducer tests', () => {
},
},
transcripts: [],
};
} as any;
it('should return unaltered state for non-matching action type', () => {
const emptyAction: ChatAction = { type: null, payload: null };
@ -134,7 +134,7 @@ describe('Chat reducer tests', () => {
timestamp: 123,
items: [
{
type: 'text',
type: LogItemType.Text,
payload: {
level: 0,
text: 'testing',
@ -164,7 +164,7 @@ describe('Chat reducer tests', () => {
timestamp: 1234,
items: [
{
type: 'text',
type: LogItemType.Text,
payload: {
level: 0,
text: 'testing',
@ -217,7 +217,7 @@ describe('Chat reducer tests', () => {
},
},
transcripts: ['xs1', 'xs2', 'xs3'],
};
} as any;
const action = closeNonGlobalTabs();
const state = chat(alteredState, action);
expect(state.changeKey).toBe(0);

Просмотреть файл

@ -31,9 +31,6 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { PresentationAction, PresentationActions } from '../action/presentationActions';
export interface PresentationState {
@ -68,7 +65,5 @@ function setEnabled(enabled: boolean, state: PresentationState): PresentationSta
const newState = { ...state };
newState.enabled = enabled;
CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.SetFullscreen, enabled);
return newState;
}

Просмотреть файл

@ -30,7 +30,7 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { CommandRegistryImpl } from '@bfemulator/sdk-shared';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { SharedConstants } from '@bfemulator/app-shared';
import { ServiceTypes } from 'botframework-config/lib/schema';
@ -42,27 +42,46 @@ import {
AzureLoginSuccessDialogContainer,
DialogService,
} from '../../ui/dialogs';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { registerCommands } from '../../commands/uiCommands';
import { azureAuthSagas } from './azureAuthSaga';
jest.mock('../../ui/dialogs', () => ({
AzureLoginPromptDialogContainer: () => undefined,
AzureLoginSuccessDialogContainer: () => undefined,
BotCreationDialog: () => undefined,
DialogService: { showDialog: () => Promise.resolve(true) },
PostMigrationDialogContainer: () => undefined,
SecretPromptDialog: () => undefined,
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
jest.mock('../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
remoteCall: () => Promise.resolve(true),
},
jest.mock('../../ui/dialogs', () => ({
DialogService: { showDialog: () => Promise.resolve(true) },
}));
describe('The azureAuthSaga', () => {
let commandService: CommandServiceImpl;
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
});
it('should contain a single step if the token in the store is valid', () => {
store.dispatch(azureArmTokenDataChanged('a valid access_token'));
const it = azureAuthSagas()
@ -87,12 +106,6 @@ describe('The azureAuthSaga', () => {
});
describe('with an invalid token in the store', () => {
let registry: CommandRegistryImpl;
beforeAll(() => {
registry = new CommandRegistryImpl();
registerCommands(registry);
});
it('should contain just 2 steps when the Azure login dialog prompt is canceled', async () => {
store.dispatch(azureArmTokenDataChanged(''));
// @ts-ignore
@ -133,7 +146,7 @@ describe('The azureAuthSaga', () => {
store.dispatch(azureArmTokenDataChanged(''));
// @ts-ignore
DialogService.showDialog = () => Promise.resolve(1);
(CommandServiceImpl as any).remoteCall = () => Promise.resolve(false);
jest.spyOn(commandService, 'remoteCall').mockResolvedValueOnce(false);
const it = azureAuthSagas()
.next()
.value.FORK.args[1](
@ -146,7 +159,7 @@ describe('The azureAuthSaga', () => {
);
let val = undefined;
let ct = 0;
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall');
const remoteCallSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue(true);
// eslint-disable-next-line no-constant-condition
while (true) {
const next = it.next(val);
@ -165,7 +178,7 @@ describe('The azureAuthSaga', () => {
}
} else if ('CALL' in val) {
val = val.CALL.fn.call(null, val.CALL.args);
if (val instanceof Promise) {
if (val[Symbol.toStringTag] === 'Promise') {
val = await val;
if (ct === 2) {
// Login was unsuccessful
@ -184,7 +197,7 @@ describe('The azureAuthSaga', () => {
store.dispatch(azureArmTokenDataChanged(''));
// @ts-ignore
DialogService.showDialog = () => Promise.resolve(1);
(CommandServiceImpl as any).remoteCall = args => {
commandService.remoteCall = args => {
switch (args[0]) {
case SharedConstants.Commands.Azure.RetrieveArmToken:
// eslint-disable-next-line typescript/camelcase
@ -194,7 +207,7 @@ describe('The azureAuthSaga', () => {
return Promise.resolve({ persistLogin: true });
default:
return Promise.resolve(false);
return Promise.resolve(false) as any;
}
};
const it = azureAuthSagas()
@ -209,7 +222,7 @@ describe('The azureAuthSaga', () => {
);
let val = undefined;
let ct = 0;
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall');
const remoteCallSpy = jest.spyOn(commandService, 'remoteCall');
// eslint-disable-next-line no-constant-condition
while (true) {
const next = it.next(val);
@ -228,13 +241,13 @@ describe('The azureAuthSaga', () => {
}
} else if ('CALL' in val) {
val = val.CALL.fn.call(null, val.CALL.args);
if (val instanceof Promise) {
if (val[Symbol.toStringTag] === 'Promise') {
val = await val;
if (ct === 2) {
if (ct === 3) {
// Login was successful
expect(val.access_token).toBe('a valid access_token');
expect(remoteCallSpy).toHaveBeenCalledWith([SharedConstants.Commands.Azure.RetrieveArmToken]);
} else if (ct === 4) {
} else if (ct === 5) {
expect(val.persistLogin).toBe(true);
expect(remoteCallSpy).toHaveBeenCalledWith([SharedConstants.Commands.Azure.PersistAzureLoginChanged, 1]);
}

Просмотреть файл

@ -32,8 +32,8 @@
//
import { SharedConstants } from '@bfemulator/app-shared';
import { call, ForkEffect, put, select, takeEvery } from 'redux-saga/effects';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { DialogService } from '../../ui/dialogs';
import {
AZURE_BEGIN_AUTH_WORKFLOW,
@ -46,31 +46,40 @@ import { RootState } from '../store';
const getArmTokenFromState = (state: RootState) => state.azureAuth;
export function* getArmToken(action: AzureAuthAction<AzureAuthWorkflow>): IterableIterator<any> {
let azureAuth: AzureAuthState = yield select(getArmTokenFromState);
if (azureAuth.access_token) {
export class AzureAuthSaga {
@CommandServiceInstance()
private static commandService: CommandServiceImpl;
public static *getArmToken(action: AzureAuthAction<AzureAuthWorkflow>): IterableIterator<any> {
let azureAuth: AzureAuthState = yield select(getArmTokenFromState);
if (azureAuth.access_token) {
return azureAuth;
}
const result = yield DialogService.showDialog(action.payload.promptDialog, action.payload.promptDialogProps);
if (result !== 1) {
// Result must be 1 which is a confirmation to sign in to Azure
return result;
}
const { RetrieveArmToken, PersistAzureLoginChanged } = SharedConstants.Commands.Azure;
const { TrackEvent } = SharedConstants.Commands.Telemetry;
azureAuth = yield call([AzureAuthSaga.commandService, AzureAuthSaga.commandService.remoteCall], RetrieveArmToken);
if (azureAuth && !('error' in azureAuth)) {
const persistLogin = yield DialogService.showDialog(action.payload.loginSuccessDialog, azureAuth);
yield call(
AzureAuthSaga.commandService.remoteCall.bind(AzureAuthSaga.commandService),
PersistAzureLoginChanged,
persistLogin
);
AzureAuthSaga.commandService.remoteCall(TrackEvent, 'signIn_success').catch(_e => void 0);
} else {
yield DialogService.showDialog(action.payload.loginFailedDialog);
AzureAuthSaga.commandService.remoteCall(TrackEvent, 'signIn_failure').catch(_e => void 0);
}
yield put(azureArmTokenDataChanged(azureAuth.access_token));
return azureAuth;
}
const result = yield DialogService.showDialog(action.payload.promptDialog, action.payload.promptDialogProps);
if (result !== 1) {
// Result must be 1 which is a confirmation to sign in to Azure
return result;
}
const { RetrieveArmToken, PersistAzureLoginChanged } = SharedConstants.Commands.Azure;
const { TrackEvent } = SharedConstants.Commands.Telemetry;
azureAuth = yield call(CommandServiceImpl.remoteCall.bind(CommandServiceImpl), RetrieveArmToken);
if (azureAuth && !('error' in azureAuth)) {
const persistLogin = yield DialogService.showDialog(action.payload.loginSuccessDialog, azureAuth);
yield call(CommandServiceImpl.remoteCall.bind(CommandServiceImpl), PersistAzureLoginChanged, persistLogin);
CommandServiceImpl.remoteCall(TrackEvent, 'signIn_success').catch(_e => void 0);
} else {
yield DialogService.showDialog(action.payload.loginFailedDialog);
CommandServiceImpl.remoteCall(TrackEvent, 'signIn_failure').catch(_e => void 0);
}
yield put(azureArmTokenDataChanged(azureAuth.access_token));
return azureAuth;
}
export function* azureAuthSagas(): IterableIterator<ForkEffect> {
yield takeEvery(AZURE_BEGIN_AUTH_WORKFLOW, getArmToken);
yield takeEvery(AZURE_BEGIN_AUTH_WORKFLOW, AzureAuthSaga.getArmToken);
}

Просмотреть файл

@ -31,9 +31,10 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { newNotification, SharedConstants, DebugMode } from '@bfemulator/app-shared';
import { DebugMode, newNotification, SharedConstants } from '@bfemulator/app-shared';
import { BotConfigWithPath, ConversationService } from '@bfemulator/sdk-shared';
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { ActiveBotHelper } from '../../ui/helpers/activeBotHelper';
import {
@ -45,12 +46,9 @@ import {
} from '../action/botActions';
import { beginAdd } from '../action/notificationActions';
import { generateHash } from '../botHelpers';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { botSagas, browseForBot, generateHashForActiveBot, openBotViaFilePath, openBotViaUrl } from './botSagas';
import { refreshConversationMenu } from './sharedSagas';
jest.mock('../../ui/dialogs', () => ({}));
import { botSagas, BotSagas } from './botSagas';
import { SharedSagas } from './sharedSagas';
jest.mock('../store', () => ({
get store() {
@ -58,31 +56,59 @@ jest.mock('../store', () => ({
},
}));
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
const mockSharedConstants = SharedConstants;
let mockRemoteCommandsCalled = [];
let mockLocalCommandsCalled = [];
jest.mock('../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
call: async (commandName: string, ...args: any[]) => {
describe('The botSagas', () => {
let commandService: CommandServiceImpl;
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
commandService.call = async (commandName: string, ...args: any[]) => {
mockLocalCommandsCalled.push({ commandName, args: args });
switch (commandName) {
case mockSharedConstants.Commands.Bot.OpenBrowse:
return Promise.resolve(true);
default:
return Promise.resolve(true);
return Promise.resolve(true) as any;
}
},
remoteCall: async (commandName: string, ...args: any[]) => {
};
commandService.remoteCall = async (commandName: string, ...args: any[]) => {
mockRemoteCommandsCalled.push({ commandName, args: args });
return Promise.resolve(true);
},
},
}));
return Promise.resolve(true) as any;
};
});
describe('The botSagas', () => {
beforeEach(() => {
mockRemoteCommandsCalled = [];
mockLocalCommandsCalled = [];
@ -92,14 +118,17 @@ describe('The botSagas', () => {
it('should initialize the root saga', () => {
const gen = botSagas();
expect(gen.next().value).toEqual(takeEvery(BotActionType.browse, browseForBot));
expect(gen.next().value).toEqual(takeEvery(BotActionType.browse, BotSagas.browseForBot));
expect(gen.next().value).toEqual(takeEvery(BotActionType.openViaUrl, openBotViaUrl));
expect(gen.next().value).toEqual(takeEvery(BotActionType.openViaFilePath, openBotViaFilePath));
expect(gen.next().value).toEqual(takeEvery(BotActionType.setActive, generateHashForActiveBot));
expect(gen.next().value).toEqual(takeEvery(BotActionType.openViaUrl, BotSagas.openBotViaUrl));
expect(gen.next().value).toEqual(takeEvery(BotActionType.openViaFilePath, BotSagas.openBotViaFilePath));
expect(gen.next().value).toEqual(takeEvery(BotActionType.setActive, BotSagas.generateHashForActiveBot));
expect(gen.next().value).toEqual(
takeLatest([BotActionType.setActive, BotActionType.load, BotActionType.close], refreshConversationMenu)
takeLatest(
[BotActionType.setActive, BotActionType.load, BotActionType.close],
SharedSagas.refreshConversationMenu
)
);
expect(gen.next().done).toBe(true);
@ -121,7 +150,7 @@ describe('The botSagas', () => {
bot: botConfigPath,
},
};
const gen = generateHashForActiveBot(setActiveBotAction);
const gen = BotSagas.generateHashForActiveBot(setActiveBotAction);
const generatedHash = gen.next().value;
expect(generatedHash).toEqual(call(generateHash, botConfigPath));
@ -132,13 +161,13 @@ describe('The botSagas', () => {
});
it('should open native open file dialog to browse for .bot file', () => {
const gen = browseForBot();
const gen = BotSagas.browseForBot();
expect(gen.next().value).toEqual(call([ActiveBotHelper, ActiveBotHelper.confirmAndOpenBotFromFile]));
expect(gen.next().done).toBe(true);
});
it('should open a bot from a url', () => {
const gen = openBotViaUrl(
const gen = BotSagas.openBotViaUrl(
openBotViaUrlAction({
appPassword: 'password',
appId: '1234abcd',
@ -159,7 +188,7 @@ describe('The botSagas', () => {
const callToSaveUrl = gen.next(DebugMode.Normal).value;
expect(callToSaveUrl).toEqual(
call(
[CommandServiceImpl, CommandServiceImpl.remoteCall],
[commandService, commandService.remoteCall],
SharedConstants.Commands.Settings.SaveBotUrl,
'http://localhost/api/messages'
)
@ -169,7 +198,7 @@ describe('The botSagas', () => {
});
it('should open a bot from a url with the custom user id', () => {
const gen = openBotViaUrl(
const gen = BotSagas.openBotViaUrl(
openBotViaUrlAction({
appPassword: 'password',
appId: '1234abcd',
@ -186,7 +215,7 @@ describe('The botSagas', () => {
const callToSetCurrentUser = gen.next(users).value;
expect(callToSetCurrentUser).toEqual(
call(
[CommandServiceImpl, CommandServiceImpl.remoteCall],
[commandService, commandService.remoteCall],
SharedConstants.Commands.Emulator.SetCurrentUser,
'customUserId'
)
@ -199,7 +228,7 @@ describe('The botSagas', () => {
const callToSaveUrl = gen.next(DebugMode.Normal).value;
expect(callToSaveUrl).toEqual(
call(
[CommandServiceImpl, CommandServiceImpl.remoteCall],
[commandService, commandService.remoteCall],
SharedConstants.Commands.Settings.SaveBotUrl,
'http://localhost/api/messages'
)
@ -209,7 +238,7 @@ describe('The botSagas', () => {
});
it('should send a notification if opening a bot from a URL fails', () => {
const gen = openBotViaUrl(
const gen = BotSagas.openBotViaUrl(
openBotViaUrlAction({
appPassword: 'password',
appId: '1234abcd',
@ -240,7 +269,7 @@ describe('The botSagas', () => {
});
it('should send the "/INSPECT open" command when in debug mode and opening from url', () => {
const gen = openBotViaUrl(
const gen = BotSagas.openBotViaUrl(
openBotViaUrlAction({
appPassword: 'password',
appId: '1234abcd',
@ -268,7 +297,7 @@ describe('The botSagas', () => {
};
expect(callToPostActivity).toEqual(
call(
[CommandServiceImpl, CommandServiceImpl.remoteCall],
[commandService, commandService.remoteCall],
SharedConstants.Commands.Emulator.PostActivityToConversation,
'someConversationId',
activity
@ -278,7 +307,7 @@ describe('The botSagas', () => {
const callToRememberEndpoint = gen.next({ statusCode: 200 });
expect(callToRememberEndpoint.value).toEqual(
call(
[CommandServiceImpl, CommandServiceImpl.remoteCall],
[commandService, commandService.remoteCall],
SharedConstants.Commands.Settings.SaveBotUrl,
'http://localhost/api/messages'
)
@ -288,7 +317,7 @@ describe('The botSagas', () => {
});
it('should spawn a notification if posting the "/INSPECT open" command fails', () => {
const gen = openBotViaUrl(
const gen = BotSagas.openBotViaUrl(
openBotViaUrlAction({
appPassword: 'password',
appId: '1234abcd',
@ -319,7 +348,7 @@ describe('The botSagas', () => {
});
it('should spawn a notification if parsing the conversation id from the response fails', () => {
const gen = openBotViaUrl(
const gen = BotSagas.openBotViaUrl(
openBotViaUrlAction({
appPassword: 'password',
appId: '1234abcd',
@ -350,7 +379,7 @@ describe('The botSagas', () => {
});
it('should open a bot from a file path', () => {
const gen = openBotViaFilePath(openBotViaFilePathAction('/some/path.bot'));
const gen = BotSagas.openBotViaFilePath(openBotViaFilePathAction('/some/path.bot'));
jest.spyOn(ActiveBotHelper, 'confirmAndOpenBotFromFile').mockResolvedValue(true);
expect(gen.next().value).toEqual(
@ -359,7 +388,7 @@ describe('The botSagas', () => {
});
it('should send a notification when opening a bot from a file path fails', () => {
const gen = openBotViaFilePath(openBotViaFilePathAction('/some/path.bot'));
const gen = BotSagas.openBotViaFilePath(openBotViaFilePathAction('/some/path.bot'));
const callOpenBot = gen.next().value;
expect(callOpenBot).toEqual(call([ActiveBotHelper, ActiveBotHelper.confirmAndOpenBotFromFile], '/some/path.bot'));
const putNotification = gen.throw(new Error('oh noes!'));

Просмотреть файл

@ -34,102 +34,113 @@
import { DebugMode, newNotification, SharedConstants, UserSettings } from '@bfemulator/app-shared';
import { ConversationService, StartConversationParams } from '@bfemulator/sdk-shared';
import { call, ForkEffect, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { ActiveBotHelper } from '../../ui/helpers/activeBotHelper';
import { BotAction, BotActionType, BotConfigWithPathPayload, botHashGenerated } from '../action/botActions';
import { beginAdd } from '../action/notificationActions';
import { generateHash } from '../botHelpers';
import { RootState } from '../store';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { refreshConversationMenu } from './sharedSagas';
import { SharedSagas } from './sharedSagas';
/** Opens up native open file dialog to browse for a .bot file */
export function* browseForBot(): IterableIterator<any> {
yield call([ActiveBotHelper, ActiveBotHelper.confirmAndOpenBotFromFile]);
}
export class BotSagas {
@CommandServiceInstance()
private static commandService: CommandServiceImpl;
export function* generateHashForActiveBot(action: BotAction<BotConfigWithPathPayload>): IterableIterator<any> {
const { bot } = action.payload;
const generatedHash = yield call(generateHash, bot);
yield put(botHashGenerated(generatedHash));
}
export function* openBotViaFilePath(action: BotAction<string>) {
try {
yield call([ActiveBotHelper, ActiveBotHelper.confirmAndOpenBotFromFile], action.payload);
} catch (e) {
const errorNotification = beginAdd(newNotification(`An Error occurred opening the bot at ${action.payload}: ${e}`));
yield put(errorNotification);
public static *browseForBot(): IterableIterator<any> {
yield call([ActiveBotHelper, ActiveBotHelper.confirmAndOpenBotFromFile]);
}
}
export function* openBotViaUrl(action: BotAction<Partial<StartConversationParams>>) {
const serverUrl = yield select((state: RootState) => state.clientAwareSettings.serverUrl);
if (!action.payload.user) {
// If no user is provided, select the current user
const customUserId = yield select((state: RootState) => state.framework.userGUID);
const users: UserSettings = yield select((state: RootState) => state.clientAwareSettings.users);
action.payload.user = customUserId || users.usersById[users.currentUserId];
if (customUserId) {
action.payload.user = customUserId;
yield call(
[CommandServiceImpl, CommandServiceImpl.remoteCall],
SharedConstants.Commands.Emulator.SetCurrentUser,
customUserId
public static *generateHashForActiveBot(action: BotAction<BotConfigWithPathPayload>): IterableIterator<any> {
const { bot } = action.payload;
const generatedHash = yield call(generateHash, bot);
yield put(botHashGenerated(generatedHash));
}
public static *openBotViaFilePath(action: BotAction<string>) {
try {
yield call([ActiveBotHelper, ActiveBotHelper.confirmAndOpenBotFromFile], action.payload);
} catch (e) {
const errorNotification = beginAdd(
newNotification(`An Error occurred opening the bot at ${action.payload}: ${e}`)
);
yield put(errorNotification);
}
}
let error;
try {
const response = yield ConversationService.startConversation(serverUrl, action.payload);
if (!response.ok) {
error = `An Error occurred opening the bot at ${action.payload.endpoint}: ${response.statusText}`;
}
const debugMode = yield select((state: RootState) => state.clientAwareSettings.debugMode);
if (debugMode === DebugMode.Sidecar) {
// extract the conversation id from the body
const parsedBody = yield response.json();
const conversationId = parsedBody.id || '';
if (conversationId) {
// post debug init command to conversation
const activity = {
type: 'message',
text: '/INSPECT open',
};
const postActivityResponse = yield call(
[CommandServiceImpl, CommandServiceImpl.remoteCall],
SharedConstants.Commands.Emulator.PostActivityToConversation,
conversationId,
activity
public static *openBotViaUrl(action: BotAction<Partial<StartConversationParams>>) {
const serverUrl = yield select((state: RootState) => state.clientAwareSettings.serverUrl);
if (!action.payload.user) {
// If no user is provided, select the current user
const customUserId = yield select((state: RootState) => state.framework.userGUID);
const users: UserSettings = yield select((state: RootState) => state.clientAwareSettings.users);
action.payload.user = customUserId || users.usersById[users.currentUserId];
if (customUserId) {
action.payload.user = customUserId;
yield call(
[BotSagas.commandService, BotSagas.commandService.remoteCall],
SharedConstants.Commands.Emulator.SetCurrentUser,
customUserId
);
if (postActivityResponse.statusCode >= 400) {
throw new Error(`An error occurred while POSTing "/INSPECT open" command to conversation ${conversationId}`);
}
} else {
throw new Error('An error occurred while trying to grab conversation ID from new conversation.');
}
}
} catch (e) {
error = e.message;
}
if (error) {
const errorNotification = beginAdd(newNotification(error));
yield put(errorNotification);
} else {
// remember the endpoint
yield call(
[CommandServiceImpl, CommandServiceImpl.remoteCall],
SharedConstants.Commands.Settings.SaveBotUrl,
action.payload.endpoint
);
let error;
try {
const response = yield ConversationService.startConversation(serverUrl, action.payload);
if (!response.ok) {
error = `An Error occurred opening the bot at ${action.payload.endpoint}: ${response.statusText}`;
}
const debugMode = yield select((state: RootState) => state.clientAwareSettings.debugMode);
if (debugMode === DebugMode.Sidecar) {
// extract the conversation id from the body
const parsedBody = yield response.json();
const conversationId = parsedBody.id || '';
if (conversationId) {
// post debug init command to conversation
const activity = {
type: 'message',
text: '/INSPECT open',
};
const postActivityResponse = yield call(
[BotSagas.commandService, BotSagas.commandService.remoteCall],
SharedConstants.Commands.Emulator.PostActivityToConversation,
conversationId,
activity
);
if (postActivityResponse.statusCode >= 400) {
throw new Error(
`An error occurred while POSTing "/INSPECT open" command to conversation ${conversationId}`
);
}
} else {
throw new Error('An error occurred while trying to grab conversation ID from new conversation.');
}
}
} catch (e) {
error = e.message;
}
if (error) {
const errorNotification = beginAdd(newNotification(error));
yield put(errorNotification);
} else {
// remember the endpoint
yield call(
[BotSagas.commandService, BotSagas.commandService.remoteCall],
SharedConstants.Commands.Settings.SaveBotUrl,
action.payload.endpoint
);
}
}
}
export function* botSagas(): IterableIterator<ForkEffect> {
yield takeEvery(BotActionType.browse, browseForBot);
yield takeEvery(BotActionType.openViaUrl, openBotViaUrl);
yield takeEvery(BotActionType.openViaFilePath, openBotViaFilePath);
yield takeEvery(BotActionType.setActive, generateHashForActiveBot);
yield takeLatest([BotActionType.setActive, BotActionType.load, BotActionType.close], refreshConversationMenu);
yield takeEvery(BotActionType.browse, BotSagas.browseForBot);
yield takeEvery(BotActionType.openViaUrl, BotSagas.openBotViaUrl);
yield takeEvery(BotActionType.openViaFilePath, BotSagas.openBotViaFilePath);
yield takeEvery(BotActionType.setActive, BotSagas.generateHashForActiveBot);
yield takeLatest(
[BotActionType.setActive, BotActionType.load, BotActionType.close],
SharedSagas.refreshConversationMenu
);
}

Просмотреть файл

@ -35,13 +35,13 @@ import sagaMiddlewareFactory from 'redux-saga';
import { ActivityTypes } from 'botframework-schema';
import * as Electron from 'electron';
import { SharedConstants, ValueTypes } from '@bfemulator/app-shared';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { bot } from '../reducer/bot';
import { chat } from '../reducer/chat';
import { editor } from '../reducer/editor';
import { presentation } from '../reducer/presentation';
import * as Constants from '../../constants';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { closeConversation, newChat, showContextMenuForActivity } from '../action/chatActions';
import { chatSagas } from './chatSagas';
@ -58,7 +58,29 @@ jest.mock('../store', () => ({
jest.mock('electron', () => {
return {
clipboard: { writeText: () => true },
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
clipboard: { writeText: (textFromActivity: string) => true },
};
});
@ -597,8 +619,14 @@ describe('The ChatSagas,', () => {
});
describe('when showing a context menu for an activity', () => {
let commandService: CommandServiceImpl;
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
});
it('should handle the "copy message" selection', async () => {
const commandServiceSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue({ id: 'copy' });
const commandServiceSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue({ id: 'copy' });
const clipboardSpy = jest.spyOn(Electron.clipboard, 'writeText');
const activity = {
valueType: ValueTypes.Activity,
@ -612,21 +640,21 @@ describe('The ChatSagas,', () => {
});
it('should handle the "copy json" selection', async () => {
const commandServiceSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue({ id: 'json' });
const commandServiceSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue({ id: 'json' });
const clipboardSpy = jest.spyOn(Electron.clipboard, 'writeText');
const activity = {
valueType: '',
type: ActivityTypes.Trace,
value: { type: ActivityTypes.Message, text: 'Hello Bot!' },
};
mockStore.dispatch(showContextMenuForActivity(activity));
await mockStore.dispatch(showContextMenuForActivity(activity));
await Promise.resolve(true);
expect(commandServiceSpy).toHaveBeenCalled();
expect(clipboardSpy).toHaveBeenCalledWith(JSON.stringify(activity, null, 2));
});
it('should handle the "Compare with previous" selection', async () => {
const commandServiceSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue({ id: 'diff' });
const commandServiceSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue({ id: 'diff' });
const activity = mockStoreState.chat.chats.doc1.log.entries[2].items[0].payload.obj;
mockStore.dispatch(showContextMenuForActivity(activity));
await Promise.resolve(true);
@ -705,36 +733,36 @@ describe('The ChatSagas,', () => {
])
);
});
});
it('when closing a document it should notify the main process so it can remove the conversation', async () => {
const commandServiceSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue(true);
mockStore.dispatch(closeConversation('doc1'));
expect(commandServiceSpy).toHaveBeenCalledWith(SharedConstants.Commands.Emulator.DeleteConversation, 'convo1');
await Promise.resolve(); // wait for the comand service call to complete
expect(mockStore.getState().chat.chats.doc1).toBeUndefined();
});
it('when closing a document it should notify the main process so it can remove the conversation', async () => {
const commandServiceSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue(true);
mockStore.dispatch(closeConversation('doc1'));
expect(commandServiceSpy).toHaveBeenCalledWith(SharedConstants.Commands.Emulator.DeleteConversation, 'convo1');
await Promise.resolve(); // wait for the comand service call to complete
expect(mockStore.getState().chat.chats.doc1).toBeUndefined();
});
it('when starting a new conversation, should create a speech token ponyfill factory', async () => {
const commandServiceSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue('mockSpeechToken');
it('when starting a new conversation, should create a speech token ponyfill factory', async () => {
const commandServiceSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue('mockSpeechToken');
mockStore.dispatch(
newChat('doc2', 'livechat', {
conversationId: 'convo2',
endpointId: 'endpoint2',
userId: 'someUserId2',
})
);
mockStore.dispatch(
newChat('doc2', 'livechat', {
conversationId: 'convo2',
endpointId: 'endpoint2',
userId: 'someUserId2',
})
);
await Promise.resolve();
const state = mockStore.getState();
expect(state.chat.chats.doc2).not.toBeUndefined();
expect(state.chat.webSpeechFactories.doc2).not.toBeUndefined();
expect(state.chat.webSpeechFactories.doc2()).toBe('Yay! ponyfill!');
expect(commandServiceSpy).toHaveBeenCalledWith(
SharedConstants.Commands.Emulator.GetSpeechToken,
'endpoint2',
false
);
await Promise.resolve();
const state = mockStore.getState();
expect(state.chat.chats.doc2).not.toBeUndefined();
expect(state.chat.webSpeechFactories.doc2).not.toBeUndefined();
expect(state.chat.webSpeechFactories.doc2()).toBe('Yay! ponyfill!');
expect(commandServiceSpy).toHaveBeenCalledWith(
SharedConstants.Commands.Emulator.GetSpeechToken,
'endpoint2',
false
);
});
});
});

Просмотреть файл

@ -34,7 +34,13 @@ import * as Electron from 'electron';
import { MenuItemConstructorOptions } from 'electron';
import { Activity } from 'botframework-schema';
import { SharedConstants, ValueTypes } from '@bfemulator/app-shared';
import { InspectableObjectLogItem, LogItem, LogItemType } from '@bfemulator/sdk-shared';
import {
CommandServiceImpl,
CommandServiceInstance,
InspectableObjectLogItem,
LogItem,
LogItemType,
} from '@bfemulator/sdk-shared';
import { diff } from 'deep-diff';
import { IEndpointService } from 'botframework-config/lib/schema';
import { createCognitiveServicesBingSpeechPonyfillFactory } from 'botframework-webchat';
@ -53,7 +59,6 @@ import {
webChatStoreUpdated,
webSpeechFactoryUpdated,
} from '../action/chatActions';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { RootState } from '../store';
import { isSpeechEnabled } from '../../utils';
@ -103,175 +108,186 @@ const getChatFromDocumentId = (state: RootState, documentId: string): any => {
return state.chat.chats[documentId];
};
export function* showContextMenuForActivity(action: ChatAction<Activity>): Iterable<any> {
const { payload: activity } = action;
const previousBotState = yield select(getPreviousBotState, activity);
const diffEnabled = activity.valueType.endsWith('botState') && !!previousBotState;
const menuItems = [
{ label: 'Copy text', id: 'copy' },
{ label: 'Copy json', id: 'json' },
{ type: 'separator' },
{ label: 'Compare with previous', id: 'diff', enabled: diffEnabled },
] as MenuItemConstructorOptions[];
export class ChatSagas {
@CommandServiceInstance()
private static commandService: CommandServiceImpl;
const { DisplayContextMenu } = SharedConstants.Commands.Electron;
const response: { id: string } = yield call(
CommandServiceImpl.remoteCall.bind(CommandServiceImpl),
DisplayContextMenu,
menuItems
);
public static *showContextMenuForActivity(action: ChatAction<Activity>): Iterable<any> {
const { payload: activity } = action;
const previousBotState = yield select(getPreviousBotState, activity);
const diffEnabled = activity.valueType.endsWith('botState') && !!previousBotState;
const menuItems = [
{ label: 'Copy text', id: 'copy' },
{ label: 'Copy json', id: 'json' },
{ type: 'separator' },
{ label: 'Compare with previous', id: 'diff', enabled: diffEnabled },
] as MenuItemConstructorOptions[];
if (!response) {
return; // canceled context menu
}
switch (response.id) {
case 'copy':
return Electron.clipboard.writeText(getTextFromActivity(activity));
const { DisplayContextMenu } = SharedConstants.Commands.Electron;
const response: { id: string } = yield call(
[ChatSagas.commandService, ChatSagas.commandService.remoteCall],
DisplayContextMenu,
menuItems
);
case 'json':
return Electron.clipboard.writeText(JSON.stringify(activity, null, 2));
default:
yield* diffWithPreviousBotState(activity);
}
}
export function* closeConversation(action: ChatAction<DocumentIdPayload>): Iterable<any> {
const conversationId = yield select(getConversationIdFromDocumentId, action.payload.documentId);
const { DeleteConversation } = SharedConstants.Commands.Emulator;
const { documentId } = action.payload;
const chat = yield select(getChatFromDocumentId, documentId);
if (chat.directLine) {
chat.directLine.end(); // stop polling
}
yield put(closeDocument(documentId));
// remove the webchat store when the document is closed
yield put(webChatStoreUpdated(documentId, null));
yield call([CommandServiceImpl, CommandServiceImpl.remoteCall], DeleteConversation, conversationId);
}
export function* newChat(action: ChatAction<NewChatPayload>): Iterable<any> {
const { documentId } = action.payload;
// Create a new webchat store for this documentId
yield put(webChatStoreUpdated(documentId, createWebChatStore()));
// Each time a new chat is open, retrieve the speech token
// if the endpoint is speech enabled and create a bind speech
// pony fill factory. This is consumed by WebChat...
yield put(webSpeechFactoryUpdated(documentId, null)); // remove the old factory
const endpoint: IEndpointService = yield select(getEndpointServiceByDocumentId, documentId);
if (!isSpeechEnabled(endpoint)) {
return;
}
yield put(updatePendingSpeechTokenRetrieval(true));
// If an existing factory is found, refresh the token
const existingFactory: string = yield select(getWebSpeechFactoryForDocumentId, documentId);
const { GetSpeechToken: command } = SharedConstants.Commands.Emulator;
const token = yield call(
[CommandServiceImpl, CommandServiceImpl.remoteCall],
command,
endpoint.id,
!!existingFactory
);
if (token) {
const factory = yield call(createCognitiveServicesBingSpeechPonyfillFactory, {
authorizationToken: token,
});
yield put(webSpeechFactoryUpdated(documentId, factory)); // Provide the new factory to the store
}
yield put(updatePendingSpeechTokenRetrieval(false));
}
export function* diffWithPreviousBotState(currentBotState: Activity): Iterable<any> {
const previousBotState: Activity = yield select(getPreviousBotState, currentBotState);
const lhs = [];
const rhs = [];
const deltas = diff(previousBotState.value, currentBotState.value);
(deltas || []).forEach(diff => {
switch (diff.kind) {
case 'A':
{
const { item, path } = diff;
path.push(diff.index);
if (item.kind === 'D') {
lhs.push(path);
} else if (item.kind === 'E') {
rhs.push(path);
lhs.push(path);
} else {
rhs.push(path);
}
}
break;
case 'D':
lhs.push(diff.path);
break;
case 'E':
rhs.push(diff.path);
lhs.push(diff.path);
break;
case 'N':
rhs.push(diff.path);
break;
if (!response) {
return; // canceled context menu
}
});
switch (response.id) {
case 'copy':
return Electron.clipboard.writeText(ChatSagas.getTextFromActivity(activity));
// Clone the bot state and update the keys to show changes
const botStateClone: Activity = JSON.parse(
JSON.stringify(currentBotState, (key: string, value: any) => {
if (value instanceof Array) {
return Object.keys(value).reduce((conversion: any, key) => {
conversion['' + key] = value[key];
return conversion;
}, {});
case 'json':
return Electron.clipboard.writeText(JSON.stringify(activity, null, 2));
default:
yield* ChatSagas.diffWithPreviousBotState(activity);
}
}
public static *closeConversation(action: ChatAction<DocumentIdPayload>): Iterable<any> {
const conversationId = yield select(getConversationIdFromDocumentId, action.payload.documentId);
const { DeleteConversation } = SharedConstants.Commands.Emulator;
const { documentId } = action.payload;
const chat = yield select(getChatFromDocumentId, documentId);
if (chat && chat.directLine) {
chat.directLine.end(); // stop polling
}
yield put(closeDocument(documentId));
// remove the webchat store when the document is closed
yield put(webChatStoreUpdated(documentId, null));
yield call([ChatSagas.commandService, ChatSagas.commandService.remoteCall], DeleteConversation, conversationId);
}
public static *newChat(action: ChatAction<NewChatPayload>): Iterable<any> {
const { documentId, resolver } = action.payload;
// Create a new webchat store for this documentId
yield put(webChatStoreUpdated(documentId, createWebChatStore()));
// Each time a new chat is open, retrieve the speech token
// if the endpoint is speech enabled and create a bind speech
// pony fill factory. This is consumed by WebChat...
yield put(webSpeechFactoryUpdated(documentId, null)); // remove the old factory
const endpoint: IEndpointService = yield select(getEndpointServiceByDocumentId, documentId);
if (!isSpeechEnabled(endpoint)) {
if (resolver) {
resolver();
}
return value;
})
);
botStateClone.valueType = ValueTypes.Diff;
// values that were added
rhs.forEach(path => {
buildDiff('+', path, botStateClone.value, botStateClone.value);
});
// values that were removed
lhs.forEach(path => {
buildDiff('-', path, botStateClone.value, previousBotState.value);
});
const documentId = yield select(getCurrentDocumentId);
yield put(setHighlightedObjects(documentId, [previousBotState, currentBotState]));
yield put(setInspectorObjects(documentId, botStateClone));
}
function getTextFromActivity(activity: Activity): string {
if (activity.valueType === ValueTypes.Command) {
return activity.value;
} else if (activity.valueType === ValueTypes.Activity) {
return 'text' in activity.value ? activity.value.text : activity.label;
}
return activity.text || activity.label || '';
}
function buildDiff(prependWith: string, path: (string | number)[], target: any, source: any): void {
let key;
for (let i = 0; i < path.length; i++) {
key = path[i];
if (key in target && target[key] !== null && typeof target[key] === 'object') {
target = target[key];
source = source[key];
} else {
break;
return;
}
yield put(updatePendingSpeechTokenRetrieval(true));
// If an existing factory is found, refresh the token
const existingFactory: string = yield select(getWebSpeechFactoryForDocumentId, documentId);
const { GetSpeechToken: command } = SharedConstants.Commands.Emulator;
const token = yield call(
[ChatSagas.commandService, ChatSagas.commandService.remoteCall],
command,
endpoint.id,
!!existingFactory
);
if (token) {
const factory = yield call(createCognitiveServicesBingSpeechPonyfillFactory, {
authorizationToken: token,
});
yield put(webSpeechFactoryUpdated(documentId, factory)); // Provide the new factory to the store
}
yield put(updatePendingSpeechTokenRetrieval(false));
if (resolver) {
resolver();
}
}
const value = source[key];
delete target[key];
target[prependWith + key] = value;
public static *diffWithPreviousBotState(currentBotState: Activity): Iterable<any> {
const previousBotState: Activity = yield select(getPreviousBotState, currentBotState);
const lhs = [];
const rhs = [];
const deltas = diff(previousBotState.value, currentBotState.value);
(deltas || []).forEach(diff => {
switch (diff.kind) {
case 'A':
{
const { item, path } = diff;
path.push(diff.index);
if (item.kind === 'D') {
lhs.push(path);
} else if (item.kind === 'E') {
rhs.push(path);
lhs.push(path);
} else {
rhs.push(path);
}
}
break;
case 'D':
lhs.push(diff.path);
break;
case 'E':
rhs.push(diff.path);
lhs.push(diff.path);
break;
case 'N':
rhs.push(diff.path);
break;
}
});
// Clone the bot state and update the keys to show changes
const botStateClone: Activity = JSON.parse(
JSON.stringify(currentBotState, (key: string, value: any) => {
if (value instanceof Array) {
return Object.keys(value).reduce((conversion: any, key) => {
conversion['' + key] = value[key];
return conversion;
}, {});
}
return value;
})
);
botStateClone.valueType = ValueTypes.Diff;
// values that were added
rhs.forEach(path => {
ChatSagas.buildDiff('+', path, botStateClone.value, botStateClone.value);
});
// values that were removed
lhs.forEach(path => {
ChatSagas.buildDiff('-', path, botStateClone.value, previousBotState.value);
});
const documentId = yield select(getCurrentDocumentId);
yield put(setHighlightedObjects(documentId, [previousBotState, currentBotState]));
yield put(setInspectorObjects(documentId, botStateClone));
}
private static getTextFromActivity(activity: Activity): string {
if (activity.valueType === ValueTypes.Command) {
return activity.value;
} else if (activity.valueType === ValueTypes.Activity) {
return 'text' in activity.value ? activity.value.text : activity.label;
}
return activity.text || activity.label || '';
}
public static buildDiff(prependWith: string, path: (string | number)[], target: any, source: any): void {
let key;
for (let i = 0; i < path.length; i++) {
key = path[i];
if (key in target && target[key] !== null && typeof target[key] === 'object') {
target = target[key];
source = source[key];
} else {
break;
}
}
const value = source[key];
delete target[key];
target[prependWith + key] = value;
}
}
export function* chatSagas(): IterableIterator<ForkEffect> {
yield takeEvery(ChatActions.showContextMenuForActivity, showContextMenuForActivity);
yield takeEvery(ChatActions.closeConversation, closeConversation);
yield takeLatest([ChatActions.newChat, ChatActions.clearLog], newChat);
yield takeEvery(ChatActions.showContextMenuForActivity, ChatSagas.showContextMenuForActivity);
yield takeEvery(ChatActions.closeConversation, ChatSagas.closeConversation);
yield takeLatest([ChatActions.newChat, ChatActions.clearLog], ChatSagas.newChat);
}

Просмотреть файл

@ -30,40 +30,35 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { ForkEffect, put, takeEvery } from 'redux-saga/effects';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { newNotification } from '@bfemulator/app-shared';
import { IPC, isObject } from '@bfemulator/sdk-shared';
import { CommandAction, CommandActionPayload, EXECUTE_COMMAND } from '../action/commandAction';
import { beginAdd } from '../action/notificationActions';
export interface Process {
pid: number;
send?(message: any);
on(event: 'message', listener: NodeJS.MessageListener);
on(event: 'exit', listener: NodeJS.ExitListener);
}
export class CommandSagas {
@CommandServiceInstance()
private static commandService: CommandServiceImpl;
export class ProcessIPC extends IPC {
get id(): number {
return this._process.pid;
}
constructor(private _process: Process) {
super();
this._process.on('message', message => {
if (isObject(message) && message.type === 'ipc:message' && Array.isArray(message.args)) {
const channelName = message.args.shift();
const channel = super.getChannel(channelName);
if (channel) {
channel.onMessage(...message.args);
}
public static *executeCommand(action: CommandAction<CommandActionPayload>): IterableIterator<any> {
const { isRemote, commandName, args, resolver } = action.payload;
try {
const result = isRemote
? yield CommandSagas.commandService.remoteCall(commandName, ...args)
: yield CommandSagas.commandService.call(commandName, ...args);
if (resolver) {
resolver(result);
}
});
}
public send(...args: any[]): void {
if (this._process.send) {
this._process.send({
type: 'ipc:message',
args,
});
} catch (e) {
const type = isRemote ? 'remote' : 'local';
yield put(
beginAdd(newNotification(`An error occurred while executing the ${type} command: ${commandName}\n ${e}`))
);
}
}
}
export function* commandSagas(): IterableIterator<ForkEffect> {
yield takeEvery(EXECUTE_COMMAND, CommandSagas.executeCommand);
}

Просмотреть файл

@ -33,11 +33,12 @@
import { SharedConstants } from '@bfemulator/app-shared';
import { call, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { EditorActions, removeDocPendingChange } from '../action/editorActions';
import { checkActiveDocForPendingChanges, editorSagas, promptUserToReloadDocument } from './editorSagas';
import { refreshConversationMenu, editorSelector } from './sharedSagas';
import { editorSagas, EditorSagas } from './editorSagas';
import { SharedSagas, editorSelector } from './sharedSagas';
jest.mock('../store', () => ({
get store() {
@ -45,43 +46,63 @@ jest.mock('../store', () => ({
},
}));
jest.mock('../../ui/dialogs', () => ({}));
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
const mockSharedConstants = SharedConstants;
let mockRemoteCommandsCalled = [];
let mockLocalCommandsCalled = [];
const mockMessageResponse = false;
jest.mock('../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
call: async (commandName: string, ...args: any[]) => {
describe('The Editor Sagas', () => {
let commandService: CommandServiceImpl;
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
commandService.call = async (commandName: string, ...args: any[]) => {
mockLocalCommandsCalled.push({ commandName, args: args });
},
remoteCall: async (commandName: string, ...args: any[]) => {
return true as any;
};
commandService.remoteCall = async (commandName: string, ...args: any[]) => {
mockRemoteCommandsCalled.push({ commandName, args: args });
switch (commandName) {
case mockSharedConstants.Commands.Electron.ShowMessageBox:
if (mockMessageResponse) {
return Promise.resolve(true);
} else {
return Promise.resolve(false);
}
case SharedConstants.Commands.Electron.ShowMessageBox:
return Promise.resolve(false);
default:
return Promise.resolve(true);
return Promise.resolve(true) as any;
}
},
},
}));
};
});
describe('The Editor Sagas', () => {
beforeEach(() => {
mockRemoteCommandsCalled = [];
mockLocalCommandsCalled = [];
});
it('should check the active doc for pending changes', () => {
const gen = checkActiveDocForPendingChanges();
const gen = EditorSagas.checkActiveDocForPendingChanges();
const stateData = gen.next().value;
expect(stateData).toEqual(select(editorSelector));
@ -98,7 +119,7 @@ describe('The Editor Sagas', () => {
};
// should return the inner generator that we delegate to
const innerGen = gen.next(mockEditorState).value;
expect(innerGen).toEqual(call(promptUserToReloadDocument, mockActiveDocId));
expect(innerGen).toEqual(call(EditorSagas.promptUserToReloadDocument, mockActiveDocId));
expect(gen.next().done).toBe(true);
});
@ -110,7 +131,7 @@ describe('The Editor Sagas', () => {
title: 'File change detected',
message: 'We have detected a change in this file on disk. Would you like to reload it in the Emulator?',
};
const gen = promptUserToReloadDocument(mockChatFileName);
const gen = EditorSagas.promptUserToReloadDocument(mockChatFileName);
gen.next();
@ -138,7 +159,7 @@ describe('The Editor Sagas', () => {
title: 'File change detected',
message: 'We have detected a change in this file on disk. Would you like to reload it in the Emulator?',
};
const gen = promptUserToReloadDocument(mockTranscriptFile);
const gen = EditorSagas.promptUserToReloadDocument(mockTranscriptFile);
gen.next();
@ -170,7 +191,7 @@ describe('The Editor Sagas', () => {
EditorActions.setActiveTab,
EditorActions.open,
],
checkActiveDocForPendingChanges
EditorSagas.checkActiveDocForPendingChanges
)
);
@ -179,7 +200,7 @@ describe('The Editor Sagas', () => {
expect(refreshConversationMenuYield).toEqual(
takeLatest(
[EditorActions.close, EditorActions.open, EditorActions.setActiveEditor, EditorActions.setActiveTab],
refreshConversationMenu
SharedSagas.refreshConversationMenu
)
);

Просмотреть файл

@ -33,46 +33,51 @@
import { isChatFile, isTranscriptFile, SharedConstants } from '@bfemulator/app-shared';
import { call, ForkEffect, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { EditorActions, removeDocPendingChange } from '../action/editorActions';
import { editorSelector, refreshConversationMenu } from './sharedSagas';
import { editorSelector, SharedSagas } from './sharedSagas';
export function* promptUserToReloadDocument(filename: string): IterableIterator<any> {
const { Commands } = SharedConstants;
const options = {
buttons: ['Cancel', 'Reload'],
title: 'File change detected',
message: 'We have detected a change in this file on disk. Would you like to reload it in the Emulator?',
};
const confirmation = yield CommandServiceImpl.remoteCall(Commands.Electron.ShowMessageBox, options);
export class EditorSagas {
@CommandServiceInstance()
private static commandService: CommandServiceImpl;
// clear the doc of pending changes
yield put(removeDocPendingChange(filename));
public static *promptUserToReloadDocument(filename: string): IterableIterator<any> {
const { Commands } = SharedConstants;
const options = {
buttons: ['Cancel', 'Reload'],
title: 'File change detected',
message: 'We have detected a change in this file on disk. Would you like to reload it in the Emulator?',
};
const confirmation = yield EditorSagas.commandService.remoteCall(Commands.Electron.ShowMessageBox, options);
// reload the file, otherwise proceed without reloading
const { OpenChatFile, ReloadTranscript } = SharedConstants.Commands.Emulator;
// clear the doc of pending changes
yield put(removeDocPendingChange(filename));
if (confirmation) {
if (isChatFile(filename)) {
yield CommandServiceImpl.call(OpenChatFile, filename, true);
} else if (isTranscriptFile(filename)) {
yield CommandServiceImpl.call(ReloadTranscript, filename);
// reload the file, otherwise proceed without reloading
const { OpenChatFile, ReloadTranscript } = SharedConstants.Commands.Emulator;
if (confirmation) {
if (isChatFile(filename)) {
yield EditorSagas.commandService.call(OpenChatFile, filename, true);
} else if (isTranscriptFile(filename)) {
yield EditorSagas.commandService.call(ReloadTranscript, filename);
}
}
}
}
export function* checkActiveDocForPendingChanges(): IterableIterator<any> {
const stateData = yield select(editorSelector);
public static *checkActiveDocForPendingChanges(): IterableIterator<any> {
const stateData = yield select(editorSelector);
// if currently active document has pending changes, prompt the user to reload it
const activeDocId = stateData.editors[stateData.activeEditor].activeDocumentId;
if (stateData.docsWithPendingChanges.some(doc => doc === activeDocId)) {
// TODO: active document ID is not always the filename
yield call(promptUserToReloadDocument, activeDocId);
// if currently active document has pending changes, prompt the user to reload it
const activeDocId = stateData.editors[stateData.activeEditor].activeDocumentId;
if (stateData.docsWithPendingChanges.some(doc => doc === activeDocId)) {
// TODO: active document ID is not always the filename
yield call(EditorSagas.promptUserToReloadDocument, activeDocId);
}
return;
}
return;
}
export function* editorSagas(): IterableIterator<ForkEffect> {
@ -80,11 +85,11 @@ export function* editorSagas(): IterableIterator<ForkEffect> {
// is focused, check to see if the active document has pending changes
yield takeEvery(
[EditorActions.addDocPendingChange, EditorActions.setActiveEditor, EditorActions.setActiveTab, EditorActions.open],
checkActiveDocForPendingChanges
EditorSagas.checkActiveDocForPendingChanges
);
yield takeLatest(
[EditorActions.close, EditorActions.open, EditorActions.setActiveEditor, EditorActions.setActiveTab],
refreshConversationMenu
SharedSagas.refreshConversationMenu
);
}

Просмотреть файл

@ -34,11 +34,11 @@ import { applyMiddleware, combineReducers, createStore } from 'redux';
import sagaMiddlewareFactory from 'redux-saga';
import { Component } from 'react';
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { bot } from '../reducer/bot';
import { loadBotInfos, setActiveBot } from '../action/botActions';
import { launchEndpointEditor, openEndpointExplorerContextMenu } from '../action/endpointServiceActions';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { DialogService } from '../../ui/dialogs/service';
import { endpointSagas } from './endpointSagas';
@ -75,32 +75,46 @@ const mockBot = JSON.parse(`{
}]
}`);
jest.mock('../../ui/dialogs', () => ({
AzureLoginPromptDialogContainer: function mock() {
return undefined;
},
AzureLoginSuccessDialogContainer: function mock() {
return undefined;
},
BotCreationDialog: function mock() {
return undefined;
},
DialogService: {
showDialog: () => Promise.resolve(mockBot.services),
},
SecretPromptDialog: function mock() {
return undefined;
},
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
describe('The endpoint sagas', () => {
let commandService: CommandServiceImpl;
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
});
beforeEach(() => {
mockStore.dispatch(loadBotInfos([mockBot]));
mockStore.dispatch(setActiveBot(mockBot));
});
it('should launch the endpoint editor and execute a command to save the edited services', async () => {
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall');
const remoteCallSpy = jest.spyOn(commandService, 'remoteCall');
const dialogServiceSpy = jest.spyOn(DialogService, 'showDialog').mockResolvedValue(mockBot.services);
await mockStore.dispatch(launchEndpointEditor(mockComponentClass, mockBot.services[0]));
const { AddOrUpdateService } = SharedConstants.Commands.Bot;
@ -121,7 +135,7 @@ describe('The endpoint sagas', () => {
const { DisplayContextMenu, ShowMessageBox } = SharedConstants.Commands.Electron;
const { NewLiveChat } = SharedConstants.Commands.Emulator;
it('should launch the endpoint editor when that menu option is chosen', () => {
const commandServiceSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue({ id: 'edit' });
const commandServiceSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue({ id: 'edit' });
const dialogServiceSpy = jest.spyOn(DialogService, 'showDialog').mockResolvedValue(mockBot.services);
mockStore.dispatch(openEndpointExplorerContextMenu(mockComponentClass, mockBot.services[0]));
@ -132,10 +146,8 @@ describe('The endpoint sagas', () => {
});
it('should open a deep link when that menu option is chosen', async () => {
const commandServiceRemoteCallSpy = jest
.spyOn(CommandServiceImpl, 'remoteCall')
.mockResolvedValue({ id: 'open' });
const commandServiceCallSpy = jest.spyOn(CommandServiceImpl, 'call').mockResolvedValue(true);
const commandServiceRemoteCallSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue({ id: 'open' });
const commandServiceCallSpy = jest.spyOn(commandService, 'call').mockResolvedValue(true);
await mockStore.dispatch(openEndpointExplorerContextMenu(mockComponentClass, mockBot.services[0]));
expect(commandServiceRemoteCallSpy).toHaveBeenCalledWith(DisplayContextMenu, menuItems);
@ -144,12 +156,12 @@ describe('The endpoint sagas', () => {
it('should forget the service when that menu item is chosen', async () => {
const remoteCallArgs = [];
CommandServiceImpl.remoteCall = async (commandName, ...args) => {
commandService.remoteCall = async (commandName, ...args) => {
remoteCallArgs.push({ commandName, args: args });
if (commandName === DisplayContextMenu) {
return { id: 'forget' };
}
return true;
return true as any;
};
const { RemoveService } = SharedConstants.Commands.Bot;
await mockStore.dispatch(openEndpointExplorerContextMenu(mockComponentClass, mockBot.services[0]));

Просмотреть файл

@ -35,8 +35,8 @@ import { SharedConstants } from '@bfemulator/app-shared';
import { IBotService, IEndpointService, ServiceTypes } from 'botframework-config/lib/schema';
import { ComponentClass } from 'react';
import { call, ForkEffect, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { DialogService } from '../../ui/dialogs/service';
import { openServiceDeepLink } from '../action/connectedServiceActions';
import {
@ -55,90 +55,111 @@ const getConnectedAbs = (state: RootState, endpointAppId: string) => {
});
};
function* launchEndpointEditor(action: EndpointServiceAction<EndpointEditorPayload>): IterableIterator<any> {
const { endpointEditorComponent, endpointService = {} } = action.payload;
const servicesToUpdate = yield DialogService.showDialog<ComponentClass<any>, IEndpointService[]>(
endpointEditorComponent,
{ endpointService }
);
export class EndpointSagas {
@CommandServiceInstance()
private static commandService: CommandServiceImpl;
if (servicesToUpdate) {
const { AddOrUpdateService, RemoveService } = SharedConstants.Commands.Bot;
let i = servicesToUpdate.length;
while (i--) {
const service = servicesToUpdate[i];
let shouldBeRemoved = false;
if (service.type === ServiceTypes.Bot) {
// Since we could end up with an invalid ABS
// naively validate and remove it if all fields are missing
const { serviceName, resourceGroup, subscriptionId, tenantId } = service as IBotService;
shouldBeRemoved = !serviceName && !resourceGroup && !subscriptionId && !tenantId;
public static *launchEndpointEditor(action: EndpointServiceAction<EndpointEditorPayload>): IterableIterator<any> {
const { endpointEditorComponent, endpointService = {} } = action.payload;
const servicesToUpdate = yield DialogService.showDialog<ComponentClass<any>, IEndpointService[]>(
endpointEditorComponent,
{ endpointService }
);
if (servicesToUpdate) {
const { AddOrUpdateService, RemoveService } = SharedConstants.Commands.Bot;
let i = servicesToUpdate.length;
while (i--) {
const service = servicesToUpdate[i];
let shouldBeRemoved = false;
if (service.type === ServiceTypes.Bot) {
// Since we could end up with an invalid ABS
// naively validate and remove it if all fields are missing
const { serviceName, resourceGroup, subscriptionId, tenantId } = service as IBotService;
shouldBeRemoved = !serviceName && !resourceGroup && !subscriptionId && !tenantId;
}
yield EndpointSagas.commandService.remoteCall(
shouldBeRemoved ? RemoveService : AddOrUpdateService,
service.type,
service
);
}
yield CommandServiceImpl.remoteCall(shouldBeRemoved ? RemoveService : AddOrUpdateService, service.type, service);
}
}
public static *openEndpointContextMenu(
action: EndpointServiceAction<EndpointServicePayload | EndpointEditorPayload>
): IterableIterator<any> {
const connectedAbs = yield select<RootState, string>(getConnectedAbs, action.payload.endpointService.appId);
const menuItems = [
{ label: 'Open in Emulator', id: 'open' },
{ label: 'Open in portal', id: 'absLink', enabled: !!connectedAbs },
{ label: 'Edit configuration', id: 'edit' },
{ label: 'Remove', id: 'forget' },
];
const { DisplayContextMenu } = SharedConstants.Commands.Electron;
const response = yield call(
[EndpointSagas.commandService, EndpointSagas.commandService.remoteCall],
DisplayContextMenu,
menuItems
);
switch (response.id) {
case 'edit':
yield* EndpointSagas.launchEndpointEditor(action);
break;
case 'open':
yield* EndpointSagas.openEndpointInEmulator(action);
break;
case 'absLink':
yield put(openServiceDeepLink(connectedAbs));
break;
case 'forget':
yield* EndpointSagas.removeEndpointServiceFromActiveBot(action.payload.endpointService);
break;
default:
// canceled context menu
return;
}
}
// eslint-disable-next-line require-yield
public static *openEndpointInEmulator(action: EndpointServiceAction<EndpointServicePayload>): IterableIterator<any> {
const { endpointService, focusExistingChatIfAvailable: focusExisting = false } = action.payload;
return EndpointSagas.commandService.call(
SharedConstants.Commands.Emulator.NewLiveChat,
endpointService,
focusExisting
);
}
public static *removeEndpointServiceFromActiveBot(endpointService: IEndpointService): IterableIterator<any> {
const result = yield EndpointSagas.commandService.remoteCall(
SharedConstants.Commands.Electron.ShowMessageBox,
true,
{
type: 'question',
buttons: ['Cancel', 'OK'],
defaultId: 1,
message: `Remove endpoint ${endpointService.name}. Are you sure?`,
cancelId: 0,
}
);
if (result) {
yield EndpointSagas.commandService.remoteCall(
SharedConstants.Commands.Bot.RemoveService,
endpointService.type,
endpointService.id
);
}
}
}
function* openEndpointContextMenu(
action: EndpointServiceAction<EndpointServicePayload | EndpointEditorPayload>
): IterableIterator<any> {
const connectedAbs = yield select<RootState, string>(getConnectedAbs, action.payload.endpointService.appId);
const menuItems = [
{ label: 'Open in Emulator', id: 'open' },
{ label: 'Open in portal', id: 'absLink', enabled: !!connectedAbs },
{ label: 'Edit configuration', id: 'edit' },
{ label: 'Remove', id: 'forget' },
];
const { DisplayContextMenu } = SharedConstants.Commands.Electron;
const response = yield call(CommandServiceImpl.remoteCall.bind(CommandServiceImpl), DisplayContextMenu, menuItems);
switch (response.id) {
case 'edit':
yield* launchEndpointEditor(action);
break;
case 'open':
yield* openEndpointInEmulator(action);
break;
case 'absLink':
yield put(openServiceDeepLink(connectedAbs));
break;
case 'forget':
yield* removeEndpointServiceFromActiveBot(action.payload.endpointService);
break;
default:
// canceled context menu
return;
}
}
// eslint-disable-next-line require-yield
function* openEndpointInEmulator(action: EndpointServiceAction<EndpointServicePayload>): IterableIterator<any> {
const { endpointService, focusExistingChatIfAvailable: focusExisting = false } = action.payload;
return CommandServiceImpl.call(SharedConstants.Commands.Emulator.NewLiveChat, endpointService, focusExisting);
}
function* removeEndpointServiceFromActiveBot(endpointService: IEndpointService): IterableIterator<any> {
const result = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.ShowMessageBox, true, {
type: 'question',
buttons: ['Cancel', 'OK'],
defaultId: 1,
message: `Remove endpoint ${endpointService.name}. Are you sure?`,
cancelId: 0,
});
if (result) {
yield CommandServiceImpl.remoteCall(
SharedConstants.Commands.Bot.RemoveService,
endpointService.type,
endpointService.id
);
}
}
export function* endpointSagas(): IterableIterator<ForkEffect> {
yield takeLatest(LAUNCH_ENDPOINT_EDITOR, launchEndpointEditor);
yield takeEvery(OPEN_ENDPOINT_CONTEXT_MENU, openEndpointContextMenu);
yield takeEvery(OPEN_ENDPOINT_IN_EMULATOR, openEndpointInEmulator);
yield takeLatest(LAUNCH_ENDPOINT_EDITOR, EndpointSagas.launchEndpointEditor);
yield takeEvery(OPEN_ENDPOINT_CONTEXT_MENU, EndpointSagas.openEndpointContextMenu);
yield takeEvery(OPEN_ENDPOINT_IN_EMULATOR, EndpointSagas.openEndpointInEmulator);
}

Просмотреть файл

@ -30,47 +30,56 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { SharedConstants, newNotification } from '@bfemulator/app-shared';
import { newNotification, SharedConstants } from '@bfemulator/app-shared';
import { applyMiddleware, combineReducers, createStore } from 'redux';
import sagaMiddlewareFactory from 'redux-saga';
import { call, put, takeEvery, select } from 'redux-saga/effects';
import { call, put, select, takeEvery } from 'redux-saga/effects';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { CONTENT_TYPE_APP_SETTINGS, DOCUMENT_ID_APP_SETTINGS } from '../../constants';
import * as EditorActions from '../action/editorActions';
import {
getFrameworkSettings as getFrameworkSettingsAction,
saveFrameworkSettings as saveFrameworkSettingsAction,
} from '../action/frameworkSettingsActions';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { beginAdd } from '../action/notificationActions';
import { editor } from '../reducer/editor';
import {
frameworkSettingsChanged,
GET_FRAMEWORK_SETTINGS,
getFrameworkSettings as getFrameworkSettingsAction,
SAVE_FRAMEWORK_SETTINGS,
saveFrameworkSettings as saveFrameworkSettingsAction,
} from '../action/frameworkSettingsActions';
import { beginAdd } from '../action/notificationActions';
import { editor } from '../reducer/editor';
import { framework } from '../reducer/frameworkSettingsReducer';
import {
activeDocumentSelector,
frameworkSettingsSagas,
getFrameworkSettings,
FrameworkSettingsSagas,
normalizeSettingsData,
saveFrameworkSettings,
} from './frameworkSettingsSagas';
jest.mock(
'../../ui/dialogs/',
() =>
new Proxy(
{},
{
get(): any {
return {};
},
}
)
);
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
const sagaMiddleWare = sagaMiddlewareFactory();
const mockStore = createStore(combineReducers({ framework, editor }), {}, applyMiddleware(sagaMiddleWare));
@ -91,16 +100,23 @@ mockStore.dispatch(
})
);
describe('The frameworkSettingsSagas', () => {
let commandService: CommandServiceImpl;
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
});
it('should register the expected generators', () => {
const it = frameworkSettingsSagas();
expect(it.next().value).toEqual(takeEvery(GET_FRAMEWORK_SETTINGS, getFrameworkSettings));
expect(it.next().value).toEqual(takeEvery(SAVE_FRAMEWORK_SETTINGS, saveFrameworkSettings));
expect(it.next().value).toEqual(takeEvery(GET_FRAMEWORK_SETTINGS, FrameworkSettingsSagas.getFrameworkSettings));
expect(it.next().value).toEqual(takeEvery(SAVE_FRAMEWORK_SETTINGS, FrameworkSettingsSagas.saveFrameworkSettings));
});
it('should get the framework settings when using the happy path', async () => {
const it = getFrameworkSettings();
const it = FrameworkSettingsSagas.getFrameworkSettings();
let next = it.next();
expect(next.value).toEqual(CommandServiceImpl.remoteCall(SharedConstants.Commands.Settings.LoadAppSettings));
expect(next.value).toEqual(commandService.remoteCall(SharedConstants.Commands.Settings.LoadAppSettings));
next = it.next({});
const normalized = await next.value.CALL.fn({});
@ -108,23 +124,23 @@ describe('The frameworkSettingsSagas', () => {
});
it('should send a notification when something goes wrong while getting the framework settings', () => {
const it = getFrameworkSettings();
const it = FrameworkSettingsSagas.getFrameworkSettings();
it.next();
const errMsg = `Error while loading emulator settings: oh noes!`;
const notification = newNotification(errMsg);
notification.timestamp = jasmine.any(Number);
notification.id = jasmine.any(String);
notification.timestamp = jasmine.any(Number) as any;
notification.id = jasmine.any(String) as any;
expect(it.throw('oh noes!').value).toEqual(put(beginAdd(notification)));
});
it('should save the framework settings', async () => {
const it = saveFrameworkSettings(saveFrameworkSettingsAction({}));
const it = FrameworkSettingsSagas.saveFrameworkSettings(saveFrameworkSettingsAction({}));
const normalize = it.next().value;
expect(normalize).toEqual(call(normalizeSettingsData, {}));
const normalized = await normalize.CALL.fn({});
// remote call to save the settings
expect(it.next(normalized).value).toEqual(
CommandServiceImpl.remoteCall(SharedConstants.Commands.Settings.SaveAppSettings, normalized)
commandService.remoteCall(SharedConstants.Commands.Settings.SaveAppSettings, normalized)
);
// get the settings from the main side again
expect(it.next().value).toEqual(put(getFrameworkSettingsAction()));
@ -137,12 +153,12 @@ describe('The frameworkSettingsSagas', () => {
});
it('should send a notification when saving the settings fails', () => {
const it = saveFrameworkSettings(saveFrameworkSettingsAction({}));
const it = FrameworkSettingsSagas.saveFrameworkSettings(saveFrameworkSettingsAction({}));
it.next();
const errMsg = `Error while saving emulator settings: oh noes!`;
const notification = newNotification(errMsg);
notification.timestamp = jasmine.any(Number);
notification.id = jasmine.any(String);
notification.timestamp = jasmine.any(Number) as any;
notification.id = jasmine.any(String) as any;
expect(it.throw('oh noes!').value).toEqual(put(beginAdd(notification)));
});
});

Просмотреть файл

@ -32,8 +32,8 @@
//
import { frameworkDefault, FrameworkSettings, newNotification, SharedConstants } from '@bfemulator/app-shared';
import { call, ForkEffect, put, select, takeEvery } from 'redux-saga/effects';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import * as EditorActions from '../action/editorActions';
import {
FrameworkSettingsAction,
@ -61,34 +61,44 @@ export const activeDocumentSelector = (state: RootState) => {
return editors[activeEditor].documents[activeDocumentId];
};
export function* getFrameworkSettings(): IterableIterator<any> {
try {
const framework = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Settings.LoadAppSettings);
const normalized = yield call(normalizeSettingsData, framework);
yield put(frameworkSettingsChanged(normalized));
} catch (e) {
const errMsg = `Error while loading emulator settings: ${e}`;
const notification = newNotification(errMsg);
yield put(beginAdd(notification));
}
}
export class FrameworkSettingsSagas {
@CommandServiceInstance()
private static commandService: CommandServiceImpl;
export function* saveFrameworkSettings(action: FrameworkSettingsAction<FrameworkSettings>): IterableIterator<any> {
try {
// trim keys that do not belong and generate a hash
const normalized = yield call(normalizeSettingsData, action.payload);
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Settings.SaveAppSettings, normalized);
yield put(getFrameworkSettingsAction()); // sync with main - do not assume main hasn't processed this in some way
const activeDoc: Document = yield select(activeDocumentSelector);
yield put(EditorActions.setDirtyFlag(activeDoc.documentId, false)); // mark as clean
} catch (e) {
const errMsg = `Error while saving emulator settings: ${e}`;
const notification = newNotification(errMsg);
yield put(beginAdd(notification));
public static *getFrameworkSettings(): IterableIterator<any> {
try {
const framework = yield FrameworkSettingsSagas.commandService.remoteCall(
SharedConstants.Commands.Settings.LoadAppSettings
);
const normalized = yield call(normalizeSettingsData, framework);
yield put(frameworkSettingsChanged(normalized));
} catch (e) {
const errMsg = `Error while loading emulator settings: ${e}`;
const notification = newNotification(errMsg);
yield put(beginAdd(notification));
}
}
public static *saveFrameworkSettings(action: FrameworkSettingsAction<FrameworkSettings>): IterableIterator<any> {
try {
// trim keys that do not belong and generate a hash
const normalized = yield call(normalizeSettingsData, action.payload);
yield FrameworkSettingsSagas.commandService.remoteCall(
SharedConstants.Commands.Settings.SaveAppSettings,
normalized
);
yield put(getFrameworkSettingsAction()); // sync with main - do not assume main hasn't processed this in some way
const activeDoc: Document = yield select(activeDocumentSelector);
yield put(EditorActions.setDirtyFlag(activeDoc.documentId, false)); // mark as clean
} catch (e) {
const errMsg = `Error while saving emulator settings: ${e}`;
const notification = newNotification(errMsg);
yield put(beginAdd(notification));
}
}
}
export function* frameworkSettingsSagas(): IterableIterator<ForkEffect> {
yield takeEvery(GET_FRAMEWORK_SETTINGS, getFrameworkSettings);
yield takeEvery(SAVE_FRAMEWORK_SETTINGS, saveFrameworkSettings);
yield takeEvery(GET_FRAMEWORK_SETTINGS, FrameworkSettingsSagas.getFrameworkSettings);
yield takeEvery(SAVE_FRAMEWORK_SETTINGS, FrameworkSettingsSagas.saveFrameworkSettings);
}

Просмотреть файл

@ -42,16 +42,20 @@ import { resourceSagas } from './resourcesSagas';
import { servicesExplorerSagas } from './servicesExplorerSagas';
import { welcomePageSagas } from './welcomePageSagas';
import { chatSagas } from './chatSagas';
import { commandSagas } from './commandSagas';
import { presentationSagas } from './presentationSagas';
export const applicationSagas = [
azureAuthSagas,
botSagas,
chatSagas,
commandSagas,
editorSagas,
endpointSagas,
frameworkSettingsSagas,
navBarSagas,
notificationSagas,
presentationSagas,
resourceSagas,
servicesExplorerSagas,
welcomePageSagas,

Просмотреть файл

@ -39,26 +39,6 @@ import { markAllAsRead } from '../action/notificationActions';
import { markNotificationsAsRead } from './navBarSagas';
jest.mock('../../ui/dialogs', () => ({
AzureLoginPromptDialogContainer: function mock() {
return undefined;
},
AzureLoginSuccessDialogContainer: function mock() {
return undefined;
},
BotCreationDialog: function mock() {
return undefined;
},
DialogService: { showDialog: () => Promise.resolve(true) },
SecretPromptDialog: function mock() {
return undefined;
},
}));
jest.mock('../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
remoteCall: () => Promise.resolve(true),
},
}));
describe('Nav bar sagas', () => {
test('markNotificationsAsRead()', () => {
const gen = markNotificationsAsRead(select(Constants.NAVBAR_NOTIFICATIONS));

Просмотреть файл

@ -30,38 +30,26 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { call, ForkEffect, takeEvery } from 'redux-saga/effects';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { SharedConstants } from '@bfemulator/app-shared';
export interface Disposable {
dispose(): void;
}
import { PresentationAction, PresentationActions } from '../action/presentationActions';
export function isDisposable(obj: any): boolean {
return obj && typeof obj.dispose === 'function';
}
export class PresentationSagas {
@CommandServiceInstance()
private static commandService: CommandServiceImpl;
export function dispose<T extends Disposable>(obj: T): T;
export function dispose<T extends Disposable>(...arr: T[]): T[];
export function dispose<T extends Disposable>(arr: T[]): T[];
export function dispose<T extends Disposable>(arg: T | T[]): T | T[] {
if (Array.isArray(arg)) {
arg.forEach(elem => elem && elem.dispose());
return [];
} else {
if (arg) {
arg.dispose();
}
return undefined;
public static *presentationModeChanged(action: PresentationAction): IterableIterator<any> {
const enabled = action.type === PresentationActions.enable;
yield call(
[PresentationSagas.commandService, PresentationSagas.commandService.remoteCall],
SharedConstants.Commands.Electron.SetFullscreen,
enabled
);
}
}
export abstract class DisposableImpl implements Disposable {
private _toDispose: Disposable[] = [];
public dispose(): void {
this._toDispose = dispose(this._toDispose);
}
public toDispose(...objs: Disposable[]): void {
this._toDispose.push(...objs);
}
export function* presentationSagas(): IterableIterator<ForkEffect> {
yield takeEvery([PresentationActions.disable, PresentationActions.enable], PresentationSagas.presentationModeChanged);
}

Просмотреть файл

@ -32,7 +32,7 @@
//
import { applyMiddleware, combineReducers, createStore } from 'redux';
import { BotConfigWithPathImpl } from '@bfemulator/sdk-shared';
import { BotConfigWithPathImpl, CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { ServiceTypes } from 'botframework-config/lib/schema';
import sagaMiddlewareFactory from 'redux-saga';
import { SharedConstants } from '@bfemulator/app-shared/built';
@ -57,6 +57,32 @@ jest.mock('../store', () => ({
return mockStore;
},
}));
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
jest.mock('../../ui/dialogs/service', () => ({
DialogService: {
showDialog: () => Promise.resolve(true),
@ -68,9 +94,14 @@ const mockRemoteCommandsCalled = [];
const mockLocalCommandsCalled = [];
const mockSharedConstants = SharedConstants; // thanks Jest!
let mockContextMenuResponse;
jest.mock('../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
remoteCall: async (commandName: string, ...args: any[]) => {
describe('The ResourceSagas', () => {
let commandService: CommandServiceImpl;
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
commandService.remoteCall = async (commandName: string, ...args: any[]) => {
mockRemoteCommandsCalled.push({ commandName, args: args });
switch (commandName) {
case mockSharedConstants.Commands.Electron.DisplayContextMenu:
@ -82,14 +113,14 @@ jest.mock('../../platform/commands/commandServiceImpl', () => ({
default:
return true;
}
},
call: async (commandName: string, ...args: any[]) => {
mockLocalCommandsCalled.push({ commandName, args: args });
},
},
}));
};
commandService.call = async (commandName: string, ...args: any[]) => {
mockLocalCommandsCalled.push({ commandName, args: args });
return true as any;
};
});
describe('The ResourceSagas', () => {
beforeEach(() => {
mockRemoteCommandsCalled.length = 0;
mockLocalCommandsCalled.length = 0;

Просмотреть файл

@ -35,8 +35,8 @@ import { newNotification } from '@bfemulator/app-shared/built';
import { IFileService } from 'botframework-config/lib/schema';
import { ComponentClass } from 'react';
import { ForkEffect, put, takeEvery } from 'redux-saga/effects';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { DialogService } from '../../ui/dialogs/service';
import { beginAdd } from '../action/notificationActions';
import {
@ -48,93 +48,106 @@ import {
ResourcesAction,
} from '../action/resourcesAction';
function* openContextMenuForResource(action: ResourcesAction<IFileService>): IterableIterator<any> {
const menuItems = [{ label: 'Open file location', id: 0 }, { label: 'Rename', id: 1 }, { label: 'Delete', id: 2 }];
export class ResourcesSagas {
@CommandServiceInstance()
private static commandService: CommandServiceImpl;
const result = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.DisplayContextMenu, menuItems);
switch (result.id) {
case 0:
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.OpenFileLocation, action.payload.path);
break;
public static *openContextMenuForResource(action: ResourcesAction<IFileService>): IterableIterator<any> {
const menuItems = [{ label: 'Open file location', id: 0 }, { label: 'Rename', id: 1 }, { label: 'Delete', id: 2 }];
case 1:
yield put(editResource(action.payload));
break;
const result = yield ResourcesSagas.commandService.remoteCall(
SharedConstants.Commands.Electron.DisplayContextMenu,
menuItems
);
switch (result.id) {
case 0:
yield ResourcesSagas.commandService.remoteCall(
SharedConstants.Commands.Electron.OpenFileLocation,
action.payload.path
);
break;
case 2:
yield* deleteFile(action);
break;
case 1:
yield put(editResource(action.payload));
break;
default:
// Canceled context menu
break;
case 2:
yield* ResourcesSagas.deleteFile(action);
break;
default:
// Canceled context menu
break;
}
}
}
function* deleteFile(action: ResourcesAction<IFileService>): IterableIterator<any> {
const { name, path } = action.payload;
const { ShowMessageBox, UnlinkFile } = SharedConstants.Commands.Electron;
const result = yield CommandServiceImpl.remoteCall(ShowMessageBox, true, {
type: 'info',
title: 'Delete this file',
buttons: ['Cancel', 'Delete'],
defaultId: 1,
message: `This action cannot be undone. Are you sure you want to delete ${name}?`,
cancelId: 0,
});
if (result) {
yield CommandServiceImpl.remoteCall(UnlinkFile, path);
}
}
function* doRename(action: ResourcesAction<IFileService>) {
const { payload } = action;
const { ShowMessageBox, RenameFile } = SharedConstants.Commands.Electron;
if (!payload.name) {
return CommandServiceImpl.remoteCall(ShowMessageBox, true, {
type: 'error',
title: 'Invalid file name',
buttons: ['Ok'],
public static *deleteFile(action: ResourcesAction<IFileService>): IterableIterator<any> {
const { name, path } = action.payload;
const { ShowMessageBox, UnlinkFile } = SharedConstants.Commands.Electron;
const result = yield ResourcesSagas.commandService.remoteCall(ShowMessageBox, true, {
type: 'info',
title: 'Delete this file',
buttons: ['Cancel', 'Delete'],
defaultId: 1,
message: `A valid file name must be used`,
message: `This action cannot be undone. Are you sure you want to delete ${name}?`,
cancelId: 0,
});
if (result) {
yield ResourcesSagas.commandService.remoteCall(UnlinkFile, path);
}
}
yield CommandServiceImpl.remoteCall(RenameFile, payload);
yield put(editResource(null));
}
function* doOpenResource(action: ResourcesAction<IFileService>): IterableIterator<any> {
const { OpenChatFile, OpenTranscript } = SharedConstants.Commands.Emulator;
const { TrackEvent } = SharedConstants.Commands.Telemetry;
const { path, name } = action.payload;
if (isChatFile(path)) {
yield CommandServiceImpl.call(OpenChatFile, path, true);
CommandServiceImpl.remoteCall(TrackEvent, 'chatFile_open').catch(_e => void 0);
} else if (isTranscriptFile(path)) {
yield CommandServiceImpl.call(OpenTranscript, path, name);
CommandServiceImpl.remoteCall(TrackEvent, 'transcriptFile_open', {
method: 'resources_pane',
}).catch(_e => void 0);
public static *doRename(action: ResourcesAction<IFileService>) {
const { payload } = action;
const { ShowMessageBox, RenameFile } = SharedConstants.Commands.Electron;
if (!payload.name) {
return ResourcesSagas.commandService.remoteCall(ShowMessageBox, true, {
type: 'error',
title: 'Invalid file name',
buttons: ['Ok'],
defaultId: 1,
message: `A valid file name must be used`,
cancelId: 0,
});
}
yield ResourcesSagas.commandService.remoteCall(RenameFile, payload);
yield put(editResource(null));
}
// unknown types just fall into the abyss
}
function* launchResourcesSettingsModal(action: ResourcesAction<{ dialog: ComponentClass<any> }>) {
const result: Partial<BotInfo> = yield DialogService.showDialog(action.payload.dialog);
if (result) {
try {
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.PatchBotList, result.path, result);
} catch (e) {
const notification = newNotification('Unable to save resource settings', NotificationType.Error);
yield put(beginAdd(notification));
public static *doOpenResource(action: ResourcesAction<IFileService>): IterableIterator<any> {
const { OpenChatFile, OpenTranscript } = SharedConstants.Commands.Emulator;
const { TrackEvent } = SharedConstants.Commands.Telemetry;
const { path, name } = action.payload;
if (isChatFile(path)) {
yield ResourcesSagas.commandService.call(OpenChatFile, path, true);
ResourcesSagas.commandService.remoteCall(TrackEvent, 'chatFile_open').catch(_e => void 0);
} else if (isTranscriptFile(path)) {
yield ResourcesSagas.commandService.call(OpenTranscript, path, name);
ResourcesSagas.commandService
.remoteCall(TrackEvent, 'transcriptFile_open', {
method: 'resources_pane',
})
.catch();
}
// unknown types just fall into the abyss
}
public static *launchResourcesSettingsModal(action: ResourcesAction<{ dialog: ComponentClass<any> }>) {
const result: Partial<BotInfo> = yield DialogService.showDialog(action.payload.dialog);
if (result) {
try {
yield ResourcesSagas.commandService.remoteCall(SharedConstants.Commands.Bot.PatchBotList, result.path, result);
} catch (e) {
const notification = newNotification('Unable to save resource settings', NotificationType.Error);
yield put(beginAdd(notification));
}
}
}
}
export function* resourceSagas(): IterableIterator<ForkEffect> {
yield takeEvery(OPEN_CONTEXT_MENU_FOR_RESOURCE, openContextMenuForResource);
yield takeEvery(RENAME_RESOURCE, doRename);
yield takeEvery(OPEN_RESOURCE, doOpenResource);
yield takeEvery(OPEN_RESOURCE_SETTINGS, launchResourcesSettingsModal);
yield takeEvery(OPEN_CONTEXT_MENU_FOR_RESOURCE, ResourcesSagas.openContextMenuForResource);
yield takeEvery(RENAME_RESOURCE, ResourcesSagas.doRename);
yield takeEvery(OPEN_RESOURCE, ResourcesSagas.doOpenResource);
yield takeEvery(OPEN_RESOURCE_SETTINGS, ResourcesSagas.launchResourcesSettingsModal);
}

Просмотреть файл

@ -35,8 +35,9 @@ import { ServiceTypes } from 'botframework-config/lib/schema';
import { applyMiddleware, combineReducers, createStore } from 'redux';
import sagaMiddlewareFactory from 'redux-saga';
import { call } from 'redux-saga/effects';
import { CommandServiceInstance } from '@bfemulator/sdk-shared';
import { CommandServiceImpl } from '@bfemulator/sdk-shared';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import {
AzureLoginFailedDialogContainer,
AzureLoginSuccessDialogContainer,
@ -61,7 +62,7 @@ import {
import { azureAuth } from '../reducer/azureAuthReducer';
import { bot } from '../reducer/bot';
import { launchExternalLink as launchExternalLinkSaga, servicesExplorerSagas } from './servicesExplorerSagas';
import { ServicesExplorerSagas, servicesExplorerSagas } from './servicesExplorerSagas';
const sagaMiddleWare = sagaMiddlewareFactory();
const mockStore = createStore(combineReducers({ azureAuth, bot }), {}, applyMiddleware(sagaMiddleWare));
@ -69,66 +70,62 @@ sagaMiddleWare.run(servicesExplorerSagas);
const mockArmToken = 'bm90aGluZw==.eyJ1cG4iOiJnbGFzZ293QHNjb3RsYW5kLmNvbSJ9.7gjdshgfdsk98458205jfds9843fjds';
jest.mock('../../ui/dialogs', () => ({
AzureLoginPromptDialogContainer: function mock() {
return undefined;
},
AzureLoginSuccessDialogContainer: function mock() {
return undefined;
},
BotCreationDialog: function mock() {
return undefined;
},
DialogService: {
showDialog: () => Promise.resolve(true),
},
SecretPromptDialog: function mock() {
return undefined;
},
}));
jest.mock('../../ui/shell/explorer/servicesExplorer/connectedServiceEditor', () => ({
ConnectedServiceEditorContainer: function mock() {
return undefined;
},
}));
jest.mock('../../ui/shell/explorer/servicesExplorer', () => ({
ConnectedServicePicker: function mock() {
return undefined;
},
}));
jest.mock('../store', () => ({
get store() {
return mockStore;
},
}));
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
jest.mock('./azureAuthSaga', () => ({
getArmToken: function*() {
// eslint-disable-next-line typescript/camelcase
yield { access_token: mockArmToken };
AzureAuthSaga: {
getArmToken: function*() {
// eslint-disable-next-line typescript/camelcase
yield { access_token: mockArmToken };
},
},
}));
jest.mock('../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
remoteCall: () => Promise.resolve(true),
},
}));
CommandServiceImpl.remoteCall = async function(type: string) {
switch (type) {
case SharedConstants.Commands.ConnectedService.GetConnectedServicesByType:
return { services: [{ id: 'a luis service' }], code: ServiceCodes.OK };
default:
return null;
}
};
describe('The ServiceExplorerSagas', () => {
let commandService: CommandServiceImpl;
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
commandService.remoteCall = (type: string) => {
switch (type) {
case SharedConstants.Commands.ConnectedService.GetConnectedServicesByType:
return { services: [{ id: 'a luis service' }], code: ServiceCodes.OK };
default:
return null as any;
}
};
});
describe(' launchConnectedServicePicker happy path', () => {
let launchConnectedServicePickerGen;
let payload: ConnectedServicePickerPayload;
@ -218,11 +215,11 @@ describe('The ServiceExplorerSagas', () => {
const botConfig = it.next(newModels).value.SELECT.selector(mockStore.getState());
let _type;
let _args;
CommandServiceImpl.remoteCall = function(type: string, ...args: any[]) {
commandService.remoteCall = function(type: string, ...args: any[]) {
_type = type;
_args = args;
return Promise.resolve(true);
};
} as any;
const result = await it.next(botConfig).value;
expect(result).toBeTruthy();
@ -257,7 +254,7 @@ describe('The ServiceExplorerSagas', () => {
});
it('should open the service deep link when the "open" menu item is selected', async () => {
CommandServiceImpl.remoteCall = async () => ({ id: 'open' });
commandService.remoteCall = async () => ({ id: 'open' } as any);
const it = contextMenuGen(action);
const result = await it.next().value;
@ -273,14 +270,14 @@ describe('The ServiceExplorerSagas', () => {
});
it('should open the luis editor when the "edit" item is selected', async () => {
CommandServiceImpl.remoteCall = async () => ({ id: 'edit' });
commandService.remoteCall = async () => ({ id: 'edit' } as any);
const it = contextMenuGen(action);
const result = await it.next().value;
expect(result.id).toBe('edit');
DialogService.showDialog = () => Promise.resolve(mockService);
CommandServiceImpl.remoteCall = () => Promise.resolve(true);
commandService.remoteCall = () => Promise.resolve(true) as any;
const responseFromEditor = await it.next(result).value;
expect(responseFromEditor).toEqual(mockService);
@ -290,17 +287,17 @@ describe('The ServiceExplorerSagas', () => {
});
it('should ask the main process to remove the selected luis service from the active bot', async () => {
CommandServiceImpl.remoteCall = async () => ({ id: 'forget' });
commandService.remoteCall = async () => ({ id: 'forget' } as any);
const it = contextMenuGen(action);
let result = await it.next().value;
expect(result.id).toBe('forget');
let _type;
let _args;
CommandServiceImpl.remoteCall = async (type: string, ...args: any[]) => {
commandService.remoteCall = async (type: string, ...args: any[]) => {
_type = type;
_args = args;
return true;
return true as any;
};
result = await it.next(result).value;
@ -321,7 +318,7 @@ describe('The ServiceExplorerSagas', () => {
});
it('should ask the main process to remove the selected QnA Maker service from the active bot', async () => {
CommandServiceImpl.remoteCall = async () => ({ id: 'forget' });
commandService.remoteCall = async () => ({ id: 'forget' } as any);
action.payload.connectedService = JSON.parse(`{
"type": "qna",
@ -339,10 +336,10 @@ describe('The ServiceExplorerSagas', () => {
let _type;
let _args;
CommandServiceImpl.remoteCall = async (type: string, ...args: any[]) => {
commandService.remoteCall = async (type: string, ...args: any[]) => {
_type = type;
_args = args;
return true;
return true as any;
};
result = await it.next(result).value;
@ -363,7 +360,7 @@ describe('The ServiceExplorerSagas', () => {
});
it('should ask the main process to remove the selected dispatch Maker service from the active bot', async () => {
CommandServiceImpl.remoteCall = async () => ({ id: 'forget' });
commandService.remoteCall = async () => ({ id: 'forget' } as any);
action.payload.connectedService = JSON.parse(`{
"type": "dispatch",
@ -381,10 +378,10 @@ describe('The ServiceExplorerSagas', () => {
let _type;
let _args;
CommandServiceImpl.remoteCall = async (type: string, ...args: any[]) => {
commandService.remoteCall = async (type: string, ...args: any[]) => {
_type = type;
_args = args;
return true;
return true as any;
};
result = await it.next(result).value;
@ -407,7 +404,6 @@ describe('The ServiceExplorerSagas', () => {
describe(' launchExternalLink', () => {
let action: ConnectedServiceAction<ConnectedServicePayload>;
let openConnectedServiceGen;
let sagaIt;
it(' should open a LIUS external link', async () => {
@ -424,14 +420,14 @@ describe('The ServiceExplorerSagas', () => {
};
action = launchExternalLink(payload as any);
sagaIt = launchExternalLinkSaga;
sagaIt = ServicesExplorerSagas.launchExternalLink;
const it = sagaIt(action);
const result = it.next().value;
expect(result).toEqual(
call(
[CommandServiceImpl, CommandServiceImpl.remoteCall],
[commandService, commandService.remoteCall],
SharedConstants.Commands.Electron.OpenExternal,
'https://luis.ai'
)
@ -452,14 +448,14 @@ describe('The ServiceExplorerSagas', () => {
};
action = launchExternalLink(payload as any);
sagaIt = launchExternalLinkSaga;
sagaIt = ServicesExplorerSagas.launchExternalLink;
const it = sagaIt(action);
const result = it.next().value;
expect(result).toEqual(
call(
[CommandServiceImpl, CommandServiceImpl.remoteCall],
[commandService, commandService.remoteCall],
SharedConstants.Commands.Electron.OpenExternal,
'https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-tutorial-dispatch?view=azure-bot-service-4.0&tabs=csharp'
)
@ -480,14 +476,14 @@ describe('The ServiceExplorerSagas', () => {
};
action = launchExternalLink(payload as any);
sagaIt = launchExternalLinkSaga;
sagaIt = ServicesExplorerSagas.launchExternalLink;
const it = sagaIt(action);
const result = it.next().value;
expect(result).toEqual(
call(
[CommandServiceImpl, CommandServiceImpl.remoteCall],
[commandService, commandService.remoteCall],
SharedConstants.Commands.Electron.OpenExternal,
'https://www.qnamaker.ai/'
)
@ -520,7 +516,7 @@ describe('The ServiceExplorerSagas', () => {
});
it('should launch the luis connected service picker workflow when the luis menu item is selected', async () => {
CommandServiceImpl.remoteCall = async () => ({ id: ServiceTypes.Luis });
commandService.remoteCall = async () => ({ id: ServiceTypes.Luis } as any);
const it = contextMenuGen(action);
let result = await it.next().value;

Просмотреть файл

@ -32,7 +32,7 @@
//
import { ServiceCodes, SharedConstants } from '@bfemulator/app-shared';
import { BotConfigWithPath } from '@bfemulator/sdk-shared';
import { BotConfigWithPath, CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { BotConfigurationBase } from 'botframework-config/lib/botConfigurationBase';
import {
IAzureService,
@ -44,7 +44,6 @@ import {
} from 'botframework-config/lib/schema';
import { call, ForkEffect, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { DialogService } from '../../ui/dialogs/service';
import { serviceTypeLabels } from '../../utils/serviceTypeLables';
import { ArmTokenData, beginAzureAuthWorkflow } from '../action/azureAuthActions';
@ -54,17 +53,17 @@ import {
ConnectedServicePickerPayload,
LAUNCH_CONNECTED_SERVICE_EDITOR,
LAUNCH_CONNECTED_SERVICE_PICKER,
LAUNCH_EXTERNAL_LINK,
OPEN_ADD_CONNECTED_SERVICE_CONTEXT_MENU,
OPEN_CONNECTED_SERVICE_SORT_CONTEXT_MENU,
OPEN_CONTEXT_MENU_FOR_CONNECTED_SERVICE,
OPEN_SERVICE_DEEP_LINK,
LAUNCH_EXTERNAL_LINK,
} from '../action/connectedServiceActions';
import { sortExplorerContents } from '../action/explorerActions';
import { SortCriteria } from '../reducer/explorer';
import { RootState } from '../store';
import { getArmToken } from './azureAuthSaga';
import { AzureAuthSaga } from './azureAuthSaga';
declare interface ServicesPayload {
services: IConnectedService[];
@ -76,325 +75,373 @@ const geBotConfigFromState = (state: RootState): BotConfigWithPath => state.bot.
const getSortSelection = (state: RootState): { [paneldId: string]: SortCriteria } =>
state.explorer.sortSelectionByPanelId;
function* launchConnectedServicePicker(
action: ConnectedServiceAction<ConnectedServicePickerPayload>
): IterableIterator<any> {
// To retrieve azure services, luis models and KBs,
// we must have the authoring key.
// To get the authoring key, we need the arm token.
let armTokenData: ArmTokenData & number = yield select(getArmTokenFromState);
if (!armTokenData || !armTokenData.access_token) {
const { promptDialog, loginSuccessDialog, loginFailedDialog } = action.payload.azureAuthWorkflowComponents;
armTokenData = yield* getArmToken(
beginAzureAuthWorkflow(
promptDialog,
{ serviceType: action.payload.serviceType },
loginSuccessDialog,
loginFailedDialog
)
);
export class ServicesExplorerSagas {
@CommandServiceInstance()
private static commandService: CommandServiceImpl;
public static *launchConnectedServicePicker(
action: ConnectedServiceAction<ConnectedServicePickerPayload>
): IterableIterator<any> {
// To retrieve azure services, luis models and KBs,
// we must have the authoring key.
// To get the authoring key, we need the arm token.
let armTokenData: ArmTokenData & number = yield select(getArmTokenFromState);
if (!armTokenData || !armTokenData.access_token) {
const { promptDialog, loginSuccessDialog, loginFailedDialog } = action.payload.azureAuthWorkflowComponents;
armTokenData = yield* AzureAuthSaga.getArmToken(
beginAzureAuthWorkflow(
promptDialog,
{ serviceType: action.payload.serviceType },
loginSuccessDialog,
loginFailedDialog
)
);
}
// 2 means the user has chosen to manually enter the connected service
if (armTokenData === 2) {
yield* ServicesExplorerSagas.launchConnectedServiceEditor(action);
return;
}
if (!armTokenData || 'error' in armTokenData) {
return null; // canceled or failed somewhere
}
// Add the authenticated user to the action since we now have the token
const pJson = JSON.parse(atob(armTokenData.access_token.split('.')[1]));
action.payload.authenticatedUser = pJson.upn || pJson.unique_name || pJson.name || pJson.email;
const { serviceType, progressIndicatorComponent } = action.payload;
if (progressIndicatorComponent) {
DialogService.showDialog(progressIndicatorComponent).catch();
}
const payload: ServicesPayload = yield* ServicesExplorerSagas.retrieveServicesByServiceType(serviceType);
if (progressIndicatorComponent) {
DialogService.hideDialog();
}
if (payload.code !== ServiceCodes.OK || !payload.services.length) {
const { getStartedDialog, authenticatedUser } = action.payload;
const result = yield DialogService.showDialog(getStartedDialog, {
serviceType,
authenticatedUser,
showNoModelsFoundContent: !payload.services.length,
});
// Sign up with XXXX
if (result === 1) {
yield* ServicesExplorerSagas.launchExternalLink(action);
}
// Add services manually
if (result === 2) {
yield* ServicesExplorerSagas.launchConnectedServiceEditor(action);
}
} else {
const servicesToAdd = yield* ServicesExplorerSagas.launchConnectedServicePickList(
action,
payload.services,
serviceType
);
if (servicesToAdd) {
const botFile: BotConfigWithPath = yield select(geBotConfigFromState);
botFile.services.push(...servicesToAdd);
const { Bot } = SharedConstants.Commands;
yield ServicesExplorerSagas.commandService.remoteCall(Bot.Save, botFile);
}
}
}
// 2 means the user has chosen to manually enter the connected service
if (armTokenData === 2) {
yield* launchConnectedServiceEditor(action);
return;
}
if (!armTokenData || 'error' in armTokenData) {
return null; // canceled or failed somewhere
}
// Add the authenticated user to the action since we now have the token
const pJson = JSON.parse(atob(armTokenData.access_token.split('.')[1]));
action.payload.authenticatedUser = pJson.upn || pJson.unique_name || pJson.name || pJson.email;
const { serviceType, progressIndicatorComponent } = action.payload;
if (progressIndicatorComponent) {
DialogService.showDialog(progressIndicatorComponent).catch();
}
const payload: ServicesPayload = yield* retrieveServicesByServiceType(serviceType);
if (progressIndicatorComponent) {
DialogService.hideDialog();
}
if (payload.code !== ServiceCodes.OK || !payload.services.length) {
const { getStartedDialog, authenticatedUser } = action.payload;
const result = yield DialogService.showDialog(getStartedDialog, {
serviceType,
public static *launchConnectedServicePickList(
action: ConnectedServiceAction<ConnectedServicePickerPayload>,
availableServices: IConnectedService[],
serviceType: ServiceTypes
): IterableIterator<any> {
const { pickerComponent, authenticatedUser, serviceType: type } = action.payload;
let result = yield DialogService.showDialog(pickerComponent, {
availableServices,
authenticatedUser,
showNoModelsFoundContent: !payload.services.length,
serviceType,
});
// Sign up with XXXX
if (result === 1) {
yield* launchExternalLink(action);
action.payload.connectedService = BotConfigurationBase.serviceFromJSON({
type,
hostname: '' /* defect workaround */,
} as any);
result = yield* ServicesExplorerSagas.launchConnectedServiceEditor(action);
}
// Add services manually
if (result === 2) {
yield* launchConnectedServiceEditor(action);
return result;
}
public static *retrieveServicesByServiceType(serviceType: ServiceTypes): IterableIterator<any> {
const armTokenData: ArmTokenData = yield select(getArmTokenFromState);
if (!armTokenData || !armTokenData.access_token) {
throw new Error('Auth credentials do not exist.');
}
} else {
const servicesToAdd = yield* launchConnectedServicePickList(action, payload.services, serviceType);
if (servicesToAdd) {
const botFile: BotConfigWithPath = yield select(geBotConfigFromState);
botFile.services.push(...servicesToAdd);
const { Bot } = SharedConstants.Commands;
yield CommandServiceImpl.remoteCall(Bot.Save, botFile);
}
}
}
function* launchConnectedServicePickList(
action: ConnectedServiceAction<ConnectedServicePickerPayload>,
availableServices: IConnectedService[],
serviceType: ServiceTypes
): IterableIterator<any> {
const { pickerComponent, authenticatedUser, serviceType: type } = action.payload;
let result = yield DialogService.showDialog(pickerComponent, {
availableServices,
authenticatedUser,
serviceType,
});
if (result === 1) {
action.payload.connectedService = BotConfigurationBase.serviceFromJSON({
type,
hostname: '' /* defect workaround */,
} as any);
result = yield* launchConnectedServiceEditor(action);
}
return result;
}
function* retrieveServicesByServiceType(serviceType: ServiceTypes): IterableIterator<any> {
const armTokenData: ArmTokenData = yield select(getArmTokenFromState);
if (!armTokenData || !armTokenData.access_token) {
throw new Error('Auth credentials do not exist.');
}
const { GetConnectedServicesByType } = SharedConstants.Commands.ConnectedService;
let payload: ServicesPayload;
try {
payload = yield CommandServiceImpl.remoteCall(GetConnectedServicesByType, armTokenData.access_token, serviceType);
} catch (e) {
payload = { services: [], code: ServiceCodes.Error };
}
return payload;
}
// eslint-disable-next-line require-yield
function* openConnectedServiceDeepLink(action: ConnectedServiceAction<ConnectedServicePayload>): IterableIterator<any> {
const { connectedService } = action.payload;
switch (connectedService.type) {
case ServiceTypes.AppInsights:
return openAzureProviderDeepLink('microsoft.insights/components', connectedService as IAzureService);
case ServiceTypes.BlobStorage:
return openAzureProviderDeepLink('Microsoft.DocumentDB/storageAccounts', connectedService as IAzureService);
case ServiceTypes.Bot:
return openAzureProviderDeepLink('Microsoft.BotService/botServices', connectedService as IAzureService);
case ServiceTypes.CosmosDB:
return openAzureProviderDeepLink('Microsoft.DocumentDb/databaseAccounts', connectedService as IAzureService);
case ServiceTypes.Generic:
return window.open((connectedService as IGenericService).url);
case ServiceTypes.Luis:
return openLuisDeepLink(connectedService as ILuisService);
case ServiceTypes.QnA:
return openQnaMakerDeepLink(connectedService as IQnAService);
default:
return window.open('https://portal.azure.com');
}
}
export function* launchExternalLink(action: ConnectedServiceAction<ConnectedServicePayload>): IterableIterator<any> {
const serviceType = action.payload.serviceType;
switch (serviceType) {
case ServiceTypes.QnA:
yield call(
[CommandServiceImpl, CommandServiceImpl.remoteCall],
SharedConstants.Commands.Electron.OpenExternal,
'https://www.qnamaker.ai/'
const { GetConnectedServicesByType } = SharedConstants.Commands.ConnectedService;
let payload: ServicesPayload;
try {
payload = yield ServicesExplorerSagas.commandService.remoteCall(
GetConnectedServicesByType,
armTokenData.access_token,
serviceType
);
break;
case ServiceTypes.Dispatch:
yield call(
[CommandServiceImpl, CommandServiceImpl.remoteCall],
SharedConstants.Commands.Electron.OpenExternal,
'https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-tutorial-dispatch?view=azure-bot-service-4.0&tabs=csharp'
);
break;
case ServiceTypes.Luis:
yield call(
[CommandServiceImpl, CommandServiceImpl.remoteCall],
SharedConstants.Commands.Electron.OpenExternal,
'https://luis.ai'
);
break;
default:
return;
} catch (e) {
payload = { services: [], code: ServiceCodes.Error };
}
return payload;
}
}
function* openContextMenuForService(action: ConnectedServiceAction<ConnectedServicePayload>): IterableIterator<any> {
const menuItems = [
{ label: 'Manage service', id: 'open' },
{ label: 'Edit configuration', id: 'edit' },
{ label: 'Disconnect this service', id: 'forget' },
];
const response = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.DisplayContextMenu, menuItems);
const { connectedService } = action.payload;
action.payload.serviceType = connectedService.type;
switch (response.id) {
case 'open':
yield* openConnectedServiceDeepLink(action);
break;
// eslint-disable-next-line require-yield
public static *openConnectedServiceDeepLink(
action: ConnectedServiceAction<ConnectedServicePayload>
): IterableIterator<any> {
const { connectedService } = action.payload;
switch (connectedService.type) {
case ServiceTypes.AppInsights:
return ServicesExplorerSagas.openAzureProviderDeepLink(
'microsoft.insights/components',
connectedService as IAzureService
);
case 'edit':
yield* launchConnectedServiceEditor(action);
break;
case ServiceTypes.BlobStorage:
return ServicesExplorerSagas.openAzureProviderDeepLink(
'Microsoft.DocumentDB/storageAccounts',
connectedService as IAzureService
);
case 'forget':
yield* removeServiceFromActiveBot(connectedService);
break;
case ServiceTypes.Bot:
return ServicesExplorerSagas.openAzureProviderDeepLink(
'Microsoft.BotService/botServices',
connectedService as IAzureService
);
default:
// canceled context menu
return;
}
}
case ServiceTypes.CosmosDB:
return ServicesExplorerSagas.openAzureProviderDeepLink(
'Microsoft.DocumentDb/databaseAccounts',
connectedService as IAzureService
);
function* openAddConnectedServiceContextMenu(
action: ConnectedServiceAction<ConnectedServicePickerPayload>
): IterableIterator<any> {
const menuItems = [
{ label: 'Add Language Understanding (LUIS)', id: ServiceTypes.Luis },
{ label: 'Add QnA Maker', id: ServiceTypes.QnA },
{ label: 'Add Dispatch', id: ServiceTypes.Dispatch },
{ type: 'separator' },
{ label: 'Add Azure Cosmos DB account', id: ServiceTypes.CosmosDB },
{ label: 'Add Azure Storage account', id: ServiceTypes.BlobStorage },
{ label: 'Add Azure Application Insights', id: ServiceTypes.AppInsights },
{ type: 'separator' },
{ label: 'Add other service …', id: ServiceTypes.Generic },
];
case ServiceTypes.Generic:
return window.open((connectedService as IGenericService).url);
const response = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.DisplayContextMenu, menuItems);
const { id: serviceType } = response;
action.payload.serviceType = serviceType;
if (serviceType === ServiceTypes.Generic || serviceType === ServiceTypes.AppInsights) {
yield* launchConnectedServiceEditor(action);
} else {
yield* launchConnectedServicePicker(action);
}
}
case ServiceTypes.Luis:
return ServicesExplorerSagas.openLuisDeepLink(connectedService as ILuisService);
function* openSortContextMenu(action: ConnectedServiceAction<ConnectedServicePayload>): IterableIterator<any> {
const sortSelectionByPanelId = yield select(getSortSelection);
const currentSort = sortSelectionByPanelId[action.payload.panelId];
const menuItems = [
{
label: 'Sort by name',
id: 'name',
type: 'checkbox',
checked: currentSort === 'name',
},
{
label: 'Sort by type',
id: 'type',
type: 'checkbox',
checked: currentSort === 'type',
},
];
const response = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.DisplayContextMenu, menuItems);
yield response.id ? put(sortExplorerContents(action.payload.panelId, response.id)) : null;
}
case ServiceTypes.QnA:
return ServicesExplorerSagas.openQnaMakerDeepLink(connectedService as IQnAService);
function* removeServiceFromActiveBot(connectedService: IConnectedService): IterableIterator<any> {
// TODO - localization
const result = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.ShowMessageBox, true, {
type: 'question',
buttons: ['Cancel', 'OK'],
defaultId: 1,
message: `Remove ${serviceTypeLabels[connectedService.type]} service: ${connectedService.name}. Are you sure?`,
cancelId: 0,
});
if (result) {
const { RemoveService } = SharedConstants.Commands.Bot;
yield CommandServiceImpl.remoteCall(RemoveService, connectedService.type, connectedService.id);
}
}
function* launchConnectedServiceEditor(action: ConnectedServiceAction<ConnectedServicePayload>): IterableIterator<any> {
const { editorComponent, authenticatedUser, connectedService, serviceType } = action.payload;
const servicesToUpdate: IConnectedService[] = yield DialogService.showDialog(editorComponent, {
connectedService,
authenticatedUser,
serviceType,
});
if (servicesToUpdate) {
let i = servicesToUpdate.length;
while (i--) {
const service = servicesToUpdate[i];
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.AddOrUpdateService, service.type, service);
default:
return window.open('https://portal.azure.com');
}
}
return null;
}
function openAzureProviderDeepLink(provider: string, azureService: IAzureService): void {
const { tenantId, subscriptionId, resourceGroup, serviceName } = azureService;
const bits = [
`https://ms.portal.azure.com/#@${tenantId}/resource/`,
`subscriptions/${subscriptionId}/`,
`resourceGroups/${encodeURI(resourceGroup)}/`,
`providers/${provider}/${encodeURI(serviceName)}/overview`,
];
public static *launchExternalLink(action: ConnectedServiceAction<ConnectedServicePayload>): IterableIterator<any> {
const serviceType = action.payload.serviceType;
switch (serviceType) {
case ServiceTypes.QnA:
yield call(
[ServicesExplorerSagas.commandService, ServicesExplorerSagas.commandService.remoteCall],
SharedConstants.Commands.Electron.OpenExternal,
'https://www.qnamaker.ai/'
);
break;
window.open(bits.join(''));
}
case ServiceTypes.Dispatch:
yield call(
[ServicesExplorerSagas.commandService, ServicesExplorerSagas.commandService.remoteCall],
SharedConstants.Commands.Electron.OpenExternal,
'https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-tutorial-dispatch?view=azure-bot-service-4.0&tabs=csharp'
);
break;
function openLuisDeepLink(luisService: ILuisService) {
const { appId, version, region } = luisService;
let regionPrefix: string;
switch (region) {
case 'westeurope':
regionPrefix = 'eu.';
break;
case ServiceTypes.Luis:
yield call(
[ServicesExplorerSagas.commandService, ServicesExplorerSagas.commandService.remoteCall],
SharedConstants.Commands.Electron.OpenExternal,
'https://luis.ai'
);
break;
case 'australiaeast':
regionPrefix = 'au.';
break;
default:
regionPrefix = '';
break;
default:
return;
}
}
const linkArray = ['https://', `${encodeURI(regionPrefix)}`, 'luis.ai/applications/'];
linkArray.push(`${encodeURI(appId)}`, '/versions/', `${encodeURI(version)}`, '/build');
const link = linkArray.join('');
window.open(link);
}
function openQnaMakerDeepLink(service: IQnAService) {
const { kbId } = service;
const link = `https://www.qnamaker.ai/Edit/KnowledgeBase?kbId=${encodeURIComponent(kbId)}`;
window.open(link);
public static *openContextMenuForService(
action: ConnectedServiceAction<ConnectedServicePayload>
): IterableIterator<any> {
const menuItems = [
{ label: 'Manage service', id: 'open' },
{ label: 'Edit configuration', id: 'edit' },
{ label: 'Disconnect this service', id: 'forget' },
];
const response = yield ServicesExplorerSagas.commandService.remoteCall(
SharedConstants.Commands.Electron.DisplayContextMenu,
menuItems
);
const { connectedService } = action.payload;
action.payload.serviceType = connectedService.type;
switch (response.id) {
case 'open':
yield* ServicesExplorerSagas.openConnectedServiceDeepLink(action);
break;
case 'edit':
yield* ServicesExplorerSagas.launchConnectedServiceEditor(action);
break;
case 'forget':
yield* ServicesExplorerSagas.removeServiceFromActiveBot(connectedService);
break;
default:
// canceled context menu
return;
}
}
public static *openAddConnectedServiceContextMenu(
action: ConnectedServiceAction<ConnectedServicePickerPayload>
): IterableIterator<any> {
const menuItems = [
{ label: 'Add Language Understanding (LUIS)', id: ServiceTypes.Luis },
{ label: 'Add QnA Maker', id: ServiceTypes.QnA },
{ label: 'Add Dispatch', id: ServiceTypes.Dispatch },
{ type: 'separator' },
{ label: 'Add Azure Cosmos DB account', id: ServiceTypes.CosmosDB },
{ label: 'Add Azure Storage account', id: ServiceTypes.BlobStorage },
{ label: 'Add Azure Application Insights', id: ServiceTypes.AppInsights },
{ type: 'separator' },
{ label: 'Add other service …', id: ServiceTypes.Generic },
];
const response = yield ServicesExplorerSagas.commandService.remoteCall(
SharedConstants.Commands.Electron.DisplayContextMenu,
menuItems
);
const { id: serviceType } = response;
action.payload.serviceType = serviceType;
if (serviceType === ServiceTypes.Generic || serviceType === ServiceTypes.AppInsights) {
yield* ServicesExplorerSagas.launchConnectedServiceEditor(action);
} else {
yield* ServicesExplorerSagas.launchConnectedServicePicker(action);
}
}
public static *openSortContextMenu(action: ConnectedServiceAction<ConnectedServicePayload>): IterableIterator<any> {
const sortSelectionByPanelId = yield select(getSortSelection);
const currentSort = sortSelectionByPanelId[action.payload.panelId];
const menuItems = [
{
label: 'Sort by name',
id: 'name',
type: 'checkbox',
checked: currentSort === 'name',
},
{
label: 'Sort by type',
id: 'type',
type: 'checkbox',
checked: currentSort === 'type',
},
];
const response = yield ServicesExplorerSagas.commandService.remoteCall(
SharedConstants.Commands.Electron.DisplayContextMenu,
menuItems
);
yield response.id ? put(sortExplorerContents(action.payload.panelId, response.id)) : null;
}
public static *removeServiceFromActiveBot(connectedService: IConnectedService): IterableIterator<any> {
// TODO - localization
const result = yield ServicesExplorerSagas.commandService.remoteCall(
SharedConstants.Commands.Electron.ShowMessageBox,
true,
{
type: 'question',
buttons: ['Cancel', 'OK'],
defaultId: 1,
message: `Remove ${serviceTypeLabels[connectedService.type]} service: ${connectedService.name}. Are you sure?`,
cancelId: 0,
}
);
if (result) {
const { RemoveService } = SharedConstants.Commands.Bot;
yield ServicesExplorerSagas.commandService.remoteCall(RemoveService, connectedService.type, connectedService.id);
}
}
public static *launchConnectedServiceEditor(
action: ConnectedServiceAction<ConnectedServicePayload>
): IterableIterator<any> {
const { editorComponent, authenticatedUser, connectedService, serviceType } = action.payload;
const servicesToUpdate: IConnectedService[] = yield DialogService.showDialog(editorComponent, {
connectedService,
authenticatedUser,
serviceType,
});
if (servicesToUpdate) {
let i = servicesToUpdate.length;
while (i--) {
const service = servicesToUpdate[i];
yield ServicesExplorerSagas.commandService.remoteCall(
SharedConstants.Commands.Bot.AddOrUpdateService,
service.type,
service
);
}
}
return null;
}
public static openAzureProviderDeepLink(provider: string, azureService: IAzureService): void {
const { tenantId, subscriptionId, resourceGroup, serviceName } = azureService;
const bits = [
`https://ms.portal.azure.com/#@${tenantId}/resource/`,
`subscriptions/${subscriptionId}/`,
`resourceGroups/${encodeURI(resourceGroup)}/`,
`providers/${provider}/${encodeURI(serviceName)}/overview`,
];
window.open(bits.join(''));
}
public static openLuisDeepLink(luisService: ILuisService) {
const { appId, version, region } = luisService;
let regionPrefix: string;
switch (region) {
case 'westeurope':
regionPrefix = 'eu.';
break;
case 'australiaeast':
regionPrefix = 'au.';
break;
default:
regionPrefix = '';
break;
}
const linkArray = ['https://', `${encodeURI(regionPrefix)}`, 'luis.ai/applications/'];
linkArray.push(`${encodeURI(appId)}`, '/versions/', `${encodeURI(version)}`, '/build');
const link = linkArray.join('');
window.open(link);
}
public static openQnaMakerDeepLink(service: IQnAService) {
const { kbId } = service;
const link = `https://www.qnamaker.ai/Edit/KnowledgeBase?kbId=${encodeURIComponent(kbId)}`;
window.open(link);
}
}
export function* servicesExplorerSagas(): IterableIterator<ForkEffect> {
yield takeLatest(LAUNCH_CONNECTED_SERVICE_PICKER, launchConnectedServicePicker);
yield takeLatest(LAUNCH_CONNECTED_SERVICE_EDITOR, launchConnectedServiceEditor);
yield takeEvery(LAUNCH_EXTERNAL_LINK, launchExternalLink);
yield takeEvery(OPEN_SERVICE_DEEP_LINK, openConnectedServiceDeepLink);
yield takeEvery(OPEN_CONTEXT_MENU_FOR_CONNECTED_SERVICE, openContextMenuForService);
yield takeEvery(OPEN_ADD_CONNECTED_SERVICE_CONTEXT_MENU, openAddConnectedServiceContextMenu);
yield takeEvery(OPEN_CONNECTED_SERVICE_SORT_CONTEXT_MENU, openSortContextMenu);
yield takeLatest(LAUNCH_CONNECTED_SERVICE_PICKER, ServicesExplorerSagas.launchConnectedServicePicker);
yield takeLatest(LAUNCH_CONNECTED_SERVICE_EDITOR, ServicesExplorerSagas.launchConnectedServiceEditor);
yield takeEvery(LAUNCH_EXTERNAL_LINK, ServicesExplorerSagas.launchExternalLink);
yield takeEvery(OPEN_SERVICE_DEEP_LINK, ServicesExplorerSagas.openConnectedServiceDeepLink);
yield takeEvery(OPEN_CONTEXT_MENU_FOR_CONNECTED_SERVICE, ServicesExplorerSagas.openContextMenuForService);
yield takeEvery(OPEN_ADD_CONNECTED_SERVICE_CONTEXT_MENU, ServicesExplorerSagas.openAddConnectedServiceContextMenu);
yield takeEvery(OPEN_CONNECTED_SERVICE_SORT_CONTEXT_MENU, ServicesExplorerSagas.openSortContextMenu);
}

Просмотреть файл

@ -33,22 +33,50 @@
import { SharedConstants } from '@bfemulator/app-shared';
import { select } from 'redux-saga/effects';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { RootState } from '../store';
import { editorSelector, refreshConversationMenu } from './sharedSagas';
import { editorSelector, SharedSagas } from './sharedSagas';
let mockRemoteCommandsCalled = [];
jest.mock('../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
remoteCall: async (commandName: string, ...args: any[]) => {
mockRemoteCommandsCalled.push({ commandName, args: args });
},
},
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
describe('The sharedSagas', () => {
const editorState = { activeEditor: 'primary' };
let mockRemoteCommandsCalled = [];
let commandService: CommandServiceImpl;
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
commandService.remoteCall = async (commandName: string, ...args: any[]) => {
mockRemoteCommandsCalled.push({ commandName, args: args });
return true as any;
};
});
beforeEach(() => {
mockRemoteCommandsCalled = [];
@ -61,7 +89,7 @@ describe('The sharedSagas', () => {
});
it('should refresh the conversation menu', () => {
const gen = refreshConversationMenu();
const gen = SharedSagas.refreshConversationMenu();
const editorSelection = gen.next().value;
expect(editorSelection).toEqual(select(editorSelector));

Просмотреть файл

@ -33,15 +33,20 @@
import { SharedConstants } from '@bfemulator/app-shared';
import { select } from 'redux-saga/effects';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { RootState } from '../store';
export function editorSelector(state: RootState) {
return state.editor;
}
export function* refreshConversationMenu(): IterableIterator<any> {
const stateData = yield select(editorSelector);
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.UpdateConversationMenu, stateData);
export class SharedSagas {
@CommandServiceInstance()
private static commandService: CommandServiceImpl;
public static *refreshConversationMenu(): IterableIterator<any> {
const stateData = yield select(editorSelector);
yield SharedSagas.commandService.remoteCall(SharedConstants.Commands.Electron.UpdateConversationMenu, stateData);
}
}

Просмотреть файл

@ -33,8 +33,8 @@
import { SharedConstants } from '@bfemulator/app-shared';
import sagaMiddlewareFactory from 'redux-saga';
import { applyMiddleware, combineReducers, createStore } from 'redux';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { openContextMenuForBot } from '../action/welcomePageActions';
import { bot } from '../reducer/bot';
import notification from '../reducer/notification';
@ -47,11 +47,6 @@ const mockBot = {
displayName: 'AuthBot',
secret: 'secret',
};
jest.mock('../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
remoteCall: async () => null,
},
}));
const sagaMiddleWare = sagaMiddlewareFactory();
const mockStore = createStore(
@ -68,14 +63,46 @@ jest.mock('../store', () => ({
},
}));
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
sagaMiddleWare.run(welcomePageSagas);
sagaMiddleWare.run(notificationSagas);
describe('The WelcomePageSagas', () => {
let commandService: CommandServiceImpl;
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
});
describe(', when invoking a context menu over a bot in the list', () => {
it('should call the series of commands that move the bot file to a new location.', async () => {
const remoteCalls = [];
CommandServiceImpl.remoteCall = async function(...args: any[]) {
commandService.remoteCall = async function(...args: any[]) {
remoteCalls.push(args);
switch (args[0]) {
case SharedConstants.Commands.Electron.DisplayContextMenu:
@ -85,7 +112,7 @@ describe('The WelcomePageSagas', () => {
return 'this/is/a/new/location.bot';
default:
return null;
return null as any;
}
};
await mockStore.dispatch(openContextMenuForBot(mockBot));
@ -142,7 +169,7 @@ describe('The WelcomePageSagas', () => {
});
it('should add a notification if a remote command fails when moving a bot file', async () => {
CommandServiceImpl.remoteCall = async function(...args: any[]) {
commandService.remoteCall = async function(...args: any[]) {
switch (args[0]) {
case SharedConstants.Commands.Electron.DisplayContextMenu:
return { id: 0 };
@ -154,7 +181,7 @@ describe('The WelcomePageSagas', () => {
throw new Error('oh noes!');
default:
return null;
return null as any;
}
};
await mockStore.dispatch(openContextMenuForBot(mockBot));
@ -166,7 +193,7 @@ describe('The WelcomePageSagas', () => {
it('should call the appropriate command when opening the bot file location', async () => {
let openFileLocationArgs;
CommandServiceImpl.remoteCall = async function(...args: any[]) {
commandService.remoteCall = async function(...args: any[]) {
switch (args[0]) {
case SharedConstants.Commands.Electron.DisplayContextMenu:
return { id: 1 };
@ -175,7 +202,7 @@ describe('The WelcomePageSagas', () => {
return (openFileLocationArgs = args);
default:
return null;
return null as any;
}
};
@ -185,8 +212,8 @@ describe('The WelcomePageSagas', () => {
});
it('should call the appropriate command when removing a bot from the list', async () => {
let removeBotFromListArgs;
CommandServiceImpl.remoteCall = async function(...args: any[]) {
let removeBotFromListArgs = null;
commandService.remoteCall = async function(...args: any[]) {
switch (args[0]) {
case SharedConstants.Commands.Electron.DisplayContextMenu:
return { id: 2 };
@ -195,7 +222,7 @@ describe('The WelcomePageSagas', () => {
return (removeBotFromListArgs = args);
default:
return null;
return null as any;
}
};
await mockStore.dispatch(openContextMenuForBot(mockBot));

Просмотреть файл

@ -32,60 +32,75 @@
//
import { BotInfo, newNotification, SharedConstants } from '@bfemulator/app-shared';
import { ForkEffect, put, takeEvery } from 'redux-saga/effects';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { beginAdd } from '../action/notificationActions';
import { OPEN_CONTEXT_MENU_FOR_BOT, WelcomePageAction } from '../action/welcomePageActions';
export class WelcomePageSagas {
@CommandServiceInstance()
private static commandService: CommandServiceImpl;
function* openContextMenuForBot(action: WelcomePageAction<BotInfo>): IterableIterator<any> {
const menuItems = [
{ label: 'Move...', id: 0 },
{ label: 'Open file location', id: 1 },
{ label: 'Forget this bot', id: 2 },
];
public static *openContextMenuForBot(action: WelcomePageAction<BotInfo>): IterableIterator<any> {
const menuItems = [
{ label: 'Move...', id: 0 },
{ label: 'Open file location', id: 1 },
{ label: 'Forget this bot', id: 2 },
];
const result = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.DisplayContextMenu, menuItems);
switch (result.id) {
case 0:
yield* moveBotToNewLocation(action.payload);
break;
const result = yield WelcomePageSagas.commandService.remoteCall(
SharedConstants.Commands.Electron.DisplayContextMenu,
menuItems
);
switch (result.id) {
case 0:
yield* WelcomePageSagas.moveBotToNewLocation(action.payload);
break;
case 1:
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.OpenFileLocation, action.payload.path);
break;
case 1:
yield WelcomePageSagas.commandService.remoteCall(
SharedConstants.Commands.Electron.OpenFileLocation,
action.payload.path
);
break;
case 2:
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.RemoveFromBotList, action.payload.path);
break;
case 2:
yield WelcomePageSagas.commandService.remoteCall(
SharedConstants.Commands.Bot.RemoveFromBotList,
action.payload.path
);
break;
default:
// Canceled context menu
break;
default:
// Canceled context menu
break;
}
}
public static *moveBotToNewLocation(bot: BotInfo): IterableIterator<any> {
const newPath = yield WelcomePageSagas.commandService.remoteCall(SharedConstants.Commands.Electron.ShowSaveDialog, {
defaultPath: bot.path,
buttonLabel: 'Move',
nameFieldLabel: 'Name',
filters: [{ extensions: ['.bot'] }],
});
if (!newPath) {
return;
}
try {
const { path: oldPath } = bot;
bot.path = newPath;
yield WelcomePageSagas.commandService.remoteCall(SharedConstants.Commands.Electron.RenameFile, {
path: oldPath,
newPath,
});
yield WelcomePageSagas.commandService.remoteCall(SharedConstants.Commands.Bot.PatchBotList, oldPath, bot);
} catch (e) {
const errMsg = `Error occurred while moving the bot file: ${e}`;
const notification = newNotification(errMsg);
yield put(beginAdd(notification));
}
}
}
function* moveBotToNewLocation(bot: BotInfo): IterableIterator<any> {
const newPath = yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.ShowSaveDialog, {
defaultPath: bot.path,
buttonLabel: 'Move',
nameFieldLabel: 'Name',
filters: [{ extensions: ['.bot'] }],
});
if (!newPath) {
return;
}
try {
const { path: oldPath } = bot;
bot.path = newPath;
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.RenameFile, { path: oldPath, newPath });
yield CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.PatchBotList, oldPath, bot);
} catch (e) {
const errMsg = `Error occurred while moving the bot file: ${e}`;
const notification = newNotification(errMsg);
yield put(beginAdd(notification));
}
}
export function* welcomePageSagas(): IterableIterator<ForkEffect> {
yield takeEvery(OPEN_CONTEXT_MENU_FOR_BOT, openContextMenuForBot);
yield takeEvery(OPEN_CONTEXT_MENU_FOR_BOT, WelcomePageSagas.openContextMenuForBot);
}

Просмотреть файл

@ -32,20 +32,10 @@
//
import { SharedConstants } from '@bfemulator/app-shared';
import {
CommandRegistryImpl,
CommandService,
CommandServiceImpl,
ExtensionConfig,
ExtensionInspector,
} from '@bfemulator/sdk-shared';
import { ElectronIPC } from './ipc';
import { Command, ExtensionConfig, ExtensionInspector } from '@bfemulator/sdk-shared';
// =============================================================================
export class Extension {
private _ext: CommandService;
public get unid(): string {
return this._unid;
}
@ -54,14 +44,7 @@ export class Extension {
return this._config;
}
public constructor(private _config: ExtensionConfig, private _unid: string) {
this._ext = new CommandServiceImpl(ElectronIPC, `ext-${this._unid}`);
/*
this._ext.remoteCall('ext-ping')
.then(reply => console.log(reply))
.catch(err => console.log('ping failed', err));
*/
}
public constructor(private _config: ExtensionConfig, private _unid: string) {}
public inspectorForObject(obj: any): GetInspectorResult | null {
const inspectors = this.config.client.inspectors || [];
@ -73,10 +56,6 @@ export class Extension {
}
: null;
}
public call(commandName: string, ...args: any[]): Promise<any> {
return this._ext.remoteCall(commandName, ...args);
}
}
// =============================================================================
@ -149,29 +128,15 @@ export interface GetInspectorResult {
inspector: ExtensionInspector;
}
// =============================================================================
export interface ExtensionManager {
registerCommands(commandRegistry: CommandRegistryImpl);
addExtension(config: ExtensionConfig, unid: string);
removeExtension(unid: string);
getExtensions(): Extension[];
inspectorForObject(obj: any, defaultToJson: boolean): GetInspectorResult | null;
}
const { Connect, Disconnect } = SharedConstants.Commands.Extension;
// =============================================================================
class EmulatorExtensionManager implements ExtensionManager {
class EmulatorExtensionManager {
private extensions: { [unid: string]: Extension } = {};
public addExtension(config: ExtensionConfig, unid: string) {
this.removeExtension(unid);
// eslint-disable-next-line no-console
console.log(`adding extension ${config.name}`);
const ext = new Extension(config, unid);
this.extensions[unid] = ext;
this.extensions[unid] = new Extension(config, unid);
}
public removeExtension(unid: string) {
@ -198,7 +163,7 @@ class EmulatorExtensionManager implements ExtensionManager {
if (!result && defaultToJson) {
// Default to the JSON inspector
// eslint-disable-next-line typescript/no-use-before-define
const jsonExtension = ExtensionManager.findExtension('JSON');
const jsonExtension = this.findExtension('JSON');
if (jsonExtension) {
result = {
extension: jsonExtension,
@ -209,17 +174,16 @@ class EmulatorExtensionManager implements ExtensionManager {
return result;
}
public registerCommands(commandRegistry: CommandRegistryImpl) {
const { Connect, Disconnect } = SharedConstants.Commands.Extension;
commandRegistry.registerCommand(Connect, (config: ExtensionConfig) => {
// eslint-disable-next-line typescript/no-use-before-define
ExtensionManager.addExtension(config, config.location);
});
@Command(Connect)
protected connectExtension(config: ExtensionConfig) {
// eslint-disable-next-line typescript/no-use-before-define
this.addExtension(config, config.location);
}
commandRegistry.registerCommand(Disconnect, (location: string) => {
// eslint-disable-next-line typescript/no-use-before-define
ExtensionManager.removeExtension(location);
});
@Command(Disconnect)
protected disconnectExtension(location: string) {
// eslint-disable-next-line typescript/no-use-before-define
this.removeExtension(location);
}
}

Просмотреть файл

@ -32,8 +32,9 @@
//
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { navigate } from './hyperlinkHandler';
import { HyperlinkHandler } from './hyperlinkHandler';
let mockParse;
jest.mock('url', () => ({
@ -41,23 +42,45 @@ jest.mock('url', () => ({
return mockParse;
},
}));
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
const mockUniqueId = 'id1234';
jest.mock('@bfemulator/sdk-shared', () => ({
jest.mock('@bfemulator/sdk-shared/build/utils/misc', () => ({
uniqueId: () => mockUniqueId,
}));
let mockRemoteCallsMade;
let mockRemoteCall;
jest.mock('./platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
get remoteCall() {
return mockRemoteCall;
},
},
}));
describe('hyperlinkHandler', () => {
let commandService: CommandServiceImpl;
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
});
beforeEach(() => {
mockParse = jest.fn(url => {
if (url) {
@ -69,7 +92,7 @@ describe('hyperlinkHandler', () => {
});
mockRemoteCallsMade = [];
(window as any)._openExternal.mockClear();
mockRemoteCall = jest.fn((commandName: string, ...args: any[]) => {
commandService.remoteCall = jest.fn((commandName: string, ...args: any[]) => {
mockRemoteCallsMade.push({ commandName, args });
return Promise.resolve(true);
});
@ -77,7 +100,7 @@ describe('hyperlinkHandler', () => {
it('should navigate to an emulated ouath url', async () => {
const url = 'oauth://someoauthurl.com/auth&&&ending';
await navigate(url);
await HyperlinkHandler.navigate(url);
expect(mockRemoteCallsMade).toHaveLength(1);
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.OAuth.SendTokenResponse);
@ -86,7 +109,7 @@ describe('hyperlinkHandler', () => {
it('should navigate to an ouath url', async () => {
const url = 'oauthlink://someoauthurl.com/auth&&&ending';
await navigate(url);
await HyperlinkHandler.navigate(url);
expect(mockRemoteCallsMade).toHaveLength(1);
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.OAuth.CreateOAuthWindow);
@ -95,7 +118,7 @@ describe('hyperlinkHandler', () => {
it('should open a data url', async () => {
const url = 'data:image/png;base64;somedata';
await navigate(url);
await HyperlinkHandler.navigate(url);
expect(mockRemoteCallsMade).toHaveLength(1);
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
@ -105,7 +128,7 @@ describe('hyperlinkHandler', () => {
it('should open a normal url', async () => {
const url = 'https://aka.ms/bot-framework-emulator';
await navigate(url);
await HyperlinkHandler.navigate(url);
expect(mockRemoteCallsMade).toHaveLength(1);
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
@ -119,7 +142,7 @@ describe('hyperlinkHandler', () => {
throw new Error();
});
const url = '';
await navigate(url);
await HyperlinkHandler.navigate(url);
expect(mockRemoteCallsMade).toHaveLength(1);
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);

Просмотреть файл

@ -34,51 +34,52 @@
import * as URL from 'url';
import { SharedConstants } from '@bfemulator/app-shared';
import { uniqueId } from '@bfemulator/sdk-shared';
import { CommandServiceImpl, CommandServiceInstance, uniqueId } from '@bfemulator/sdk-shared';
import { CommandServiceImpl } from './platform/commands/commandServiceImpl';
const Electron = (window as any).require('electron');
const { shell } = Electron;
export function navigate(url: string = '') {
const { TrackEvent } = SharedConstants.Commands.Telemetry;
try {
const parsed = URL.parse(url) || { protocol: '' };
if ((parsed.protocol || '').startsWith('oauth:')) {
navigateEmulatedOAuthUrl(url.substring(8));
} else if (parsed.protocol.startsWith('oauthlink:')) {
navigateOAuthUrl(url.substring(12));
} else {
CommandServiceImpl.remoteCall(TrackEvent, 'app_openLink', { url }).catch(_e => void 0);
// manually create and click a download link for data url's
if (url.startsWith('data:')) {
const a = document.createElement('a');
a.href = url;
a.download = '';
a.click();
export class HyperlinkHandler {
@CommandServiceInstance()
private static commandService: CommandServiceImpl;
public static navigate(url: string = '') {
const { TrackEvent } = SharedConstants.Commands.Telemetry;
try {
const parsed = URL.parse(url) || { protocol: '' };
if ((parsed.protocol || '').startsWith('oauth:')) {
this.navigateEmulatedOAuthUrl(url.substring(8));
} else if (parsed.protocol.startsWith('oauthlink:')) {
this.navigateOAuthUrl(url.substring(12));
} else {
shell.openExternal(url, { activate: true });
this.commandService.remoteCall(TrackEvent, 'app_openLink', { url }).catch(_e => void 0);
// manually create and click a download link for data url's
if (url.startsWith('data:')) {
const a = document.createElement('a');
a.href = url;
a.download = '';
a.click();
} else {
shell.openExternal(url, { activate: true });
}
}
} catch (e) {
this.commandService.remoteCall(TrackEvent, 'app_openLink', { url }).catch(_e => void 0);
shell.openExternal(url, { activate: true });
}
} catch (e) {
CommandServiceImpl.remoteCall(TrackEvent, 'app_openLink', { url }).catch(_e => void 0);
shell.openExternal(url, { activate: true });
}
private static navigateEmulatedOAuthUrl(oauthParam: string) {
const { Commands } = SharedConstants;
const parts = oauthParam.split('&&&');
this.commandService
.remoteCall(Commands.OAuth.SendTokenResponse, parts[0], parts[1], 'emulatedToken_' + uniqueId())
.catch();
}
private static navigateOAuthUrl(oauthParam: string) {
const { Commands } = SharedConstants;
const parts = oauthParam.split('&&&');
this.commandService.remoteCall(Commands.OAuth.CreateOAuthWindow, parts[0], parts[1]).catch();
}
}
function navigateEmulatedOAuthUrl(oauthParam: string) {
const { Commands } = SharedConstants;
const parts = oauthParam.split('&&&');
CommandServiceImpl.remoteCall(
Commands.OAuth.SendTokenResponse,
parts[0],
parts[1],
'emulatedToken_' + uniqueId()
).catch();
}
function navigateOAuthUrl(oauthParam: string) {
const { Commands } = SharedConstants;
const parts = oauthParam.split('&&&');
CommandServiceImpl.remoteCall(Commands.OAuth.CreateOAuthWindow, parts[0], parts[1]).catch();
}

Просмотреть файл

@ -34,22 +34,16 @@
import { Provider } from 'react-redux';
import * as ReactDOM from 'react-dom';
import * as React from 'react';
import { newNotification, SharedConstants } from '@bfemulator/app-shared';
import './commands';
import interceptError from './interceptError';
import interceptHyperlink from './interceptHyperlink';
import Main from './ui/shell/mainContainer';
import { store } from './data/store';
import { CommandServiceImpl } from './platform/commands/commandServiceImpl';
import { showWelcomePage } from './data/editorHelpers';
import { CommandRegistry, registerAllCommands } from './commands';
import { beginAdd } from './data/action/notificationActions';
import { globalHandlers } from './utils/eventHandlers';
import './ui/styles/globals.scss';
interceptError();
interceptHyperlink();
registerAllCommands(CommandRegistry);
// Start rendering the UI
ReactDOM.render(
@ -57,21 +51,6 @@ ReactDOM.render(
document.getElementById('root')
);
// Tell the main process we're loaded
CommandServiceImpl.remoteCall(SharedConstants.Commands.ClientInit.Loaded)
.then(async () => {
showWelcomePage();
// do actions on main side that might open a document, so that they will be active over the welcome screen
await CommandServiceImpl.remoteCall(SharedConstants.Commands.ClientInit.PostWelcomeScreen);
window.addEventListener('keydown', globalHandlers, true);
})
.catch(err => {
const errMsg = `Error occurred while client was loading: ${err}`;
const notification = newNotification(errMsg);
store.dispatch(beginAdd(notification));
window.removeEventListener('keydown', globalHandlers, true);
});
if (module.hasOwnProperty('hot')) {
(module as any).hot.accept();
}

Просмотреть файл

@ -31,7 +31,7 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { navigate } from './hyperlinkHandler';
import { HyperlinkHandler } from './hyperlinkHandler';
export default function interceptHyperlink() {
const interceptClickEvent = (e: Event) => {
@ -40,7 +40,7 @@ export default function interceptHyperlink() {
while (target) {
if (target.href) {
e.preventDefault();
navigate(target.href);
HyperlinkHandler.navigate(target.href);
return;
}
@ -52,6 +52,6 @@ export default function interceptHyperlink() {
// Monkey patch window.open
window.open = (url: string): any => {
navigate(url);
HyperlinkHandler.navigate(url);
};
}

Просмотреть файл

@ -1,72 +0,0 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import {
CommandHandler,
CommandService,
CommandServiceImpl as InternalSharedService,
Disposable,
DisposableImpl,
} from '@bfemulator/sdk-shared';
import { CommandRegistry } from '../../commands';
import { ElectronIPC } from '../../ipc';
class CServiceImpl extends DisposableImpl implements CommandService {
private readonly _service: InternalSharedService;
public get registry() {
return this._service.registry;
}
constructor() {
super();
this._service = new InternalSharedService(ElectronIPC, 'command-service', CommandRegistry);
super.toDispose(this._service);
}
public call(commandName: string, ...args: any[]): Promise<any> {
return this._service.call(commandName, ...args);
}
public remoteCall(commandName: string, ...args: any[]): Promise<any> {
return this._service.remoteCall(commandName, ...args);
}
public on(event: string, handler?: CommandHandler): Disposable;
public on(event: 'command-not-found', handler?: (commandName: string, ...args: any[]) => any) {
return this._service.on(event, handler);
}
}
export const CommandServiceImpl = new CServiceImpl();

Просмотреть файл

@ -32,31 +32,29 @@
//
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandRegistryImpl, LogEntry } from '@bfemulator/sdk-shared';
import { Command, LogEntry } from '@bfemulator/sdk-shared';
import * as ChatActions from '../../data/action/chatActions';
import * as chatHelpers from '../../data/chatHelpers';
import { store } from '../../data/store';
export class LogService {
public static logToChat(conversationId: string, entry: LogEntry): void {
class LogService {
public logToChat(conversationId: string, entry: LogEntry): void {
const documentId = chatHelpers.documentIdForConversation(conversationId);
if (documentId) {
// eslint-disable-next-line typescript/no-use-before-define
LogService.logToDocument(documentId, entry);
this.logToDocument(documentId, entry);
}
}
public static logToDocument(documentId: string, entry: LogEntry): void {
public logToDocument(documentId: string, entry: LogEntry): void {
store.dispatch(ChatActions.appendToLog(documentId, entry));
}
@Command(SharedConstants.Commands.Emulator.AppendToLog)
protected appendToLog(conversationId: string, entry: LogEntry): any {
this.logToChat(conversationId, entry);
}
}
export function registerCommands(commandRegistry: CommandRegistryImpl) {
commandRegistry.registerCommand(
SharedConstants.Commands.Emulator.AppendToLog,
(conversationId: string, entry: LogEntry): any => {
LogService.logToChat(conversationId, entry);
}
);
}
export const logService = new LogService();

Просмотреть файл

@ -32,7 +32,7 @@
//
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandRegistryImpl, DisposableImpl } from '@bfemulator/sdk-shared';
import { Command } from '@bfemulator/sdk-shared';
export interface EmulatorSettings {
url?: string;
@ -50,6 +50,7 @@ class EmulatorSettingsImpl implements EmulatorSettings {
}
return this._url;
}
set url(value: string) {
this._url = value;
}
@ -75,8 +76,8 @@ class EmulatorSettingsImpl implements EmulatorSettings {
}
}
class EmulatorSettingsService extends DisposableImpl {
private readonly _emulator: EmulatorSettingsImpl;
class EmulatorSettingsService {
private _emulator: EmulatorSettingsImpl;
get emulator(): EmulatorSettingsImpl {
return this._emulator;
@ -87,19 +88,14 @@ class EmulatorSettingsService extends DisposableImpl {
}
constructor() {
super();
this._emulator = new EmulatorSettingsImpl();
}
@Command(SharedConstants.Commands.Settings.ReceiveGlobalSettings)
protected receiveGlobalSettings(settings: { url: string; cwd: string }): any {
this.emulator.url = (settings.url || '').replace('[::]', 'localhost');
this.emulator.cwd = (settings.cwd || '').replace(/\\/g, '/');
}
}
export const SettingsService = new EmulatorSettingsService();
export function registerCommands(commandRegistry: CommandRegistryImpl) {
commandRegistry.registerCommand(
SharedConstants.Commands.Settings.ReceiveGlobalSettings,
(settings: { url: string; cwd: string }): any => {
SettingsService.emulator.url = (settings.url || '').replace('[::]', 'localhost');
SettingsService.emulator.cwd = (settings.cwd || '').replace(/\\/g, '/');
}
);
}

Просмотреть файл

@ -33,13 +33,12 @@
import { mount } from 'enzyme';
import * as React from 'react';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
import { ActiveBotHelper } from '../../helpers/activeBotHelper';
import { BotCreationDialog, BotCreationDialogState } from './botCreationDialog';
jest.mock('./botCreationDialog.scss', () => ({}));
jest.mock('../index', () => null);
jest.mock('../../../utils', () => ({
generateBotSecret: () => {
@ -47,6 +46,31 @@ jest.mock('../../../utils', () => ({
},
}));
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
jest.mock('../../helpers/activeBotHelper', () => ({
ActiveBotHelper: {
confirmAndCreateBot: async () => true,
@ -54,6 +78,13 @@ jest.mock('../../helpers/activeBotHelper', () => ({
}));
describe('BotCreationDialog tests', () => {
let commandService: CommandServiceImpl;
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
});
let testWrapper;
beforeEach(() => {
testWrapper = mount(<BotCreationDialog />);
@ -174,7 +205,7 @@ describe('BotCreationDialog tests', () => {
it('should save and connect', async () => {
const instance = testWrapper.instance();
const remoteCallSpy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue('some/path');
const remoteCallSpy = jest.spyOn(commandService, 'remoteCall').mockResolvedValue('some/path');
const confirmAndCreateSpy = jest.spyOn(ActiveBotHelper, 'confirmAndCreateBot').mockResolvedValue(true);
await instance.onSaveAndConnect();
expect(remoteCallSpy).toHaveBeenCalledWith('shell:showExplorer-save-dialog', {

Просмотреть файл

@ -47,10 +47,10 @@ import { EndpointService } from 'botframework-config/lib/models';
import { IEndpointService, ServiceTypes } from 'botframework-config/lib/schema';
import { ChangeEvent } from 'react';
import * as React from 'react';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { beginAdd } from '../../../data/action/notificationActions';
import { store } from '../../../data/store';
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
import { generateBotSecret } from '../../../utils';
import { ActiveBotHelper } from '../../helpers/activeBotHelper';
import { DialogService } from '../service';
@ -67,6 +67,9 @@ export interface BotCreationDialogState {
}
export class BotCreationDialog extends React.Component<{}, BotCreationDialogState> {
@CommandServiceInstance()
public commandService: CommandServiceImpl;
public constructor(props: {}, context: BotCreationDialogState) {
super(props, context);
@ -321,7 +324,7 @@ export class BotCreationDialog extends React.Component<{}, BotCreationDialogStat
private showBotSaveDialog = async (): Promise<any> => {
const { Commands } = SharedConstants;
// get a safe bot file name
const botFileName = await CommandServiceImpl.remoteCall(Commands.File.SanitizeString, this.state.bot.name);
const botFileName = await this.commandService.remoteCall(Commands.File.SanitizeString, this.state.bot.name);
// TODO - Localization
const dialogOptions = {
filters: [
@ -336,7 +339,7 @@ export class BotCreationDialog extends React.Component<{}, BotCreationDialogStat
buttonLabel: 'Save',
};
return CommandServiceImpl.remoteCall(Commands.Electron.ShowSaveDialog, dialogOptions);
return this.commandService.remoteCall(Commands.Electron.ShowSaveDialog, dialogOptions);
};
/** Checks the endpoint to see if it has the correct route syntax at the end (/api/messages) */

Просмотреть файл

@ -38,6 +38,7 @@ import { mount } from 'enzyme';
import { combineReducers, createStore } from 'redux';
import { BotConfigWithPathImpl } from '@bfemulator/sdk-shared';
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { bot } from '../../../data/reducer/bot';
import { setActiveBot } from '../../../data/action/botActions';
@ -73,7 +74,6 @@ const mockWindow = {
},
};
jest.mock('./botSettingsEditor.scss', () => ({}));
jest.mock('../../../data/store', () => ({
get store() {
return mockStore;
@ -87,26 +87,33 @@ jest.mock('../service', () => ({
},
}));
const mockRemoteCommandsCalled = [];
const mockSharedConstants = SharedConstants; // thanks Jest!
jest.mock('../../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
remoteCall: async (commandName: string, ...args: any[]) => {
mockRemoteCommandsCalled.push({ commandName, args: args });
switch (commandName) {
case mockSharedConstants.Commands.File.SanitizeString:
return args[0];
case mockSharedConstants.Commands.Electron.ShowSaveDialog:
return '/test/path';
default:
return true;
}
},
},
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
const mockRemoteCommandsCalled = [];
jest.mock('../../../utils', () => ({
generateBotSecret: () => {
return Math.random() + '';
@ -114,6 +121,25 @@ jest.mock('../../../utils', () => ({
}));
describe('The BotSettingsEditor dialog should', () => {
let commandService: CommandServiceImpl;
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
commandService.remoteCall = async (commandName: string, ...args: any[]) => {
mockRemoteCommandsCalled.push({ commandName, args: args });
switch (commandName) {
case SharedConstants.Commands.File.SanitizeString:
return args[0];
case SharedConstants.Commands.Electron.ShowSaveDialog:
return '/test/path';
default:
return true;
}
};
});
let parent;
let node;
beforeEach(() => {

Просмотреть файл

@ -46,9 +46,9 @@ import {
import { IConnectedService, ServiceTypes } from 'botframework-config/lib/schema';
import { ChangeEvent } from 'react';
import * as React from 'react';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { getBotInfoByPath } from '../../../data/botHelpers';
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
import { generateBotSecret } from '../../../utils';
import { ActiveBotHelper } from '../../helpers/activeBotHelper';
@ -70,6 +70,9 @@ export interface BotSettingsEditorState extends BotConfigWithPath {
}
export class BotSettingsEditor extends React.Component<BotSettingsEditorProps, BotSettingsEditorState> {
@CommandServiceInstance()
private commandService: CommandServiceImpl;
private _generatedSecret: string;
constructor(props: BotSettingsEditorProps, context: BotSettingsEditorState) {
@ -225,14 +228,14 @@ export class BotSettingsEditor extends React.Component<BotSettingsEditorProps, B
path: newPath,
secret: this.state.secret,
};
await CommandServiceImpl.remoteCall(PatchBotList, SharedConstants.TEMP_BOT_IN_MEMORY_PATH, botInfo);
await CommandServiceImpl.remoteCall(Save, bot);
await this.commandService.remoteCall(PatchBotList, SharedConstants.TEMP_BOT_IN_MEMORY_PATH, botInfo);
await this.commandService.remoteCall(Save, bot);
// need to set the new bot as active now that it is no longer a placeholder bot in memory
await ActiveBotHelper.setActiveBot(bot);
this.setState({ ...bot });
if (connectArg && endpointService) {
await CommandServiceImpl.call(SharedConstants.Commands.Emulator.NewLiveChat, endpointService);
await this.commandService.call(SharedConstants.Commands.Emulator.NewLiveChat, endpointService);
}
this.props.cancel();
};
@ -243,11 +246,11 @@ export class BotSettingsEditor extends React.Component<BotSettingsEditorProps, B
// write updated bot entry to bots.json so main side can pick up possible changes to secret
const botInfo: BotInfo = getBotInfoByPath(bot.path) || {};
botInfo.secret = this.state.secret;
await CommandServiceImpl.remoteCall(PatchBotList, bot.path, botInfo);
await this.commandService.remoteCall(PatchBotList, bot.path, botInfo);
// save bot
try {
await CommandServiceImpl.remoteCall(Save, bot);
await this.commandService.remoteCall(Save, bot);
} catch {
const note = newNotification(
'There was an error updating your bot settings. ' +
@ -257,7 +260,7 @@ export class BotSettingsEditor extends React.Component<BotSettingsEditorProps, B
this.props.sendNotification(note);
return;
}
await CommandServiceImpl.remoteCall(PatchBotList, bot.path, botInfo);
await this.commandService.remoteCall(PatchBotList, bot.path, botInfo);
this.props.cancel();
};
@ -265,7 +268,7 @@ export class BotSettingsEditor extends React.Component<BotSettingsEditorProps, B
// get a safe bot file name
// TODO - localization
const { SanitizeString } = SharedConstants.Commands.File;
const botFileName = await CommandServiceImpl.remoteCall(SanitizeString, this.state.name);
const botFileName = await this.commandService.remoteCall(SanitizeString, this.state.name);
const dialogOptions = {
filters: [
{
@ -278,7 +281,7 @@ export class BotSettingsEditor extends React.Component<BotSettingsEditorProps, B
title: 'Save as',
buttonLabel: 'Save',
};
return CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.ShowSaveDialog, dialogOptions);
return this.commandService.remoteCall(SharedConstants.Commands.Electron.ShowSaveDialog, dialogOptions);
};
private onRevealSecretClick = (): void => {

Просмотреть файл

@ -36,8 +36,8 @@ import { connect } from 'react-redux';
import { beginAdd } from '../../../data/action/notificationActions';
import { RootState } from '../../../data/store';
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
import { DialogService } from '../service';
import { executeCommand } from '../../../data/action/commandAction';
import { BotSettingsEditor, BotSettingsEditorProps } from './botSettingsEditor';
@ -53,7 +53,7 @@ const mapDispatchToProps = dispatch => ({
cancel: () => DialogService.hideDialog(0),
sendNotification: notification => dispatch(beginAdd(notification)),
onAnchorClick: (url: string) => {
CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.OpenExternal, url).catch();
dispatch(executeCommand(true, SharedConstants.Commands.Electron.OpenExternal, null, url));
},
});

Просмотреть файл

@ -34,18 +34,42 @@ import * as React from 'react';
import { Provider } from 'react-redux';
import { mount } from 'enzyme';
import { combineReducers, createStore } from 'redux';
import { SharedConstants } from '@bfemulator/app-shared';
import { bot } from '../../../data/reducer/bot';
import { resources } from '../../../data/reducer/resourcesReducer';
import { loadBotInfos, setActiveBot } from '../../../data/action/botActions';
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
import { executeCommand } from '../../../data/action/commandAction';
import { ResourcesSettings } from './resourcesSettings';
import { ResourcesSettingsContainer } from './resourcesSettingsContainer';
const mockStore = createStore(combineReducers({ resources, bot }));
jest.mock('./resourcesSettings.scss', () => ({}));
jest.mock('../dialogStyles.scss', () => ({}));
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
jest.mock('../service', () => ({
DialogService: {
@ -54,17 +78,6 @@ jest.mock('../service', () => ({
},
}));
jest.mock('../../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
remoteCall: async (commandName: string, ...args: any[]) => {
//
},
call: async (commandName: string, ...args: any[]) => {
//
},
},
}));
jest.mock('../../../data/store', () => ({
RootState: () => ({}),
get store() {
@ -79,6 +92,8 @@ jest.mock('../../../data/botHelpers', () => ({
describe('The ResourcesSettings component should', () => {
let parent;
let node;
let dispatchSpy;
beforeEach(() => {
const mockBot = JSON.parse(`{
"name": "TestBot",
@ -96,6 +111,7 @@ describe('The ResourcesSettings component should', () => {
mockStore.dispatch(loadBotInfos([mockBot]));
mockStore.dispatch(setActiveBot(mockBot));
dispatchSpy = jest.spyOn(mockStore, 'dispatch');
parent = mount(
<Provider store={mockStore}>
<ResourcesSettingsContainer label="test" progress={50} />
@ -137,10 +153,11 @@ describe('The ResourcesSettings component should', () => {
it('should open the browse dialog when the browse anchor is clicked', async () => {
const instance = node.instance();
const spy = jest.spyOn(CommandServiceImpl, 'remoteCall');
await instance.onBrowseClick({
currentTarget: { getAttribute: () => 'attr' },
});
expect(spy).toHaveBeenCalled();
} as any);
expect(dispatchSpy).toHaveBeenCalledWith(
executeCommand(true, SharedConstants.Commands.Electron.ShowOpenDialog, null, { properties: ['openDirectory'] })
);
});
});

Просмотреть файл

@ -35,8 +35,8 @@ import { connect } from 'react-redux';
import { getBotInfoByPath } from '../../../data/botHelpers';
import { RootState } from '../../../data/store';
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
import { DialogService } from '../service';
import { executeCommand } from '../../../data/action/commandAction';
import { ResourcesSettings, ResourcesSettingsProps } from './resourcesSettings';
@ -47,10 +47,12 @@ const mapStateToProps = (state: RootState, ownProps: ResourcesSettingsProps) =>
return { transcriptsPath, chatsPath, path, ...ownProps };
};
const mapDispatchToProps = _dispatch => ({
const mapDispatchToProps = dispatch => ({
save: (settings: Partial<BotInfo>) => DialogService.hideDialog(settings),
showOpenDialog: () =>
CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.ShowOpenDialog, { properties: ['openDirectory'] }),
dispatch(
executeCommand(true, SharedConstants.Commands.Electron.ShowOpenDialog, null, { properties: ['openDirectory'] })
),
cancel: () => DialogService.hideDialog(0),
});
export const ResourcesSettingsContainer = connect(

Просмотреть файл

@ -46,15 +46,6 @@ import { SecretPromptDialog } from './secretPromptDialog';
const mockStore = createStore(combineReducers({ bot }));
const mockBot = BotConfigWithPathImpl.fromJSON({});
jest.mock('../../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
remoteCall: async () => true,
},
}));
jest.mock('../dialogStyles.scss', () => ({}));
jest.mock('./secretPromptDialog.scss', () => ({}));
jest.mock('../../../data/store', () => ({
get store() {
return mockStore;

Просмотреть файл

@ -34,6 +34,7 @@
import { mount } from 'enzyme';
import * as React from 'react';
import { combineReducers, createStore } from 'redux';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import * as EditorActions from '../../../data/action/editorActions';
import {
@ -47,39 +48,52 @@ import { framework } from '../../../data/reducer/frameworkSettingsReducer';
import { AppSettingsEditor } from './appSettingsEditor';
import { AppSettingsEditorContainer } from './appSettingsEditorConainer';
jest.mock('./appSettingsEditor.scss', () => ({}));
jest.mock('../../layout/genericDocument.scss', () => ({}));
jest.mock(
'../../dialogs/',
() =>
new Proxy(
{},
{
get(): any {
return {};
},
}
)
);
const mockCallsMade = [];
const mockRemoteCallsMade = [];
jest.mock('../../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
call: (commandName, ...args) => {
mockCallsMade.push({ commandName, args });
return Promise.resolve();
},
remoteCall: (commandName, ...args) => {
mockRemoteCallsMade.push({ commandName, args });
return Promise.resolve('hai!');
},
},
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
describe('The AppSettingsEditorContainer', () => {
let instance: AppSettingsEditor;
let node;
let mockDispatch;
let mockStore;
let commandService: CommandServiceImpl;
const mockCallsMade = [];
const mockRemoteCallsMade = [];
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
commandService.call = (commandName, ...args) => {
mockCallsMade.push({ commandName, args });
return Promise.resolve(true) as any;
};
commandService.remoteCall = (commandName, ...args) => {
mockRemoteCallsMade.push({ commandName, args });
return Promise.resolve('hai!') as any;
};
});
beforeEach(() => {
mockStore = createStore(combineReducers({ framework }));
mockStore.dispatch(
@ -130,23 +144,19 @@ describe('The AppSettingsEditorContainer', () => {
});
it('should call a remote command to open a browse window when "onClickBrowse" is called', async () => {
const dispatchSpy = jest.spyOn(mockStore, 'dispatch');
await (instance as any).onClickBrowse();
expect(mockRemoteCallsMade[0]).toEqual({
args: [
{
buttonLabel: 'Select ngrok',
properties: ['openFile'],
title: 'Browse for ngrok',
},
],
commandName: 'shell:showExplorer-open-dialog',
const dispatchSpy = jest.spyOn(mockStore, 'dispatch').mockImplementation(action => {
if (action.payload.resolver) {
action.payload.resolver('some/path');
}
});
await (instance as any).onClickBrowse();
expect(dispatchSpy).toHaveBeenCalledWith({
expect(dispatchSpy).toHaveBeenLastCalledWith({
payload: { dirty: true, documentId: undefined },
type: 'EDITOR/SET_DIRTY_FLAG',
});
expect(instance.state.ngrokPath).toBe('some/path');
});
it('should discard the changes when "discardChanges" is called', () => {

Просмотреть файл

@ -93,9 +93,22 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
}
public render(): JSX.Element {
const { state } = this;
const {
ngrokPath = '',
useCustomId = false,
bypassNgrokLocalhost = true,
runNgrokAtStartup = false,
localhost = '',
locale = '',
use10Tokens = false,
useCodeValidation = false,
userGUID = '',
autoUpdate = false,
usePrereleases = false,
} = this.state;
const inputProps = {
disabled: !state.useCustomId,
disabled: !useCustomId,
};
return (
@ -123,7 +136,7 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
className={styles.appSettingsInput}
inputContainerClassName={styles.inputContainer}
readOnly={false}
value={state.ngrokPath}
value={ngrokPath}
onChange={this.onInputChange}
name="ngrokPath"
label={'Path to ngrok'}
@ -132,7 +145,7 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
</Row>
<Checkbox
className={styles.checkboxOverrides}
checked={state.bypassNgrokLocalhost}
checked={bypassNgrokLocalhost}
onChange={this.onChangeCheckBox}
id="ngrok-bypass"
label="Bypass ngrok for local addresses"
@ -140,7 +153,7 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
/>
<Checkbox
className={styles.checkboxOverrides}
checked={state.runNgrokAtStartup}
checked={runNgrokAtStartup}
onChange={this.onChangeCheckBox}
id="ngrok-startup"
label="Run ngrok when the Emulator starts up"
@ -151,7 +164,7 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
className={styles.appSettingsInput}
inputContainerClassName={styles.inputContainer}
readOnly={false}
value={state.localhost}
value={localhost}
onChange={this.onInputChange}
name="localhost"
label="localhost override"
@ -162,7 +175,7 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
className={styles.appSettingsInput}
inputContainerClassName={styles.inputContainer}
readOnly={false}
value={state.locale}
value={locale}
name="locale"
onChange={this.onInputChange}
label="Locale"
@ -173,7 +186,7 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
<SmallHeader>User settings</SmallHeader>
<Checkbox
className={styles.checkboxOverrides}
checked={state.use10Tokens}
checked={use10Tokens}
onChange={this.onChangeCheckBox}
id="auth-token-version"
label="Use version 1.0 authentication tokens"
@ -181,7 +194,7 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
/>
<Checkbox
className={styles.checkboxOverrides}
checked={state.useCodeValidation}
checked={useCodeValidation}
onChange={this.onChangeCheckBox}
id="use-validation-code"
label="Use a sign-in verification code for OAuthCards"
@ -189,7 +202,7 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
/>
<Checkbox
className={styles.checkboxOverrides}
checked={state.useCustomId}
checked={useCustomId}
onChange={this.onChangeCheckBox}
id="use-custom-id"
label="Use your own user ID to communicate with the bot"
@ -200,29 +213,29 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
<TextField
{...inputProps}
label="User ID"
placeholder={state.useCustomId ? '' : 'There is no ID configured'}
placeholder={useCustomId ? '' : 'There is no ID configured'}
className={styles.appSettingsInput}
inputContainerClassName={styles.inputContainer}
readOnly={false}
value={state.userGUID}
value={userGUID}
name="userGUID"
onChange={this.onInputChange}
required={state.useCustomId}
errorMessage={state.userGUID ? '' : 'Enter a User ID'}
required={useCustomId}
errorMessage={userGUID ? '' : 'Enter a User ID'}
/>
</Row>
<SmallHeader>Application Updates</SmallHeader>
<Checkbox
className={styles.checkboxOverrides}
checked={state.autoUpdate}
checked={autoUpdate}
onChange={this.onChangeCheckBox}
label="Automatically download and install updates"
name="autoUpdate"
/>
<Checkbox
className={styles.checkboxOverrides}
checked={state.usePrereleases}
checked={usePrereleases}
onChange={this.onChangeCheckBox}
label="Use pre-release versions"
name="usePrereleases"

Просмотреть файл

@ -39,9 +39,9 @@ import * as EditorActions from '../../../data/action/editorActions';
import { getFrameworkSettings } from '../../../data/action/frameworkSettingsActions';
import { getTabGroupForDocument } from '../../../data/editorHelpers';
import { RootState } from '../../../data/store';
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
import { debounce } from '../../../utils';
import { saveFrameworkSettings } from '../../../data/action/frameworkSettingsActions';
import { executeCommand } from '../../../data/action/commandAction';
import { AppSettingsEditor, AppSettingsEditorProps } from './appSettingsEditor';
@ -59,7 +59,9 @@ const mapDispatchToProps = (dispatch: (action: Action) => void, ownProps: AppSet
buttonLabel: 'Select ngrok',
properties: ['openFile'],
};
return CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.ShowOpenDialog, dialogOptions);
return new Promise(resolve => {
dispatch(executeCommand(true, SharedConstants.Commands.Electron.ShowOpenDialog, resolve, dialogOptions));
});
},
getFrameworkSettings: () => dispatch(getFrameworkSettings()),
saveFrameworkSettings: (framework: FrameworkSettings) => dispatch(saveFrameworkSettings(framework)),

Просмотреть файл

@ -35,14 +35,14 @@ import * as React from 'react';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { mount, shallow } from 'enzyme';
import { DebugMode, newNotification, SharedConstants } from '@bfemulator/app-shared';
import { DebugMode, SharedConstants } from '@bfemulator/app-shared';
import base64Url from 'base64url';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { disable, enable } from '../../../data/action/presentationActions';
import { clearLog, newConversation, setInspectorObjects } from '../../../data/action/chatActions';
import { updateDocument } from '../../../data/action/editorActions';
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
import { beginAdd } from '../../../data/action/notificationActions';
import { executeCommand } from '../../../data/action/commandAction';
import { Emulator, RestartConversationOptions } from './emulator';
import { EmulatorContainer } from './emulatorContainer';
@ -51,28 +51,6 @@ const { encode } = base64Url;
let mockCallsMade, mockRemoteCallsMade;
const mockSharedConstants = SharedConstants;
jest.mock('../../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
call: (commandName, ...args) => {
mockCallsMade.push({ commandName, args });
return Promise.resolve();
},
remoteCall: (commandName, ...args) => {
mockRemoteCallsMade.push({ commandName, args });
if (commandName === mockSharedConstants.Commands.Emulator.NewTranscript) {
return Promise.resolve({ conversationId: 'someConvoId' });
}
if (commandName === mockSharedConstants.Commands.Emulator.FeedTranscriptFromDisk) {
return Promise.resolve({ meta: 'some file info' });
}
if (commandName === mockSharedConstants.Commands.Settings.LoadAppSettings) {
return Promise.resolve({ framework: { userGUID: '' } });
}
return Promise.resolve();
},
},
}));
jest.mock('./chatPanel/chatPanel', () => {
return jest.fn(() => <div />);
});
@ -89,7 +67,7 @@ jest.mock('./parts', () => {
jest.mock('./toolbar/toolbar', () => {
return jest.fn(() => <div />);
});
jest.mock('@bfemulator/sdk-shared', () => ({
jest.mock('@bfemulator/sdk-shared/build/utils/misc', () => ({
uniqueId: () => 'someUniqueId',
uniqueIdv4: () => 'newUserId',
}));
@ -98,6 +76,31 @@ jest.mock('botframework-webchat', () => ({
createDirectLine: args => ({ ...args }),
}));
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
describe('<EmulatorContainer/>', () => {
let wrapper;
let node;
@ -106,6 +109,30 @@ describe('<EmulatorContainer/>', () => {
let mockStoreState;
const mockUnsubscribe = jest.fn(() => null);
let commandService: CommandServiceImpl;
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
commandService.call = (commandName, ...args) => {
mockCallsMade.push({ commandName, args });
return Promise.resolve() as any;
};
commandService.remoteCall = (commandName, ...args) => {
mockRemoteCallsMade.push({ commandName, args });
if (commandName === mockSharedConstants.Commands.Emulator.NewTranscript) {
return Promise.resolve({ conversationId: 'someConvoId' });
}
if (commandName === mockSharedConstants.Commands.Emulator.FeedTranscriptFromDisk) {
return Promise.resolve({ meta: 'some file info' });
}
if (commandName === mockSharedConstants.Commands.Settings.LoadAppSettings) {
return Promise.resolve({ framework: { userGUID: '' } });
}
return Promise.resolve() as any;
};
});
beforeEach(() => {
mockUnsubscribe.mockClear();
mockCallsMade = [];
@ -134,7 +161,11 @@ describe('<EmulatorContainer/>', () => {
clientAwareSettings: { debugMode: DebugMode.Normal },
};
const mockStore = createStore((_state, _action) => mockStoreState);
mockDispatch = jest.spyOn(mockStore, 'dispatch');
mockDispatch = jest.spyOn(mockStore, 'dispatch').mockImplementation(action => {
if (action && action.payload && action.payload.resolver) {
action.payload.resolver();
}
});
wrapper = mount(
<Provider store={mockStore}>
<EmulatorContainer documentId={'doc1'} url={'someUrl'} mode={'livechat'} conversationId={'convo1'} />
@ -288,18 +319,10 @@ describe('<EmulatorContainer/>', () => {
it('should export a transcript', async () => {
await instance.onExportTranscriptClick();
expect(mockRemoteCallsMade).toHaveLength(3);
expect(mockRemoteCallsMade[2].commandName).toBe(SharedConstants.Commands.Emulator.SaveTranscriptToFile);
expect(mockRemoteCallsMade[2].args).toEqual([32, 'convo1']);
});
it('should report a notification when exporting a transcript fails', async () => {
jest.spyOn(CommandServiceImpl, 'remoteCall').mockRejectedValueOnce({ message: 'oh noes!' });
await instance.onExportTranscriptClick();
const notification = newNotification('oh noes!');
notification.timestamp = jasmine.any(Number) as any;
expect(mockDispatch).toHaveBeenCalledWith(beginAdd(notification));
expect(mockRemoteCallsMade).toHaveLength(2);
expect(mockDispatch).toHaveBeenCalledWith(
executeCommand(true, SharedConstants.Commands.Emulator.SaveTranscriptToFile, null, 32, 'convo1')
);
});
it('should start a new conversation', async () => {
@ -358,18 +381,13 @@ describe('<EmulatorContainer/>', () => {
const mockStartNewConversation = jest.fn(async () => Promise.resolve(true));
instance.startNewConversation = mockStartNewConversation;
await instance.onStartOverClick();
expect(mockDispatch).toHaveBeenCalledWith(clearLog('doc1'));
expect(mockDispatch).toHaveBeenCalledWith(clearLog('doc1', jasmine.any(Function)));
expect(mockDispatch).toHaveBeenCalledWith(setInspectorObjects('doc1', []));
expect(mockRemoteCallsMade).toHaveLength(3);
expect(mockRemoteCallsMade.some(cmd => cmd.commandName === SharedConstants.Commands.Telemetry.TrackEvent)).toBe(
true
);
expect(
mockRemoteCallsMade.some(cmd => {
return cmd.args[0] === 'conversation_restart' && cmd.args[1].userId === 'new';
expect(mockDispatch).toHaveBeenCalledWith(
executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, 'conversation_restart', {
userId: 'new',
})
).toBe(true);
);
expect(mockStartNewConversation).toHaveBeenCalledWith(undefined, true, true);
});
@ -378,17 +396,13 @@ describe('<EmulatorContainer/>', () => {
instance.startNewConversation = mockStartNewConversation;
await instance.onStartOverClick(RestartConversationOptions.SameUserId);
expect(mockDispatch).toHaveBeenCalledWith(clearLog('doc1'));
expect(mockDispatch).toHaveBeenCalledWith(clearLog('doc1', jasmine.any(Function)));
expect(mockDispatch).toHaveBeenCalledWith(setInspectorObjects('doc1', []));
expect(mockRemoteCallsMade).toHaveLength(3);
expect(mockRemoteCallsMade.some(cmd => cmd.commandName === SharedConstants.Commands.Telemetry.TrackEvent)).toBe(
true
);
expect(
mockRemoteCallsMade.some(cmd => {
return cmd.args[0] === 'conversation_restart' && cmd.args[1].userId === 'same';
expect(mockDispatch).toHaveBeenCalledWith(
executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, 'conversation_restart', {
userId: 'same',
})
).toBe(true);
);
expect(mockStartNewConversation).toHaveBeenCalledWith(undefined, true, false);
});

Просмотреть файл

@ -45,9 +45,10 @@ import {
SharedConstants,
ValueTypesMask,
} from '@bfemulator/app-shared';
import { CommandServiceImpl } from '@bfemulator/sdk-shared';
import { CommandServiceInstance } from '@bfemulator/sdk-shared';
import { Document } from '../../../data/reducer/editor';
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
import { debounce } from '../../../utils';
import ChatPanel from './chatPanel/chatPanel';
@ -68,7 +69,7 @@ export type EmulatorMode = 'transcript' | 'livechat';
export interface EmulatorProps {
activeDocumentId?: string;
clearLog?: (documentId: string) => void;
clearLog?: (documentId: string) => Promise<void>;
conversationId?: string;
createErrorNotification?: (notification: Notification) => void;
debugMode?: DebugMode;
@ -90,6 +91,9 @@ export interface EmulatorProps {
}
export class Emulator extends React.Component<EmulatorProps, {}> {
@CommandServiceInstance()
private commandService: CommandServiceImpl;
private readonly onVerticalSizeChange = debounce(sizes => {
this.props.document.ui = {
...this.props.document.ui,
@ -157,12 +161,12 @@ export class Emulator extends React.Component<EmulatorProps, {}> {
userId = uniqueIdv4();
} else {
// use the previous id, or custom id
const framework: FrameworkSettings = await CommandServiceImpl.remoteCall(
const framework: FrameworkSettings = await this.commandService.remoteCall(
SharedConstants.Commands.Settings.LoadAppSettings
);
userId = props.document.userId || framework.userGUID;
}
await CommandServiceImpl.remoteCall(SharedConstants.Commands.Emulator.SetCurrentUser, userId);
await this.commandService.remoteCall(SharedConstants.Commands.Emulator.SetCurrentUser, userId);
const options = {
conversationId,
@ -171,15 +175,11 @@ export class Emulator extends React.Component<EmulatorProps, {}> {
userId,
};
if (props.document.directLine) {
props.document.directLine.end();
}
await Promise.resolve();
this.initConversation(props, options);
if (props.mode === 'transcript') {
try {
const conversation = await CommandServiceImpl.remoteCall(
const conversation = await this.commandService.remoteCall<any>(
SharedConstants.Commands.Emulator.NewTranscript,
conversationId
);
@ -188,7 +188,7 @@ export class Emulator extends React.Component<EmulatorProps, {}> {
try {
// transcript was deep linked via protocol or is generated in-memory via chatdown,
// and should just be fed its own activities attached to the document
await CommandServiceImpl.remoteCall(
await this.commandService.remoteCall<any>(
SharedConstants.Commands.Emulator.FeedTranscriptFromMemory,
conversation.conversationId,
props.document.botId,
@ -204,7 +204,7 @@ export class Emulator extends React.Component<EmulatorProps, {}> {
const fileInfo: {
fileName: string;
filePath: string;
} = await CommandServiceImpl.remoteCall(
} = await this.commandService.remoteCall<any>(
SharedConstants.Commands.Emulator.FeedTranscriptFromDisk,
conversation.conversationId,
props.document.botId,
@ -363,8 +363,11 @@ export class Emulator extends React.Component<EmulatorProps, {}> {
private onStartOverClick = async (option: string = RestartConversationOptions.NewUserId): Promise<void> => {
const { NewUserId, SameUserId } = RestartConversationOptions;
this.props.clearLog(this.props.document.documentId);
this.props.setInspectorObjects(this.props.document.documentId, []);
if (this.props.document.directLine) {
this.props.document.directLine.end();
}
await this.props.clearLog(this.props.document.documentId);
switch (option) {
case NewUserId: {

Просмотреть файл

@ -31,8 +31,7 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { connect } from 'react-redux';
import { Notification, SharedConstants } from '@bfemulator/app-shared';
import { ValueTypesMask } from '@bfemulator/app-shared/src';
import { Notification, SharedConstants, ValueTypesMask } from '@bfemulator/app-shared';
import { RootState } from '../../../data/store';
import * as PresentationActions from '../../../data/action/presentationActions';
@ -40,7 +39,7 @@ import * as ChatActions from '../../../data/action/chatActions';
import { Document } from '../../../data/reducer/editor';
import { updateDocument } from '../../../data/action/editorActions';
import { beginAdd } from '../../../data/action/notificationActions';
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
import { executeCommand } from '../../../data/action/commandAction';
import { Emulator, EmulatorProps } from './emulator';
@ -59,15 +58,21 @@ const mapDispatchToProps = (dispatch): EmulatorProps => ({
enablePresentationMode: enable =>
enable ? dispatch(PresentationActions.enable()) : dispatch(PresentationActions.disable()),
setInspectorObjects: (documentId, objects) => dispatch(ChatActions.setInspectorObjects(documentId, objects)),
clearLog: documentId => dispatch(ChatActions.clearLog(documentId)),
clearLog: (documentId: string) => {
return new Promise(resolve => {
dispatch(ChatActions.clearLog(documentId, resolve));
});
},
newConversation: (documentId, options) => dispatch(ChatActions.newConversation(documentId, options)),
updateChat: (documentId: string, updatedValues: any) => dispatch(ChatActions.updateChat(documentId, updatedValues)),
updateDocument: (documentId, updatedValues: Partial<Document>) => dispatch(updateDocument(documentId, updatedValues)),
createErrorNotification: (notification: Notification) => dispatch(beginAdd(notification)),
trackEvent: (name: string, properties?: { [key: string]: any }) =>
CommandServiceImpl.remoteCall(SharedConstants.Commands.Telemetry.TrackEvent, name, properties).catch(),
dispatch(executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, name, properties)),
exportItems: (valueTypes: ValueTypesMask, conversationId: string) =>
CommandServiceImpl.remoteCall(SharedConstants.Commands.Emulator.SaveTranscriptToFile, valueTypes, conversationId),
dispatch(
executeCommand(true, SharedConstants.Commands.Emulator.SaveTranscriptToFile, null, valueTypes, conversationId)
),
});
export const EmulatorContainer = connect(

Просмотреть файл

@ -38,18 +38,44 @@ import ReactWebChat, { createDirectLine } from 'botframework-webchat';
import { ActivityTypes } from 'botframework-schema';
import { DebugMode, ValueTypes } from '@bfemulator/app-shared';
import { combineReducers, createStore } from 'redux';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { CommandServiceImpl } from '../../../../../platform/commands/commandServiceImpl';
import { EmulatorMode } from '../../emulator';
import { bot } from '../../../../../data/reducer/bot';
import { chat } from '../../../../../data/reducer/chat';
import { editor } from '../../../../../data/reducer/editor';
import { clientAwareSettings } from '../../../../../data/reducer/clientAwareSettingsReducer';
import { BotCommands } from '../../../../../commands/botCommands';
import webChatStyleOptions from './webChatTheme';
import { ChatContainer } from './chatContainer';
import { ChatProps } from './chat';
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
const mockStore = createStore(combineReducers({ bot, chat, clientAwareSettings, editor }), {
clientAwareSettings: {
currentUser: { id: '123', name: 'Current User' },
@ -95,6 +121,14 @@ function render(overrides: Partial<ChatProps> = {}): ReactWrapper {
}
describe('<ChatContainer />', () => {
let commandService: CommandServiceImpl;
beforeAll(() => {
new BotCommands();
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
});
describe('when there is no direct line client', () => {
it('renders a `not connected` message', () => {
const component = render({ document: {} } as any);
@ -208,7 +242,7 @@ describe('<ChatContainer />', () => {
describe('speech services', () => {
it('displays a message when fetching the speech token', () => {
(CommandServiceImpl as any).remoteCall = jest.fn();
commandService.remoteCall = jest.fn().mockResolvedValueOnce(true);
const component = render({ pendingSpeechTokenRetrieval: true });
expect(component.find('div').text()).toEqual('Connecting...');
});

Просмотреть файл

@ -30,7 +30,14 @@
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { logEntry, LogLevel, textItem, luisEditorDeepLinkItem } from '@bfemulator/sdk-shared';
import {
CommandServiceImpl,
CommandServiceInstance,
logEntry,
LogLevel,
luisEditorDeepLinkItem,
textItem,
} from '@bfemulator/sdk-shared';
import { SharedConstants } from '@bfemulator/app-shared';
import { mount } from 'enzyme';
import * as React from 'react';
@ -43,7 +50,8 @@ import { bot } from '../../../../../data/reducer/bot';
import { clientAwareSettings } from '../../../../../data/reducer/clientAwareSettingsReducer';
import { theme } from '../../../../../data/reducer/themeReducer';
import { ExtensionManager } from '../../../../../extensions';
import { LogService } from '../../../../../platform/log/logService';
import { logService } from '../../../../../platform/log/logService';
import { executeCommand } from '../../../../../data/action/commandAction';
import { Inspector } from './inspector';
import { InspectorContainer } from './inspectorContainer';
@ -52,22 +60,35 @@ const mockStore = createStore(combineReducers({ theme, bot, clientAwareSettings
clientAwareSettings: { appPath: 'app-path' },
});
jest.mock('../../../panel/panel.scss', () => ({}));
jest.mock('../../../../../data/store', () => ({
get store() {
return mockStore;
},
}));
let mockRemoteCallsMade;
jest.mock('../../../../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
remoteCall: (commandName, ...args) => {
mockRemoteCallsMade.push({ commandName, args });
return Promise.resolve();
},
},
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
const mockState = {
@ -273,6 +294,18 @@ describe('The Inspector component', () => {
return el;
};
let commandService: CommandServiceImpl;
let mockRemoteCallsMade = [];
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
commandService.remoteCall = (commandName, ...args) => {
mockRemoteCallsMade.push({ commandName, args });
return true as any;
};
});
beforeEach(() => {
mockStore.dispatch(switchTheme('light', ['vars.css', 'light.css']));
mockStore.dispatch(loadBotInfos([mockState.bot]));
@ -331,7 +364,9 @@ describe('The Inspector component', () => {
});
describe('when there is an object to be inspected', () => {
let dispatchSpy;
beforeEach(() => {
dispatchSpy = jest.spyOn(mockStore, 'dispatch');
parent = mount(
<Provider store={mockStore}>
<InspectorContainer document={mockState.document} inspector={{ src }} />
@ -434,7 +469,7 @@ describe('The Inspector component', () => {
it('"logger.log" or "logger.error"', () => {
event.channel = 'logger.log';
const logSpy = jest.spyOn(LogService, 'logToDocument');
const logSpy = jest.spyOn(logService, 'logToDocument');
const inspectorName = mockExtensions[0].name;
const text = `[${inspectorName}] ${event.args[0]}`;
instance.ipcMessageEventHandler(event);
@ -444,7 +479,7 @@ describe('The Inspector component', () => {
it('"logger.luis-editor-deep-link"', () => {
event.channel = 'logger.luis-editor-deep-link';
const logSpy = jest.spyOn(LogService, 'logToDocument');
const logSpy = jest.spyOn(logService, 'logToDocument');
const inspectorName = mockExtensions[0].name;
const text = `[${inspectorName}] ${event.args[0]}`;
instance.ipcMessageEventHandler(event);
@ -452,25 +487,20 @@ describe('The Inspector component', () => {
expect(logSpy).toHaveBeenCalledWith(mockState.document.documentId, logEntry(luisEditorDeepLinkItem(text)));
});
it('"track-event"', () => {
it('"track-event"', async () => {
event.channel = 'track-event';
event.args[0] = 'someEvent';
event.args[1] = { some: 'data' };
instance.ipcMessageEventHandler(event);
await instance.ipcMessageEventHandler(event);
expect(dispatchSpy).toHaveBeenCalledWith(
executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, ...event.args)
);
expect(mockRemoteCallsMade).toHaveLength(1);
expect(mockRemoteCallsMade[0]).toEqual({
commandName: SharedConstants.Commands.Telemetry.TrackEvent,
args: ['someEvent', { some: 'data' }],
});
event.args[1] = undefined;
event.args[1] = {};
instance.ipcMessageEventHandler(event);
expect(mockRemoteCallsMade).toHaveLength(2);
expect(mockRemoteCallsMade[1]).toEqual({
commandName: SharedConstants.Commands.Telemetry.TrackEvent,
args: ['someEvent', {}],
});
expect(dispatchSpy).toHaveBeenCalledWith(
executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, ...event.args)
);
});
});
});

Просмотреть файл

@ -46,7 +46,7 @@ import { IBotConfiguration } from 'botframework-config/lib/schema';
import * as React from 'react';
import { ExtensionManager, GetInspectorResult, InspectorAPI } from '../../../../../extensions';
import { LogService } from '../../../../../platform/log/logService';
import { logService } from '../../../../../platform/log/logService';
import Panel, { PanelContent, PanelControls } from '../../../panel/panel';
import * as styles from './inspector.scss';
@ -383,7 +383,7 @@ export class Inspector extends React.Component<InspectorProps, InspectorState> {
const { documentId } = this.props.document;
const inspectorName = this._state.titleOverride || this.state.inspector.name || 'inspector';
const text = `[${inspectorName}] ${event.args[0]}`;
LogService.logToDocument(documentId, logEntry(textItem(logLevel, text)));
logService.logToDocument(documentId, logEntry(textItem(logLevel, text)));
break;
}
@ -391,7 +391,7 @@ export class Inspector extends React.Component<InspectorProps, InspectorState> {
const { documentId } = this.props.document;
const inspectorName = this._state.titleOverride || this.state.inspector.name || 'inspector';
const text = `[${inspectorName}] ${event.args[0]}`;
LogService.logToDocument(documentId, logEntry(luisEditorDeepLinkItem(text)));
logService.logToDocument(documentId, logEntry(luisEditorDeepLinkItem(text)));
break;
}

Просмотреть файл

@ -35,7 +35,7 @@ import { connect } from 'react-redux';
import { SharedConstants } from '@bfemulator/app-shared';
import { RootState } from '../../../../../data/store';
import { CommandServiceImpl } from '../../../../../platform/commands/commandServiceImpl';
import { executeCommand } from '../../../../../data/action/commandAction';
import { Inspector } from './inspector';
@ -50,12 +50,10 @@ const mapStateToProps = (state: RootState, ownProps: any) => {
};
};
const mapDispatchToProps = _dispatch => {
const mapDispatchToProps = dispatch => {
return {
trackEvent: (name: string, properties?: { [key: string]: any }) => {
CommandServiceImpl.remoteCall(SharedConstants.Commands.Telemetry.TrackEvent, name, properties).catch(
_e => void 0
);
dispatch(executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, name, properties));
},
};
};

Просмотреть файл

@ -39,10 +39,29 @@ import { mount, ReactWrapper } from 'enzyme';
import { Log, LogProps } from './log';
import { LogEntry } from './logEntry';
jest.mock('./log.scss', () => ({}));
jest.mock('../../../../../platform/commands/commandServiceImpl', () => ({
call: (...args: any[]) => null,
remoteCall: (...args: any[]) => null,
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
describe('log component', () => {

Просмотреть файл

@ -31,7 +31,7 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { LogLevel, textItem } from '@bfemulator/sdk-shared';
import { CommandServiceImpl, CommandServiceInstance, LogLevel, textItem } from '@bfemulator/sdk-shared';
import { mount, ReactWrapper } from 'enzyme';
import * as React from 'react';
import { Provider } from 'react-redux';
@ -50,6 +50,7 @@ import {
} from '../../../../dialogs';
import { ConnectedServiceEditorContainer } from '../../../../shell/explorer/servicesExplorer/connectedServiceEditor';
import { ConnectedServicePickerContainer } from '../../../../shell/explorer/servicesExplorer';
import { executeCommand } from '../../../../../data/action/commandAction';
import { LogEntry as LogEntryContainer } from './logEntryContainer';
import { LogEntry, LogEntryProps, number2, timestamp } from './logEntry';
@ -62,17 +63,30 @@ jest.mock('./log.scss', () => ({}));
let mockRemoteCallsMade;
let mockCallsMade;
jest.mock('../../../../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
call: (commandName, ...args) => {
mockCallsMade.push({ commandName, args });
return Promise.resolve(true);
},
remoteCall: (commandName, ...args) => {
mockRemoteCallsMade.push({ commandName, args });
return Promise.resolve(true);
},
},
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
describe('logEntry component', () => {
@ -82,6 +96,21 @@ describe('logEntry component', () => {
let props: LogEntryProps;
let mockDispatch;
let commandService: CommandServiceImpl;
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
commandService.remoteCall = (commandName: string, ...args: any[]) => {
mockRemoteCallsMade.push({ commandName, args });
return Promise.resolve(true) as any;
};
commandService.call = (commandName: string, ...args: any[]) => {
mockCallsMade.push({ commandName, args });
return Promise.resolve(true) as any;
};
});
beforeEach(() => {
mockRemoteCallsMade = [];
mockCallsMade = [];
@ -159,14 +188,14 @@ describe('logEntry component', () => {
const mockInspectableObj = { some: 'data', type: 'message', id: 'someId' };
instance.inspectAndHighlightInWebchat(mockInspectableObj);
expect(mockRemoteCallsMade).toHaveLength(1);
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
expect(mockRemoteCallsMade[0].args).toEqual(['log_inspectActivity', { type: 'message' }]);
expect(mockDispatch).toHaveBeenLastCalledWith(
executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, 'log_inspectActivity', {
type: 'message',
})
);
mockInspectableObj.type = undefined;
instance.inspectAndHighlightInWebchat(mockInspectableObj);
expect(mockRemoteCallsMade[1].args).toEqual(['log_inspectActivity', { type: '' }]);
});
it('should highlight an object', () => {

Просмотреть файл

@ -36,17 +36,17 @@ import { connect } from 'react-redux';
import { ServiceTypes } from 'botframework-config/lib/schema';
import * as ConnectedServiceActions from '../../../../../data/action/connectedServiceActions';
import { CommandServiceImpl } from '../../../../../platform/commands/commandServiceImpl';
import {
ConnectServicePromptDialogContainer,
AzureLoginSuccessDialogContainer,
AzureLoginFailedDialogContainer,
AzureLoginSuccessDialogContainer,
ConnectServicePromptDialogContainer,
GetStartedWithCSDialogContainer,
ProgressIndicatorContainer,
} from '../../../../dialogs';
import { ConnectedServicePickerContainer } from '../../../../shell/explorer/servicesExplorer';
import { ConnectedServiceEditorContainer } from '../../../../shell/explorer/servicesExplorer/connectedServiceEditor';
import { setHighlightedObjects, setInspectorObjects } from '../../../../../data/action/chatActions';
import { executeCommand } from '../../../../../data/action/commandAction';
import { LogEntry as LogEntryComponent, LogEntryProps } from './logEntry';
@ -71,17 +71,14 @@ function mapDispatchToProps(dispatch: any): Partial<LogEntryProps> {
setInspectorObjects: (documentId: string, obj: any) => dispatch(setInspectorObjects(documentId, obj)),
setHighlightedObjects: (documentId: string, obj: any) => dispatch(setHighlightedObjects(documentId, obj)),
reconnectNgrok: () => {
const { Ngrok } = SharedConstants.Commands;
return CommandServiceImpl.remoteCall(Ngrok.Reconnect);
dispatch(executeCommand(true, SharedConstants.Commands.Ngrok.Reconnect));
},
showAppSettings: () => {
const { UI } = SharedConstants.Commands;
return CommandServiceImpl.call(UI.ShowAppSettings);
return dispatch(executeCommand(false, UI.ShowAppSettings));
},
trackEvent: (name: string, properties?: { [key: string]: any }) => {
CommandServiceImpl.remoteCall(SharedConstants.Commands.Telemetry.TrackEvent, name, properties).catch(
_e => void 0
);
dispatch(executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, name, properties));
},
};
}

Просмотреть файл

@ -41,7 +41,7 @@ import * as BotActions from '../../../data/action/botActions';
import { beginAdd } from '../../../data/action/notificationActions';
import { openContextMenuForBot } from '../../../data/action/welcomePageActions';
import { bot } from '../../../data/reducer/bot';
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
import { executeCommand } from '../../../data/action/commandAction';
import { RecentBotsList } from './recentBotsList';
import { RecentBotsListContainer } from './recentBotsListContainer';
@ -93,23 +93,7 @@ describe('The RecentBotsList', () => {
expect(mockDispatch).toHaveBeenCalledWith(openContextMenuForBot((mockStore.getState() as any).bot.botFiles[0]));
});
it('should send a notification when a bot fails to delete', async () => {
await instance.onDeleteBotClick({
currentTarget: {
dataset: {
index: 1,
},
},
} as any);
const message = `An Error occurred on the Recent Bots List: TypeError: Cannot read property 'path' of undefined`;
const notification = beginAdd(newNotification(message));
notification.payload.notification.timestamp = jasmine.any(Number) as any;
notification.payload.notification.id = jasmine.any(String) as any;
expect(mockDispatch).toHaveBeenCalledWith(notification);
});
it('should call the appropriate command when a bot from the list is deleted', async () => {
const spy = jest.spyOn(CommandServiceImpl, 'remoteCall').mockResolvedValue(true);
await instance.onDeleteBotClick({
currentTarget: {
dataset: {
@ -118,7 +102,7 @@ describe('The RecentBotsList', () => {
},
} as any);
const { RemoveFromBotList } = SharedConstants.Commands.Bot;
expect(spy).toHaveBeenCalledWith(RemoveFromBotList, '/some/path');
expect(mockDispatch).toHaveBeenCalledWith(executeCommand(true, RemoveFromBotList, null, '/some/path'));
});
it('should call the onBotSelected function passed in the props when a bot it selected from the list', () => {

Просмотреть файл

@ -40,9 +40,8 @@ import * as styles from './recentBotsList.scss';
export interface RecentBotsListProps {
onBotSelected?: (bot: BotInfo) => void;
onDeleteBotClick?: (path: string) => Promise<any>;
onDeleteBotClick?: (path: string) => void;
recentBots?: BotInfo[];
sendNotification?: (error: Error) => void;
showContextMenuForBot?: (bot: BotInfo) => void;
}
@ -101,10 +100,6 @@ export class RecentBotsList extends Component<RecentBotsListProps, {}> {
private onDeleteBotClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
const { index } = event.currentTarget.dataset;
const bot = this.props.recentBots[index];
try {
await this.props.onDeleteBotClick(bot.path);
} catch (e) {
this.props.sendNotification(e);
}
this.props.onDeleteBotClick(bot.path);
};
}

Просмотреть файл

@ -31,14 +31,13 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { BotInfo, newNotification, SharedConstants } from '@bfemulator/app-shared';
import { BotInfo, SharedConstants } from '@bfemulator/app-shared';
import { connect } from 'react-redux';
import { Action } from 'redux';
import { beginAdd } from '../../../data/action/notificationActions';
import { openContextMenuForBot } from '../../../data/action/welcomePageActions';
import { RootState } from '../../../data/store';
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
import { executeCommand } from '../../../data/action/commandAction';
import { RecentBotsList, RecentBotsListProps } from './recentBotsList';
@ -51,10 +50,8 @@ const mapStateToProps = (state: RootState, ownProps: { [propName: string]: any }
const mapDispatchToProps = (dispatch: (action: Action) => void): RecentBotsListProps => {
return {
onDeleteBotClick: (path: string): Promise<any> =>
CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.RemoveFromBotList, path),
sendNotification: (error: Error) =>
dispatch(beginAdd(newNotification(`An Error occurred on the Recent Bots List: ${error}`))),
onDeleteBotClick: (path: string): void =>
dispatch(executeCommand(true, SharedConstants.Commands.Bot.RemoveFromBotList, null, path)),
showContextMenuForBot: (bot: BotInfo): void => dispatch(openContextMenuForBot(bot)),
};
};

Просмотреть файл

@ -41,7 +41,7 @@ import * as BotActions from '../../../data/action/botActions';
import { azureAuth } from '../../../data/reducer/azureAuthReducer';
import { clientAwareSettings } from '../../../data/reducer/clientAwareSettingsReducer';
import { bot } from '../../../data/reducer/bot';
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
import { executeCommand } from '../../../data/action/commandAction';
import { WelcomePage } from './welcomePage';
import { WelcomePageContainer } from './welcomePageContainer';
@ -54,6 +54,31 @@ jest.mock('../../../data/store', () => ({
},
}));
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
const mockArmToken = 'bm90aGluZw==.eyJ1cG4iOiJnbGFzZ293QHNjb3RsYW5kLmNvbSJ9.7gjdshgfdsk98458205jfds9843fjds';
const bots = [
{
@ -73,9 +98,11 @@ describe('The AzureLoginFailedDialogContainer component should', () => {
let parent;
let node;
let instance: any;
let mockDispatch;
beforeEach(() => {
mockStore.dispatch(azureArmTokenDataChanged(mockArmToken));
mockStore.dispatch(BotActions.loadBotInfos(bots));
mockDispatch = jest.spyOn(mockStore, 'dispatch');
parent = mount(
<Provider store={mockStore}>
<WelcomePageContainer />
@ -91,25 +118,22 @@ describe('The AzureLoginFailedDialogContainer component should', () => {
});
it('should call the appropriate command when a recent bot is clicked', () => {
const spy = jest.spyOn(CommandServiceImpl, 'call');
instance.onBotSelected(bots[1]);
const { Switch } = SharedConstants.Commands.Bot;
expect(spy).toHaveBeenCalledWith(Switch, '/Users/microsoft/Documents/testbot/TestBot.bot');
expect(mockDispatch).toHaveBeenCalledWith(
executeCommand(false, SharedConstants.Commands.Bot.Switch, null, '/Users/microsoft/Documents/testbot/TestBot.bot')
);
});
it('should call the appropriate command when onOpenBotClick is called', async () => {
const spy = jest.spyOn(CommandServiceImpl, 'call');
await instance.onOpenBotClick();
expect(spy).toHaveBeenCalledWith(SharedConstants.Commands.UI.ShowOpenBotDialog);
expect(mockDispatch).toHaveBeenCalledWith(executeCommand(false, SharedConstants.Commands.UI.ShowOpenBotDialog));
});
it('should call the appropriate command when openBotInspectorDocs is called', async () => {
const callSpy = jest.spyOn(CommandServiceImpl, 'call');
instance.props.openBotInspectorDocs();
expect(callSpy).toHaveBeenCalledWith(
SharedConstants.Commands.UI.ShowMarkdownPage,
SharedConstants.Channels.ReadmeUrl,
SharedConstants.Channels.HelpLabel
const { Commands, Channels } = SharedConstants;
expect(mockDispatch).toHaveBeenCalledWith(
executeCommand(false, Commands.UI.ShowMarkdownPage, null, Channels.ReadmeUrl, Channels.HelpLabel)
);
});
});

Просмотреть файл

@ -45,11 +45,11 @@ import * as styles from './welcomePage.scss';
export interface WelcomePageProps {
accessToken?: string;
onNewBotClick?: () => void;
showOpenBotDialog: () => Promise<any>;
showOpenBotDialog: () => void;
sendNotification?: (error: Error) => void;
signInWithAzure?: () => void;
signOutWithAzure?: () => void;
switchToBot?: (path: string) => Promise<any>;
switchToBot?: (path: string) => void;
openBotInspectorDocs: () => void;
debugMode?: number;
}
@ -134,19 +134,11 @@ export class WelcomePage extends React.Component<WelcomePageProps, {}> {
}
private onBotSelected = async (bot: BotInfo) => {
try {
await this.props.switchToBot(bot.path);
} catch (e) {
this.props.sendNotification(e);
}
this.props.switchToBot(bot.path);
};
private onOpenBotClick = async () => {
try {
await this.props.showOpenBotDialog();
} catch (e) {
this.props.sendNotification(e);
}
this.props.showOpenBotDialog();
};
private get signInSection(): JSX.Element {

Просмотреть файл

@ -31,13 +31,12 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { newNotification, SharedConstants } from '@bfemulator/app-shared';
import { SharedConstants } from '@bfemulator/app-shared';
import { connect } from 'react-redux';
import { Action } from 'redux';
import { beginAdd } from '../../../data/action/notificationActions';
import { RootState } from '../../../data/store';
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
import { executeCommand } from '../../../data/action/commandAction';
import { WelcomePage, WelcomePageProps } from './welcomePage';
@ -52,22 +51,16 @@ function mapStateToProps(state: RootState, ownProps: WelcomePageProps): WelcomeP
function mapDispatchToProps(dispatch: (action: Action) => void): WelcomePageProps {
const { Commands, Channels } = SharedConstants;
return {
onNewBotClick: () => {
CommandServiceImpl.call(Commands.UI.ShowBotCreationDialog).catch();
},
showOpenBotDialog: (): Promise<any> => CommandServiceImpl.call(SharedConstants.Commands.UI.ShowOpenBotDialog),
sendNotification: (error: Error) =>
dispatch(beginAdd(newNotification(`An Error occurred on the Welcome page: ${error}`))),
signInWithAzure: () => {
CommandServiceImpl.call(Commands.UI.SignInToAzure).catch();
},
onNewBotClick: () => dispatch(executeCommand(false, Commands.UI.ShowBotCreationDialog)),
showOpenBotDialog: () => dispatch(executeCommand(false, SharedConstants.Commands.UI.ShowOpenBotDialog)),
signInWithAzure: () => dispatch(executeCommand(false, Commands.UI.SignInToAzure)),
signOutWithAzure: () => {
CommandServiceImpl.remoteCall(Commands.Azure.SignUserOutOfAzure).catch();
CommandServiceImpl.call(Commands.UI.InvalidateAzureArmToken).catch();
dispatch(executeCommand(true, Commands.Azure.SignUserOutOfAzure));
dispatch(executeCommand(false, Commands.UI.InvalidateAzureArmToken));
},
switchToBot: (path: string) => CommandServiceImpl.call(Commands.Bot.Switch, path),
switchToBot: (path: string) => dispatch(executeCommand(false, Commands.Bot.Switch, null, path)),
openBotInspectorDocs: () =>
CommandServiceImpl.call(Commands.UI.ShowMarkdownPage, Channels.ReadmeUrl, Channels.HelpLabel),
dispatch(executeCommand(false, Commands.UI.ShowMarkdownPage, null, Channels.ReadmeUrl, Channels.HelpLabel)),
};
}

Просмотреть файл

@ -33,37 +33,52 @@
import { SharedConstants } from '@bfemulator/app-shared';
import { BotConfigWithPath } from '@bfemulator/sdk-shared';
import { IEndpointService, ServiceTypes } from 'botframework-config/lib/schema';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import * as botHelpers from '../../data/botHelpers';
import * as editorHelpers from '../../data/editorHelpers';
import { store } from '../../data/store';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
import { ActiveBotHelper } from './activeBotHelper';
jest.mock('../../ui/dialogs', () => ({
AzureLoginPromptDialogContainer: function mock() {
return undefined;
},
AzureLoginSuccessDialogContainer: function mock() {
return undefined;
},
BotCreationDialog: function mock() {
return undefined;
},
DialogService: { showDialog: () => Promise.resolve(true) },
SecretPromptDialog: function mock() {
return undefined;
},
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
describe('ActiveBotHelper tests', () => {
let commandService: CommandServiceImpl;
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
});
it('confirmSwitchBot() functionality', async () => {
(editorHelpers as any).hasNonGlobalTabs = jest
.fn()
.mockReturnValueOnce(true)
.mockReturnValueOnce(false);
(CommandServiceImpl as any).remoteCall = jest.fn().mockResolvedValue('done');
commandService.remoteCall = jest.fn().mockResolvedValue('done');
const result1 = await ActiveBotHelper.confirmSwitchBot();
expect(result1).toBe('done');
@ -78,7 +93,7 @@ describe('ActiveBotHelper tests', () => {
.mockReturnValueOnce(true)
.mockReturnValueOnce(false);
(CommandServiceImpl as any).remoteCall = jest.fn().mockResolvedValue('done');
commandService.remoteCall = jest.fn().mockResolvedValue('done');
const result1 = await ActiveBotHelper.confirmCloseBot();
expect(result1).toBe('done');
@ -89,21 +104,21 @@ describe('ActiveBotHelper tests', () => {
it('closeActiveBot() functionality', async () => {
const mockRemoteCall1 = jest.fn().mockResolvedValue(true);
(CommandServiceImpl as any).remoteCall = mockRemoteCall1;
commandService.remoteCall = mockRemoteCall1;
(store as any).dispatch = () => null;
await ActiveBotHelper.closeActiveBot();
expect(mockRemoteCall1).toHaveBeenCalledTimes(2);
const mockRemoteCall2 = jest.fn().mockRejectedValue('err');
(CommandServiceImpl as any).remoteCall = mockRemoteCall2;
commandService.remoteCall = mockRemoteCall2;
expect(ActiveBotHelper.closeActiveBot()).rejects.toEqual(new Error('Error while closing active bot: err'));
});
it('botAlreadyOpen() functionality', async () => {
const mockRemoteCall = jest.fn().mockResolvedValue(true);
(CommandServiceImpl as any).remoteCall = mockRemoteCall;
commandService.remoteCall = mockRemoteCall;
await ActiveBotHelper.botAlreadyOpen();
@ -112,7 +127,7 @@ describe('ActiveBotHelper tests', () => {
it('browseForBotFile() functionality', async () => {
const mockRemoteCall = jest.fn().mockResolvedValue(true);
(CommandServiceImpl as any).remoteCall = mockRemoteCall;
commandService.remoteCall = mockRemoteCall;
await ActiveBotHelper.browseForBotFile();
@ -160,7 +175,7 @@ describe('ActiveBotHelper tests', () => {
(store as any).dispatch = mockDispatch;
let mockRemoteCall = jest.fn().mockResolvedValue({});
(CommandServiceImpl as any).remoteCall = mockRemoteCall;
commandService.remoteCall = mockRemoteCall;
await ActiveBotHelper.setActiveBot(bot);
expect(mockDispatch).toHaveBeenCalledTimes(2);
@ -168,7 +183,7 @@ describe('ActiveBotHelper tests', () => {
expect(mockRemoteCall).toHaveBeenCalledWith(SharedConstants.Commands.Bot.SetActive, bot);
mockRemoteCall = jest.fn().mockRejectedValueOnce('error');
(CommandServiceImpl as any).remoteCall = mockRemoteCall;
commandService.remoteCall = mockRemoteCall;
expect(ActiveBotHelper.setActiveBot(bot)).rejects.toEqual(new Error('Error while setting active bot: error'));
});
@ -199,8 +214,8 @@ describe('ActiveBotHelper tests', () => {
};
let mockRemoteCall = jest.fn().mockResolvedValue(bot);
const mockCall = jest.fn().mockResolvedValue(null);
(CommandServiceImpl as any).remoteCall = mockRemoteCall;
(CommandServiceImpl as any).call = mockCall;
commandService.remoteCall = mockRemoteCall;
commandService.call = mockCall;
await ActiveBotHelper.confirmAndCreateBot(bot, 'someSecret');
expect(mockDispatch).toHaveBeenCalledTimes(3);
@ -208,8 +223,8 @@ describe('ActiveBotHelper tests', () => {
expect(mockRemoteCall).toHaveBeenCalledWith(SharedConstants.Commands.Bot.Create, bot, 'someSecret');
mockRemoteCall = jest.fn().mockRejectedValue('err');
(CommandServiceImpl as any).remoteCall = mockRemoteCall;
(CommandServiceImpl as any).call = mockCall;
commandService.remoteCall = mockRemoteCall;
commandService.call = mockCall;
expect(ActiveBotHelper.confirmAndCreateBot(bot, 'someSecret')).rejects.toEqual(
new Error('Error during bot create: err')
@ -255,8 +270,8 @@ describe('ActiveBotHelper tests', () => {
.mockResolvedValueOnce(bot)
.mockResolvedValue(null);
const mockCall = jest.fn().mockResolvedValue(null);
(CommandServiceImpl as any).remoteCall = mockRemoteCall;
(CommandServiceImpl as any).call = mockCall;
commandService.remoteCall = mockRemoteCall;
commandService.call = mockCall;
await ActiveBotHelper.confirmAndOpenBotFromFile();
expect(mockDispatch).toHaveBeenCalledTimes(1);
@ -305,8 +320,8 @@ describe('ActiveBotHelper tests', () => {
.mockResolvedValueOnce(bot)
.mockResolvedValue(null);
const mockCall = jest.fn().mockResolvedValue(null);
(CommandServiceImpl as any).remoteCall = mockRemoteCall;
(CommandServiceImpl as any).call = mockCall;
commandService.remoteCall = mockRemoteCall;
commandService.call = mockCall;
await ActiveBotHelper.confirmAndOpenBotFromFile();
expect(mockRemoteCall).toHaveBeenCalledWith(SharedConstants.Commands.Emulator.SetCurrentUser, 'customUserId');
@ -328,7 +343,7 @@ describe('ActiveBotHelper tests', () => {
});
it('should throw when confirmAndSwitchBots fails', async () => {
jest.spyOn(CommandServiceImpl, 'call').mockRejectedValueOnce('oh noes!');
jest.spyOn(commandService, 'call').mockRejectedValueOnce('oh noes!');
jest.spyOn(botHelpers, 'getActiveBot').mockReturnValueOnce({ path: '' });
try {
await ActiveBotHelper.confirmAndSwitchBots('');
@ -375,8 +390,8 @@ describe('ActiveBotHelper tests', () => {
(botHelpers.getActiveBot as any) = () => otherBot;
const mockRemoteCall = jest.fn().mockResolvedValue(bot);
const mockCall = jest.fn().mockResolvedValue(null);
(CommandServiceImpl as any).call = mockCall;
(CommandServiceImpl as any).remoteCall = mockRemoteCall;
commandService.call = mockCall;
commandService.remoteCall = mockRemoteCall;
ActiveBotHelper.confirmSwitchBot = () => new Promise((resolve, reject) => resolve(true));
ActiveBotHelper.setActiveBot = (arg: any) => new Promise((resolve, reject) => resolve(null));

Просмотреть файл

@ -34,6 +34,7 @@
import { getBotDisplayName, newNotification, SharedConstants } from '@bfemulator/app-shared';
import { BotConfigWithPath, mergeEndpoints } from '@bfemulator/sdk-shared';
import { IEndpointService, ServiceTypes } from 'botframework-config/lib/schema';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import * as Constants from '../../constants';
import * as BotActions from '../../data/action/botActions';
@ -45,14 +46,16 @@ import { beginAdd } from '../../data/action/notificationActions';
import { getActiveBot } from '../../data/botHelpers';
import { hasNonGlobalTabs } from '../../data/editorHelpers';
import { store } from '../../data/store';
import { CommandServiceImpl } from '../../platform/commands/commandServiceImpl';
const { Bot, Electron, Telemetry } = SharedConstants.Commands;
export const ActiveBotHelper = new (class {
async confirmSwitchBot(): Promise<any> {
export class ActiveBotHelper {
@CommandServiceInstance()
private static commandService: CommandServiceImpl;
static async confirmSwitchBot(): Promise<any> {
if (hasNonGlobalTabs()) {
return await CommandServiceImpl.remoteCall(Electron.ShowMessageBox, true, {
return await this.commandService.remoteCall(Electron.ShowMessageBox, true, {
buttons: ['Cancel', 'OK'],
cancelId: 0,
defaultId: 1,
@ -64,11 +67,11 @@ export const ActiveBotHelper = new (class {
}
}
confirmCloseBot(): Promise<any> {
static confirmCloseBot(): Promise<any> {
const hasTabs = hasNonGlobalTabs();
// TODO - localization
if (hasTabs) {
return CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.ShowMessageBox, true, {
return this.commandService.remoteCall(SharedConstants.Commands.Electron.ShowMessageBox, true, {
type: 'question',
buttons: ['Cancel', 'OK'],
defaultId: 1,
@ -83,17 +86,17 @@ export const ActiveBotHelper = new (class {
/** Sets a bot as active
* @param bot Bot to set as active
*/
async setActiveBot(bot: BotConfigWithPath): Promise<void> {
static async setActiveBot(bot: BotConfigWithPath): Promise<void> {
try {
// set the bot as active on the server side
const botDirectory = await CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.SetActive, bot);
const botDirectory = await this.commandService.remoteCall<string>(SharedConstants.Commands.Bot.SetActive, bot);
store.dispatch(BotActions.setActiveBot(bot));
store.dispatch(FileActions.setRoot(botDirectory));
// update the app file menu and title bar
await Promise.all([
CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.UpdateFileMenu),
CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.SetTitleBar, getBotDisplayName(bot)),
this.commandService.remoteCall(SharedConstants.Commands.Electron.UpdateFileMenu),
this.commandService.remoteCall(SharedConstants.Commands.Electron.SetTitleBar, getBotDisplayName(bot)),
]);
} catch (e) {
const errMsg = `Error while setting active bot: ${e}`;
@ -104,11 +107,11 @@ export const ActiveBotHelper = new (class {
}
/** tell the server-side the active bot is now closed */
async closeActiveBot(): Promise<void> {
static async closeActiveBot(): Promise<void> {
try {
await CommandServiceImpl.remoteCall(Bot.Close);
await this.commandService.remoteCall(Bot.Close);
store.dispatch(BotActions.closeBot());
await CommandServiceImpl.remoteCall(SharedConstants.Commands.Electron.SetTitleBar, '');
await this.commandService.remoteCall(SharedConstants.Commands.Electron.SetTitleBar, '');
} catch (err) {
const errMsg = `Error while closing active bot: ${err}`;
const notification = newNotification(errMsg);
@ -116,9 +119,9 @@ export const ActiveBotHelper = new (class {
}
}
async botAlreadyOpen(): Promise<void> {
static async botAlreadyOpen(): Promise<void> {
// TODO - localization
return await CommandServiceImpl.remoteCall(Electron.ShowMessageBox, true, {
return await this.commandService.remoteCall<void>(Electron.ShowMessageBox, true, {
buttons: ['OK'],
cancelId: 0,
defaultId: 0,
@ -129,7 +132,7 @@ export const ActiveBotHelper = new (class {
});
}
async confirmAndCreateBot(botToCreate: BotConfigWithPath, secret: string): Promise<void> {
static async confirmAndCreateBot(botToCreate: BotConfigWithPath, secret: string): Promise<void> {
// prompt the user to confirm the switch
const result = await this.confirmSwitchBot();
@ -138,7 +141,7 @@ export const ActiveBotHelper = new (class {
try {
// create the bot and save to disk
const bot: BotConfigWithPath = await CommandServiceImpl.remoteCall(
const bot = await this.commandService.remoteCall<BotConfigWithPath>(
SharedConstants.Commands.Bot.Create,
botToCreate,
secret
@ -151,7 +154,7 @@ export const ActiveBotHelper = new (class {
) as IEndpointService;
if (endpoint) {
CommandServiceImpl.call(SharedConstants.Commands.Emulator.NewLiveChat, endpoint);
this.commandService.call(SharedConstants.Commands.Emulator.NewLiveChat, endpoint);
}
store.dispatch(NavBarActions.select(Constants.NAVBAR_BOT_EXPLORER));
@ -165,8 +168,8 @@ export const ActiveBotHelper = new (class {
}
}
browseForBotFile(): Promise<any> {
return CommandServiceImpl.remoteCall(Electron.ShowOpenDialog, {
static browseForBotFile(): Promise<any> {
return this.commandService.remoteCall(Electron.ShowOpenDialog, {
buttonLabel: 'Choose file',
filters: [
{
@ -179,7 +182,7 @@ export const ActiveBotHelper = new (class {
});
}
async confirmAndOpenBotFromFile(filename?: string): Promise<any> {
static async confirmAndOpenBotFromFile(filename?: string): Promise<any> {
try {
if (!filename) {
filename = await this.browseForBotFile();
@ -188,27 +191,32 @@ export const ActiveBotHelper = new (class {
if (filename) {
const activeBot = getActiveBot();
if (activeBot && activeBot.path === filename) {
await CommandServiceImpl.call(SharedConstants.Commands.Bot.Switch, activeBot);
await this.commandService.call(SharedConstants.Commands.Bot.Switch, activeBot);
return;
}
const result = this.confirmSwitchBot();
if (result) {
store.dispatch(EditorActions.closeNonGlobalTabs());
const bot = await CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.Open, filename);
const bot = await this.commandService.remoteCall<BotConfigWithPath>(
SharedConstants.Commands.Bot.Open,
filename
);
if (!bot) {
return;
}
const state = store.getState();
const currentUserId = state.clientAwareSettings.users.currentUserId || state.framework.userGUID;
await CommandServiceImpl.remoteCall(SharedConstants.Commands.Emulator.SetCurrentUser, currentUserId);
await CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.SetActive, bot);
await CommandServiceImpl.call(SharedConstants.Commands.Bot.Load, bot);
await this.commandService.remoteCall(SharedConstants.Commands.Emulator.SetCurrentUser, currentUserId);
await this.commandService.remoteCall(SharedConstants.Commands.Bot.SetActive, bot);
await this.commandService.call(SharedConstants.Commands.Bot.Load, bot);
const numOfServices = bot.services && bot.services.length;
CommandServiceImpl.remoteCall(Telemetry.TrackEvent, `bot_open`, {
method: 'file_browse',
numOfServices,
}).catch(_e => void 0);
this.commandService
.remoteCall(Telemetry.TrackEvent, `bot_open`, {
method: 'file_browse',
numOfServices,
})
.catch(_e => void 0);
}
}
} catch (err) {
@ -221,14 +229,14 @@ export const ActiveBotHelper = new (class {
* a livechat session.
* @param bot The bot to be switched to. Can be a bot object with a path, or the bot path itself
*/
async confirmAndSwitchBots(bot: BotConfigWithPath | string): Promise<any> {
static async confirmAndSwitchBots(bot: BotConfigWithPath | string): Promise<any> {
const currentActiveBot = getActiveBot();
const botPath = typeof bot === 'object' ? bot.path : bot;
if (currentActiveBot && currentActiveBot.path === botPath) {
// the bot is already open, so open a new live chat tab
try {
await CommandServiceImpl.call(SharedConstants.Commands.Emulator.NewLiveChat, currentActiveBot.services[0]);
await this.commandService.call(SharedConstants.Commands.Emulator.NewLiveChat, currentActiveBot.services[0]);
} catch (e) {
throw new Error(`[confirmAndSwitchBots] Error while trying to open bot at ${botPath}: ${e}`);
}
@ -249,7 +257,10 @@ export const ActiveBotHelper = new (class {
let newActiveBot: BotConfigWithPath;
if (typeof bot === 'string') {
try {
newActiveBot = await CommandServiceImpl.remoteCall(SharedConstants.Commands.Bot.Open, bot);
newActiveBot = await this.commandService.remoteCall<BotConfigWithPath>(
SharedConstants.Commands.Bot.Open,
bot
);
} catch (e) {
throw new Error(`[confirmAndSwitchBots] Error while trying to open bot at ${botPath}: ${e}`);
}
@ -280,7 +291,7 @@ export const ActiveBotHelper = new (class {
// open a livechat with the configured endpoint
if (endpoint) {
await CommandServiceImpl.call(SharedConstants.Commands.Emulator.NewLiveChat, endpoint);
await this.commandService.call(SharedConstants.Commands.Emulator.NewLiveChat, endpoint);
}
store.dispatch(NavBarActions.select(Constants.NAVBAR_BOT_EXPLORER));
@ -294,7 +305,7 @@ export const ActiveBotHelper = new (class {
}
}
confirmAndCloseBot(): Promise<any> {
static confirmAndCloseBot(): Promise<any> {
const activeBot = getActiveBot();
if (!activeBot) {
return Promise.resolve();
@ -317,4 +328,4 @@ export const ActiveBotHelper = new (class {
throw new Error(errMsg);
});
}
})();
}

Просмотреть файл

@ -40,8 +40,8 @@ import { combineReducers, createStore } from 'redux';
import { beginAdd } from '../../../../data/action/notificationActions';
import { bot } from '../../../../data/reducer/bot';
import { chat } from '../../../../data/reducer/chat';
import { CommandServiceImpl } from '../../../../platform/commands/commandServiceImpl';
import { ActiveBotHelper } from '../../../helpers/activeBotHelper';
import { executeCommand } from '../../../../data/action/commandAction';
import { BotNotOpenExplorer } from './botNotOpenExplorer';
import { BotNotOpenExplorerContainer } from './botNotOpenExplorerContainer';
@ -55,7 +55,31 @@ jest.mock('../../../dialogs', () => ({
},
}));
jest.mock('./botNotOpenExplorer.scss', () => ({}));
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
jest.mock('../../../../data/store', () => ({
get store() {
return mockStore;
@ -67,6 +91,7 @@ describe('The EndpointExplorer component should', () => {
let node;
let mockDispatch;
let instance;
beforeEach(() => {
mockDispatch = jest.spyOn(mockStore, 'dispatch');
parent = mount(
@ -79,22 +104,10 @@ describe('The EndpointExplorer component should', () => {
});
it('should make the appropriate calls when onCreateNewBotClick in called', async () => {
const commandServiceSpy = jest.spyOn(CommandServiceImpl, 'call').mockResolvedValue(true);
await instance.onCreateNewBotClick();
expect(commandServiceSpy).toHaveBeenLastCalledWith(SharedConstants.Commands.UI.ShowBotCreationDialog);
});
it('should send a notification when onCreateNewBotClick fails', async () => {
const commandServiceSpy = jest.spyOn(CommandServiceImpl, 'call').mockRejectedValue('oh noes!');
await instance.onCreateNewBotClick();
const message = `An Error occurred on the Bot Not Open Explorer: oh noes!`;
const notification = newNotification(message);
const action = beginAdd(notification);
notification.timestamp = jasmine.any(Number) as any;
notification.id = jasmine.any(String) as any;
expect(mockDispatch).toHaveBeenLastCalledWith(action);
expect(commandServiceSpy).toHaveBeenLastCalledWith(SharedConstants.Commands.UI.ShowBotCreationDialog);
expect(mockDispatch).toHaveBeenLastCalledWith(
executeCommand(false, SharedConstants.Commands.UI.ShowBotCreationDialog)
);
});
it('should make the appropriate calls when onOpenBotFileClick in called', async () => {

Просмотреть файл

@ -39,8 +39,7 @@ import * as styles from './botNotOpenExplorer.scss';
export interface BotNotOpenExplorerProps {
hasChat?: boolean;
openBotFile?: () => Promise<any>;
showCreateNewBotDialog?: () => Promise<void>;
sendNotification: (error: Error) => void;
showCreateNewBotDialog?: () => void;
}
export class BotNotOpenExplorer extends React.Component<BotNotOpenExplorerProps, {}> {
@ -70,18 +69,10 @@ export class BotNotOpenExplorer extends React.Component<BotNotOpenExplorerProps,
}
private onCreateNewBotClick = async () => {
try {
await this.props.showCreateNewBotDialog();
} catch (e) {
this.props.sendNotification(e);
}
this.props.showCreateNewBotDialog();
};
private onOpenBotFileClick = async () => {
try {
await this.props.openBotFile();
} catch (e) {
this.props.sendNotification(e);
}
await this.props.openBotFile();
};
}

Просмотреть файл

@ -31,25 +31,30 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { newNotification, SharedConstants } from '@bfemulator/app-shared';
import { SharedConstants } from '@bfemulator/app-shared';
import { connect } from 'react-redux';
import { Action } from 'redux';
import { newNotification } from '@bfemulator/app-shared';
import { beginAdd } from '../../../../data/action/notificationActions';
import { RootState } from '../../../../data/store';
import { CommandServiceImpl } from '../../../../platform/commands/commandServiceImpl';
import { ActiveBotHelper } from '../../../helpers/activeBotHelper';
import { executeCommand } from '../../../../data/action/commandAction';
import { beginAdd } from '../../../../data/action/notificationActions';
import { BotNotOpenExplorer as BotNotOpenExplorerComp, BotNotOpenExplorerProps } from './botNotOpenExplorer';
const mapStateToProps = (state: RootState): any => ({
hasChat: !!Object.keys(state.chat.chats).length,
showCreateNewBotDialog: () => CommandServiceImpl.call(SharedConstants.Commands.UI.ShowBotCreationDialog),
});
const mapDispatchToProps = (dispatch: (action: Action) => void): BotNotOpenExplorerProps => ({
openBotFile: () => ActiveBotHelper.confirmAndOpenBotFromFile(),
sendNotification: (error: Error) =>
dispatch(beginAdd(newNotification(`An Error occurred on the Bot Not Open Explorer: ${error}`))),
openBotFile: async () => {
try {
await ActiveBotHelper.confirmAndOpenBotFromFile();
} catch (e) {
dispatch(beginAdd(newNotification(`An Error occurred on the Bot Not Open Explorer: ${e}`)));
}
},
showCreateNewBotDialog: () => dispatch(executeCommand(false, SharedConstants.Commands.UI.ShowBotCreationDialog)),
});
export const BotNotOpenExplorerContainer = connect(

Просмотреть файл

@ -164,7 +164,8 @@ describe('The ConnectedServiceEditor component should render the correct content
"version": "0.1",
"appId": "121221",
"authoringKey": "poo",
"subscriptionKey": "emoji"
"subscriptionKey": "emoji",
"hostname": "http://localhost"
}`);
const services = [
ServiceTypes.Luis,

Просмотреть файл

@ -44,30 +44,17 @@ function mockProxy() {
}
);
}
jest.mock('./main.scss', () => ({}));
jest.mock('./explorer', () => mockProxy());
jest.mock('./mdi', () => mockProxy());
jest.mock('./navBar', () => mockProxy());
jest.mock('./statusBar/statusBar.scss', () => ({}));
jest.mock('../debug/storeVisualizer.scss', () => ({}));
jest.mock('../../ui/dialogs', () => ({
AzureLoginPromptDialogContainer: function mock() {
return undefined;
},
AzureLoginSuccessDialogContainer: function mock() {
return undefined;
},
BotCreationDialog: function mock() {
return undefined;
},
DialogService: { showDialog: () => Promise.resolve(true) },
SecretPromptDialog: function mock() {
return undefined;
},
}));
describe('The Main component', () => {
it('should pass an empty test', () => {
const parent = shallow(<Main />);
const parent = shallow(<Main applicationMountComplete={() => void 0} />);
expect(parent.find(Main)).not.toBe(null);
});
});

Просмотреть файл

@ -48,6 +48,7 @@ import { NavBar } from './navBar';
import { StatusBar } from './statusBar/statusBar';
export interface MainProps {
applicationMountComplete?: () => void;
primaryEditor?: Editor;
secondaryEditor?: Editor;
explorerIsVisible?: boolean;
@ -77,6 +78,10 @@ export class Main extends React.Component<MainProps, MainState> {
}
}
public componentDidMount() {
this.props.applicationMountComplete();
}
public render() {
const tabGroup1 = this.props.primaryEditor && (
<div className={styles.mdiWrapper} key={'primaryEditor'}>

Просмотреть файл

@ -31,10 +31,14 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { connect } from 'react-redux';
import { SharedConstants } from '@bfemulator/app-shared';
import * as Constants from '../../constants';
import * as PresentationActions from '../../data/action/presentationActions';
import { RootState } from '../../data/store';
import { executeCommand } from '../../data/action/commandAction';
import { showWelcomePage } from '../../data/editorHelpers';
import { globalHandlers } from '../../utils/eventHandlers';
import { Main, MainProps } from './main';
@ -52,6 +56,16 @@ const mapDispatchToProps = (dispatch): MainProps => ({
dispatch(PresentationActions.disable());
}
},
applicationMountComplete: async () => {
await new Promise(resolve => {
dispatch(executeCommand(true, SharedConstants.Commands.ClientInit.Loaded, resolve));
});
showWelcomePage();
await new Promise(resolve => {
dispatch(executeCommand(true, SharedConstants.Commands.ClientInit.PostWelcomeScreen, resolve));
});
window.addEventListener('keydown', globalHandlers, true);
},
});
export default connect(

Просмотреть файл

@ -36,10 +36,10 @@ import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { SharedConstants } from '@bfemulator/app-shared';
import { MouseEvent } from 'react';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { enable } from '../../../../data/action/presentationActions';
import { setActiveTab, appendTab, splitTab } from '../../../../data/action/editorActions';
import { appendTab, setActiveTab, splitTab } from '../../../../data/action/editorActions';
import {
CONTENT_TYPE_APP_SETTINGS,
CONTENT_TYPE_LIVE_CHAT,
@ -47,6 +47,7 @@ import {
CONTENT_TYPE_TRANSCRIPT,
CONTENT_TYPE_WELCOME_PAGE,
} from '../../../../constants';
import { executeCommand } from '../../../../data/action/commandAction';
import { TabBarContainer } from './tabBarContainer';
import { TabBar } from './tabBar';
@ -57,29 +58,45 @@ const mockTab = class Tab extends React.Component {
}
};
jest.mock('./tabBar.scss', () => ({}));
jest.mock('../../../../data/reducer/editor', () => ({
Document: {},
Editor: {},
}));
jest.mock('../../../../data/editorHelpers', () => ({
getTabGroupForDocument: () => null,
getOtherTabGroup: (tabGroup: string) => (tabGroup === 'primary' ? 'secondary' : 'primary'),
}));
jest.mock('../tab/tab', () => ({
get Tab() {
return mockTab;
},
}));
let mockRemoteCallsMade;
jest.mock('../../../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
remoteCall: (commandName, ...args) => {
mockRemoteCallsMade.push({ commandName, args });
return Promise.resolve(true);
},
},
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
describe('TabBar', () => {
@ -88,6 +105,18 @@ describe('TabBar', () => {
let instance;
let mockStore;
let mockDispatch;
let commandService: CommandServiceImpl;
let mockRemoteCallsMade = [];
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
commandService.remoteCall = (commandName, ...args) => {
mockRemoteCallsMade.push({ commandName, args });
return true as any;
};
commandService.call = () => true as any;
});
beforeEach(() => {
const defaultState = {
@ -132,11 +161,10 @@ describe('TabBar', () => {
it('should enable presentation mode', async () => {
await instance.onPresentationModeClick();
expect(mockDispatch).toHaveBeenCalledWith(
executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, 'tabBar_presentationMode')
);
expect(mockDispatch).toHaveBeenCalledWith(enable());
expect(mockRemoteCallsMade).toHaveLength(1);
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
expect(mockRemoteCallsMade[0].args).toEqual(['tabBar_presentationMode']);
});
it('should load widgets', () => {
@ -189,9 +217,9 @@ describe('TabBar', () => {
});
it('should handle a key press', () => {
const mockOtherKeyPress = { key: 'a', currentTarget: { dataset: { index: 0 } } as any };
const mockSpaceKeyPress = { key: ' ', currentTarget: { dataset: { index: 0 } } as any };
const mockEnterKeyPress = { key: 'enter', currentTarget: { dataset: { index: 0 } } as any };
const mockOtherKeyPress = { key: 'a', currentTarget: { dataset: { index: 0 } } as any } as any;
const mockSpaceKeyPress = { key: ' ', currentTarget: { dataset: { index: 0 } } as any } as any;
const mockEnterKeyPress = { key: 'enter', currentTarget: { dataset: { index: 0 } } as any } as any;
// simulate neither key press
instance.handleKeyDown(mockOtherKeyPress);
@ -206,18 +234,17 @@ describe('TabBar', () => {
expect(mockDispatch).toHaveBeenCalledTimes(2);
});
it('should handle a split click', () => {
instance.onSplitClick();
it('should handle a split click', async () => {
await instance.onSplitClick();
expect(mockDispatch).toHaveBeenCalledWith(
executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, 'tabBar_splitTab')
);
expect(mockDispatch).toHaveBeenCalledWith(splitTab('transcript', 'doc1', 'primary', 'secondary'));
expect(mockRemoteCallsMade).toHaveLength(1);
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
expect(mockRemoteCallsMade[0].args).toEqual(['tabBar_splitTab']);
});
it('should handle a drag enter event', () => {
const mockPreventDefault = jest.fn(() => null);
const mockDragEvent = { preventDefault: mockPreventDefault };
const mockDragEvent = { preventDefault: mockPreventDefault } as any;
instance.onDragEnter(mockDragEvent);
expect(mockPreventDefault).toHaveBeenCalled();

Просмотреть файл

@ -39,7 +39,7 @@ import { appendTab, close, setActiveTab, splitTab } from '../../../../data/actio
import { enable as enablePresentationMode } from '../../../../data/action/presentationActions';
import { getTabGroupForDocument } from '../../../../data/editorHelpers';
import { RootState } from '../../../../data/store';
import { CommandServiceImpl } from '../../../../platform/commands/commandServiceImpl';
import { executeCommand } from '../../../../data/action/commandAction';
import { TabBar, TabBarProps } from './tabBar';
@ -56,13 +56,13 @@ const mapStateToProps = (state: RootState, ownProps: TabBarProps): TabBarProps =
const mapDispatchToProps = (dispatch): TabBarProps => ({
splitTab: (contentType: string, documentId: string, srcEditorKey: string, destEditorKey: string) => {
CommandServiceImpl.remoteCall(SharedConstants.Commands.Telemetry.TrackEvent, 'tabBar_splitTab').catch(_e => void 0);
dispatch(executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, 'tabBar_splitTab'));
dispatch(splitTab(contentType, documentId, srcEditorKey, destEditorKey));
},
appendTab: (srcEditorKey: string, destEditorKey: string, tabId: string) =>
dispatch(appendTab(srcEditorKey, destEditorKey, tabId)),
enablePresentationMode: async () => {
await CommandServiceImpl.remoteCall(SharedConstants.Commands.Telemetry.TrackEvent, 'tabBar_presentationMode');
dispatch(executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, 'tabBar_presentationMode'));
dispatch(enablePresentationMode());
},
setActiveTab: (documentId: string) => dispatch(setActiveTab(documentId)),

Просмотреть файл

@ -35,27 +35,17 @@ import * as React from 'react';
import { mount, shallow } from 'enzyme';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import * as Constants from '../../../constants';
import { select } from '../../../data/action/navBarActions';
import { open } from '../../../data/action/editorActions';
import { showExplorer } from '../../../data/action/explorerActions';
import { BotCommands } from '../../../commands/botCommands';
import { NavBarComponent as NavBar } from './navBar';
import { NavBar as NavBarContainer } from './navBarContainer';
let mockRemoteCallsMade;
jest.mock('../../../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
remoteCall: jest.fn((commandName, ...args) => {
mockRemoteCallsMade.push({ commandName, args });
return Promise.resolve();
}),
},
}));
jest.mock('./navBar.scss', () => ({}));
let mockState;
const mockNotifications = {
id1: { read: true },
@ -68,11 +58,48 @@ jest.mock('../../../notificationManager', () => ({
},
}));
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
describe('<NavBar/>', () => {
let mockDispatch;
let wrapper;
let instance;
let node;
let mockRemoteCallsMade;
let commandService: CommandServiceImpl;
beforeAll(() => {
new BotCommands();
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
commandService.remoteCall = (...args) => {
mockRemoteCallsMade.push(args);
return true as any;
};
});
beforeEach(() => {
mockState = {
@ -119,11 +146,7 @@ describe('<NavBar/>', () => {
const mockEvent = {
currentTarget,
};
instance.onLinkClick(mockEvent);
expect(mockRemoteCallsMade).toHaveLength(1);
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
expect(mockRemoteCallsMade[0].args).toEqual(['navbar_selection', { selection: 'notifications' }]);
instance.onLinkClick(mockEvent as any);
expect(mockDispatch).toHaveBeenCalledWith(select('navbar.notifications'));
expect(instance.state.selection).toBe('navbar.notifications');
});

Просмотреть файл

@ -39,7 +39,7 @@ import * as EditorActions from '../../../data/action/editorActions';
import * as ExplorerActions from '../../../data/action/explorerActions';
import * as NavBarActions from '../../../data/action/navBarActions';
import { RootState } from '../../../data/store';
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
import { executeCommand } from '../../../data/action/commandAction';
import { NavBarComponent, NavBarProps } from './navBar';
@ -62,9 +62,8 @@ const mapDispatchToProps = (dispatch): NavBarProps => ({
})
);
},
trackEvent: (name: string, properties?: { [key: string]: any }) => {
CommandServiceImpl.remoteCall(SharedConstants.Commands.Telemetry.TrackEvent, name, properties).catch(_e => void 0);
},
trackEvent: (name: string, properties?: { [key: string]: any }) =>
dispatch(executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, name, properties)),
});
export const NavBar = connect(

Просмотреть файл

@ -32,8 +32,7 @@
//
import { SharedConstants } from '@bfemulator/app-shared';
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { globalHandlers } from './eventHandlers';
@ -44,16 +43,43 @@ const {
} = SharedConstants;
let mockLocalCommandsCalled = [];
jest.mock('../platform/commands/commandServiceImpl', () => ({
CommandServiceImpl: {
call: async (commandName: string, ...args: any[]) => {
mockLocalCommandsCalled.push({ commandName, args: args });
},
},
jest.mock('electron', () => ({
ipcMain: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
ipcRenderer: new Proxy(
{},
{
get(): any {
return () => ({});
},
has() {
return true;
},
}
),
}));
describe('#globalHandlers', () => {
let commandService: CommandServiceImpl;
beforeAll(() => {
const decorator = CommandServiceInstance();
const descriptor = decorator({ descriptor: {} }, 'none') as any;
commandService = descriptor.descriptor.get();
commandService.call = (commandName: string, ...args: any[]) => {
mockLocalCommandsCalled.push({ commandName, args: args });
return true as any;
};
});
beforeEach(() => {
mockLocalCommandsCalled = [];
});
@ -102,7 +128,7 @@ describe('#globalHandlers', () => {
it('should send a notification if a command fails', async () => {
const event = new KeyboardEvent('keydown', { ctrlKey: true, key: 'N' });
const spy = jest.spyOn(CommandServiceImpl, 'call').mockRejectedValueOnce('oh noes!');
const spy = jest.spyOn(commandService, 'call').mockRejectedValueOnce('oh noes!');
await globalHandlers(event);
expect(spy).toHaveBeenLastCalledWith('notification:add', {

Просмотреть файл

@ -32,40 +32,46 @@
//
import { Notification, NotificationType, SharedConstants } from '@bfemulator/app-shared';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { CommandServiceImpl } from '../platform/commands/commandServiceImpl';
class EventHandlers {
@CommandServiceInstance()
public static commandService: CommandServiceImpl;
export const globalHandlers: EventListener = async (event: KeyboardEvent): Promise<any> => {
// Meta corresponds to 'Command' on Mac
const ctrlOrCmdPressed = event.ctrlKey || event.metaKey;
const key = event.key.toLowerCase();
const {
Commands: {
UI: { ShowBotCreationDialog, ShowOpenBotDialog },
Notifications: { Add },
},
} = SharedConstants;
public static async globalHandles(event: KeyboardEvent): Promise<any> {
// Meta corresponds to 'Command' on Mac
const ctrlOrCmdPressed = event.ctrlKey || event.metaKey;
const key = event.key.toLowerCase();
const {
Commands: {
UI: { ShowBotCreationDialog, ShowOpenBotDialog },
Notifications: { Add },
},
} = SharedConstants;
let awaitable: Promise<any>;
if (ctrlOrCmdPressed && key === 'o') {
awaitable = CommandServiceImpl.call(ShowOpenBotDialog);
}
let awaitable: Promise<any>;
if (ctrlOrCmdPressed && key === 'o') {
awaitable = EventHandlers.commandService.call(ShowOpenBotDialog);
}
if (ctrlOrCmdPressed && key === 'n') {
awaitable = CommandServiceImpl.call(ShowBotCreationDialog);
}
if (ctrlOrCmdPressed && key === 'n') {
awaitable = EventHandlers.commandService.call(ShowBotCreationDialog);
}
if (awaitable) {
// Prevents the char from showing up if an input is focused
event.preventDefault();
event.stopPropagation();
try {
await awaitable;
} catch (e) {
await CommandServiceImpl.call(Add, {
message: '' + e,
type: NotificationType.Error,
} as Notification);
if (awaitable) {
// Prevents the char from showing up if an input is focused
event.preventDefault();
event.stopPropagation();
try {
await awaitable;
} catch (e) {
await EventHandlers.commandService.call(Add, {
message: '' + e,
type: NotificationType.Error,
} as Notification);
}
}
}
};
}
export const globalHandlers = EventHandlers.globalHandles;

Просмотреть файл

@ -1,7 +1,11 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "build/dist"
"outDir": "build/dist",
"lib": ["dom", "es2015.proxy", "es5", "esnext", "es7"],
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "esnext"
},
"include": ["./src"]
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше