diff --git a/.eslintrc.js b/.eslintrc.js
index d9f9fd180a2..f61850ee1e4 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -436,10 +436,8 @@ module.exports = {
'src/datascience-ui/interactive-common/redux/reducers/transfer.ts',
'src/datascience-ui/interactive-common/redux/reducers/types.ts',
'src/datascience-ui/interactive-common/redux/reducers/variables.ts',
- 'src/datascience-ui/interactive-common/redux/reducers/commonEffects.ts',
'src/datascience-ui/interactive-common/redux/reducers/kernel.ts',
'src/datascience-ui/interactive-common/redux/postOffice.ts',
- 'src/datascience-ui/interactive-common/redux/store.ts',
'src/datascience-ui/interactive-common/transforms.tsx',
'src/datascience-ui/interactive-common/contentPanel.tsx',
'src/datascience-ui/interactive-common/inputHistory.ts',
@@ -760,7 +758,6 @@ module.exports = {
'src/client/common/terminal/environmentActivationProviders/commandPrompt.ts',
'src/client/common/terminal/environmentActivationProviders/bash.ts',
'src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts',
- 'src/client/common/utils/decorators.ts',
'src/client/common/utils/enum.ts',
'src/client/common/utils/async.ts',
'src/client/common/utils/text.ts',
@@ -1033,8 +1030,6 @@ module.exports = {
'src/client/datascience/interactive-common/types.ts',
'src/client/datascience/interactive-common/linkProvider.ts',
'src/client/datascience/interactive-common/notebookUsageTracker.ts',
- 'src/client/datascience/interactive-common/interactiveWindowTypes.ts',
- 'src/client/datascience/interactive-common/synchronization.ts',
'src/client/datascience/interactive-common/notebookProvider.ts',
'src/client/datascience/interactive-common/interactiveWindowMessageListener.ts',
'src/client/datascience/interactive-common/intellisense/wordHelper.ts',
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 8943b53d7e8..310ba276c39 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -207,6 +207,7 @@
"VSC_FORCE_REAL_JUPYTER": "true", // Enalbe tests that require Jupyter.
"VSC_JUPYTER_CI_RUN_NON_PYTHON_NB_TEST": "", // Initialize this to run tests again Julia & other kernels.
"VSC_JUPYTER_RUN_NB_TEST": "true", // Initialize this to run notebook tests (must be using VSC Insiders).
+ "VSC_JUPYTER_WEBVIEW_TEST_MIDDLEWARE": "true", // Initialize to create the webview test middleware
"VSC_JUPYTER_LOAD_EXPERIMENTS_FROM_FILE": "true",
"TEST_FILES_SUFFIX": "vscode.test",
"XVSC_JUPYTER_INSTRUMENT_CODE_FOR_COVERAGE": "1",
diff --git a/news/3 Code Health/4355.md b/news/3 Code Health/4355.md
new file mode 100644
index 00000000000..bf21c4f1128
--- /dev/null
+++ b/news/3 Code Health/4355.md
@@ -0,0 +1 @@
+Add .vscode tests to test the new variable view
\ No newline at end of file
diff --git a/src/client/common/application/webviews/webview.ts b/src/client/common/application/webviews/webview.ts
index 82f383b5eb0..746587c2b96 100644
--- a/src/client/common/application/webviews/webview.ts
+++ b/src/client/common/application/webviews/webview.ts
@@ -92,6 +92,9 @@ export abstract class Webview implements IWebview {
)
)
.toString();
+
+ // Check to see if we should force on Test middleware for our react code
+ const forceTestMiddleware = process.env.VSC_JUPYTER_WEBVIEW_TEST_MIDDLEWARE || 'false';
return `
@@ -121,6 +124,9 @@ export abstract class Webview implements IWebview {
return "${uriBase}" + relativePath;
}
+ function forceTestMiddleware() {
+ return ${forceTestMiddleware};
+ }
${uris.map((uri) => ``).join('\n')}
diff --git a/src/client/common/utils/decorators.ts b/src/client/common/utils/decorators.ts
index 403fcd8a912..86308353aff 100644
--- a/src/client/common/utils/decorators.ts
+++ b/src/client/common/utils/decorators.ts
@@ -204,7 +204,7 @@ export function displayProgress(title: string, location = ProgressLocation.Windo
// eslint-disable-next-line no-invalid-this
const promise = originalMethod.apply(this, args);
if (!isTestExecution()) {
- window.withProgress(progressOptions, () => promise);
+ void window.withProgress(progressOptions, () => promise);
}
return promise;
};
@@ -244,3 +244,20 @@ export function trace(log: (c: CallInfo, t: TraceInfo) => void) {
return descriptor;
};
}
+
+// Mark a method to be used only in tests
+export function testOnlyMethod() {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return function (_target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor) {
+ const originalMethod = descriptor.value;
+ // eslint-disable-next-line , @typescript-eslint/no-explicit-any
+ descriptor.value = function (...args: any[]) {
+ if (!isTestExecution()) {
+ throw new Error(`Function: ${propertyKey} can only be called from test code`);
+ }
+ return originalMethod.apply(this, args);
+ };
+
+ return descriptor;
+ };
+}
diff --git a/src/client/datascience/interactive-common/interactiveWindowTypes.ts b/src/client/datascience/interactive-common/interactiveWindowTypes.ts
index 4a29fb10042..191ffdcd50a 100644
--- a/src/client/datascience/interactive-common/interactiveWindowTypes.ts
+++ b/src/client/datascience/interactive-common/interactiveWindowTypes.ts
@@ -150,7 +150,9 @@ export enum InteractiveWindowMessages {
GetCellCode = 'get_cell_code',
ReturnCellCode = 'return_cell_code',
GetAllCellCode = 'get_all_cell_code',
- ReturnAllCellCode = 'return_all_cell_code'
+ ReturnAllCellCode = 'return_all_cell_code',
+ GetHTMLByIdRequest = 'get_html_by_id_request',
+ GetHTMLByIdResponse = 'get_html_by_id_response'
}
export enum IPyWidgetMessages {
@@ -715,4 +717,6 @@ export class IInteractiveWindowMapping {
public [InteractiveWindowMessages.HasCellResponse]: { id: string; result: boolean };
public [InteractiveWindowMessages.UpdateExternalCellButtons]: IExternalWebviewCellButton[];
public [InteractiveWindowMessages.ExecuteExternalCommand]: IExternalCommandFromWebview;
+ public [InteractiveWindowMessages.GetHTMLByIdRequest]: string;
+ public [InteractiveWindowMessages.GetHTMLByIdResponse]: string;
}
diff --git a/src/client/datascience/interactive-common/synchronization.ts b/src/client/datascience/interactive-common/synchronization.ts
index 852914001b8..fb2bf4c852c 100644
--- a/src/client/datascience/interactive-common/synchronization.ts
+++ b/src/client/datascience/interactive-common/synchronization.ts
@@ -225,6 +225,8 @@ const messageWithMessageTypes: MessageMapping & Messa
[InteractiveWindowMessages.ConvertUriForUseInWebViewResponse]: MessageType.other,
[InteractiveWindowMessages.UpdateExternalCellButtons]: MessageType.other,
[InteractiveWindowMessages.ExecuteExternalCommand]: MessageType.other,
+ [InteractiveWindowMessages.GetHTMLByIdRequest]: MessageType.other,
+ [InteractiveWindowMessages.GetHTMLByIdResponse]: MessageType.other,
// Types from CssMessages
[CssMessages.GetCssRequest]: MessageType.other,
[CssMessages.GetCssResponse]: MessageType.other,
diff --git a/src/client/datascience/variablesView/types.ts b/src/client/datascience/variablesView/types.ts
index 2b938ffbc70..63cac205656 100644
--- a/src/client/datascience/variablesView/types.ts
+++ b/src/client/datascience/variablesView/types.ts
@@ -27,6 +27,8 @@ export class IVariableViewPanelMapping {
public [SharedMessages.LocInit]: string;
public [InteractiveWindowMessages.FinishCell]: IFinishCell;
public [InteractiveWindowMessages.UpdateVariableViewExecutionCount]: { executionCount: number };
+ public [InteractiveWindowMessages.GetHTMLByIdRequest]: string;
+ public [InteractiveWindowMessages.GetHTMLByIdResponse]: string;
}
export const INotebookWatcher = Symbol('INotebookWatcher');
@@ -37,4 +39,6 @@ export interface INotebookWatcher {
}
export const IVariableViewProvider = Symbol('IVariableViewProvider');
-export interface IVariableViewProvider extends IVSCWebviewViewProvider {}
+export interface IVariableViewProvider extends IVSCWebviewViewProvider {
+ //readonly activeVariableView: Promise;
+}
diff --git a/src/client/datascience/variablesView/variableViewProvider.ts b/src/client/datascience/variablesView/variableViewProvider.ts
index f939e524ff6..b10a5ee0d6a 100644
--- a/src/client/datascience/variablesView/variableViewProvider.ts
+++ b/src/client/datascience/variablesView/variableViewProvider.ts
@@ -5,7 +5,9 @@
import { inject, injectable, named } from 'inversify';
import { CancellationToken, WebviewView, WebviewViewResolveContext } from 'vscode';
import { IApplicationShell, IWebviewViewProvider, IWorkspaceService } from '../../common/application/types';
+import { isTestExecution } from '../../common/constants';
import { IConfigurationService, IDisposableRegistry } from '../../common/types';
+import { createDeferred, Deferred } from '../../common/utils/async';
import { Identifiers } from '../constants';
import { IDataViewerFactory } from '../data-viewing/types';
import { ICodeCssGenerator, IJupyterVariableDataProviderFactory, IJupyterVariables, IThemeFinder } from '../types';
@@ -17,6 +19,23 @@ import { VariableView } from './variableView';
export class VariableViewProvider implements IVariableViewProvider {
public readonly viewType = 'jupyterViewVariables';
+ // Either return the active variable view or wait until it's created and return it
+ // @ts-ignore Property will be accessed in test code via casting to ITestVariableViewProviderInterface
+ private get activeVariableView(): Promise {
+ if (!isTestExecution()) {
+ throw new Error('activeVariableView only for test code');
+ }
+ // If we have already created the view, then just return it
+ if (this.variableView) {
+ return Promise.resolve(this.variableView);
+ }
+
+ // If not wait until created and then return
+ this.activeVariableViewPromise = createDeferred();
+ return this.activeVariableViewPromise.promise;
+ }
+ private activeVariableViewPromise?: Deferred;
+
private variableView?: VariableView;
constructor(
@@ -56,6 +75,11 @@ export class VariableViewProvider implements IVariableViewProvider {
this.notebookWatcher
);
+ // If someone is waiting for the variable view resolve that here
+ if (this.activeVariableViewPromise) {
+ this.activeVariableViewPromise.resolve(this.variableView);
+ }
+
await this.variableView.load(webviewView);
}
}
diff --git a/src/client/datascience/webviews/webviewHost.ts b/src/client/datascience/webviews/webviewHost.ts
index dcdef1440e1..f54dcc86ede 100644
--- a/src/client/datascience/webviews/webviewHost.ts
+++ b/src/client/datascience/webviews/webviewHost.ts
@@ -24,8 +24,10 @@ import * as localize from '../../common/utils/localize';
import { StopWatch } from '../../common/utils/stopWatch';
import { captureTelemetry, sendTelemetryEvent } from '../../telemetry';
import { DefaultTheme, PythonExtension, Telemetry } from '../constants';
+import { InteractiveWindowMessages } from '../interactive-common/interactiveWindowTypes';
import { CssMessages, IGetCssRequest, IGetMonacoThemeRequest, SharedMessages } from '../messages';
import { ICodeCssGenerator, IJupyterExtraSettings, IThemeFinder } from '../types';
+import { testOnlyMethod } from '../../common/utils/decorators';
@injectable() // For some reason this is necessary to get the class hierarchy to work.
export abstract class WebviewHost implements IDisposable {
@@ -42,6 +44,14 @@ export abstract class WebviewHost implements IDisposable {
protected readonly _disposables: IDisposable[] = [];
private startupStopwatch = new StopWatch();
+
+ // For testing, holds the current request for webview HTML
+ private activeHTMLRequest?: Deferred;
+
+ // For testing, broadcast messages to the following listeners
+ // tslint:disable-next-line:no-any
+ private onMessageListeners: ((message: string, payload: any) => void)[] = [];
+
constructor(
@unmanaged() protected configService: IConfigurationService,
@unmanaged() private cssGenerator: ICodeCssGenerator,
@@ -79,6 +89,40 @@ export abstract class WebviewHost implements IDisposable {
}
}
+ // This function is used for testing webview by fetching HTML from the webview via a message
+ // @ts-ignore Property will be accessed in test code via casting to ITestWebviewHost
+ @testOnlyMethod()
+ // @ts-ignore Property will be accessed in test code via casting to ITestWebviewHost
+ private getHTMLById(id: string): Promise {
+ if (!this.activeHTMLRequest) {
+ this.activeHTMLRequest = createDeferred();
+ this.postMessageInternal(InteractiveWindowMessages.GetHTMLByIdRequest, id).ignoreErrors();
+ } else {
+ throw new Error('getHTMLById request already in progress');
+ }
+
+ return this.activeHTMLRequest.promise;
+ }
+
+ // For testing add a callback listening to messages from the webview
+ // tslint:disable-next-line:no-any
+ @testOnlyMethod()
+ // @ts-ignore Property will be accessed in test code via casting to ITestWebviewHost
+ private addMessageListener(callback: (message: string, payload: any) => void) {
+ this.onMessageListeners.push(callback);
+ }
+
+ // For testing remove a callback listening to messages from the webview
+ // tslint:disable-next-line:no-any
+ @testOnlyMethod()
+ // @ts-ignore Property will be accessed in test code via casting to ITestWebviewHost
+ private removeMessageListener(callback: (message: string, payload: any) => void) {
+ const index = this.onMessageListeners.indexOf(callback);
+ if (index >= 0) {
+ this.onMessageListeners.splice(index, 1);
+ }
+ }
+
protected abstract provideWebview(
cwd: string,
settings: IJupyterExtraSettings,
@@ -120,9 +164,22 @@ export abstract class WebviewHost implements IDisposable {
this.handleMonacoThemeRequest(payload as IGetMonacoThemeRequest).ignoreErrors();
break;
+ case InteractiveWindowMessages.GetHTMLByIdResponse:
+ // Webview has returned HTML, resolve the request and clear it
+ if (this.activeHTMLRequest) {
+ this.activeHTMLRequest.resolve(payload);
+ this.activeHTMLRequest = undefined;
+ }
+ break;
+
default:
break;
}
+
+ // Broadcast to any onMessage listeners
+ this.onMessageListeners.forEach((listener) => {
+ listener(message, payload);
+ });
}
protected async loadWebview(cwd: string, webView?: vscodeWebviewPanel | vscodeWebviewView) {
diff --git a/src/datascience-ui/interactive-common/redux/reducers/commonEffects.ts b/src/datascience-ui/interactive-common/redux/reducers/commonEffects.ts
index e2f5fddb1f1..fedf02a978a 100644
--- a/src/datascience-ui/interactive-common/redux/reducers/commonEffects.ts
+++ b/src/datascience-ui/interactive-common/redux/reducers/commonEffects.ts
@@ -318,4 +318,14 @@ export namespace CommonEffects {
externalButtons: arg.payload.data
};
}
+
+ // Extension has requested HTML for the webview, get it by ID and send it back as a message
+ export function getHTMLByIdRequest(arg: CommonReducerArg): IMainState {
+ const element = document.getElementById(arg.payload.data);
+
+ if (element) {
+ postActionToExtension(arg, InteractiveWindowMessages.GetHTMLByIdResponse, element.innerHTML);
+ }
+ return arg.prevState;
+ }
}
diff --git a/src/datascience-ui/interactive-common/redux/store.ts b/src/datascience-ui/interactive-common/redux/store.ts
index b4b7c626d48..8b047a9b510 100644
--- a/src/datascience-ui/interactive-common/redux/store.ts
+++ b/src/datascience-ui/interactive-common/redux/store.ts
@@ -34,6 +34,9 @@ import { generateMonacoReducer, IMonacoState } from './reducers/monaco';
import { CommonActionType } from './reducers/types';
import { generateVariableReducer, IVariableState } from './reducers/variables';
+// Externally defined function to see if we need to force on test middleware
+export declare function forceTestMiddleware(): boolean;
+
function generateDefaultState(
skipDefault: boolean,
testMode: boolean,
@@ -306,7 +309,11 @@ function createMiddleWare(testMode: boolean, postOffice: PostOffice): Redux.Midd
// Or if testing in UI Test.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isUITest = (postOffice.acquireApi() as any)?.handleMessage ? true : false;
- const testMiddleware = testMode || isUITest ? createTestMiddleware() : undefined;
+ let forceOnTestMiddleware = false;
+ if (typeof forceTestMiddleware !== 'undefined') {
+ forceOnTestMiddleware = forceTestMiddleware();
+ }
+ const testMiddleware = forceOnTestMiddleware || testMode || isUITest ? createTestMiddleware() : undefined;
// Create the logger if we're not in production mode or we're forcing logging
const reduceLogMessage = '';
diff --git a/src/datascience-ui/variable-view/redux/reducers/index.ts b/src/datascience-ui/variable-view/redux/reducers/index.ts
index 20b3378c362..be8e824ff9e 100644
--- a/src/datascience-ui/variable-view/redux/reducers/index.ts
+++ b/src/datascience-ui/variable-view/redux/reducers/index.ts
@@ -17,5 +17,6 @@ export const reducerMap: Partial = {
[CssMessages.GetCssResponse]: CommonEffects.handleCss,
[SharedMessages.UpdateSettings]: Effects.updateSettings,
[SharedMessages.LocInit]: CommonEffects.handleLocInit,
- [CommonActionType.VARIABLE_VIEW_LOADED]: Transfer.variableViewStarted
+ [CommonActionType.VARIABLE_VIEW_LOADED]: Transfer.variableViewStarted,
+ [InteractiveWindowMessages.GetHTMLByIdRequest]: CommonEffects.getHTMLByIdRequest
};
diff --git a/src/test/datascience/testInterfaces.ts b/src/test/datascience/testInterfaces.ts
new file mode 100644
index 00000000000..8b266112bd7
--- /dev/null
+++ b/src/test/datascience/testInterfaces.ts
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+'use strict';
+
+// Interfaces here to expose specific private functionality to test code
+export interface ITestWebviewHost {
+ getHTMLById(id: string): Promise;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ addMessageListener(callback: (message: string, payload: any) => void): void;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ removeMessageListener(callback: (message: string, payload: any) => void): void;
+}
diff --git a/src/test/datascience/variableView/variableView.vscode.test.ts b/src/test/datascience/variableView/variableView.vscode.test.ts
new file mode 100644
index 00000000000..b94fa75505d
--- /dev/null
+++ b/src/test/datascience/variableView/variableView.vscode.test.ts
@@ -0,0 +1,110 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+'use strict';
+import { assert } from 'chai';
+import * as sinon from 'sinon';
+import { ICommandManager, IVSCodeNotebook } from '../../../client/common/application/types';
+import { IDisposable } from '../../../client/common/types';
+import { Commands, VSCodeNotebookProvider } from '../../../client/datascience/constants';
+import { IVariableViewProvider } from '../../../client/datascience/variablesView/types';
+import { IExtensionTestApi } from '../../common';
+import { initialize } from '../../initialize';
+import {
+ canRunNotebookTests,
+ closeNotebooks,
+ closeNotebooksAndCleanUpAfterTests,
+ deleteAllCellsAndWait,
+ executeCell,
+ insertCodeCell,
+ startJupyter,
+ trustAllNotebooks,
+ waitForExecutionCompletedSuccessfully,
+ waitForKernelToGetAutoSelected
+} from '../notebook/helper';
+import { INotebookEditorProvider } from '../../../client/datascience/types';
+import { OnMessageListener } from '../vscodeTestHelpers';
+import { InteractiveWindowMessages } from '../../../client/datascience/interactive-common/interactiveWindowTypes';
+import { verifyViewVariables } from './variableViewHelpers';
+import { ITestVariableViewProvider } from './variableViewTestInterfaces';
+import { ITestWebviewHost } from '../testInterfaces';
+
+suite('DataScience - VariableView', () => {
+ let api: IExtensionTestApi;
+ const disposables: IDisposable[] = [];
+ let commandManager: ICommandManager;
+ let variableViewProvider: ITestVariableViewProvider;
+ let editorProvider: INotebookEditorProvider;
+ let vscodeNotebook: IVSCodeNotebook;
+ suiteSetup(async function () {
+ this.timeout(120_000);
+ api = await initialize();
+
+ // Don't run if we can't use the native notebook interface
+ if (!(await canRunNotebookTests())) {
+ return this.skip();
+ }
+ await trustAllNotebooks();
+ await startJupyter(true);
+ sinon.restore();
+ commandManager = api.serviceContainer.get(ICommandManager);
+ const coreVariableViewProvider = api.serviceContainer.get(IVariableViewProvider);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ variableViewProvider = (coreVariableViewProvider as any) as ITestVariableViewProvider; // Cast to expose the test interfaces
+ vscodeNotebook = api.serviceContainer.get(IVSCodeNotebook);
+ editorProvider = api.serviceContainer.get(VSCodeNotebookProvider);
+ });
+ setup(async function () {
+ sinon.restore();
+
+ // Create an editor to use for our tests
+ await editorProvider.createNew();
+ await waitForKernelToGetAutoSelected();
+ await deleteAllCellsAndWait();
+ assert.isOk(vscodeNotebook.activeNotebookEditor, 'No active notebook');
+ });
+ teardown(async function () {
+ await closeNotebooks(disposables);
+ await closeNotebooksAndCleanUpAfterTests(disposables);
+ });
+
+ // Cleanup after suite is finished
+ suiteTeardown(() => closeNotebooksAndCleanUpAfterTests(disposables));
+
+ // Test showing the basic variable view with a value or two
+ test('Can show VariableView', async function () {
+ this.skip(); // Re-enable in CI when #4412 is fixed
+ // Add one simple cell and execute it
+ await insertCodeCell('test = "MYTESTVALUE"', { index: 0 });
+ const cell = vscodeNotebook.activeNotebookEditor?.document.cells![0]!;
+ await executeCell(cell);
+ await waitForExecutionCompletedSuccessfully(cell);
+
+ // Send the command to open the view
+ await commandManager.executeCommand(Commands.OpenVariableView);
+
+ // Aquire the variable view from the provider
+ const coreVariableView = await variableViewProvider.activeVariableView;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const variableView = (coreVariableView as any) as ITestWebviewHost;
+
+ // Add our message listener
+ const onMessageListener = new OnMessageListener(variableView);
+
+ // Send a second cell
+ await insertCodeCell('test2 = "MYTESTVALUE2"', { index: 1 });
+ const cell2 = vscodeNotebook.activeNotebookEditor?.document.cells![1]!;
+ await executeCell(cell2);
+
+ // Wait until our VariablesComplete message to see that we have the new variables and have rendered them
+ await onMessageListener.waitForMessage(InteractiveWindowMessages.VariablesComplete);
+
+ const htmlResult = await variableView?.getHTMLById('variable-view-main-panel');
+
+ // Parse the HTML for our expected variables
+ const expectedVariables = [
+ { name: 'test', type: 'str', length: '11', value: ' MYTESTVALUE' },
+ { name: 'test2', type: 'str', length: '12', value: ' MYTESTVALUE2' }
+ ];
+ verifyViewVariables(expectedVariables, htmlResult);
+ });
+});
diff --git a/src/test/datascience/variableView/variableViewHelpers.ts b/src/test/datascience/variableView/variableViewHelpers.ts
new file mode 100644
index 00000000000..dfa2ef79551
--- /dev/null
+++ b/src/test/datascience/variableView/variableViewHelpers.ts
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+'use strict';
+import { expect } from 'chai';
+
+// Basic shape of a variable result
+export interface IVariableInfo {
+ name: string;
+ type: string;
+ length: string;
+ value: string;
+}
+
+// For the given html, verify that the expected variables are in it
+export function verifyViewVariables(expected: IVariableInfo[], html: string) {
+ const htmlVariables = parseVariableViewHTML(html);
+
+ // Check our size first
+ expect(htmlVariables.length).to.be.equal(expected.length, 'Did not find expected number of variables');
+
+ expected.forEach((expectedInfo, index) => {
+ compareVariableInfos(expectedInfo, htmlVariables[index]);
+ });
+}
+
+// Helper function to parse the view HTML
+function parseVariableViewHTML(html: string): IVariableInfo[] {
+ const parser = new DOMParser();
+ const htmlDoc = parser.parseFromString(html, 'text/html');
+ const variableRows = htmlDoc.getElementsByClassName('react-grid-Row');
+
+ const variableInfos: IVariableInfo[] = [];
+ // HTMLCollectionOf doesn't support nice iterators
+ for (let index = 0; index < variableRows.length; index++) {
+ variableInfos.push(extractVariableFromRow(variableRows[index]));
+ }
+
+ return variableInfos;
+}
+
+// From a single row pull out the values we care about
+function extractVariableFromRow(variableHTMLRow: Element): IVariableInfo {
+ const cellElements = variableHTMLRow.querySelectorAll('[role=cell]');
+ return {
+ name: cellElements[0].innerHTML,
+ type: cellElements[1].innerHTML,
+ length: cellElements[2].innerHTML,
+ value: cellElements[3].innerHTML
+ };
+}
+
+// Compare two variable infos
+function compareVariableInfos(expected: IVariableInfo, actual: IVariableInfo) {
+ expect(expected).to.deep.equal(actual, 'Found Variable incorrect');
+}
diff --git a/src/test/datascience/variableView/variableViewTestInterfaces.ts b/src/test/datascience/variableView/variableViewTestInterfaces.ts
new file mode 100644
index 00000000000..bdc793e017b
--- /dev/null
+++ b/src/test/datascience/variableView/variableViewTestInterfaces.ts
@@ -0,0 +1,10 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+'use strict';
+
+import { IVariableViewProvider } from '../../../client/datascience/variablesView/types';
+import { VariableView } from '../../../client/datascience/variablesView/variableView';
+
+export interface ITestVariableViewProvider extends IVariableViewProvider {
+ readonly activeVariableView: Promise;
+}
diff --git a/src/test/datascience/vscodeTestHelpers.ts b/src/test/datascience/vscodeTestHelpers.ts
new file mode 100644
index 00000000000..8e07027552d
--- /dev/null
+++ b/src/test/datascience/vscodeTestHelpers.ts
@@ -0,0 +1,110 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+'use strict';
+import { createDeferred } from '../../client/common/utils/async';
+
+// Basic shape that something needs to support to hook up to this
+interface IOnMessageListener {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ addMessageListener(callback: (message: string, payload: any) => void): void;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ removeMessageListener(callback: (message: string, payload: any) => void): void;
+}
+
+export type WaitForMessageOptions = {
+ /**
+ * Timeout for waiting for message.
+ * Defaults to 65_000ms.
+ *
+ * @type {number}
+ */
+ timeoutMs?: number;
+ /**
+ * Number of times the message should be received.
+ * Defaults to 1.
+ *
+ * @type {number}
+ */
+ numberOfTimes?: number;
+
+ // Optional check for the payload of the message
+ // will only return (or count) message if this returns true
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ withPayload?(payload: any): boolean;
+};
+
+// This class is for usage in .vscode test to hook up to webviews which expose the addMessageListener and removeMessageListener functions
+export class OnMessageListener {
+ private target: IOnMessageListener;
+ constructor(target: IOnMessageListener) {
+ this.target = target;
+ }
+
+ // For our target object wait for a specific message to come in from onMessage function
+ public async waitForMessage(message: string, options?: WaitForMessageOptions): Promise {
+ const timeoutMs = options && options.timeoutMs ? options.timeoutMs : undefined;
+ const numberOfTimes = options && options.numberOfTimes ? options.numberOfTimes : 1;
+
+ const promise = createDeferred();
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let handler: (m: string, p: any) => void;
+ const timer = timeoutMs
+ ? setTimeout(() => {
+ if (!promise.resolved) {
+ promise.reject(new Error(`Waiting for ${message} timed out`));
+ }
+ }, timeoutMs)
+ : undefined;
+ let timesMessageReceived = 0;
+ const dispatchedAction = `DISPATCHED_ACTION_${message}`;
+ // Create the handler that we will hook up to the on message listener
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ handler = (m: string, payload: any) => {
+ if (m === message || m === dispatchedAction) {
+ // First verify the payload matches
+ if (options?.withPayload) {
+ if (!options.withPayload(payload)) {
+ return;
+ }
+ }
+
+ timesMessageReceived += 1;
+ if (timesMessageReceived < numberOfTimes) {
+ return;
+ }
+ if (timer) {
+ clearTimeout(timer);
+ }
+ this.removeMessageListener(handler);
+ if (m === message) {
+ promise.resolve();
+ } else {
+ // It could a redux dispatched message.
+ // Wait for 10ms, wait for other stuff to finish.
+ // We can wait for 100ms or 1s. But thats too long.
+ // The assumption is that currently we do not have any setTimeouts
+ // in UI code that's in the magnitude of 100ms or more.
+ // We do have a couple of setTiemout's, but they wait for 1ms, not 100ms.
+ // 10ms more than sufficient for all the UI timeouts.
+ setTimeout(() => promise.resolve(), 10);
+ }
+ }
+ };
+
+ this.addMessageListener(handler);
+ return promise.promise;
+ }
+
+ // Add the callback on the target
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ public addMessageListener(callback: (m: string, p: any) => void) {
+ this.target.addMessageListener(callback);
+ }
+
+ // Remove the callback on the target
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ public removeMessageListener(callback: (m: string, p: any) => void) {
+ this.target.removeMessageListener(callback);
+ }
+}