From c2be8c8baed8c3d46fa86b857b86d874c3506993 Mon Sep 17 00:00:00 2001 From: Brian Leighty Date: Tue, 27 Dec 2022 13:11:48 -0500 Subject: [PATCH 01/22] Fix focus scrolling to work properly the first time even if all levels were collapsed --- .../NodeBranchPage.svelte | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/webviews/src/views/SceneGraphInspectorView/NodeBranchPage.svelte b/webviews/src/views/SceneGraphInspectorView/NodeBranchPage.svelte index f0365b62..9be8b71d 100644 --- a/webviews/src/views/SceneGraphInspectorView/NodeBranchPage.svelte +++ b/webviews/src/views/SceneGraphInspectorView/NodeBranchPage.svelte @@ -29,18 +29,22 @@ let selected = false; export let focusedNode = -1; $: { + // If we are the focused node then we want to scroll down to this node if (focusedNode !== -1 && nodeTree.ref === focusedNode) { + // We need to expand all the parents before we can calculate how far we need to scroll down + dispatch('childExpanded'); selected = true; - if (self) { - const rect = self.getBoundingClientRect() - document.getElementById('container').scrollTo({ - left: rect.left, - top: rect.top - 90, - behavior: 'smooth' - }); - } - dispatch('childExpanded'); + setTimeout(() => { + if (self) { + const rect = self.getBoundingClientRect() + document.getElementById('container').scrollTo({ + left: rect.left, + top: rect.top - 90, + behavior: 'smooth' + }); + } + }, 0); } else { selected = false; } From 310d37bba72937bb09dbb16b9d7bd6484a08c6ac Mon Sep 17 00:00:00 2001 From: Brian Leighty Date: Tue, 27 Dec 2022 16:33:50 -0500 Subject: [PATCH 02/22] update to RTA v2.0 beta 11 --- package-lock.json | 14 +++++++------- package.json | 2 +- src/viewProviders/BaseRdbViewProvider.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 76104bc4..4c144b6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "pretty-bytes": "^5.6.0", "roku-debug": "^0.17.3", "roku-deploy": "^3.9.2", - "roku-test-automation": "^2.0.0-beta.9", + "roku-test-automation": "^2.0.0-beta.11", "semver": "^7.1.3", "source-map": "^0.7.3", "thenby": "^1.3.4", @@ -8675,9 +8675,9 @@ } }, "node_modules/roku-test-automation": { - "version": "2.0.0-beta.9", - "resolved": "https://registry.npmjs.org/roku-test-automation/-/roku-test-automation-2.0.0-beta.9.tgz", - "integrity": "sha512-JCcaAWfo99ogwwSezoc8kpurn0APT1TrqtujR0ojwy1zYWm0mhP68yTPEGYrMYtL8UifUWiSTNK/I4em/i6oWA==", + "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==", "dependencies": { "@suitest/types": "^4.6.0", "ajv": "^6.12.6", @@ -17480,9 +17480,9 @@ } }, "roku-test-automation": { - "version": "2.0.0-beta.9", - "resolved": "https://registry.npmjs.org/roku-test-automation/-/roku-test-automation-2.0.0-beta.9.tgz", - "integrity": "sha512-JCcaAWfo99ogwwSezoc8kpurn0APT1TrqtujR0ojwy1zYWm0mhP68yTPEGYrMYtL8UifUWiSTNK/I4em/i6oWA==", + "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==", "requires": { "@suitest/types": "^4.6.0", "ajv": "^6.12.6", diff --git a/package.json b/package.json index 915332a4..e0e482e3 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "pretty-bytes": "^5.6.0", "roku-debug": "^0.17.3", "roku-deploy": "^3.9.2", - "roku-test-automation": "^2.0.0-beta.9", + "roku-test-automation": "^2.0.0-beta.11", "semver": "^7.1.3", "source-map": "^0.7.3", "thenby": "^1.3.4", diff --git a/src/viewProviders/BaseRdbViewProvider.ts b/src/viewProviders/BaseRdbViewProvider.ts index f91617ab..7cab0c75 100644 --- a/src/viewProviders/BaseRdbViewProvider.ts +++ b/src/viewProviders/BaseRdbViewProvider.ts @@ -15,7 +15,7 @@ export abstract class BaseRdbViewProvider extends BaseWebviewViewProvider { 'getNodesInfo', 'hasFocus', 'isInFocusChain', - 'observeField', + 'onFieldChangeOnce', 'readRegistry', 'setValue', 'writeRegistry', From 7b0032ddcca26b73c1789722cd828d5150439eaa Mon Sep 17 00:00:00 2001 From: Brian Leighty Date: Tue, 27 Dec 2022 17:19:59 -0500 Subject: [PATCH 03/22] Update absolute key path to properly be able to dig into ArrayGrids and use `#` before findNode keys --- .../NodeDetailPage.svelte | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/webviews/src/views/SceneGraphInspectorView/NodeDetailPage.svelte b/webviews/src/views/SceneGraphInspectorView/NodeDetailPage.svelte index 0cb43cb2..39870ffb 100644 --- a/webviews/src/views/SceneGraphInspectorView/NodeDetailPage.svelte +++ b/webviews/src/views/SceneGraphInspectorView/NodeDetailPage.svelte @@ -17,8 +17,24 @@ let nodeTree = inspectNodeNodeTree; if (inspectNodeNodeTree) { while (nodeTree) { - if (nodeTree.id) { - absoluteKeyPathParts.unshift(nodeTree.id); + if (nodeTree.subtype === 'RowListItem') { + // If we encounter a RowListItem then we know we need to modify the keypath structure + const position = absoluteKeyPathParts.shift(); + for (const child of nodeTree.children) { + if (child.position === position) { + if (child.subtype === 'MarkupGrid') { + absoluteKeyPathParts.unshift('items'); + } else if (child.subtype === 'Group') { + absoluteKeyPathParts.unshift('title'); + } else { + console.log('Encountered unexpected subtype ' + child.subtype); + } + break; + } + } + absoluteKeyPathParts.unshift(nodeTree.position); + } else if (nodeTree.id) { + absoluteKeyPathParts.unshift('#' + nodeTree.id); } else if (nodeTree.position >= 0) { absoluteKeyPathParts.unshift(nodeTree.position); } From 9140b6a5fb18d5409767f7c192e54e0bf5961fce Mon Sep 17 00:00:00 2001 From: Brian Leighty Date: Tue, 27 Dec 2022 17:20:23 -0500 Subject: [PATCH 04/22] expand focused node for easier access to its children --- .../src/views/SceneGraphInspectorView/NodeBranchPage.svelte | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webviews/src/views/SceneGraphInspectorView/NodeBranchPage.svelte b/webviews/src/views/SceneGraphInspectorView/NodeBranchPage.svelte index 9be8b71d..ee19e307 100644 --- a/webviews/src/views/SceneGraphInspectorView/NodeBranchPage.svelte +++ b/webviews/src/views/SceneGraphInspectorView/NodeBranchPage.svelte @@ -35,6 +35,9 @@ dispatch('childExpanded'); selected = true; + // Go ahead and expand us as well to speed up digging into children if desired + expanded = true + setTimeout(() => { if (self) { const rect = self.getBoundingClientRect() From 416f3a860fa97ef1ccb1495e5fa943971a439304 Mon Sep 17 00:00:00 2001 From: Brian Leighty Date: Fri, 6 Jan 2023 19:01:11 -0500 Subject: [PATCH 05/22] screenshot select progress commit and RTA changes --- .vscode/settings.json | 3 +- src/viewProviders/BaseRdbViewProvider.ts | 26 +- src/viewProviders/BaseWebviewViewProvider.ts | 4 +- webviews/src/ExtensionIntermediary.ts | 12 +- .../SceneGraphInspectorView.svelte | 22 +- .../ScreenshotSelectPage.svelte | 281 ++++++++++++++++++ .../SettingsPage.svelte | 4 +- webviews/svelte.config.js | 1 - 8 files changed, 337 insertions(+), 16 deletions(-) create mode 100644 webviews/src/views/SceneGraphInspectorView/ScreenshotSelectPage.svelte diff --git a/.vscode/settings.json b/.vscode/settings.json index b2f602f2..d96421dd 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/src/viewProviders/BaseRdbViewProvider.ts b/src/viewProviders/BaseRdbViewProvider.ts index 7cab0c75..161ced39 100644 --- a/src/viewProviders/BaseRdbViewProvider.ts +++ b/src/viewProviders/BaseRdbViewProvider.ts @@ -5,6 +5,7 @@ import { BaseWebviewViewProvider } from './BaseWebviewViewProvider'; export abstract class BaseRdbViewProvider extends BaseWebviewViewProvider { protected onDeviceComponent?: rta.OnDeviceComponent; + // TODO see if we can have this pull from json file instead protected odcCommands: Array = [ 'callFunc', 'deleteEntireRegistry', @@ -20,7 +21,9 @@ export abstract class BaseRdbViewProvider extends BaseWebviewViewProvider { 'setValue', 'writeRegistry', 'storeNodeReferences', - 'deleteNodeReferences' + 'deleteNodeReferences', + 'getNodesWithProperties', + 'findNodesAtLocation' ]; // @param odc - The OnDeviceComponent class instance. If undefined existing instance will be removed. Used to notify webview of change in ODC status @@ -54,17 +57,34 @@ export abstract class BaseRdbViewProvider extends BaseWebviewViewProvider { RokuDevice: { devices: [{ host: context.ipAddress, - password: '' + password: context.password }] }, OnDeviceComponent: { disableTelnet: true, - disableCallOriginationLine: true + disableCallOriginationLine: true, + clientDebugLogging: false } }; onDeviceComponent.setConfig(rtaConfig); this.setOnDeviceComponent(onDeviceComponent); + return true; + } else if (command === 'getScreenshot') { + try { + const { buffer, format } = await rta.device.getScreenshot(); + + // TODO figure out how to handle format doesn't seem to matter + this.postMessage({ + name: 'screenshotAvailable', + image: `data:image/jpg;base64, ${buffer.toString('base64')}` + }); + } catch (e) { + this.postMessage({ + name: 'screenshotFailed' + }); + } + return true; } return false; diff --git a/src/viewProviders/BaseWebviewViewProvider.ts b/src/viewProviders/BaseWebviewViewProvider.ts index 61a9e83b..29f9ecdf 100644 --- a/src/viewProviders/BaseWebviewViewProvider.ts +++ b/src/viewProviders/BaseWebviewViewProvider.ts @@ -57,7 +57,9 @@ export abstract class BaseWebviewViewProvider implements vscode.WebviewViewProvi this.onViewReady(); this.postQueuedMessages(); } else { - await this.handleViewMessage(message); + if (!await this.handleViewMessage(message)) { + console.log('Did not handle message', message); + } } } catch (e) { this.postMessage({ diff --git a/webviews/src/ExtensionIntermediary.ts b/webviews/src/ExtensionIntermediary.ts index 872fa2a2..a6138fe9 100644 --- a/webviews/src/ExtensionIntermediary.ts +++ b/webviews/src/ExtensionIntermediary.ts @@ -126,8 +126,8 @@ class ODCIntermediary { return this.sendOdcMessage>('isInFocusChain', args, options); } - public async observeField(args: rta.ODC.ObserveFieldArgs, options?: rta.ODC.RequestOptions) { - return this.sendOdcMessage>('observeField', args, options); + public async onFieldChangeOnce(args: rta.ODC.OnFieldChangeOnceArgs, options?: rta.ODC.RequestOptions) { + return this.sendOdcMessage>('onFieldChangeOnce', args, options); } public async setValue(args: rta.ODC.SetValueArgs, options?: rta.ODC.RequestOptions) { @@ -149,6 +149,14 @@ class ODCIntermediary { public async deleteNodeReferences(args: rta.ODC.DeleteNodeReferencesArgs, options?: rta.ODC.RequestOptions) { return this.sendOdcMessage>('deleteNodeReferences', args, options); } + + public async getNodesWithProperties(args: rta.ODC.GetNodesWithPropertiesArgs, options?: rta.ODC.RequestOptions) { + return this.sendOdcMessage>('getNodesWithProperties', args, options); + } + + public async findNodesAtLocation(args: rta.ODC.FindNodesAtLocationArgs, options?: rta.ODC.RequestOptions) { + return this.sendOdcMessage>('findNodesAtLocation', args, options); + } } type ObserverCallback = (message) => void; diff --git a/webviews/src/views/SceneGraphInspectorView/SceneGraphInspectorView.svelte b/webviews/src/views/SceneGraphInspectorView/SceneGraphInspectorView.svelte index b014f8e2..95f29de8 100644 --- a/webviews/src/views/SceneGraphInspectorView/SceneGraphInspectorView.svelte +++ b/webviews/src/views/SceneGraphInspectorView/SceneGraphInspectorView.svelte @@ -1,3 +1,4 @@ + + + + + +
+
+ + +
+ {#if displayScreenshotOverlay} +
+
x: {currentNode?.rect.x}
+ +
+
y: {currentNode?.rect.y}
+ +
+ {/if} + +
+
    + {#each matchingNodes as matchingNode, i} +
  • {matchingNode.node.subtype}({matchingNode.node.id})
  • + {/each} +
+
Hover over the node you are interested in. Use scrollwheel to shift between nodes. Right click to jump to node
+
diff --git a/webviews/src/views/SceneGraphInspectorView/SettingsPage.svelte b/webviews/src/views/SceneGraphInspectorView/SettingsPage.svelte index fe752c75..41811068 100644 --- a/webviews/src/views/SceneGraphInspectorView/SettingsPage.svelte +++ b/webviews/src/views/SceneGraphInspectorView/SettingsPage.svelte @@ -1,7 +1,7 @@ - - - - -
-
- - -
- {#if displayScreenshotOverlay} -
-
x: {currentNode?.rect.x}
- -
-
y: {currentNode?.rect.y}
- -
- {/if} - -
-
    - {#each matchingNodes as matchingNode, i} -
  • {matchingNode.node.subtype}({matchingNode.node.id})
  • - {/each} -
-
Hover over the node you are interested in. Use scrollwheel to shift between nodes. Right click to jump to node
-
From bab30a14f8329150bbeec50e10b8be26616acb8e Mon Sep 17 00:00:00 2001 From: Brian Leighty Date: Tue, 21 Feb 2023 16:23:57 -0500 Subject: [PATCH 08/22] add rokuDeviceView panel that allows showing screenshots from Roku and direct node selection, Add RtaManager and WebviewProviderManager to assist in interwebview communication, switch to pulling odcCommands list from requestTypes.schema.json, standardize naming of messages to either `command` or `event`, switch from using `handle` to `on` in event observers --- package-lock.json | 56 +-- package.json | 60 ++- src/extension.spec.ts | 61 --- src/extension.ts | 83 +---- src/managers/RtaManager.ts | 96 +++++ .../WebviewViewProviderManager.spec.ts | 94 +++++ src/managers/WebviewViewProviderManager.ts | 71 ++++ src/viewProviders/BaseRdbViewProvider.ts | 105 +++--- src/viewProviders/BaseWebviewViewProvider.ts | 44 ++- .../RokuDeviceViewViewProvider.ts | 15 + .../RokuRegistryViewProvider.spec.ts | 3 + src/viewProviders/RokuRegistryViewProvider.ts | 8 +- .../SceneGraphInspectorViewProvider.ts | 7 + webviews/index.html | 3 +- webviews/src/ExtensionIntermediary.ts | 32 +- webviews/src/main.ts | 2 + .../src/shared/OdcSetManualIpAddress.svelte | 18 +- .../RokuCommandsView/RokuCommandsView.svelte | 5 +- .../RokuDeviceView/RokuDeviceView.svelte | 351 ++++++++++++++++++ .../RokuRegistryView/RokuRegistryView.spec.ts | 2 +- .../RokuRegistryView/RokuRegistryView.svelte | 20 +- .../NodeBranchPage.svelte | 122 +++--- .../NodeCountByTypePage.svelte | 4 +- .../NodeDetailPage.svelte | 15 +- .../SceneGraphInspectorView.svelte | 48 ++- .../SettingsPage.svelte | 4 +- 26 files changed, 987 insertions(+), 342 deletions(-) create mode 100644 src/managers/RtaManager.ts create mode 100644 src/managers/WebviewViewProviderManager.spec.ts create mode 100644 src/managers/WebviewViewProviderManager.ts create mode 100644 src/viewProviders/RokuDeviceViewViewProvider.ts create mode 100644 webviews/src/views/RokuDeviceView/RokuDeviceView.svelte diff --git a/package-lock.json b/package-lock.json index 4c144b6e..2c75cc2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "pretty-bytes": "^5.6.0", "roku-debug": "^0.17.3", "roku-deploy": "^3.9.2", - "roku-test-automation": "^2.0.0-beta.11", + "roku-test-automation": "^2.0.0-beta.13", "semver": "^7.1.3", "source-map": "^0.7.3", "thenby": "^1.3.4", @@ -83,7 +83,7 @@ }, "engines": { "node": "^12.12.0", - "vscode": "^1.53.0" + "vscode": "^1.57.0" }, "optionalDependencies": { "fsevents": "~2.3.2" @@ -1159,20 +1159,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": "*", @@ -2956,9 +2956,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" } @@ -8675,9 +8675,9 @@ } }, "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.13", + "resolved": "https://registry.npmjs.org/roku-test-automation/-/roku-test-automation-2.0.0-beta.13.tgz", + "integrity": "sha512-6ofxklHSi5cfUr5QXJ+pSRCnmuE0sGykVB50PAJ8dUEIcMRs7Ao6QQtR9vV9CdCBIZuMgtL4f/rVmJRTZnENSw==", "dependencies": { "@suitest/types": "^4.6.0", "ajv": "^6.12.6", @@ -11771,20 +11771,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": "*", @@ -13146,9 +13146,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", @@ -17480,9 +17480,9 @@ } }, "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.13", + "resolved": "https://registry.npmjs.org/roku-test-automation/-/roku-test-automation-2.0.0-beta.13.tgz", + "integrity": "sha512-6ofxklHSi5cfUr5QXJ+pSRCnmuE0sGykVB50PAJ8dUEIcMRs7Ao6QQtR9vV9CdCBIZuMgtL4f/rVmJRTZnENSw==", "requires": { "@suitest/types": "^4.6.0", "ajv": "^6.12.6", diff --git a/package.json b/package.json index 13b428fa..65f89ae8 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": { @@ -70,7 +70,7 @@ "pretty-bytes": "^5.6.0", "roku-debug": "^0.17.3", "roku-deploy": "^3.9.2", - "roku-test-automation": "^2.0.0-beta.11", + "roku-test-automation": "^2.0.0-beta.13", "semver": "^7.1.3", "source-map": "^0.7.3", "thenby": "^1.3.4", @@ -193,6 +193,12 @@ "name": "SceneGraph Inspector", "type": "webview" }, + { + "id": "rokuDeviceView", + "contextualTitle": "Roku", + "name": "Device View", + "type": "webview" + }, { "id": "rokuRegistryView", "name": "Roku Registry", @@ -253,6 +259,26 @@ "command": "extension.brightscript.rokuRegistry.clearRegistry", "when": "view == rokuRegistryView", "group": "navigation" + }, + { + "command": "extension.brightscript.rokuDeviceView.inspectNodes", + "when": "view == rokuDeviceView && brightscript.isOnDeviceComponentAvailable && !brightscript.rokuDeviceView.isInspectingNodes", + "group": "navigation" + }, + { + "command": "extension.brightscript.rokuDeviceView.resumeScreenshotCapture", + "when": "view == rokuDeviceView && !brightscript.rokuDeviceView.enableScreenshotCapture", + "group": "navigation" + }, + { + "command": "extension.brightscript.rokuDeviceView.pauseScreenshotCapture", + "when": "view == rokuDeviceView && brightscript.rokuDeviceView.enableScreenshotCapture && !brightscript.rokuDeviceView.isInspectingNodes", + "group": "navigation" + }, + { + "command": "extension.brightscript.rokuDeviceView.refreshScreenshot", + "when": "view == rokuDeviceView && !brightscript.rokuDeviceView.enableScreenshotCapture", + "group": "navigation" } ], "commandPalette": [] @@ -2378,6 +2404,36 @@ "command": "extension.brightscript.languageServer.info", "title": "View BrighterScript LanguageServer Info", "category": "BrighterScript" + }, + { + "command": "extension.brightscript.rokuDeviceView.inspectNodes", + "title": "Inspect Nodes", + "category": "BrighterScript", + "icon": "$(inspect)" + }, + { + "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/extension.spec.ts b/src/extension.spec.ts index 00c940a7..830323e7 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 293e9325..9b984f45 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,9 @@ 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'; const EXTENSION_ID = 'RokuCommunity.brightscript'; @@ -43,19 +41,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 +70,10 @@ export class Extension { activeDeviceManager ); + 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; @@ -112,9 +103,6 @@ export class Extension { let onlineDevicesViewProvider = new OnlineDevicesViewProvider(context, activeDeviceManager); vscode.window.registerTreeDataProvider('onlineDevicesView', onlineDevicesViewProvider); - // register our webview providers - this.registerWebviewProviders(context); - context.subscriptions.push(vscode.commands.registerCommand('extension.brightscript.rendezvous.clearHistory', async () => { try { await vscode.debug.activeDebugSession.customRequest('rendezvous.clearHistory'); @@ -223,19 +211,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 +258,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 00000000..3e1f14f5 --- /dev/null +++ b/src/managers/RtaManager.ts @@ -0,0 +1,96 @@ +import * as rta from 'roku-test-automation'; +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 === 'findNodesAtLocation') { + if (!this.lastStoreNodesResponse) { + args.includeBoundingRectInfo = true; + await this.sendOdcRequest(requestorId, 'storeNodeReferences', args); + } + context.args.nodeTreeResponse = this.lastStoreNodesResponse; + let { matches } = await rta.odc.findNodesAtLocation(args, options); + if (requestorId === '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 === 'storeNodeReferences') { + this.lastStoreNodesResponse = await rta.odc.storeNodeReferences(args, options); + + const viewIds = []; + if (requestorId === 'rokuDeviceView') { + viewIds.push('sceneGraphInspectorView'); + } else if (requestorId === 'sceneGraphInspectorView') { + viewIds.push('rokuDeviceView'); + } + this.webviewViewProviderManager.sendMessageToWebviews(viewIds, { + event: 'storedNodeReferencesUpdated' + }); + return this.lastStoreNodesResponse; + } else { + return this.onDeviceComponent[command](args, options); + } + } + + public setWebviewViewProviderManager(manager: WebviewViewProviderManager) { + this.webviewViewProviderManager = manager; + } + + public getStoredNodeReferences() { + return this.lastStoreNodesResponse; + } +} diff --git a/src/managers/WebviewViewProviderManager.spec.ts b/src/managers/WebviewViewProviderManager.spec.ts new file mode 100644 index 00000000..907bc143 --- /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 00000000..5bec92b4 --- /dev/null +++ b/src/managers/WebviewViewProviderManager.ts @@ -0,0 +1,71 @@ +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); + } + } + } +} diff --git a/src/viewProviders/BaseRdbViewProvider.ts b/src/viewProviders/BaseRdbViewProvider.ts index 161ced39..66b7a692 100644 --- a/src/viewProviders/BaseRdbViewProvider.ts +++ b/src/viewProviders/BaseRdbViewProvider.ts @@ -1,87 +1,80 @@ import * as rta from 'roku-test-automation'; +import type * as vscode from 'vscode'; +import type { ODC } from 'roku-test-automation'; +import * as fsExtra from 'fs-extra'; +import * as path from 'path'; import { BaseWebviewViewProvider } from './BaseWebviewViewProvider'; +import type { RtaManager } from '../managers/RtaManager'; export abstract class BaseRdbViewProvider extends BaseWebviewViewProvider { - protected onDeviceComponent?: rta.OnDeviceComponent; + protected rtaManager?: RtaManager; - // TODO see if we can have this pull from json file instead - protected odcCommands: Array = [ - 'callFunc', - 'deleteEntireRegistry', - 'deleteRegistrySections', - 'getFocusedNode', - 'getValue', - 'getValues', - 'getNodesInfo', - 'hasFocus', - 'isInFocusChain', - 'onFieldChangeOnce', - 'readRegistry', - 'setValue', - 'writeRegistry', - 'storeNodeReferences', - 'deleteNodeReferences', - 'getNodesWithProperties', - 'findNodesAtLocation' - ]; + protected odcCommands: Array; - // @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; + 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; + } + + public setRtaManager(rtaManager?: RtaManager) { + this.rtaManager = rtaManager; + } + public updateDeviceAvailability() { this.postOrQueueMessage({ - name: 'onDeviceComponentStatus', - available: onDeviceComponent ? true : false + event: '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(); } 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: context.password - }] - }, - OnDeviceComponent: { - disableTelnet: true, - disableCallOriginationLine: true, - clientDebugLogging: false - } - }; + } else if (command === 'getStoredNodeReferences') { + const response = this.rtaManager.getStoredNodeReferences(); + this.postOrQueueMessage({ + ...message, + response: response + }); - onDeviceComponent.setConfig(rtaConfig); - this.setOnDeviceComponent(onDeviceComponent); + return true; + } else if (command === 'setManualIpAddress') { + this.rtaManager.setupRtaWithConfig({ + ...message.context, + injectRdbOnDeviceComponent: true + }); return true; } else if (command === 'getScreenshot') { try { - const { buffer, format } = await rta.device.getScreenshot(); - - // TODO figure out how to handle format doesn't seem to matter - this.postMessage({ - name: 'screenshotAvailable', - image: `data:image/jpg;base64, ${buffer.toString('base64')}` + const result = await this.rtaManager.device.getScreenshot(); + this.postOrQueueMessage({ + ...message, + response: { + success: true, + arrayBuffer: result.buffer.buffer + } }); } catch (e) { - this.postMessage({ - name: 'screenshotFailed' + this.postOrQueueMessage({ + ...message, + response: { + success: false + } }); } return true; diff --git a/src/viewProviders/BaseWebviewViewProvider.ts b/src/viewProviders/BaseWebviewViewProvider.ts index 29f9ecdf..4210583f 100644 --- a/src/viewProviders/BaseWebviewViewProvider.ts +++ b/src/viewProviders/BaseWebviewViewProvider.ts @@ -2,8 +2,9 @@ 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'; export abstract class BaseWebviewViewProvider implements vscode.WebviewViewProvider, vscode.Disposable { constructor(context: vscode.ExtensionContext) { @@ -22,11 +23,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,14 +53,6 @@ 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 { @@ -56,6 +62,10 @@ export abstract class BaseWebviewViewProvider implements vscode.WebviewViewProvi this.viewReady = true; this.onViewReady(); this.postQueuedMessages(); + } else if (command === 'setVscodeContext') { + await vscodeContextManager.set(message.key, message.value); + } else if (command === 'sendMessageToWebviews') { + this.webviewViewProviderManager.sendMessageToWebviews(message.viewIds, message.message); } else { if (!await this.handleViewMessage(message)) { console.log('Did not handle message', message); @@ -64,7 +74,10 @@ export abstract class BaseWebviewViewProvider implements vscode.WebviewViewProvi } catch (e) { this.postMessage({ ...message, - error: e.message + error: { + message: e.message, + stack: e.stack + } }); } }); @@ -74,6 +87,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: 'onVscodeCommandReceived', + commandName: command + }); + })); + } + protected onViewReady() { } /** Adds ability to add additional script contents to the main webview html */ @@ -87,7 +109,7 @@ export abstract class BaseWebviewViewProvider implements vscode.WebviewViewProvi // 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 + //only refresh when the index.html page is changed. Since svelte 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/RokuDeviceViewViewProvider.ts b/src/viewProviders/RokuDeviceViewViewProvider.ts new file mode 100644 index 00000000..7f4da67b --- /dev/null +++ b/src/viewProviders/RokuDeviceViewViewProvider.ts @@ -0,0 +1,15 @@ +import type * as vscode from 'vscode'; +import { BaseRdbViewProvider } from './BaseRdbViewProvider'; + +export class RokuDeviceViewViewProvider extends BaseRdbViewProvider { + public readonly id = 'rokuDeviceView'; + + constructor(context: vscode.ExtensionContext) { + super(context); + + this.registerCommandWithWebViewNotifier(context, 'extension.brightscript.rokuDeviceView.inspectNodes'); + this.registerCommandWithWebViewNotifier(context, 'extension.brightscript.rokuDeviceView.refreshScreenshot'); + this.registerCommandWithWebViewNotifier(context, 'extension.brightscript.rokuDeviceView.pauseScreenshotCapture'); + this.registerCommandWithWebViewNotifier(context, 'extension.brightscript.rokuDeviceView.resumeScreenshotCapture'); + } +} diff --git a/src/viewProviders/RokuRegistryViewProvider.spec.ts b/src/viewProviders/RokuRegistryViewProvider.spec.ts index 67691ece..7f093de8 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 9da9662f..2ca35ff9 100644 --- a/src/viewProviders/RokuRegistryViewProvider.ts +++ b/src/viewProviders/RokuRegistryViewProvider.ts @@ -11,7 +11,7 @@ export class RokuRegistryViewProvider extends BaseRdbViewProvider { subscriptions.push(vscode.commands.registerCommand('extension.brightscript.rokuRegistry.exportRegistry', 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')); }); })); @@ -27,7 +27,7 @@ export class RokuRegistryViewProvider extends BaseRdbViewProvider { })); subscriptions.push(vscode.commands.registerCommand('extension.brightscript.rokuRegistry.clearRegistry', async () => { - await this.onDeviceComponent.deleteEntireRegistry(); + await this.rtaManager.onDeviceComponent.deleteEntireRegistry(); await this.sendRegistryUpdated(); })); @@ -41,7 +41,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 +49,7 @@ export class RokuRegistryViewProvider extends BaseRdbViewProvider { } protected async sendRegistryUpdated() { - const result = await this.onDeviceComponent?.readRegistry(); + const result = await this.rtaManager.onDeviceComponent?.readRegistry(); this.postOrQueueMessage({ name: 'registryUpdated', values: result?.values }); } } diff --git a/src/viewProviders/SceneGraphInspectorViewProvider.ts b/src/viewProviders/SceneGraphInspectorViewProvider.ts index 3265b907..38ee8087 100644 --- a/src/viewProviders/SceneGraphInspectorViewProvider.ts +++ b/src/viewProviders/SceneGraphInspectorViewProvider.ts @@ -1,5 +1,12 @@ +import type * as vscode from 'vscode'; import { BaseRdbViewProvider } from './BaseRdbViewProvider'; export class SceneGraphInspectorViewProvider extends BaseRdbViewProvider { public readonly id = 'sceneGraphInspectorView'; + + constructor(context: vscode.ExtensionContext) { + super(context); + + this.registerCommandWithWebViewNotifier(context, 'extension.brightscript.sceneGraphInspectorView.refreshNodeTree'); + } } diff --git a/webviews/index.html b/webviews/index.html index 23a7bf10..e7858035 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/ExtensionIntermediary.ts b/webviews/src/ExtensionIntermediary.ts index a6138fe9..71f86e0c 100644 --- a/webviews/src/ExtensionIntermediary.ts +++ b/webviews/src/ExtensionIntermediary.ts @@ -19,8 +19,8 @@ class ExtensionIntermediary { delete this.inflightRequests[message.id]; request.callback(message); } else { - if (message.name) { - const listeners = this.observedEvents.get(message.name); + if (message.event) { + const listeners = this.observedEvents.get(message.event); if (listeners) { for (const listener of listeners) { listener(message); @@ -67,14 +67,34 @@ class ExtensionIntermediary { }); } - public observeEvent(name: string, callback: ObserverCallback) { - let observedEvent = this.observedEvents.get(name); + public setVscodeContext(key: string, value: boolean | number | string) { + window.vscode.postMessage({ + command: 'setVscodeContext', + key: key, + value: value + }); + } + + public async getStoredNodeReferences() { + return this.sendMessage>('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) { + window.vscode.postMessage({ + command: 'sendMessageToWebviews', + viewIds: viewIds, + message: message + }); } private generateRandomString(length = 7) { @@ -142,7 +162,7 @@ class ODCIntermediary { return this.sendOdcMessage>('deleteEntireRegistry', args, options); } - public async storeNodeReferences(args: rta.ODC.StoreNodeReferencesArgs, options?: rta.ODC.RequestOptions) { + public async storeNodeReferences(args?: rta.ODC.StoreNodeReferencesArgs, options?: rta.ODC.RequestOptions) { return this.sendOdcMessage>('storeNodeReferences', args, options); } diff --git a/webviews/src/main.ts b/webviews/src/main.ts index 5f33c771..9e35a81f 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 f571b6bc..b12d5391 100644 --- a/webviews/src/shared/OdcSetManualIpAddress.svelte +++ b/webviews/src/shared/OdcSetManualIpAddress.svelte @@ -4,11 +4,14 @@ import { utils } from '../utils'; let ipAddress = utils.getStorageValue('manuallySetIpAddress', ''); + let password = utils.getStorageValue('manuallySetPassword', ''); function onSaveIpButtonClicked() { utils.setStorageValue('manuallySetIpAddress', ipAddress); + utils.setStorageValue('manuallySetPassword', password); intermediary.sendMessage('setManualIpAddress', { - ipAddress: ipAddress + host: ipAddress, + password: password }); } @@ -20,13 +23,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 1915ffad..4494b0cd 100644 --- a/webviews/src/views/RokuCommandsView/RokuCommandsView.svelte +++ b/webviews/src/views/RokuCommandsView/RokuCommandsView.svelte @@ -91,8 +91,8 @@ let odcAvailable = true; - intermediary.observeEvent('onDeviceComponentStatus', (message) => { - odcAvailable = message.available; + intermediary.observeEvent('onDeviceAvailabilityChange', (message) => { + odcAvailable = message.odcAvailable; }); // Required by any view so we can know that the view is ready to receive messages @@ -102,6 +102,7 @@ + + +
+ {#if deviceAvailable} +
+ {#if focusedNodeTree} +
+
+ +
+ subtype: {focusedNodeTree.subtype}, + id: {focusedNodeTree.id}
+ x: {focusedNodeTree.sceneRect.x}, + y: {focusedNodeTree.sceneRect.y}, + width: {focusedNodeTree.sceneRect.width}, + height: {focusedNodeTree.sceneRect.height} +
+ {/if} + + +
+ {:else} +
+ +
+ {/if} +
diff --git a/webviews/src/views/RokuRegistryView/RokuRegistryView.spec.ts b/webviews/src/views/RokuRegistryView/RokuRegistryView.spec.ts index 80436fd0..c5ec479e 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 357b7a5a..c38c0c66 100644 --- a/webviews/src/views/RokuRegistryView/RokuRegistryView.svelte +++ b/webviews/src/views/RokuRegistryView/RokuRegistryView.svelte @@ -9,17 +9,19 @@ 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('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) => { diff --git a/webviews/src/views/SceneGraphInspectorView/NodeBranchPage.svelte b/webviews/src/views/SceneGraphInspectorView/NodeBranchPage.svelte index ee19e307..5534dcfc 100644 --- a/webviews/src/views/SceneGraphInspectorView/NodeBranchPage.svelte +++ b/webviews/src/views/SceneGraphInspectorView/NodeBranchPage.svelte @@ -1,4 +1,5 @@ - +