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); + } +}