diff --git a/.eslintrc.js b/.eslintrc.js index 5a6b0ec2d..22bb7f714 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -184,7 +184,7 @@ module.exports = { }, //disable some rules for certain files overrides: [{ - //these files are getting deleted soon, so ingore the eslint warnings for now + //these files are getting deleted soon, so ignore the eslint warnings for now files: ['src/brsTypes/**/*.ts'], rules: { '@typescript-eslint/no-invalid-this': 'off', diff --git a/.vscode/settings.json b/.vscode/settings.json index b2f602f22..d96421ddb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -46,5 +46,6 @@ "a11y-structure": "ignore", "a11y-mouse-events-have-key-events": "ignore", "a11y-missing-content": "ignore", + "a11y-click-events-have-key-events": "ignore" } -} \ No newline at end of file +} diff --git a/images/icons/inspect-active.svg b/images/icons/inspect-active.svg new file mode 100644 index 000000000..0de8c1f5e --- /dev/null +++ b/images/icons/inspect-active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 99df35ba5..1a7bd8f3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,14 +23,16 @@ "glob": "^7.1.3", "hex-rgb": "^5.0.0", "iconv-lite": "0.4.24", + "jpeg-js": "^0.4.4", "just-throttle": "^4.0.1", "net": "^1.0.2", "node-cache": "^4.2.0", "node-ssdp": "^4.0.0", + "pngjs": "^7.0.0", "pretty-bytes": "^5.6.0", "roku-debug": "^0.18.3", "roku-deploy": "^3.9.3", - "roku-test-automation": "^2.0.0-beta.11", + "roku-test-automation": "^2.0.0-beta.14", "semver": "^7.1.3", "source-map": "^0.7.3", "thenby": "^1.3.4", @@ -86,7 +88,7 @@ }, "engines": { "node": "^12.12.0", - "vscode": "^1.53.0" + "vscode": "^1.57.0" }, "optionalDependencies": { "fsevents": "~2.3.2" @@ -1172,20 +1174,20 @@ } }, "node_modules/@types/express": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", - "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", + "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", + "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.31", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", - "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", + "version": "4.17.33", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", + "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -1212,9 +1214,9 @@ } }, "node_modules/@types/http-proxy": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", - "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", + "version": "1.17.10", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.10.tgz", + "integrity": "sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g==", "dependencies": { "@types/node": "*" } @@ -1295,9 +1297,9 @@ "dev": true }, "node_modules/@types/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", + "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", "dependencies": { "@types/mime": "*", "@types/node": "*" @@ -2969,9 +2971,9 @@ } }, "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "engines": { "node": ">= 0.6" } @@ -4864,29 +4866,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-stack-trace": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/get-stack-trace/-/get-stack-trace-2.1.1.tgz", - "integrity": "sha512-dhqSDD9lHU/6FvIZ9KbXGmVK6IKr9ZskZtNOUvhlCiONlnqatu4FmAeRbxCfJJVuQ0NWfz6dAbibKQg19B7AmQ==", - "dependencies": { - "bluebird": "^3.7.1", - "source-map": "^0.8.0-beta.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/get-stack-trace/node_modules/source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "dependencies": { - "whatwg-url": "^7.0.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -6153,6 +6132,11 @@ "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", "dev": true }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==" + }, "node_modules/js-sdsl": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", @@ -6486,11 +6470,6 @@ "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", "dev": true }, - "node_modules/lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" - }, "node_modules/lodash.upperfirst": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", @@ -7952,6 +7931,14 @@ "node": ">=8" } }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", @@ -8778,14 +8765,13 @@ } }, "node_modules/roku-test-automation": { - "version": "2.0.0-beta.11", - "resolved": "https://registry.npmjs.org/roku-test-automation/-/roku-test-automation-2.0.0-beta.11.tgz", - "integrity": "sha512-YQ+mmqjKr9VTl8viotciGBFt80FT5KzoiT/MZ+pG8fb7OaBuRDlqy91+lmu6qkjblMKjbgJ/2SaURIuh+LgckA==", + "version": "2.0.0-beta.14", + "resolved": "https://registry.npmjs.org/roku-test-automation/-/roku-test-automation-2.0.0-beta.14.tgz", + "integrity": "sha512-q/1nSyArDopEyp3phq3s500n6hNFVvBhPFrAAcMWfCvvIx2pqYM1rcBZ4piNcqZfUX4RENgVHDgi9pPzCrlWjg==", "dependencies": { "@suitest/types": "^4.6.0", "ajv": "^6.12.6", "fs-extra": "^7.0.1", - "get-stack-trace": "^2.1.1", "http-network-proxy": "^1.0.11", "needle": "^2.9.1", "path": "^0.12.7", @@ -9872,14 +9858,6 @@ "node": ">=0.8" } }, - "node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/traverse-chain": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", @@ -10689,11 +10667,6 @@ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.8.tgz", "integrity": "sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ==" }, - "node_modules/webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -10717,16 +10690,6 @@ "node": ">=0.8.0" } }, - "node_modules/whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -11899,20 +11862,20 @@ } }, "@types/express": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", - "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", + "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", "requires": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", + "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "@types/express-serve-static-core": { - "version": "4.17.31", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", - "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", + "version": "4.17.33", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", + "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", "requires": { "@types/node": "*", "@types/qs": "*", @@ -11939,9 +11902,9 @@ } }, "@types/http-proxy": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", - "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", + "version": "1.17.10", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.10.tgz", + "integrity": "sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g==", "requires": { "@types/node": "*" } @@ -12022,9 +11985,9 @@ "dev": true }, "@types/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", + "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", "requires": { "@types/mime": "*", "@types/node": "*" @@ -13274,9 +13237,9 @@ } }, "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" }, "convert-source-map": { "version": "1.9.0", @@ -14723,25 +14686,6 @@ "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==" }, - "get-stack-trace": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/get-stack-trace/-/get-stack-trace-2.1.1.tgz", - "integrity": "sha512-dhqSDD9lHU/6FvIZ9KbXGmVK6IKr9ZskZtNOUvhlCiONlnqatu4FmAeRbxCfJJVuQ0NWfz6dAbibKQg19B7AmQ==", - "requires": { - "bluebird": "^3.7.1", - "source-map": "^0.8.0-beta.0" - }, - "dependencies": { - "source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "requires": { - "whatwg-url": "^7.0.0" - } - } - } - }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -15656,6 +15600,11 @@ } } }, + "jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==" + }, "js-sdsl": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", @@ -15938,11 +15887,6 @@ "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", "dev": true }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" - }, "lodash.upperfirst": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", @@ -17068,6 +17012,11 @@ } } }, + "pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==" + }, "portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", @@ -17696,14 +17645,13 @@ } }, "roku-test-automation": { - "version": "2.0.0-beta.11", - "resolved": "https://registry.npmjs.org/roku-test-automation/-/roku-test-automation-2.0.0-beta.11.tgz", - "integrity": "sha512-YQ+mmqjKr9VTl8viotciGBFt80FT5KzoiT/MZ+pG8fb7OaBuRDlqy91+lmu6qkjblMKjbgJ/2SaURIuh+LgckA==", + "version": "2.0.0-beta.14", + "resolved": "https://registry.npmjs.org/roku-test-automation/-/roku-test-automation-2.0.0-beta.14.tgz", + "integrity": "sha512-q/1nSyArDopEyp3phq3s500n6hNFVvBhPFrAAcMWfCvvIx2pqYM1rcBZ4piNcqZfUX4RENgVHDgi9pPzCrlWjg==", "requires": { "@suitest/types": "^4.6.0", "ajv": "^6.12.6", "fs-extra": "^7.0.1", - "get-stack-trace": "^2.1.1", "http-network-proxy": "^1.0.11", "needle": "^2.9.1", "path": "^0.12.7", @@ -18547,14 +18495,6 @@ "punycode": "^2.1.1" } }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", - "requires": { - "punycode": "^2.1.0" - } - }, "traverse-chain": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", @@ -19181,11 +19121,6 @@ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.8.tgz", "integrity": "sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ==" }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, "websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -19203,16 +19138,6 @@ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true }, - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index e02f812b3..5292743d0 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "multi-root-ready" ], "engines": { - "vscode": "^1.53.0", + "vscode": "^1.57.0", "node": "^12.12.0" }, "repository": { @@ -64,14 +64,16 @@ "glob": "^7.1.3", "hex-rgb": "^5.0.0", "iconv-lite": "0.4.24", + "jpeg-js": "^0.4.4", "just-throttle": "^4.0.1", "net": "^1.0.2", "node-cache": "^4.2.0", "node-ssdp": "^4.0.0", + "pngjs": "^7.0.0", "pretty-bytes": "^5.6.0", "roku-debug": "^0.18.3", "roku-deploy": "^3.9.3", - "roku-test-automation": "^2.0.0-beta.11", + "roku-test-automation": "^2.0.0-beta.14", "semver": "^7.1.3", "source-map": "^0.7.3", "thenby": "^1.3.4", @@ -197,6 +199,12 @@ "name": "SceneGraph Inspector", "type": "webview" }, + { + "id": "rokuDeviceView", + "contextualTitle": "Roku", + "name": "Device View", + "type": "webview" + }, { "id": "rokuRegistryView", "name": "Roku Registry", @@ -257,6 +265,31 @@ "command": "extension.brightscript.rokuRegistry.clearRegistry", "when": "view == rokuRegistryView", "group": "navigation" + }, + { + "command": "extension.brightscript.rokuDeviceView.refreshScreenshot", + "when": "view == rokuDeviceView && !brightscript.rokuDeviceView.enableScreenshotCapture", + "group": "navigation@1" + }, + { + "command": "extension.brightscript.rokuDeviceView.resumeScreenshotCapture", + "when": "view == rokuDeviceView && !brightscript.rokuDeviceView.enableScreenshotCapture && !brightscript.rokuDeviceView.isInspectingNodes", + "group": "navigation@2" + }, + { + "command": "extension.brightscript.rokuDeviceView.pauseScreenshotCapture", + "when": "view == rokuDeviceView && brightscript.rokuDeviceView.enableScreenshotCapture && !brightscript.rokuDeviceView.isInspectingNodes", + "group": "navigation@2" + }, + { + "command": "extension.brightscript.rokuDeviceView.enableNodeInspector", + "when": "view == rokuDeviceView && brightscript.isOnDeviceComponentAvailable && !brightscript.rokuDeviceView.isInspectingNodes", + "group": "navigation@3" + }, + { + "command": "extension.brightscript.rokuDeviceView.disableNodeInspector", + "when": "view == rokuDeviceView && brightscript.isOnDeviceComponentAvailable && brightscript.rokuDeviceView.isInspectingNodes", + "group": "navigation@3" } ], "commandPalette": [] @@ -2264,6 +2297,11 @@ "title": "Press the PowerOff button on the Roku remote", "category": "Brightscript" }, + { + "command": "extension.brightscript.pressPowerOn", + "title": "Press the PowerOn button on the Roku remote", + "category": "Brightscript" + }, { "command": "extension.brightscript.pressChannelUp", "title": "Press the ChannelUp button on the Roku remote", @@ -2382,6 +2420,42 @@ "command": "extension.brightscript.languageServer.info", "title": "View BrighterScript LanguageServer Info", "category": "BrighterScript" + }, + { + "command": "extension.brightscript.rokuDeviceView.enableNodeInspector", + "title": "Inspect nodes", + "category": "BrighterScript", + "icon": "$(inspect)" + }, + { + "command": "extension.brightscript.rokuDeviceView.disableNodeInspector", + "title": "Stop inspecting nodes", + "category": "BrighterScript", + "icon": "./images/icons/inspect-active.svg" + }, + { + "command": "extension.brightscript.rokuDeviceView.pauseScreenshotCapture", + "title": "Pause Screenshot Capture", + "category": "BrighterScript", + "icon": "$(debug-pause)" + }, + { + "command": "extension.brightscript.rokuDeviceView.resumeScreenshotCapture", + "title": "Resume Screenshot Capture", + "category": "BrighterScript", + "icon": "$(debug-start)" + }, + { + "command": "extension.brightscript.rokuDeviceView.refreshScreenshot", + "title": "Refresh Screenshot", + "category": "BrighterScript", + "icon": "$(refresh)" + }, + { + "command": "extension.brightscript.sceneGraphInspectorView.refreshNodeTree", + "title": "Refresh Nodetree", + "category": "BrighterScript", + "icon": "$(refresh)" } ], "keybindings": [ diff --git a/src/BrightScriptCommands.ts b/src/BrightScriptCommands.ts index a27c7adcd..b92f00b7d 100644 --- a/src/BrightScriptCommands.ts +++ b/src/BrightScriptCommands.ts @@ -169,6 +169,10 @@ export class BrightScriptCommands { await this.sendRemoteCommand('PowerOff'); }); + this.registerCommand('pressPowerOn', async () => { + await this.sendRemoteCommand('PowerOn'); + }); + this.registerCommand('pressChannelUp', async () => { await this.sendRemoteCommand('ChannelUp'); }); diff --git a/src/LanguageServerManager.spec.ts b/src/LanguageServerManager.spec.ts index 06225aa03..eec0d7992 100644 --- a/src/LanguageServerManager.spec.ts +++ b/src/LanguageServerManager.spec.ts @@ -175,7 +175,7 @@ describe('extension', () => { ).to.eql(embeddedPath); }); - it('returns value from worksapce when specified', async () => { + it('returns value from workspace when specified', async () => { vscode.workspace.workspaceFile = URI.file(s`${tempDir}/workspace.code-workspace`); setConfig(vscode.workspace.workspaceFile.fsPath, { diff --git a/src/commands/VscodeCommand.ts b/src/commands/VscodeCommand.ts new file mode 100644 index 000000000..a93713927 --- /dev/null +++ b/src/commands/VscodeCommand.ts @@ -0,0 +1,11 @@ +export enum VscodeCommand { + rokuDeviceViewRefreshScreenshot = 'extension.brightscript.rokuDeviceView.refreshScreenshot', + rokuDeviceViewResumeScreenshotCapture = 'extension.brightscript.rokuDeviceView.resumeScreenshotCapture', + rokuDeviceViewPauseScreenshotCapture = 'extension.brightscript.rokuDeviceView.pauseScreenshotCapture', + rokuDeviceViewEnableNodeInspector = 'extension.brightscript.rokuDeviceView.enableNodeInspector', + rokuDeviceViewDisableNodeInspector = 'extension.brightscript.rokuDeviceView.disableNodeInspector', + rokuRegistryExportRegistry = 'extension.brightscript.rokuRegistry.exportRegistry', + rokuRegistryImportRegistry = 'extension.brightscript.rokuRegistry.importRegistry', + rokuRegistryClearRegistry = 'extension.brightscript.rokuRegistry.clearRegistry', + rokuRegistryRefreshRegistry = 'extension.brightscript.rokuRegistry.refreshRegistry' +} diff --git a/src/extension.spec.ts b/src/extension.spec.ts index 00c940a7e..830323e71 100644 --- a/src/extension.spec.ts +++ b/src/extension.spec.ts @@ -5,8 +5,6 @@ import * as extension from './extension'; import { vscode, vscodeLanguageClient } from './mockVscode.spec'; import { BrightScriptCommands } from './BrightScriptCommands'; import { languageServerManager } from './LanguageServerManager'; -import * as rta from 'roku-test-automation'; -import type { BrightScriptLaunchConfiguration } from './DebugConfigurationProvider'; const sinon = createSandbox(); @@ -49,7 +47,6 @@ describe('extension', () => { }); afterEach(() => { - extensionInstance.odc = undefined; extensionInstance['webviews'] = originalWebviews; sinon.restore(); }); @@ -101,62 +98,4 @@ describe('extension', () => { await extension.activate(context); expect(spy.getCalls().length).to.be.greaterThan(0); }); - - describe('RDB', () => { - const config = {} as BrightScriptLaunchConfiguration; - let context; - beforeEach(() => { - context = { ...vscode.context }; - config.host = '86.75.30.9'; - config.password = 'jenny'; - }); - - describe('setupODC', () => { - it('sets up the underlying RokuDevice instance', () => { - const odc = extensionInstance['setupODC'](config); - expect(odc.device).to.be.instanceOf(rta.RokuDevice); - }); - - it('has the correct config values passed from the extension', () => { - const odc = extensionInstance['setupODC'](config); - const deviceConfig = odc.device.getCurrentDeviceConfig(); - expect(deviceConfig.host).to.equal(config.host); - expect(deviceConfig.password).to.equal(config.password); - }); - }); - - describe('registerWebViewProviders', () => { - it('initializes webview providers and calls registerWebviewViewProvider for each', () => { - extensionInstance['webviews'] = originalWebviews; - const spy = sinon.spy(vscode.window, 'registerWebviewViewProvider'); - extensionInstance['registerWebviewProviders'](context); - expect(spy.callCount).to.equal(Object.keys(originalWebviews).length); - }); - }); - - describe('debugSessionCustomEventHandler', () => { - describe('ChannelPublishedEvent', () => { - const e = { - event: 'ChannelPublishedEvent', - body: { - launchConfiguration: config - } - }; - - it('calls setupODC to create the odc instance if enabled', async () => { - config.injectRdbOnDeviceComponent = true; - const spy = sinon.stub(extensionInstance as any, 'setupODC').returns(undefined); - await extensionInstance['debugSessionCustomEventHandler'](e, {} as any, {} as any, {} as any, {} as any); - expect(spy.calledOnce).to.be.true; - }); - - it('does not call setupODC if not enabled', async () => { - config.injectRdbOnDeviceComponent = false; - const spy = sinon.stub(extensionInstance as any, 'setupODC').returns(undefined); - await extensionInstance['debugSessionCustomEventHandler'](e, {} as any, {} as any, {} as any, {} as any); - expect(spy.callCount).to.equal(0); - }); - }); - }); - }); }); diff --git a/src/extension.ts b/src/extension.ts index 293e93253..2858fd3e8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,7 +1,6 @@ import * as vscode from 'vscode'; import * as prettyBytes from 'pretty-bytes'; import { extensions } from 'vscode'; -import * as rta from 'roku-test-automation'; import * as path from 'path'; import * as fsExtra from 'fs-extra'; import { util } from './util'; @@ -23,10 +22,10 @@ import { languageServerManager } from './LanguageServerManager'; import { TelemetryManager } from './managers/TelemetryManager'; import { RemoteControlManager } from './managers/RemoteControlManager'; import { WhatsNewManager } from './managers/WhatsNewManager'; -import { SceneGraphInspectorViewProvider } from './viewProviders/SceneGraphInspectorViewProvider'; -import { RokuCommandsViewProvider } from './viewProviders/RokuCommandsViewProvider'; -import { RokuRegistryViewProvider } from './viewProviders/RokuRegistryViewProvider'; import { isChannelPublishedEvent, isChanperfEvent, isDiagnosticsEvent, isDebugServerLogOutputEvent, isLaunchStartEvent, isRendezvousEvent } from 'roku-debug'; +import { RtaManager } from './managers/RtaManager'; +import { WebviewViewProviderManager } from './managers/WebviewViewProviderManager'; +import { ViewProviderId } from './viewProviders/ViewProviderId'; const EXTENSION_ID = 'RokuCommunity.brightscript'; @@ -43,19 +42,8 @@ export class Extension { private telemetryManager: TelemetryManager; private remoteControlManager: RemoteControlManager; private brightScriptCommands: BrightScriptCommands; - - public odc?: rta.OnDeviceComponent; - - private webviews = [{ - constructor: SceneGraphInspectorViewProvider, - provider: undefined as SceneGraphInspectorViewProvider - }, { - constructor: RokuRegistryViewProvider, - provider: undefined as RokuRegistryViewProvider - }, { - constructor: RokuCommandsViewProvider, - provider: undefined as RokuCommandsViewProvider - }]; + private rtaManager: RtaManager; + private webviewViewProviderManager: WebviewViewProviderManager; public async activate(context: vscode.ExtensionContext) { const currentExtensionVersion = extensions.getExtension(EXTENSION_ID)?.packageJSON.version as string; @@ -83,6 +71,12 @@ export class Extension { activeDeviceManager ); + context.subscriptions.push( + this.rtaManager = new RtaManager(), + this.webviewViewProviderManager = new WebviewViewProviderManager(context, this.rtaManager) + ); + this.rtaManager.setWebviewViewProviderManager(this.webviewViewProviderManager); + //update the tracked version of the extension this.globalStateManager.lastRunExtensionVersion = currentExtensionVersion; @@ -106,14 +100,11 @@ export class Extension { //register a tree data provider for this extension's "RENDEZVOUS" view in the debug area let rendezvousViewProvider = new RendezvousViewProvider(context); - vscode.window.registerTreeDataProvider('rendezvousView', rendezvousViewProvider); + vscode.window.registerTreeDataProvider(ViewProviderId.rendezvousView, rendezvousViewProvider); //register a tree data provider for this extension's "Online Devices" view let onlineDevicesViewProvider = new OnlineDevicesViewProvider(context, activeDeviceManager); - vscode.window.registerTreeDataProvider('onlineDevicesView', onlineDevicesViewProvider); - - // register our webview providers - this.registerWebviewProviders(context); + vscode.window.registerTreeDataProvider(ViewProviderId.onlineDevicesView, onlineDevicesViewProvider); context.subscriptions.push(vscode.commands.registerCommand('extension.brightscript.rendezvous.clearHistory', async () => { try { @@ -223,19 +214,9 @@ export class Extension { const config = e.body as BrightScriptLaunchConfiguration; await docLinkProvider.setLaunchConfig(config); logOutputManager.setLaunchConfig(config); - } else if (isChannelPublishedEvent(e)) { - const config = e.body.launchConfiguration as BrightScriptLaunchConfiguration; - if (!config.injectRdbOnDeviceComponent) { - void this.odc?.shutdown(); - this.odc = undefined; - } else { - this.odc = this.setupODC(config); - } - this.setupRdbViewProviders(); - void this.odc?.disableScreenSaver({ disableScreensaver: config.disableScreenSaver }); + this.webviewViewProviderManager.onChannelPublishedEvent(e); //write debug server log statements to the DebugServer output channel - } else if (isDebugServerLogOutputEvent(e)) { this.extensionOutputChannel.appendLine(e.body.line); @@ -280,49 +261,6 @@ export class Extension { fsExtra.appendFileSync(extensionLogfilePath, text); } } - - private setupODC(config: BrightScriptLaunchConfiguration) { - const rtaConfig = this.getRtaConfig(config); - rta.odc.setConfig(rtaConfig); - return rta.odc; - } - - private getRtaConfig(config: BrightScriptLaunchConfiguration) { - const enableDebugging = ['info', 'debug', 'trace'].includes(config.logLevel); - const rtaConfig: rta.ConfigOptions = { - RokuDevice: { - devices: [{ - host: config.host, - password: config.password - }] - }, - OnDeviceComponent: { - logLevel: enableDebugging ? 'verbose' : undefined, - clientDebugLogging: enableDebugging, - disableTelnet: true, - disableCallOriginationLine: true - } - }; - return rtaConfig; - } - - private registerWebviewProviders(context) { - for (const webview of this.webviews) { - if (!webview.provider) { - webview.provider = new webview.constructor(context); - vscode.window.registerWebviewViewProvider(webview.provider.id, webview.provider); - } - } - } - - private setupRdbViewProviders() { - for (const webview of this.webviews) { - if (typeof webview.provider.setOnDeviceComponent === 'function') { - webview.provider.setOnDeviceComponent(this.odc); - } - } - } - } export const extension = new Extension(); export async function activate(context: vscode.ExtensionContext) { diff --git a/src/managers/RtaManager.ts b/src/managers/RtaManager.ts new file mode 100644 index 000000000..66fd06367 --- /dev/null +++ b/src/managers/RtaManager.ts @@ -0,0 +1,102 @@ +import * as rta from 'roku-test-automation'; +import { ViewProviderEvent } from '../viewProviders/ViewProviderEvent'; +import { ViewProviderId } from '../viewProviders/ViewProviderId'; +import { vscodeContextManager } from './VscodeContextManager'; +import type { WebviewViewProviderManager } from './WebviewViewProviderManager'; + +export class RtaManager { + public onDeviceComponent?: rta.OnDeviceComponent; + public device?: rta.RokuDevice; + + private webviewViewProviderManager?: WebviewViewProviderManager; + private lastStoreNodesResponse: Awaited>; + + public setupRtaWithConfig(config: { host: string; password: string; logLevel?: string; disableScreenSaver?: boolean; injectRdbOnDeviceComponent?: boolean }) { + const enableDebugging = ['info', 'debug', 'trace'].includes(config.logLevel); + rta.odc.setConfig({ + RokuDevice: { + devices: [{ + host: config.host, + password: config.password + }] + }, + OnDeviceComponent: { + logLevel: enableDebugging ? 'verbose' : undefined, + clientDebugLogging: enableDebugging, + disableTelnet: true, + disableCallOriginationLine: true + } + }); + + this.device = rta.device; + + if (config.injectRdbOnDeviceComponent) { + this.onDeviceComponent = rta.odc; + } else { + void this.onDeviceComponent?.shutdown(); + this.onDeviceComponent = undefined; + } + void vscodeContextManager.set('brightscript.isOnDeviceComponentAvailable', !!this.onDeviceComponent); + + for (const webviewProvider of this.webviewViewProviderManager.getWebviewViewProviders()) { + if (typeof webviewProvider.updateDeviceAvailability === 'function') { + webviewProvider.updateDeviceAvailability(); + } + } + + if (config.disableScreenSaver) { + void this.onDeviceComponent?.disableScreenSaver({ disableScreensaver: true }); + } + } + + public async sendOdcRequest(requestorId: string, command: string, context: { args: any; options: any }) { + const { args, options } = context; + + if (command === rta.RequestType.findNodesAtLocation) { + if (!this.lastStoreNodesResponse) { + args.includeBoundingRectInfo = true; + await this.sendOdcRequest(requestorId, rta.RequestType.storeNodeReferences, args); + } + context.args.nodeTreeResponse = this.lastStoreNodesResponse; + let { matches } = await rta.odc.findNodesAtLocation(args, options); + if (requestorId === ViewProviderId.rokuDeviceView) { + if (matches.length) { + const match = { ...matches[0] }; + // Remove children as this is where most of the payload is and we don't need this info + match.children = []; + matches = [match]; + } + } + return { + matches: matches + }; + } else if (command === rta.RequestType.storeNodeReferences) { + this.lastStoreNodesResponse = await rta.odc.storeNodeReferences(args, options); + + const viewIds = []; + if (requestorId === ViewProviderId.rokuDeviceView) { + viewIds.push(ViewProviderId.sceneGraphInspectorView); + } else if (requestorId === ViewProviderId.sceneGraphInspectorView) { + viewIds.push(ViewProviderId.rokuDeviceView); + } + this.webviewViewProviderManager.sendMessageToWebviews(viewIds, { + event: ViewProviderEvent.onStoredNodeReferencesUpdated + }); + return this.lastStoreNodesResponse; + } else { + return this.onDeviceComponent[command](args, options); + } + } + + public setWebviewViewProviderManager(manager: WebviewViewProviderManager) { + this.webviewViewProviderManager = manager; + } + + public getStoredNodeReferences() { + return this.lastStoreNodesResponse; + } + + public dispose() { + void this.onDeviceComponent?.shutdown?.(); + } +} diff --git a/src/managers/WebviewViewProviderManager.spec.ts b/src/managers/WebviewViewProviderManager.spec.ts new file mode 100644 index 000000000..907bc1439 --- /dev/null +++ b/src/managers/WebviewViewProviderManager.spec.ts @@ -0,0 +1,94 @@ +import { expect } from 'chai'; +import { createSandbox } from 'sinon'; +import { vscode } from '../mockVscode.spec'; +import type { BrightScriptLaunchConfiguration } from '../DebugConfigurationProvider'; +import { WebviewViewProviderManager } from './WebviewViewProviderManager'; +import { RtaManager } from './RtaManager'; + + +const sinon = createSandbox(); + +describe('WebviewViewProviderManager', () => { + let context: any; + + const config = {} as BrightScriptLaunchConfiguration; + let webviewViewProviderManager: WebviewViewProviderManager; + let rtaManager: RtaManager; + + before(() => { + context = { + ...vscode.context, + extensionPath: '', + subscriptions: [], + asAbsolutePath: () => { }, + globalState: { + get: () => { + + }, + update: () => { + + } + } + }; + + config.host = '86.75.30.9'; + config.password = 'jenny'; + + }); + + afterEach(() => { + sinon.restore(); + }); + + + describe('constructor', () => { + let spy; + before(() => { + spy = sinon.spy(vscode.window, 'registerWebviewViewProvider'); + rtaManager = new RtaManager(); + webviewViewProviderManager = new WebviewViewProviderManager(context, rtaManager); + }); + + it('initializes webview providers and calls registerWebviewViewProvider for each', () => { + expect(spy.callCount).to.equal(webviewViewProviderManager.getWebviewViewProviders().length); + }); + + it('assigns RtaManager to each webviewViewProvider', () => { + for (const webviewViewProvider of webviewViewProviderManager.getWebviewViewProviders()) { + expect(webviewViewProvider['rtaManager']).to.equal(rtaManager); + } + expect(spy.callCount).to.equal(webviewViewProviderManager.getWebviewViewProviders().length); + }); + }); + + + describe('onChannelPublishedEvent', () => { + let event: any; + + before(() => { + event = { + event: 'ChannelPublishedEvent', + body: { + launchConfiguration: config + } + }; + + rtaManager = new RtaManager(); + webviewViewProviderManager = new WebviewViewProviderManager(context, rtaManager); + rtaManager.setWebviewViewProviderManager(webviewViewProviderManager); + }); + + it('calls setupRtaWithConfig', () => { + const spy = sinon.stub(rtaManager, 'setupRtaWithConfig').returns(undefined); + webviewViewProviderManager.onChannelPublishedEvent(event); + expect(spy.calledOnce).to.be.true; + }); + + it('has the correct config values passed from the extension', () => { + webviewViewProviderManager.onChannelPublishedEvent(event); + const deviceConfig = rtaManager.device.getCurrentDeviceConfig(); + expect(deviceConfig.host).to.equal(config.host); + expect(deviceConfig.password).to.equal(config.password); + }); + }); +}); diff --git a/src/managers/WebviewViewProviderManager.ts b/src/managers/WebviewViewProviderManager.ts new file mode 100644 index 000000000..d74782ed4 --- /dev/null +++ b/src/managers/WebviewViewProviderManager.ts @@ -0,0 +1,77 @@ +import type { ChannelPublishedEvent } from 'roku-debug'; +import type { BrightScriptLaunchConfiguration } from '../DebugConfigurationProvider'; +import type { RtaManager } from './RtaManager'; +import * as vscode from 'vscode'; +import { RokuCommandsViewProvider } from '../viewProviders/RokuCommandsViewProvider'; +import { RokuDeviceViewViewProvider } from '../viewProviders/RokuDeviceViewViewProvider'; +import { RokuRegistryViewProvider } from '../viewProviders/RokuRegistryViewProvider'; +import { SceneGraphInspectorViewProvider } from '../viewProviders/SceneGraphInspectorViewProvider'; + + +export class WebviewViewProviderManager { + constructor(context: vscode.ExtensionContext, rtaManager: RtaManager) { + this.rtaManager = rtaManager; + + for (const webview of this.webviewViews) { + if (!webview.provider) { + webview.provider = new webview.constructor(context); + vscode.window.registerWebviewViewProvider(webview.provider.id, webview.provider); + + webview.provider.setWebviewViewProviderManager(this); + + if (typeof webview.provider.setRtaManager === 'function') { + webview.provider.setRtaManager(this.rtaManager); + } + } + } + } + + private rtaManager?: RtaManager; + + private webviewViews = [{ + constructor: SceneGraphInspectorViewProvider, + provider: undefined as SceneGraphInspectorViewProvider + }, { + constructor: RokuRegistryViewProvider, + provider: undefined as RokuRegistryViewProvider + }, { + constructor: RokuCommandsViewProvider, + provider: undefined as RokuCommandsViewProvider + }, { + constructor: RokuDeviceViewViewProvider, + provider: undefined as RokuDeviceViewViewProvider + }]; + + public getWebviewViewProviders() { + const providers = []; + for (const webview of this.webviewViews) { + providers.push(webview.provider); + } + return providers; + } + + // Notification from extension + public onChannelPublishedEvent(e: ChannelPublishedEvent) { + const config = e.body.launchConfiguration as BrightScriptLaunchConfiguration; + this.rtaManager.setupRtaWithConfig(config); + } + + // Mainly for communicating between webviews + public sendMessageToWebviews(viewIds: string | string[], message) { + if (typeof viewIds === 'string') { + viewIds = [viewIds]; + } + + for (const webviewView of this.webviewViews) { + if (viewIds.includes(webviewView.provider.id)) { + webviewView.provider.postOrQueueMessage(message); + } + } + } + + public dispose() { + for (const webviewView of this.webviewViews) { + webviewView?.provider?.dispose(); + } + } +} diff --git a/src/util.ts b/src/util.ts index a9954749d..447b8333a 100644 --- a/src/util.ts +++ b/src/util.ts @@ -188,10 +188,6 @@ class Util { this.debounceByKey[key](); } - public isExtensionHostRunning() { - return process.argv.includes('--type=extensionHost'); - } - /** * Wraps a function and calls a callback before calling the original function */ diff --git a/src/viewProviders/BaseRdbViewProvider.ts b/src/viewProviders/BaseRdbViewProvider.ts index 7cab0c756..89fb8921a 100644 --- a/src/viewProviders/BaseRdbViewProvider.ts +++ b/src/viewProviders/BaseRdbViewProvider.ts @@ -1,72 +1,145 @@ import * as rta from 'roku-test-automation'; - +import type * as vscode from 'vscode'; +import type { RequestType } from 'roku-test-automation'; +import * as fsExtra from 'fs-extra'; +import * as path from 'path'; +import * as childProcess from 'child_process'; import { BaseWebviewViewProvider } from './BaseWebviewViewProvider'; +import type { RtaManager } from '../managers/RtaManager'; +import { ViewProviderEvent } from './ViewProviderEvent'; +import { ViewProviderCommand } from './ViewProviderCommand'; +import { standardizePath as s } from 'brighterscript'; +import { PNG } from 'pngjs'; export abstract class BaseRdbViewProvider extends BaseWebviewViewProvider { - protected onDeviceComponent?: rta.OnDeviceComponent; + protected rtaManager?: RtaManager; + + protected odcCommands: Array; - protected odcCommands: Array = [ - 'callFunc', - 'deleteEntireRegistry', - 'deleteRegistrySections', - 'getFocusedNode', - 'getValue', - 'getValues', - 'getNodesInfo', - 'hasFocus', - 'isInFocusChain', - 'onFieldChangeOnce', - 'readRegistry', - 'setValue', - 'writeRegistry', - 'storeNodeReferences', - 'deleteNodeReferences' - ]; + constructor(context: vscode.ExtensionContext) { + super(context); + const requestTypesPath = path.join(rta.utils.getClientFilesPath(), 'requestTypes.schema.json'); + const json = JSON.parse(fsExtra.readFileSync(requestTypesPath, 'utf8')); + this.odcCommands = json.enum; + } - // @param odc - The OnDeviceComponent class instance. If undefined existing instance will be removed. Used to notify webview of change in ODC status - public setOnDeviceComponent(onDeviceComponent?: rta.OnDeviceComponent) { - this.onDeviceComponent = onDeviceComponent; + public setRtaManager(rtaManager?: RtaManager) { + this.rtaManager = rtaManager; + } + public updateDeviceAvailability() { this.postOrQueueMessage({ - name: 'onDeviceComponentStatus', - available: onDeviceComponent ? true : false + event: ViewProviderEvent.onDeviceAvailabilityChange, + odcAvailable: !!this.rtaManager.onDeviceComponent, + deviceAvailable: !!this.rtaManager.device }); } protected onViewReady() { - // Always post back the ODC status so we make sure the client doesn't miss it if it got refreshed - this.setOnDeviceComponent(this.onDeviceComponent); + // Always post back the device status so we make sure the client doesn't miss it if it got refreshed + this.updateDeviceAvailability(); + } + + private isFfmpegScreenshotEnabled = true; + + private ffmpegProcess: childProcess.ChildProcessWithoutNullStreams; + + private latestScreenshotBuffer = Buffer.alloc(0); + + private async getScreenshot(): Promise { + if (this.isFfmpegScreenshotEnabled) { + const imagesDir = s`${__dirname}/../../.tmp/__ffmpegImages`; + if (!this.ffmpegProcess) { + fsExtra.emptyDirSync(imagesDir); + //have ffmpeg generate screenshots at regular interval: + this.ffmpegProcess = childProcess.spawn('ffmpeg', [ + '-f', + 'dshow', + '-i', + `video="usb video"`, + '-vf', + 'fps=1', + '-s', + '480x270', + '-c:v', + 'png', + '-f', + 'image2pipe', + '-' + ], { + shell: true, + //image2pipe sends the images over channel 1 (stdout) + stdio: ['ignore', 'pipe', 'ignore'] + }); + + let buffer = Buffer.alloc(0); + this.ffmpegProcess.stdout.on('data', (data) => { + buffer = Buffer.concat([buffer, data]); + try { + const png = new PNG(buffer); + this.latestScreenshotBuffer = png.imgData; + buffer = buffer.slice(png.imgData.length); + } catch (e) { + } + }); + } + return this.latestScreenshotBuffer.buffer; + } else { + return ( + await this.rtaManager.device.getScreenshot() + ).buffer.buffer; + } } protected async handleViewMessage(message) { const { command, context } = message; if (this.odcCommands.includes(command)) { - const response = await this.onDeviceComponent[command](context.args, context.options); - this.postMessage({ + const response = await this.rtaManager.sendOdcRequest(this.id, command, context); + this.postOrQueueMessage({ ...message, response: response }); return true; - } else if (command === 'setManualIpAddress') { - const onDeviceComponent = rta.odc; - - const rtaConfig: rta.ConfigOptions = { - RokuDevice: { - devices: [{ - host: context.ipAddress, - password: '' - }] - }, - OnDeviceComponent: { - disableTelnet: true, - disableCallOriginationLine: true - } - }; + } else if (command === ViewProviderCommand.getStoredNodeReferences) { + const response = this.rtaManager.getStoredNodeReferences(); + this.postOrQueueMessage({ + ...message, + response: response + }); - onDeviceComponent.setConfig(rtaConfig); - this.setOnDeviceComponent(onDeviceComponent); + return true; + } else if (command === ViewProviderCommand.setManualIpAddress) { + this.rtaManager.setupRtaWithConfig({ + ...message.context, + injectRdbOnDeviceComponent: true + }); + return true; + } else if (command === ViewProviderCommand.getScreenshot) { + try { + const buffer = await this.getScreenshot(); + this.postOrQueueMessage({ + ...message, + response: { + success: true, + arrayBuffer: buffer + } + }); + } catch (e) { + this.postOrQueueMessage({ + ...message, + response: { + success: false + } + }); + } + return true; } return false; } + + public dispose(): void { + super.dispose(); + this.ffmpegProcess.kill(); + } } diff --git a/src/viewProviders/BaseWebviewViewProvider.ts b/src/viewProviders/BaseWebviewViewProvider.ts index 61a9e83b2..18358644a 100644 --- a/src/viewProviders/BaseWebviewViewProvider.ts +++ b/src/viewProviders/BaseWebviewViewProvider.ts @@ -2,8 +2,11 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as fsExtra from 'fs-extra'; import type { AsyncSubscription, Event } from '@parcel/watcher'; - +import { vscodeContextManager } from '../managers/VscodeContextManager'; import { util } from '../util'; +import type { WebviewViewProviderManager } from '../managers/WebviewViewProviderManager'; +import { ViewProviderEvent } from './ViewProviderEvent'; +import { ViewProviderCommand } from './ViewProviderCommand'; export abstract class BaseWebviewViewProvider implements vscode.WebviewViewProvider, vscode.Disposable { constructor(context: vscode.ExtensionContext) { @@ -22,11 +25,24 @@ export abstract class BaseWebviewViewProvider implements vscode.WebviewViewProvi private outDirWatcher: AsyncSubscription; private viewReady = false; private queuedMessages = []; + private webviewViewProviderManager: WebviewViewProviderManager; public dispose() { void this.outDirWatcher?.unsubscribe(); } + public setWebviewViewProviderManager(manager: WebviewViewProviderManager) { + this.webviewViewProviderManager = manager; + } + + public postOrQueueMessage(message) { + if (this.viewReady) { + this.postMessage(message); + } else { + this.queuedMessages.push(message); + } + } + protected postMessage(message) { this.view?.webview.postMessage(message).then(null, (reason) => { console.log('postMessage failed: ', reason); @@ -39,30 +55,31 @@ export abstract class BaseWebviewViewProvider implements vscode.WebviewViewProvi } } - protected postOrQueueMessage(message) { - if (this.viewReady) { - this.postMessage(message); - } else { - this.queuedMessages.push(message); - } - } - private setupViewMessageObserver(webview: vscode.Webview) { webview.onDidReceiveMessage(async (message) => { try { const command = message.command; - if (command === 'viewReady') { + if (command === ViewProviderCommand.viewReady) { this.viewReady = true; this.onViewReady(); this.postQueuedMessages(); + } else if (command === ViewProviderCommand.setVscodeContext) { + await vscodeContextManager.set(message.key, message.value); + } else if (command === ViewProviderCommand.sendMessageToWebviews) { + this.webviewViewProviderManager.sendMessageToWebviews(message.viewIds, message.message); } else { - await this.handleViewMessage(message); + if (!await this.handleViewMessage(message)) { + console.warn('Did not handle message', message); + } } } catch (e) { this.postMessage({ ...message, - error: e.message + error: { + message: e.message, + stack: e.stack + } }); } }); @@ -72,6 +89,15 @@ export abstract class BaseWebviewViewProvider implements vscode.WebviewViewProvi return false; } + protected registerCommandWithWebViewNotifier(context: vscode.ExtensionContext, command: string) { + context.subscriptions.push(vscode.commands.registerCommand(command, () => { + this.postOrQueueMessage({ + event: ViewProviderEvent.onVscodeCommandReceived, + commandName: command + }); + })); + } + protected onViewReady() { } /** Adds ability to add additional script contents to the main webview html */ @@ -81,11 +107,18 @@ export abstract class BaseWebviewViewProvider implements vscode.WebviewViewProvi protected async getHtmlForWebview() { try { - if (util.isExtensionHostRunning() && !this.outDirWatcher) { + let watcher; + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + watcher = require('@parcel/watcher'); + } catch (e) { + // Doing nothing if watcher does not exist + } + + if (watcher && !this.outDirWatcher) { // When in dev mode, spin up a watcher to auto-reload the webview whenever the files have changed. - // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires - this.outDirWatcher = await require('@parcel/watcher').subscribe(this.webviewBasePath, (err, events: Event[]) => { - //only refresh when the index.html page is changed. Since vite rewrites the file on every cycle, this is enough to know to reload the page + this.outDirWatcher = await watcher.subscribe(this.webviewBasePath, (err, events: Event[]) => { + //only refresh when the index.html page is changed. Since vite rewrites the file on every build, this is enough to know to reload the page if ( events.find(x => (x.type === 'create' || x.type === 'update') && x.path?.toLowerCase()?.endsWith('index.html')) ) { diff --git a/src/viewProviders/OnlineDevicesViewProvider.ts b/src/viewProviders/OnlineDevicesViewProvider.ts index 8cec8c833..5e1e22b83 100644 --- a/src/viewProviders/OnlineDevicesViewProvider.ts +++ b/src/viewProviders/OnlineDevicesViewProvider.ts @@ -5,10 +5,11 @@ import { icons } from '../icons'; import { firstBy } from 'thenby'; import { Cache } from 'brighterscript/dist/Cache'; import { util } from '../util'; +import { ViewProviderId } from './ViewProviderId'; export class OnlineDevicesViewProvider implements vscode.TreeDataProvider { - public readonly id = 'onlineDevicesView'; + public readonly id = ViewProviderId.onlineDevicesView; constructor( private context: vscode.ExtensionContext, diff --git a/src/viewProviders/RendezvousViewProvider.ts b/src/viewProviders/RendezvousViewProvider.ts index 27ce3df12..485399692 100644 --- a/src/viewProviders/RendezvousViewProvider.ts +++ b/src/viewProviders/RendezvousViewProvider.ts @@ -2,10 +2,11 @@ import * as arraySort from 'array-sort'; import * as vscode from 'vscode'; import type { RendezvousHistory } from 'roku-debug'; import { isRendezvousEvent } from 'roku-debug'; +import { ViewProviderId } from './ViewProviderId'; export class RendezvousViewProvider implements vscode.TreeDataProvider { - public readonly id = 'rendezvousView'; + public readonly id = ViewProviderId.rendezvousView; constructor(context: vscode.ExtensionContext) { this.toggleSmartSorting(); diff --git a/src/viewProviders/RokuCommandsViewProvider.spec.ts b/src/viewProviders/RokuCommandsViewProvider.spec.ts index 2bb01ea66..c8c2f79bf 100644 --- a/src/viewProviders/RokuCommandsViewProvider.spec.ts +++ b/src/viewProviders/RokuCommandsViewProvider.spec.ts @@ -16,6 +16,7 @@ Module.prototype.require = function hijacked(file) { let sinon: sinonImport.SinonSandbox; let view; let callback; +let provider: RokuCommandsViewProvider; beforeEach(() => { sinon = sinonImport.createSandbox(); view = { @@ -27,30 +28,30 @@ beforeEach(() => { }, show: () => { } }; + + provider = new RokuCommandsViewProvider(vscode.context) as any; }); afterEach(() => { + provider.dispose(); sinon.restore(); }); describe('RokuCommandsViewProvider', () => { describe('getHtmlForWebview', () => { - const provider = new RokuCommandsViewProvider(vscode.context) as any; - it('includes the contents of additionalScriptContents', async () => { - const html = await provider.getHtmlForWebview(); - for (const line of provider.additionalScriptContents()) { + const html = await provider['getHtmlForWebview'](); + for (const line of provider['additionalScriptContents']()) { expect(html).to.contain(line); } }); }); describe('resolveWebviewView', () => { - it('sets up observer to handle messages from the ui', () => { - const provider = new RokuCommandsViewProvider(vscode.context) as any; - provider.resolveWebviewView(view, {}, {}); + it('sets up observer to handle messages from the ui', async () => { + await provider['resolveWebviewView'](view, {} as any, {} as any); expect(typeof callback).to.equal('function'); - const spy = sinon.spy(provider, 'handleViewMessage'); + const spy = sinon.spy(provider as any, 'handleViewMessage'); callback({ command: 'importRegistry', context: {} diff --git a/src/viewProviders/RokuCommandsViewProvider.ts b/src/viewProviders/RokuCommandsViewProvider.ts index 66bbf0b3f..a0d4cb45b 100644 --- a/src/viewProviders/RokuCommandsViewProvider.ts +++ b/src/viewProviders/RokuCommandsViewProvider.ts @@ -2,9 +2,10 @@ import * as path from 'path'; import * as rta from 'roku-test-automation'; import * as fs from 'fs'; import { BaseRdbViewProvider } from './BaseRdbViewProvider'; +import { ViewProviderId } from './ViewProviderId'; export class RokuCommandsViewProvider extends BaseRdbViewProvider { - public readonly id = 'rokuCommandsView'; + public readonly id = ViewProviderId.rokuCommandsView; protected additionalScriptContents() { const requestArgsPath = path.join(rta.utils.getClientFilesPath(), 'requestArgs.schema.json'); diff --git a/src/viewProviders/RokuDeviceViewViewProvider.ts b/src/viewProviders/RokuDeviceViewViewProvider.ts new file mode 100644 index 000000000..9cab6128d --- /dev/null +++ b/src/viewProviders/RokuDeviceViewViewProvider.ts @@ -0,0 +1,18 @@ +import type * as vscode from 'vscode'; +import { VscodeCommand } from '../commands/VscodeCommand'; +import { BaseRdbViewProvider } from './BaseRdbViewProvider'; +import { ViewProviderId } from './ViewProviderId'; + +export class RokuDeviceViewViewProvider extends BaseRdbViewProvider { + public readonly id = ViewProviderId.rokuDeviceView; + + constructor(context: vscode.ExtensionContext) { + super(context); + + this.registerCommandWithWebViewNotifier(context, VscodeCommand.rokuDeviceViewEnableNodeInspector); + this.registerCommandWithWebViewNotifier(context, VscodeCommand.rokuDeviceViewDisableNodeInspector); + this.registerCommandWithWebViewNotifier(context, VscodeCommand.rokuDeviceViewRefreshScreenshot); + this.registerCommandWithWebViewNotifier(context, VscodeCommand.rokuDeviceViewPauseScreenshotCapture); + this.registerCommandWithWebViewNotifier(context, VscodeCommand.rokuDeviceViewResumeScreenshotCapture); + } +} diff --git a/src/viewProviders/RokuRegistryViewProvider.spec.ts b/src/viewProviders/RokuRegistryViewProvider.spec.ts index 67691ece3..7f093de89 100644 --- a/src/viewProviders/RokuRegistryViewProvider.spec.ts +++ b/src/viewProviders/RokuRegistryViewProvider.spec.ts @@ -1,5 +1,6 @@ import { expect } from 'chai'; import * as sinonImport from 'sinon'; +import { RtaManager } from '../managers/RtaManager'; import { vscode } from '../mockVscode.spec'; import { RokuRegistryViewProvider } from './RokuRegistryViewProvider'; @@ -35,6 +36,8 @@ afterEach(() => { describe('RokuRegistryViewProvider', () => { const provider = new RokuRegistryViewProvider(vscode.context); + const rtaManager = new RtaManager(); + provider.setRtaManager(rtaManager); describe('sendRegistryUpdated', () => { it('Triggers postOrQueueMessage to send message to web view that the registry was updated', async () => { diff --git a/src/viewProviders/RokuRegistryViewProvider.ts b/src/viewProviders/RokuRegistryViewProvider.ts index 9da9662f1..187b31e3d 100644 --- a/src/viewProviders/RokuRegistryViewProvider.ts +++ b/src/viewProviders/RokuRegistryViewProvider.ts @@ -1,22 +1,25 @@ import * as vscode from 'vscode'; +import { VscodeCommand } from '../commands/VscodeCommand'; import { BaseRdbViewProvider } from './BaseRdbViewProvider'; +import { ViewProviderEvent } from './ViewProviderEvent'; +import { ViewProviderId } from './ViewProviderId'; export class RokuRegistryViewProvider extends BaseRdbViewProvider { - public readonly id = 'rokuRegistryView'; + public readonly id = ViewProviderId.rokuRegistryView; constructor(context: vscode.ExtensionContext) { super(context); const subscriptions = context.subscriptions; - subscriptions.push(vscode.commands.registerCommand('extension.brightscript.rokuRegistry.exportRegistry', async () => { + subscriptions.push(vscode.commands.registerCommand(VscodeCommand.rokuRegistryExportRegistry, async () => { await vscode.window.showSaveDialog({ saveLabel: 'Save' }).then(async (uri) => { - const result = await this.onDeviceComponent?.readRegistry(); + const result = await this.rtaManager.onDeviceComponent?.readRegistry(); await vscode.workspace.fs.writeFile(uri, Buffer.from(JSON.stringify(result?.values), 'utf8')); }); })); - subscriptions.push(vscode.commands.registerCommand('extension.brightscript.rokuRegistry.importRegistry', async () => { + subscriptions.push(vscode.commands.registerCommand(VscodeCommand.rokuRegistryImportRegistry, async () => { const options: vscode.OpenDialogOptions = { canSelectMany: false, openLabel: 'Select', @@ -26,12 +29,12 @@ export class RokuRegistryViewProvider extends BaseRdbViewProvider { await vscode.window.showOpenDialog(options).then(this.importContentsToRegistry.bind(this)); })); - subscriptions.push(vscode.commands.registerCommand('extension.brightscript.rokuRegistry.clearRegistry', async () => { - await this.onDeviceComponent.deleteEntireRegistry(); + subscriptions.push(vscode.commands.registerCommand(VscodeCommand.rokuRegistryClearRegistry, async () => { + await this.rtaManager.onDeviceComponent.deleteEntireRegistry(); await this.sendRegistryUpdated(); })); - subscriptions.push(vscode.commands.registerCommand('extension.brightscript.rokuRegistry.refreshRegistry', async () => { + subscriptions.push(vscode.commands.registerCommand(VscodeCommand.rokuRegistryRefreshRegistry, async () => { await this.sendRegistryUpdated(); })); } @@ -41,7 +44,7 @@ export class RokuRegistryViewProvider extends BaseRdbViewProvider { const input = await vscode.workspace.fs.readFile(uri[0]); const data = JSON.parse(Buffer.from(input).toString('utf8')); - await this.onDeviceComponent?.writeRegistry({ + await this.rtaManager.onDeviceComponent?.writeRegistry({ values: data }); await this.sendRegistryUpdated(); @@ -49,7 +52,7 @@ export class RokuRegistryViewProvider extends BaseRdbViewProvider { } protected async sendRegistryUpdated() { - const result = await this.onDeviceComponent?.readRegistry(); - this.postOrQueueMessage({ name: 'registryUpdated', values: result?.values }); + const result = await this.rtaManager.onDeviceComponent?.readRegistry(); + this.postOrQueueMessage({ name: ViewProviderEvent.onRegistryUpdated, values: result?.values }); } } diff --git a/src/viewProviders/SceneGraphInspectorViewProvider.ts b/src/viewProviders/SceneGraphInspectorViewProvider.ts index 3265b9075..53ef47de6 100644 --- a/src/viewProviders/SceneGraphInspectorViewProvider.ts +++ b/src/viewProviders/SceneGraphInspectorViewProvider.ts @@ -1,5 +1,13 @@ +import type * as vscode from 'vscode'; import { BaseRdbViewProvider } from './BaseRdbViewProvider'; +import { ViewProviderId } from './ViewProviderId'; export class SceneGraphInspectorViewProvider extends BaseRdbViewProvider { - public readonly id = 'sceneGraphInspectorView'; + public readonly id = ViewProviderId.sceneGraphInspectorView; + + constructor(context: vscode.ExtensionContext) { + super(context); + + this.registerCommandWithWebViewNotifier(context, 'extension.brightscript.sceneGraphInspectorView.refreshNodeTree'); + } } diff --git a/src/viewProviders/ViewProviderCommand.ts b/src/viewProviders/ViewProviderCommand.ts new file mode 100644 index 000000000..329d3e08c --- /dev/null +++ b/src/viewProviders/ViewProviderCommand.ts @@ -0,0 +1,8 @@ +export enum ViewProviderCommand { + viewReady = 'viewReady', + getStoredNodeReferences = 'getStoredNodeReferences', + getScreenshot = 'getScreenshot', + setManualIpAddress = 'setManualIpAddress', + sendMessageToWebviews = 'sendMessageToWebviews', + setVscodeContext = 'setVscodeContext' +} diff --git a/src/viewProviders/ViewProviderEvent.ts b/src/viewProviders/ViewProviderEvent.ts new file mode 100644 index 000000000..b761ccedd --- /dev/null +++ b/src/viewProviders/ViewProviderEvent.ts @@ -0,0 +1,7 @@ +export enum ViewProviderEvent { + onTreeNodeFocused = 'onTreeNodeFocused', + onDeviceAvailabilityChange = 'onDeviceAvailabilityChange', + onVscodeCommandReceived = 'onVscodeCommandReceived', + onRegistryUpdated = 'onRegistryUpdated', + onStoredNodeReferencesUpdated = 'onStoredNodeReferencesUpdated' +} diff --git a/src/viewProviders/ViewProviderId.ts b/src/viewProviders/ViewProviderId.ts new file mode 100644 index 000000000..a5d76c393 --- /dev/null +++ b/src/viewProviders/ViewProviderId.ts @@ -0,0 +1,8 @@ +export enum ViewProviderId { + onlineDevicesView = 'onlineDevicesView', + rendezvousView = 'rendezvousView', + sceneGraphInspectorView = 'sceneGraphInspectorView', + rokuDeviceView = 'rokuDeviceView', + rokuRegistryView = 'rokuRegistryView', + rokuCommandsView = 'rokuCommandsView' +} diff --git a/webviews/index.html b/webviews/index.html index 23a7bf10d..e78580350 100644 --- a/webviews/index.html +++ b/webviews/index.html @@ -7,6 +7,7 @@ @@ -15,4 +16,4 @@ - \ No newline at end of file + diff --git a/webviews/src/ChangedFieldEntry.ts b/webviews/src/ChangedFieldEntry.ts index ceccfc00a..6a70a764b 100644 --- a/webviews/src/ChangedFieldEntry.ts +++ b/webviews/src/ChangedFieldEntry.ts @@ -1,10 +1,10 @@ -import type { ODC } from 'roku-test-automation'; +import type { BaseType } from 'roku-test-automation'; interface ChangedFieldEntry { ts: number; // Unix timestamp subtype: string; id: string; - base?: ODC.BaseTypes; + base?: BaseType; keyPath: string; value: any; } diff --git a/webviews/src/ExtensionIntermediary.ts b/webviews/src/ExtensionIntermediary.ts index 872fa2a2e..e097227e3 100644 --- a/webviews/src/ExtensionIntermediary.ts +++ b/webviews/src/ExtensionIntermediary.ts @@ -1,5 +1,10 @@ /** Acts as a middle man that takes request from our views and sends them through vscode message protocol and waits for replies to simplify usage in code */ import type * as rta from 'roku-test-automation'; +import type { VscodeCommand } from '../../src/commands/VscodeCommand'; +import type { ViewProviderEvent } from '../../src/viewProviders/ViewProviderEvent'; +import { ViewProviderCommand } from '../../src/viewProviders/ViewProviderCommand'; +import { RequestType } from 'roku-test-automation/client/dist/types/OnDeviceComponent'; +import type { DeleteEntireRegistrySectionsArgs, DeleteNodeReferencesArgs, DeleteRegistrySectionsArgs, FindNodesAtLocationArgs, GetFocusedNodeArgs, GetNodesInfoArgs, GetNodesWithPropertiesArgs, GetValueArgs, GetValuesArgs, HasFocusArgs, IsInFocusChainArgs, OnFieldChangeOnceArgs, ReadRegistryArgs, RequestOptions, SetValueArgs, StoreNodeReferencesArgs, WriteRegistryArgs } from 'roku-test-automation'; class ExtensionIntermediary { private inflightRequests = {}; @@ -18,23 +23,22 @@ class ExtensionIntermediary { if (request) { delete this.inflightRequests[message.id]; request.callback(message); - } else { - if (message.name) { - const listeners = this.observedEvents.get(message.name); - if (listeners) { - for (const listener of listeners) { - listener(message); - } + } else if (message.event) { + const listeners = this.observedEvents.get(message.event); + if (listeners) { + for (const listener of listeners) { + listener(message); } } } }); } - public async sendMessage(command: string, context = {}) { + public async sendObservableMessage(message: Record) { this.setupExtensionMessageObserver(); const requestId = this.generateRandomString(); + message.id = requestId; return new Promise((resolve, reject) => { const callback = (message) => { @@ -44,37 +48,80 @@ class ExtensionIntermediary { resolve(message.response); } }; - const message = { - id: requestId, - command: command, - context: context - }; this.inflightRequests[requestId] = { message: message, callback: callback }; - window.vscode.postMessage(message); + this.postMessage(message); }); } + public createCommandMessage(command: VscodeCommand | ViewProviderCommand, context = {}) { + const message = { + command: command, + context: context + }; + return message; + } + + public sendCommand(command: VscodeCommand | ViewProviderCommand, context = {}) { + return this.sendObservableMessage(this.createCommandMessage(command, context)); + } + + public createEventMessage(event: ViewProviderEvent, context = {}) { + const message = { + event: event, + context: context + }; + return message; + } + + public sendEvent(event: ViewProviderEvent, context = {}) { + this.postMessage(this.createEventMessage(event, context)); + } + public sendViewReady() { this.setupExtensionMessageObserver(); - window.vscode.postMessage({ - command: 'viewReady', + this.postMessage({ + command: ViewProviderCommand.viewReady, context: {} }); } - public observeEvent(name: string, callback: ObserverCallback) { - let observedEvent = this.observedEvents.get(name); + public setVscodeContext(key: string, value: boolean | number | string) { + this.postMessage({ + command: 'setVscodeContext', + key: key, + value: value + }); + } + + public async getStoredNodeReferences() { + return this.sendCommand>(ViewProviderCommand.getStoredNodeReferences); + } + + public observeEvent(eventName: string, callback: ObserverCallback) { + let observedEvent = this.observedEvents.get(eventName); if (!observedEvent) { observedEvent = []; } observedEvent.push(callback); - this.observedEvents.set(name, observedEvent); + this.observedEvents.set(eventName, observedEvent); + } + + public sendMessageToWebviews(viewIds: string | string[], message) { + this.postMessage({ + command: ViewProviderCommand.sendMessageToWebviews, + viewIds: viewIds, + message: message + }); + } + + private postMessage(message) { + window.vscode.postMessage(message); } private generateRandomString(length = 7) { @@ -87,67 +134,75 @@ class ExtensionIntermediary { const intermediary = new ExtensionIntermediary(); class ODCIntermediary { - public sendOdcMessage(command: string, args?, options?: rta.ODC.RequestOptions) { - return intermediary.sendMessage(command, { + public sendOdcMessage(command: RequestType, args?, options?: RequestOptions) { + return intermediary.sendCommand(command as any, { args: args, options: options }); } - public async readRegistry(args?: rta.ODC.ReadRegistryArgs, options?: rta.ODC.RequestOptions) { - return this.sendOdcMessage>('readRegistry', args, options); + public async readRegistry(args?: ReadRegistryArgs, options?: RequestOptions) { + return this.sendOdcMessage>(RequestType.readRegistry, args, options); + } + + public async writeRegistry(args: WriteRegistryArgs, options?: RequestOptions) { + return this.sendOdcMessage>(RequestType.writeRegistry, args, options); + } + + public async getFocusedNode(args?: GetFocusedNodeArgs, options?: RequestOptions) { + return this.sendOdcMessage>(RequestType.getFocusedNode, args, options); } - public async writeRegistry(args: rta.ODC.WriteRegistryArgs, options?: rta.ODC.RequestOptions) { - return this.sendOdcMessage>('writeRegistry', args, options); + public async getValue(args: GetValueArgs, options?: RequestOptions) { + return this.sendOdcMessage>(RequestType.getValue, args, options); } - public async getFocusedNode(args?: rta.ODC.GetFocusedNodeArgs, options?: rta.ODC.RequestOptions) { - return this.sendOdcMessage>('getFocusedNode', args, options); + public async getValues(args: GetValuesArgs, options?: RequestOptions) { + return this.sendOdcMessage>(RequestType.getValues, args, options); } - public async getValue(args: rta.ODC.GetValueArgs, options?: rta.ODC.RequestOptions) { - return this.sendOdcMessage>('getValue', args, options); + public async getNodesInfo(args: GetNodesInfoArgs, options?: RequestOptions) { + return this.sendOdcMessage>(RequestType.getNodesInfo, args, options); } - public async getValues(args: rta.ODC.GetValuesArgs, options?: rta.ODC.RequestOptions) { - return this.sendOdcMessage>('getValues', args, options); + public async hasFocus(args: HasFocusArgs, options?: RequestOptions) { + return this.sendOdcMessage>(RequestType.hasFocus, args, options); } - public async getNodesInfo(args: rta.ODC.GetNodesInfoArgs, options?: rta.ODC.RequestOptions) { - return this.sendOdcMessage>('getNodesInfo', args, options); + public async isInFocusChain(args: IsInFocusChainArgs, options?: RequestOptions) { + return this.sendOdcMessage>(RequestType.isInFocusChain, args, options); } - public async hasFocus(args: rta.ODC.HasFocusArgs, options?: rta.ODC.RequestOptions) { - return this.sendOdcMessage>('hasFocus', args, options); + public async onFieldChangeOnce(args: OnFieldChangeOnceArgs, options?: RequestOptions) { + return this.sendOdcMessage>(RequestType.onFieldChangeOnce, args, options); } - public async isInFocusChain(args: rta.ODC.IsInFocusChainArgs, options?: rta.ODC.RequestOptions) { - return this.sendOdcMessage>('isInFocusChain', args, options); + public async setValue(args: SetValueArgs, options?: RequestOptions) { + return this.sendOdcMessage>(RequestType.setValue, args, options); } - public async observeField(args: rta.ODC.ObserveFieldArgs, options?: rta.ODC.RequestOptions) { - return this.sendOdcMessage>('observeField', args, options); + public async deleteRegistrySections(args: DeleteRegistrySectionsArgs, options?: RequestOptions) { + return this.sendOdcMessage>(RequestType.deleteRegistrySections, args, options); } - public async setValue(args: rta.ODC.SetValueArgs, options?: rta.ODC.RequestOptions) { - return this.sendOdcMessage>('setValue', args, options); + public async deleteEntireRegistry(args?: DeleteEntireRegistrySectionsArgs, options?: RequestOptions) { + return this.sendOdcMessage>(RequestType.deleteEntireRegistry, args, options); } - public async deleteRegistrySections(args: rta.ODC.DeleteRegistrySectionsArgs, options?: rta.ODC.RequestOptions) { - return this.sendOdcMessage>('deleteRegistrySections', args, options); + public async storeNodeReferences(args?: StoreNodeReferencesArgs, options?: RequestOptions) { + return this.sendOdcMessage>(RequestType.storeNodeReferences, args, options); } - public async deleteEntireRegistry(args?: rta.ODC.DeleteEntireRegistrySectionsArgs, options?: rta.ODC.RequestOptions) { - return this.sendOdcMessage>('deleteEntireRegistry', args, options); + public async deleteNodeReferences(args: DeleteNodeReferencesArgs, options?: RequestOptions) { + return this.sendOdcMessage>(RequestType.deleteNodeReferences, args, options); } - public async storeNodeReferences(args: rta.ODC.StoreNodeReferencesArgs, options?: rta.ODC.RequestOptions) { - return this.sendOdcMessage>('storeNodeReferences', args, options); + public async getNodesWithProperties(args: GetNodesWithPropertiesArgs, options?: RequestOptions) { + return this.sendOdcMessage>(RequestType.getNodesWithProperties, args, options); } - public async deleteNodeReferences(args: rta.ODC.DeleteNodeReferencesArgs, options?: rta.ODC.RequestOptions) { - return this.sendOdcMessage>('deleteNodeReferences', args, options); + public async findNodesAtLocation(args: FindNodesAtLocationArgs, options?: RequestOptions) { + return this.sendOdcMessage>(RequestType.findNodesAtLocation, args, options); } } diff --git a/webviews/src/main.ts b/webviews/src/main.ts index 5f33c7712..9e35a81ff 100644 --- a/webviews/src/main.ts +++ b/webviews/src/main.ts @@ -1,6 +1,7 @@ /* eslint-disable object-shorthand */ import rokuRegistryView from './views/RokuRegistryView/RokuRegistryView.svelte'; import rokuCommandsView from './views/RokuCommandsView/RokuCommandsView.svelte'; +import rokuDeviceView from './views/RokuDeviceView/RokuDeviceView.svelte'; import sceneGraphInspectorView from './views/SceneGraphInspectorView/SceneGraphInspectorView.svelte'; import './style.css'; @@ -15,6 +16,7 @@ declare const viewName; const views = { rokuRegistryView, rokuCommandsView, + rokuDeviceView, sceneGraphInspectorView }; diff --git a/webviews/src/shared/OdcSetManualIpAddress.svelte b/webviews/src/shared/OdcSetManualIpAddress.svelte index f571b6bc1..a67fce166 100644 --- a/webviews/src/shared/OdcSetManualIpAddress.svelte +++ b/webviews/src/shared/OdcSetManualIpAddress.svelte @@ -1,14 +1,18 @@ @@ -20,13 +24,20 @@
- If you have the on device component already running and you would like to use this tool for a Roku device not currently being debugged enter the IP address here: + If you have the on device component already running and you would like to use this tool for a Roku device not currently being debugged enter it here:
+ + bind:value={ipAddress} />
+ + diff --git a/webviews/src/views/RokuCommandsView/RokuCommandsView.svelte b/webviews/src/views/RokuCommandsView/RokuCommandsView.svelte index 1915ffad6..b5338c731 100644 --- a/webviews/src/views/RokuCommandsView/RokuCommandsView.svelte +++ b/webviews/src/views/RokuCommandsView/RokuCommandsView.svelte @@ -5,6 +5,7 @@ import { commandsView } from './RokuCommandsView'; import OdcSetupSteps from '../../shared/OdcSetupSteps.svelte'; import { utils } from '../../utils'; + import { ViewProviderEvent } from '../../../../src/viewProviders/ViewProviderEvent'; let commandArgs: any[]; let selectedCommand; @@ -36,11 +37,10 @@ for (const key in formArgs) { let argValue = formArgs[key]; argValuesForCommand[key] = argValue; - const argType = selectedCommand.args.properties[key].type; - processedArgs[key] = commandsView.processArgToSendToExtension( - argType, - argValue - ); + const argType = selectedCommand.args.properties[key]?.type; + if (argType) { + processedArgs[key] = commandsView.processArgToSendToExtension(argType, argValue); + } } utils.setStorageValue( @@ -55,17 +55,13 @@ ); commandResponse = JSON.stringify(response, null, 2); } catch (error) { - commandResponse = error; + commandResponse = error.message; } } const commandList = []; for (const commandName of odcCommands) { - let argsKey = - 'ODC.' + - commandName.charAt(0).toUpperCase() + - commandName.slice(1) + - 'Args'; + let argsKey = commandName.charAt(0).toUpperCase() + commandName.slice(1) + 'Args'; commandList.push({ name: commandName, args: requestArgsSchema.definitions[argsKey] @@ -91,8 +87,8 @@ let odcAvailable = true; - intermediary.observeEvent('onDeviceComponentStatus', (message) => { - odcAvailable = message.available; + intermediary.observeEvent(ViewProviderEvent.onDeviceAvailabilityChange, (message) => { + odcAvailable = message.odcAvailable; }); // Required by any view so we can know that the view is ready to receive messages @@ -102,6 +98,7 @@ + + +
+ {#if deviceAvailable} +
+ {#if focusedTreeNode} +
+
+ +
+ subtype: {focusedTreeNode.subtype}, + id: {focusedTreeNode.id}
+ x: {focusedTreeNode.sceneRect.x}, + y: {focusedTreeNode.sceneRect.y}, + width: {focusedTreeNode.sceneRect.width}, + height: {focusedTreeNode.sceneRect.height} +
+ {/if} + + +
+ {:else} +
+ +
+ {/if} +
diff --git a/webviews/src/views/RokuRegistryView/RokuRegistryView.spec.ts b/webviews/src/views/RokuRegistryView/RokuRegistryView.spec.ts index 80436fd03..c5ec479e4 100644 --- a/webviews/src/views/RokuRegistryView/RokuRegistryView.spec.ts +++ b/webviews/src/views/RokuRegistryView/RokuRegistryView.spec.ts @@ -3,7 +3,7 @@ import { registryView } from './RokuRegistryView'; describe('RegistryView', () => { describe('formatValues', () => { - it('should convert json into an object with the correct correct values', () => { + it('should convert json into an object with the correct values', () => { const input = { a: '{"b": 1}' }; diff --git a/webviews/src/views/RokuRegistryView/RokuRegistryView.svelte b/webviews/src/views/RokuRegistryView/RokuRegistryView.svelte index 357b7a5a2..2853c811f 100644 --- a/webviews/src/views/RokuRegistryView/RokuRegistryView.svelte +++ b/webviews/src/views/RokuRegistryView/RokuRegistryView.svelte @@ -5,24 +5,27 @@ import { registryView } from './RokuRegistryView'; import OdcSetupSteps from '../../shared/OdcSetupSteps.svelte'; import Loader from '../../shared/Loader.svelte'; + import { ViewProviderEvent } from '../../../../src/viewProviders/ViewProviderEvent'; let loading = true; let registryValues = {}; - (async () => { - loading = true; - const { values } = await odc.readRegistry(); - registryValues = registryView.formatValues(values); - loading = false; - })(); - let odcAvailable = true; + let odcAvailable = false; - intermediary.observeEvent('onDeviceComponentStatus', (message) => { - odcAvailable = message.available; + intermediary.observeEvent(ViewProviderEvent.onDeviceAvailabilityChange, async (message) => { + odcAvailable = message.odcAvailable; + if (odcAvailable) { + loading = true; + const { values } = await odc.readRegistry(); + registryValues = registryView.formatValues(values); + loading = false; + } else { + registryValues = {} + } }); - intermediary.observeEvent('registryUpdated', (message) => { + intermediary.observeEvent(ViewProviderEvent.onRegistryUpdated, (message) => { registryValues = registryView.formatValues(message.values); }); diff --git a/webviews/src/views/SceneGraphInspectorView/NodeBranchPage.svelte b/webviews/src/views/SceneGraphInspectorView/Branch.svelte similarity index 62% rename from webviews/src/views/SceneGraphInspectorView/NodeBranchPage.svelte rename to webviews/src/views/SceneGraphInspectorView/Branch.svelte index ee19e3075..698056ada 100644 --- a/webviews/src/views/SceneGraphInspectorView/NodeBranchPage.svelte +++ b/webviews/src/views/SceneGraphInspectorView/Branch.svelte @@ -1,5 +1,5 @@
- {#if showSettings} - + {#if showSettingsPage} + {/if} {#if showNodeCountByType} @@ -192,7 +220,7 @@ {:else if !odcAvailable} {:else if error} -
{error}
+
{error.message}
If you are seeing this, please make sure you have the on device component running. This requires that both the files are included in @@ -245,10 +273,11 @@
{#each rootTree as rootNode} - {/each}
@@ -257,7 +286,6 @@ + inspectNodeTreeNode={inspectNodeTreeNode} /> {/if}
diff --git a/webviews/src/views/SceneGraphInspectorView/SettingsPage.svelte b/webviews/src/views/SceneGraphInspectorView/SettingsPage.svelte index fe752c756..cf9ed5379 100644 --- a/webviews/src/views/SceneGraphInspectorView/SettingsPage.svelte +++ b/webviews/src/views/SceneGraphInspectorView/SettingsPage.svelte @@ -1,7 +1,7 @@