diff --git a/news/3 Code Health/7367.md b/news/3 Code Health/7367.md new file mode 100644 index 000000000000..d186b7357c13 --- /dev/null +++ b/news/3 Code Health/7367.md @@ -0,0 +1 @@ +Test scaffolding for notebook editor. diff --git a/news/3 Code Health/7371.md b/news/3 Code Health/7371.md new file mode 100644 index 000000000000..0f890f88d2da --- /dev/null +++ b/news/3 Code Health/7371.md @@ -0,0 +1 @@ +Tests for the notebook editor for different mime types. diff --git a/src/client/datascience/interactive-common/interactiveBase.ts b/src/client/datascience/interactive-common/interactiveBase.ts index f95c961fe646..55d395391cbc 100644 --- a/src/client/datascience/interactive-common/interactiveBase.ts +++ b/src/client/datascience/interactive-common/interactiveBase.ts @@ -89,6 +89,7 @@ export abstract class InteractiveBase extends WebViewHost | undefined; + private setDark: boolean = false; constructor( @unmanaged() private readonly listeners: IInteractiveWindowListener[], @@ -466,6 +467,9 @@ export abstract class InteractiveBase extends WebViewHost { + if (!this.setDark) { + this.setDark = true; + + // Wait for the web panel to get the isDark setting + const knownDark = await this.isDark(); + + // Before we run any cells, update the dark setting + if (this.notebook) { + await this.notebook.setMatplotLibStyle(knownDark); + } + } + } + private async createNotebook(): Promise { traceInfo('Getting jupyter server options ...'); - // Wait for the webpanel to pass back our current theme darkness - const knownDark = await this.isDark(); - // Extract our options const options = await this.getNotebookOptions(); @@ -1044,11 +1059,6 @@ export abstract class InteractiveBase extends WebViewHost = new Map(); + private pendingSyncs: Map = new Map(); private executedCode: EventEmitter = new EventEmitter(); private activeInteractiveWindowExecuteHandler: Disposable | undefined; constructor( @inject(ILiveShareApi) liveShare: ILiveShareApi, @inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IAsyncDisposableRegistry) asyncRegistry : IAsyncDisposableRegistry, + @inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry, @inject(IDisposableRegistry) private disposables: IDisposableRegistry, @inject(IConfigurationService) private configService: IConfigurationService - ) { + ) { asyncRegistry.push(this); // Create a post office so we can make sure interactive windows are created at the same time @@ -53,15 +53,15 @@ export class InteractiveWindowProvider implements IInteractiveWindowProvider, IA this.id = uuid(); } - public getActive() : IInteractiveWindow | undefined { + public getActive(): IInteractiveWindow | undefined { return this.activeInteractiveWindow; } - public get onExecutedCode() : Event { + public get onExecutedCode(): Event { return this.executedCode.event; } - public async getOrCreateActive() : Promise { + public async getOrCreateActive(): Promise { if (!this.activeInteractiveWindow) { await this.create(); } @@ -77,7 +77,7 @@ export class InteractiveWindowProvider implements IInteractiveWindowProvider, IA throw new Error(localize.DataScience.pythonInteractiveCreateFailed()); } - public async getNotebookOptions() : Promise { + public async getNotebookOptions(): Promise { // Find the settings that we are going to launch our server with const settings = this.configService.getSettings(); let serverURI: string | undefined = settings.datascience.jupyterServerURI; @@ -96,11 +96,11 @@ export class InteractiveWindowProvider implements IInteractiveWindowProvider, IA }; } - public dispose() : Promise { + public dispose(): Promise { return this.postOffice.dispose(); } - private async create() : Promise { + private async create(): Promise { // Set it as soon as we create it. The .ctor for the interactive window // may cause a subclass to talk to the IInteractiveWindowProvider to get the active interactive window. this.activeInteractiveWindow = this.serviceContainer.get(IInteractiveWindow); @@ -110,6 +110,7 @@ export class InteractiveWindowProvider implements IInteractiveWindowProvider, IA this.activeInteractiveWindowExecuteHandler = this.activeInteractiveWindow.onExecutedCode(this.onInteractiveWindowExecute); this.disposables.push(this.activeInteractiveWindowExecuteHandler); await this.activeInteractiveWindow.ready; + return this.activeInteractiveWindow; } private onPeerCountChanged(newCount: number) { @@ -162,7 +163,7 @@ export class InteractiveWindowProvider implements IInteractiveWindowProvider, IA } } - private async synchronizeCreate() : Promise { + private async synchronizeCreate(): Promise { // Create a new pending wait if necessary if (this.postOffice.peerCount > 0 || this.postOffice.role === vsls.Role.Guest) { const key = uuid(); diff --git a/src/client/datascience/jupyter/jupyterNotebook.ts b/src/client/datascience/jupyter/jupyterNotebook.ts index eba1a1a07ab7..a7d068f9e394 100644 --- a/src/client/datascience/jupyter/jupyterNotebook.ts +++ b/src/client/datascience/jupyter/jupyterNotebook.ts @@ -138,6 +138,7 @@ export class JupyterNotebookBase implements INotebook { private pendingCellSubscriptions: CellSubscriber[] = []; private ranInitialSetup = false; private _resource: Uri; + private _disposed: boolean = false; constructor( _liveShare: ILiveShareApi, // This is so the liveshare mixin works @@ -160,8 +161,12 @@ export class JupyterNotebookBase implements INotebook { public dispose(): Promise { traceInfo(`Shutting down session ${this.resource.toString()}`); - const dispose = this.session ? this.session.dispose() : undefined; - return dispose ? dispose : Promise.resolve(); + if (!this._disposed) { + this._disposed = true; + const dispose = this.session ? this.session.dispose() : undefined; + return dispose ? dispose : Promise.resolve(); + } + return Promise.resolve(); } public get resource(): Uri { @@ -615,7 +620,10 @@ export class JupyterNotebookBase implements INotebook { // If the server crashes, cancel the current observable exitHandlerDisposable = this.launchInfo.connectionInfo.disconnected((c) => { const str = c ? c.toString() : ''; - subscriber.error(this.sessionStartTime, new Error(localize.DataScience.jupyterServerCrashed().format(str))); + // Only do an error if we're not disposed. If we're disposed we already shutdown. + if (!this._disposed) { + subscriber.error(this.sessionStartTime, new Error(localize.DataScience.jupyterServerCrashed().format(str))); + } subscriber.complete(this.sessionStartTime); }); } diff --git a/src/client/datascience/webViewHost.ts b/src/client/datascience/webViewHost.ts index eea6229e8981..2e39a71902ef 100644 --- a/src/client/datascience/webViewHost.ts +++ b/src/client/datascience/webViewHost.ts @@ -86,6 +86,15 @@ export class WebViewHost implements IDisposable { } } + public setTheme(isDark: boolean) { + if (this.themeIsDarkPromise && !this.themeIsDarkPromise.resolved) { + this.themeIsDarkPromise.resolve(isDark); + } else { + this.themeIsDarkPromise = createDeferred(); + this.themeIsDarkPromise.resolve(isDark); + } + } + protected reload() { // Make not disposed anymore this.disposed = false; @@ -200,12 +209,7 @@ export class WebViewHost implements IDisposable { @captureTelemetry(Telemetry.WebviewStyleUpdate) private async handleCssRequest(request: IGetCssRequest): Promise { - if (this.themeIsDarkPromise && !this.themeIsDarkPromise.resolved) { - this.themeIsDarkPromise.resolve(request.isDark); - } else { - this.themeIsDarkPromise = createDeferred(); - this.themeIsDarkPromise.resolve(request.isDark); - } + this.setTheme(request.isDark); const settings = this.generateDataScienceExtraSettings(); const isDark = await this.themeFinder.isThemeDark(settings.extraSettings.theme); const css = await this.cssGenerator.generateThemeCss(request.isDark, settings.extraSettings.theme); @@ -214,12 +218,7 @@ export class WebViewHost implements IDisposable { @captureTelemetry(Telemetry.WebviewMonacoStyleUpdate) private async handleMonacoThemeRequest(request: IGetMonacoThemeRequest): Promise { - if (this.themeIsDarkPromise && !this.themeIsDarkPromise.resolved) { - this.themeIsDarkPromise.resolve(request.isDark); - } else { - this.themeIsDarkPromise = createDeferred(); - this.themeIsDarkPromise.resolve(request.isDark); - } + this.setTheme(request.isDark); const settings = this.generateDataScienceExtraSettings(); const monacoTheme = await this.cssGenerator.generateMonacoTheme(request.isDark, settings.extraSettings.theme); return this.postMessageInternal(CssMessages.GetMonacoThemeResponse, { theme: monacoTheme }); diff --git a/src/datascience-ui/interactive-common/cellOutput.tsx b/src/datascience-ui/interactive-common/cellOutput.tsx index 9367d9072d7b..490eba6b9636 100644 --- a/src/datascience-ui/interactive-common/cellOutput.tsx +++ b/src/datascience-ui/interactive-common/cellOutput.tsx @@ -3,10 +3,12 @@ 'use strict'; import '../../client/common/extensions'; +// tslint:disable-next-line: no-var-requires no-require-imports +const ansiToHtml = require('ansi-to-html'); + import { nbformat } from '@jupyterlab/coreutils'; import { JSONObject } from '@phosphor/coreutils'; import ansiRegex from 'ansi-regex'; -import ansiToHtml from 'ansi-to-html'; // tslint:disable-next-line: no-require-imports import cloneDeep = require('lodash/cloneDeep'); import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; @@ -15,6 +17,7 @@ import * as React from 'react'; import { concatMultilineString } from '../../client/datascience/common'; import { Identifiers } from '../../client/datascience/constants'; import { CellState } from '../../client/datascience/types'; +import { ClassType } from '../../client/ioc/types'; import { noop } from '../../test/core'; import { Image, ImageName } from '../react-common/image'; import { ImageButton } from '../react-common/imageButton'; @@ -42,11 +45,26 @@ interface ICellOutput { } // tslint:disable: react-this-binding-issue export class CellOutput extends React.Component { - + // tslint:disable-next-line: no-any + private static ansiToHtmlClass_ctor: ClassType | undefined; constructor(prop: ICellOutputProps) { super(prop); } + // tslint:disable-next-line: no-any + private static get ansiToHtmlClass(): ClassType { + if (!CellOutput.ansiToHtmlClass_ctor) { + // ansiToHtml is different between the tests running and webpack. figure out which one + // tslint:disable-next-line: no-any + if (ansiToHtml instanceof Function) { + CellOutput.ansiToHtmlClass_ctor = ansiToHtml; + } else { + CellOutput.ansiToHtmlClass_ctor = ansiToHtml.default; + } + } + return CellOutput.ansiToHtmlClass_ctor!; + } + private static getAnsiToHtmlOptions() : { fg: string; bg: string; colors: string [] } { // Here's the default colors for ansiToHtml. We need to use the // colors from our current theme. @@ -192,7 +210,7 @@ export class CellOutput extends React.Component { // colorizing if we don't have html that needs around it (ex. <type ='string'>) try { if (ansiRegex().test(formatted)) { - const converter = new ansiToHtml(CellOutput.getAnsiToHtmlOptions()); + const converter = new CellOutput.ansiToHtmlClass(CellOutput.getAnsiToHtmlOptions()); const html = converter.toHtml(formatted); copy.data = { 'text/html': html @@ -208,7 +226,7 @@ export class CellOutput extends React.Component<ICellOutputProps> { renderWithScrollbars = true; const error = copy as nbformat.IError; try { - const converter = new ansiToHtml(CellOutput.getAnsiToHtmlOptions()); + const converter = new CellOutput.ansiToHtmlClass(CellOutput.getAnsiToHtmlOptions()); const trace = converter.toHtml(error.traceback.join('\n')); copy.data = { 'text/html': trace diff --git a/src/datascience-ui/interactive-common/mainStateController.ts b/src/datascience-ui/interactive-common/mainStateController.ts index ad7560d98fdb..623f00b9dd0f 100644 --- a/src/datascience-ui/interactive-common/mainStateController.ts +++ b/src/datascience-ui/interactive-common/mainStateController.ts @@ -106,9 +106,11 @@ export class MainStateController implements IMessageHandler { // Tell the interactive window code we have started. this.postOffice.sendMessage<IInteractiveWindowMapping, 'started'>(InteractiveWindowMessages.Started); - // Get our monaco theme and css - this.postOffice.sendUnsafeMessage(CssMessages.GetCssRequest, { isDark: this.props.expectingDark }); - this.postOffice.sendUnsafeMessage(CssMessages.GetMonacoThemeRequest, { isDark: this.props.expectingDark }); + // Get our monaco theme and css if not running a test, because these make everything async too + if (!this.props.testMode) { + this.postOffice.sendUnsafeMessage(CssMessages.GetCssRequest, { isDark: this.props.expectingDark }); + this.postOffice.sendUnsafeMessage(CssMessages.GetMonacoThemeRequest, { isDark: this.props.expectingDark }); + } } public dispose() { diff --git a/src/datascience-ui/native-editor/nativeEditor.tsx b/src/datascience-ui/native-editor/nativeEditor.tsx index aff7015d9ba1..7abf904796f4 100644 --- a/src/datascience-ui/native-editor/nativeEditor.tsx +++ b/src/datascience-ui/native-editor/nativeEditor.tsx @@ -36,10 +36,12 @@ interface INativeEditorProps { } export class NativeEditor extends React.Component<INativeEditorProps, IMainState> { + // Public so can access it from test code + public stateController: NativeEditorStateController; + private renderCount: number = 0; private mainPanelRef: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>(); private contentPanelScrollRef: React.RefObject<HTMLElement> = React.createRef<HTMLElement>(); private contentPanelRef: React.RefObject<ContentPanel> = React.createRef<ContentPanel>(); - private stateController: NativeEditorStateController; private debounceUpdateVisibleCells = debounce(this.updateVisibleCells.bind(this), 100); private cellRefs: Map<string, React.RefObject<NativeCell>> = new Map<string, React.RefObject<NativeCell>>(); private cellContainerRefs: Map<string, React.RefObject<HTMLDivElement>> = new Map<string, React.RefObject<HTMLDivElement>>(); @@ -82,6 +84,11 @@ export class NativeEditor extends React.Component<INativeEditorProps, IMainState } public render() { + // If in test mode, update our count. Use this to determine how many renders a normal update takes. + if (this.props.testMode) { + this.renderCount = this.renderCount + 1; + } + // Update the state controller with our new state this.stateController.renderUpdate(this.state); const progressBar = this.state.busy && !this.props.testMode ? <Progress /> : undefined; diff --git a/src/datascience-ui/native-editor/nativeEditorStateController.ts b/src/datascience-ui/native-editor/nativeEditorStateController.ts index 873a316c255b..e79d056c89d4 100644 --- a/src/datascience-ui/native-editor/nativeEditorStateController.ts +++ b/src/datascience-ui/native-editor/nativeEditorStateController.ts @@ -84,7 +84,7 @@ export class NativeEditorStateController extends MainStateController { this.resumeUpdates(); } - public addNewCell = () => { + public addNewCell = (): ICellViewModel | undefined => { const cells = this.getState().cellVMs; this.suspendUpdates(); const id = uuid(); @@ -95,6 +95,7 @@ export class NativeEditorStateController extends MainStateController { vm.useQuickEdit = false; } this.resumeUpdates(); + return vm; } public runAbove = (cellId?: string) => { diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index c862eac3c6f3..455e28691060 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -111,12 +111,10 @@ import { } from '../../client/datascience/interactive-common/intellisense/dotNetIntellisenseProvider'; import { NativeEditor } from '../../client/datascience/interactive-ipynb/nativeEditor'; import { NativeEditorCommandListener } from '../../client/datascience/interactive-ipynb/nativeEditorCommandListener'; -import { NativeEditorProvider } from '../../client/datascience/interactive-ipynb/nativeEditorProvider'; import { InteractiveWindow } from '../../client/datascience/interactive-window/interactiveWindow'; import { InteractiveWindowCommandListener } from '../../client/datascience/interactive-window/interactiveWindowCommandListener'; -import { InteractiveWindowProvider } from '../../client/datascience/interactive-window/interactiveWindowProvider'; import { JupyterCommandFactory } from '../../client/datascience/jupyter/jupyterCommand'; import { JupyterDebugger } from '../../client/datascience/jupyter/jupyterDebugger'; import { JupyterExecutionFactory } from '../../client/datascience/jupyter/jupyterExecutionFactory'; @@ -254,6 +252,8 @@ import { MockLanguageServer } from './mockLanguageServer'; import { MockLanguageServerAnalysisOptions } from './mockLanguageServerAnalysisOptions'; import { MockLiveShareApi } from './mockLiveShare'; import { blurWindow, createMessageEvent } from './reactHelpers'; +import { TestInteractiveWindowProvider } from './testInteractiveWindowProvider'; +import { TestNativeEditorProvider } from './testNativeEditorProvider'; export class DataScienceIocContainer extends UnitTestIocContainer { @@ -337,7 +337,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { public registerDataScienceTypes() { this.registerFileSystemTypes(); this.serviceManager.addSingleton<IJupyterExecution>(IJupyterExecution, JupyterExecutionFactory); - this.serviceManager.addSingleton<IInteractiveWindowProvider>(IInteractiveWindowProvider, InteractiveWindowProvider); + this.serviceManager.addSingleton<IInteractiveWindowProvider>(IInteractiveWindowProvider, TestInteractiveWindowProvider); this.serviceManager.addSingleton<IDataViewerProvider>(IDataViewerProvider, DataViewerProvider); this.serviceManager.addSingleton<IPlotViewerProvider>(IPlotViewerProvider, PlotViewerProvider); this.serviceManager.addSingleton<ILogger>(ILogger, Logger); @@ -367,7 +367,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.serviceManager.addSingleton<IJupyterDebugger>(IJupyterDebugger, JupyterDebugger); this.serviceManager.addSingleton<IDebugLocationTracker>(IDebugLocationTracker, DebugLocationTracker); this.serviceManager.addSingleton<IDebugLocationTrackerFactory>(IDebugLocationTrackerFactory, DebugLocationTrackerFactory); - this.serviceManager.addSingleton<INotebookEditorProvider>(INotebookEditorProvider, NativeEditorProvider); + this.serviceManager.addSingleton<INotebookEditorProvider>(INotebookEditorProvider, TestNativeEditorProvider); this.serviceManager.add<INotebookEditor>(INotebookEditor, NativeEditor); this.serviceManager.addSingleton<IDataScienceCommandListener>(IDataScienceCommandListener, NativeEditorCommandListener); diff --git a/src/test/datascience/debugger.functional.test.tsx b/src/test/datascience/debugger.functional.test.tsx index 5741184f97b6..a4e445f18cae 100644 --- a/src/test/datascience/debugger.functional.test.tsx +++ b/src/test/datascience/debugger.functional.test.tsx @@ -16,20 +16,15 @@ import { IProcessServiceFactory, Output } from '../../client/common/process/type import { createDeferred, waitForPromise } from '../../client/common/utils/async'; import { noop } from '../../client/common/utils/misc'; import { EXTENSION_ROOT_DIR } from '../../client/constants'; -import { - InteractiveWindowMessageListener -} from '../../client/datascience/interactive-common/interactiveWindowMessageListener'; -import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; import { IDataScienceCodeLensProvider, IDebugLocationTrackerFactory, - IInteractiveWindow, IInteractiveWindowProvider, IJupyterExecution } from '../../client/datascience/types'; import { InteractivePanel } from '../../datascience-ui/history-react/interactivePanel'; import { DataScienceIocContainer } from './dataScienceIocContainer'; -import { getCellResults } from './interactiveWindowTestHelpers'; +import { getInteractiveCellResults, getOrCreateInteractiveWindow } from './interactiveWindowTestHelpers'; import { getConnectionInfo, getNotebookCapableInterpreter } from './jupyterHelpers'; import { MockDebuggerService } from './mockDebugService'; import { MockDocument } from './mockDocument'; @@ -135,19 +130,6 @@ suite('DataScience Debugger tests', () => { return result; } - async function getOrCreateInteractiveWindow(): Promise<IInteractiveWindow> { - const interactiveWindowProvider = ioc.get<IInteractiveWindowProvider>(IInteractiveWindowProvider); - const result = await interactiveWindowProvider.getOrCreateActive(); - - // During testing the MainPanel sends the init message before our interactive window is created. - // Pretend like it's happening now - // tslint:disable-next-line: no-any - const listener = ((result as any).messageListener) as InteractiveWindowMessageListener; - listener.onMessage(InteractiveWindowMessages.Started, {}); - - return result; - } - async function debugCell(code: string, breakpoint?: Range, breakpointFile?: string, expectError?: boolean) : Promise<void> { // Create a dummy document with just this code const docManager = ioc.get<IDocumentManager>(IDocumentManager) as MockDocumentManager; @@ -168,12 +150,12 @@ suite('DataScience Debugger tests', () => { } // Start the jupyter server - const history = await getOrCreateInteractiveWindow(); + const history = await getOrCreateInteractiveWindow(ioc); const expectedBreakLine = breakpoint && !breakpointFile ? breakpoint.start.line : 2; // 2 because of the 'breakpoint()' that gets added // Debug this code. We should either hit the breakpoint or stop on entry - const resultPromise = getCellResults(ioc.wrapper!, 5, async () => { + const resultPromise = getInteractiveCellResults(ioc.wrapper!, 5, async () => { const breakPromise = createDeferred<void>(); disposables.push(mockDebuggerService!.onBreakpointHit(() => breakPromise.resolve())); const done = history.debugCode(code, fileName, 0, docManager.activeTextEditor); @@ -286,11 +268,11 @@ suite('DataScience Debugger tests', () => { mockDoc.forceUntitled(); // Start the jupyter server - const history = await getOrCreateInteractiveWindow(); + const history = await getOrCreateInteractiveWindow(ioc); const expectedBreakLine = 2; // 2 because of the 'breakpoint()' that gets added // Debug this code. We should either hit the breakpoint or stop on entry - const resultPromise = getCellResults(ioc.wrapper!, 5, async () => { + const resultPromise = getInteractiveCellResults(ioc.wrapper!, 5, async () => { const breakPromise = createDeferred<void>(); disposables.push(mockDebuggerService!.onBreakpointHit(() => breakPromise.resolve())); const done = history.debugCode(code, fileName, 0, docManager.activeTextEditor); diff --git a/src/test/datascience/intellisense.functional.test.tsx b/src/test/datascience/intellisense.functional.test.tsx index b65047452ac6..74bc82aec2e7 100644 --- a/src/test/datascience/intellisense.functional.test.tsx +++ b/src/test/datascience/intellisense.functional.test.tsx @@ -7,13 +7,11 @@ import { IDisposable } from 'monaco-editor'; import { Disposable } from 'vscode'; import { createDeferred } from '../../client/common/utils/async'; -import { InteractiveWindowMessageListener } from '../../client/datascience/interactive-common/interactiveWindowMessageListener'; -import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; -import { IInteractiveWindow, IInteractiveWindowProvider } from '../../client/datascience/types'; import { MonacoEditor } from '../../datascience-ui/react-common/monacoEditor'; import { noop } from '../core'; import { DataScienceIocContainer } from './dataScienceIocContainer'; -import { getEditor, runMountedTest, typeCode } from './interactiveWindowTestHelpers'; +import { getOrCreateInteractiveWindow, runMountedTest } from './interactiveWindowTestHelpers'; +import { getEditor, typeCode } from './testHelpers'; // tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string suite('DataScience Intellisense tests', () => { @@ -45,18 +43,6 @@ suite('DataScience Intellisense tests', () => { // asyncDump(); // }); - async function getOrCreateInteractiveWindow(): Promise<IInteractiveWindow> { - const interactiveWindowProvider = ioc.get<IInteractiveWindowProvider>(IInteractiveWindowProvider); - const result = await interactiveWindowProvider.getOrCreateActive(); - - // During testing the MainPanel sends the init message before our interactive window is created. - // Pretend like it's happening now - const listener = ((result as any).messageListener) as InteractiveWindowMessageListener; - listener.onMessage(InteractiveWindowMessages.Started, {}); - - return result; - } - function getIntellisenseTextLines(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) : string[] { assert.ok(wrapper); const editor = getEditor(wrapper); @@ -116,7 +102,7 @@ suite('DataScience Intellisense tests', () => { runMountedTest('Simple autocomplete', async (wrapper) => { // Create an interactive window so that it listens to the results. - const interactiveWindow = await getOrCreateInteractiveWindow(); + const interactiveWindow = await getOrCreateInteractiveWindow(ioc); await interactiveWindow.show(); // Then enter some code. Don't submit, we're just testing that autocomplete appears @@ -132,7 +118,7 @@ suite('DataScience Intellisense tests', () => { // This test only works when mocking. // Create an interactive window so that it listens to the results. - const interactiveWindow = await getOrCreateInteractiveWindow(); + const interactiveWindow = await getOrCreateInteractiveWindow(ioc); await interactiveWindow.show(); // Then enter some code. Don't submit, we're just testing that autocomplete appears @@ -149,7 +135,7 @@ suite('DataScience Intellisense tests', () => { // This test only works when mocking. // Create an interactive window so that it listens to the results. - const interactiveWindow = await getOrCreateInteractiveWindow(); + const interactiveWindow = await getOrCreateInteractiveWindow(ioc); await interactiveWindow.show(); // Force a timeout on the jupyter completions diff --git a/src/test/datascience/interactiveWindow.functional.test.tsx b/src/test/datascience/interactiveWindow.functional.test.tsx index 830129e179b7..fbfec23aeb75 100644 --- a/src/test/datascience/interactiveWindow.functional.test.tsx +++ b/src/test/datascience/interactiveWindow.functional.test.tsx @@ -15,18 +15,21 @@ import { noop } from '../../client/common/utils/misc'; import { generateCellsFromDocument } from '../../client/datascience/cellFactory'; import { concatMultilineString } from '../../client/datascience/common'; import { EditorContexts } from '../../client/datascience/constants'; -import { - InteractiveWindowMessageListener -} from '../../client/datascience/interactive-common/interactiveWindowMessageListener'; -import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; import { InteractiveWindow } from '../../client/datascience/interactive-window/interactiveWindow'; -import { IInteractiveWindow, IInteractiveWindowProvider } from '../../client/datascience/types'; import { InteractivePanel } from '../../datascience-ui/history-react/interactivePanel'; import { ImageButton } from '../../datascience-ui/react-common/imageButton'; import { DataScienceIocContainer } from './dataScienceIocContainer'; import { createDocument } from './editor-integration/helpers'; import { addCode, + getInteractiveCellResults, + getOrCreateInteractiveWindow, + runMountedTest +} from './interactiveWindowTestHelpers'; +import { MockDocumentManager } from './mockDocumentManager'; +import { MockEditor } from './mockTextEditor'; +import { waitForUpdate } from './reactHelpers'; +import { addContinuousMockData, addMockData, CellInputState, @@ -35,19 +38,14 @@ import { enterInput, escapePath, findButton, - getCellResults, getLastOutputCell, initialDataScienceSettings, - runMountedTest, srcDirectory, toggleCellExpansion, updateDataScienceSettings, verifyHtmlOnCell, verifyLastCellInputState -} from './interactiveWindowTestHelpers'; -import { MockDocumentManager } from './mockDocumentManager'; -import { MockEditor } from './mockTextEditor'; -import { waitForUpdate } from './reactHelpers'; +} from './testHelpers'; //import { asyncDump } from '../common/asyncDump'; // tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string @@ -79,18 +77,6 @@ suite('DataScience Interactive Window output tests', () => { // asyncDump(); // }); - async function getOrCreateInteractiveWindow(): Promise<IInteractiveWindow> { - const interactiveWindowProvider = ioc.get<IInteractiveWindowProvider>(IInteractiveWindowProvider); - const result = await interactiveWindowProvider.getOrCreateActive(); - - // During testing the MainPanel sends the init message before our interactive window is created. - // Pretend like it's happening now - const listener = ((result as any).messageListener) as InteractiveWindowMessageListener; - listener.onMessage(InteractiveWindowMessages.Started, {}); - - return result; - } - async function waitForMessageResponse(action: () => void): Promise<void> { ioc.wrapperCreatedPromise = createDeferred<boolean>(); action(); @@ -99,77 +85,77 @@ suite('DataScience Interactive Window output tests', () => { } runMountedTest('Simple text', async (wrapper) => { - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); + await addCode(ioc, wrapper, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); }, () => { return ioc; }); runMountedTest('Hide inputs', async (wrapper) => { initialDataScienceSettings({ ...defaultDataScienceSettings(), showCellInputCode: false }); - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); + await addCode(ioc, wrapper, 'a=1\na'); - verifyLastCellInputState(wrapper, CellInputState.Hidden); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Hidden); // Add a cell without output, this cell should not show up at all addMockData(ioc, 'a=1', undefined, 'text/plain'); - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1', 4); + await addCode(ioc, wrapper, 'a=1', 4); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.First); - verifyHtmlOnCell(wrapper, undefined, CellPosition.Last); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.First); + verifyHtmlOnCell(wrapper, 'InteractiveCell', undefined, CellPosition.Last); }, () => { return ioc; }); runMountedTest('Show inputs', async (wrapper) => { initialDataScienceSettings({ ...defaultDataScienceSettings() }); - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); + await addCode(ioc, wrapper, 'a=1\na'); - verifyLastCellInputState(wrapper, CellInputState.Visible); - verifyLastCellInputState(wrapper, CellInputState.Collapsed); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Visible); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Collapsed); }, () => { return ioc; }); runMountedTest('Expand inputs', async (wrapper) => { initialDataScienceSettings({ ...defaultDataScienceSettings(), collapseCellInputCodeByDefault: false }); - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); + await addCode(ioc, wrapper, 'a=1\na'); - verifyLastCellInputState(wrapper, CellInputState.Expanded); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Expanded); }, () => { return ioc; }); runMountedTest('Collapse / expand cell', async (wrapper) => { initialDataScienceSettings({ ...defaultDataScienceSettings() }); - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); + await addCode(ioc, wrapper, 'a=1\na'); - verifyLastCellInputState(wrapper, CellInputState.Visible); - verifyLastCellInputState(wrapper, CellInputState.Collapsed); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Visible); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Collapsed); - toggleCellExpansion(wrapper); + toggleCellExpansion(wrapper, 'InteractiveCell'); - verifyLastCellInputState(wrapper, CellInputState.Visible); - verifyLastCellInputState(wrapper, CellInputState.Expanded); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Visible); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Expanded); - toggleCellExpansion(wrapper); + toggleCellExpansion(wrapper, 'InteractiveCell'); - verifyLastCellInputState(wrapper, CellInputState.Visible); - verifyLastCellInputState(wrapper, CellInputState.Collapsed); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Visible); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Collapsed); }, () => { return ioc; }); runMountedTest('Hide / show cell', async (wrapper) => { initialDataScienceSettings({ ...defaultDataScienceSettings() }); - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); + await addCode(ioc, wrapper, 'a=1\na'); - verifyLastCellInputState(wrapper, CellInputState.Visible); - verifyLastCellInputState(wrapper, CellInputState.Collapsed); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Visible); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Collapsed); // Hide the inputs and verify - updateDataScienceSettings(wrapper, { ...defaultDataScienceSettings(), showCellInputCode: false }); + updateDataScienceSettings(wrapper, InteractivePanel, { ...defaultDataScienceSettings(), showCellInputCode: false }); - verifyLastCellInputState(wrapper, CellInputState.Hidden); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Hidden); // Show the inputs and verify - updateDataScienceSettings(wrapper, { ...defaultDataScienceSettings(), showCellInputCode: true }); + updateDataScienceSettings(wrapper, InteractivePanel, { ...defaultDataScienceSettings(), showCellInputCode: true }); - verifyLastCellInputState(wrapper, CellInputState.Visible); - verifyLastCellInputState(wrapper, CellInputState.Collapsed); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Visible); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Collapsed); }, () => { return ioc; }); runMountedTest('Mime Types', async (wrapper) => { @@ -212,27 +198,27 @@ for _ in range(50): return Promise.resolve({ result: result, haveMore: loops > 0 }); }); - await addCode(getOrCreateInteractiveWindow, wrapper, badPanda, 4, true); - verifyHtmlOnCell(wrapper, `has no attribute 'read'`, CellPosition.Last); + await addCode(ioc, wrapper, badPanda, 4, true); + verifyHtmlOnCell(wrapper, 'InteractiveCell', `has no attribute 'read'`, CellPosition.Last); - await addCode(getOrCreateInteractiveWindow, wrapper, goodPanda); - verifyHtmlOnCell(wrapper, `<td>`, CellPosition.Last); + await addCode(ioc, wrapper, goodPanda); + verifyHtmlOnCell(wrapper, 'InteractiveCell', `<td>`, CellPosition.Last); - await addCode(getOrCreateInteractiveWindow, wrapper, matPlotLib); - verifyHtmlOnCell(wrapper, matPlotLibResults, CellPosition.Last); + await addCode(ioc, wrapper, matPlotLib); + verifyHtmlOnCell(wrapper, 'InteractiveCell', matPlotLibResults, CellPosition.Last); - await addCode(getOrCreateInteractiveWindow, wrapper, spinningCursor, 4 + (ioc.mockJupyter ? (cursors.length * 3) : 0)); - verifyHtmlOnCell(wrapper, '<div>', CellPosition.Last); + await addCode(ioc, wrapper, spinningCursor, 4 + (ioc.mockJupyter ? (cursors.length * 3) : 0)); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<div>', CellPosition.Last); }, () => { return ioc; }); runMountedTest('Undo/redo commands', async (wrapper) => { - const interactiveWindow = await getOrCreateInteractiveWindow(); + const interactiveWindow = await getOrCreateInteractiveWindow(ioc); // Get a cell into the list - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); + await addCode(ioc, wrapper, 'a=1\na'); // Now verify if we undo, we have no cells - let afterUndo = await getCellResults(wrapper, 1, () => { + let afterUndo = await getInteractiveCellResults(wrapper, 1, () => { interactiveWindow.undoCells(); return Promise.resolve(); }); @@ -240,25 +226,25 @@ for _ in range(50): assert.equal(afterUndo.length, 1, `Undo should remove cells + ${afterUndo.debug()}`); // Redo should put the cells back - const afterRedo = await getCellResults(wrapper, 1, () => { + const afterRedo = await getInteractiveCellResults(wrapper, 1, () => { interactiveWindow.redoCells(); return Promise.resolve(); }); assert.equal(afterRedo.length, 2, 'Redo should put cells back'); // Get another cell into the list - const afterAdd = await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); + const afterAdd = await addCode(ioc, wrapper, 'a=1\na'); assert.equal(afterAdd.length, 3, 'Second cell did not get added'); // Clear everything - const afterClear = await getCellResults(wrapper, 1, () => { + const afterClear = await getInteractiveCellResults(wrapper, 1, () => { interactiveWindow.removeAllCells(); return Promise.resolve(); }); assert.equal(afterClear.length, 1, 'Clear didn\'t work'); // Undo should put them back - afterUndo = await getCellResults(wrapper, 1, () => { + afterUndo = await getInteractiveCellResults(wrapper, 1, () => { interactiveWindow.undoCells(); return Promise.resolve(); }); @@ -282,15 +268,15 @@ for _ in range(50): ioc.serviceManager.rebindInstance<IDocumentManager>(IDocumentManager, docManager.object); // Get a cell into the list - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); + await addCode(ioc, wrapper, 'a=1\na'); // 'Click' the buttons in the react control - const undo = findButton(wrapper, 2); - const redo = findButton(wrapper, 1); - const clear = findButton(wrapper, 0); + const undo = findButton(wrapper, InteractivePanel, 2); + const redo = findButton(wrapper, InteractivePanel, 1); + const clear = findButton(wrapper, InteractivePanel, 0); // Now verify if we undo, we have no cells - let afterUndo = await getCellResults(wrapper, 1, () => { + let afterUndo = await getInteractiveCellResults(wrapper, 1, () => { undo!.simulate('click'); return Promise.resolve(); }); @@ -298,25 +284,25 @@ for _ in range(50): assert.equal(afterUndo.length, 1, `Undo should remove cells`); // Redo should put the cells back - const afterRedo = await getCellResults(wrapper, 1, async () => { + const afterRedo = await getInteractiveCellResults(wrapper, 1, async () => { redo!.simulate('click'); return Promise.resolve(); }); assert.equal(afterRedo.length, 2, 'Redo should put cells back'); // Get another cell into the list - const afterAdd = await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); + const afterAdd = await addCode(ioc, wrapper, 'a=1\na'); assert.equal(afterAdd.length, 3, 'Second cell did not get added'); // Clear everything - const afterClear = await getCellResults(wrapper, 1, async () => { + const afterClear = await getInteractiveCellResults(wrapper, 1, async () => { clear!.simulate('click'); return Promise.resolve(); }); assert.equal(afterClear.length, 1, 'Clear didn\'t work'); // Undo should put them back - afterUndo = await getCellResults(wrapper, 1, async () => { + afterUndo = await getInteractiveCellResults(wrapper, 1, async () => { undo!.simulate('click'); return Promise.resolve(); }); @@ -336,7 +322,7 @@ for _ in range(50): assert.ok(showedEditor.resolved, 'Goto source is not jumping to editor'); // Make sure delete works - const afterDelete = await getCellResults(wrapper, 1, async () => { + const afterDelete = await getInteractiveCellResults(wrapper, 1, async () => { deleteButton.simulate('click'); return Promise.resolve(); }); @@ -360,19 +346,19 @@ for _ in range(50): ioc.serviceManager.rebindInstance<IApplicationShell>(IApplicationShell, appShell.object); // Make sure to create the interactive window after the rebind or it gets the wrong application shell. - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - const interactiveWindow = await getOrCreateInteractiveWindow(); + await addCode(ioc, wrapper, 'a=1\na'); + const interactiveWindow = await getOrCreateInteractiveWindow(ioc); // Export should cause exportCalled to change to true await waitForMessageResponse(() => interactiveWindow.exportCells()); assert.equal(exportCalled, true, 'Export is not being called during export'); // Remove the cell - const exportButton = findButton(wrapper, 5); - const undo = findButton(wrapper, 2); + const exportButton = findButton(wrapper, InteractivePanel, 5); + const undo = findButton(wrapper, InteractivePanel, 2); // Now verify if we undo, we have no cells - const afterUndo = await getCellResults(wrapper, 1, () => { + const afterUndo = await getInteractiveCellResults(wrapper, 1, () => { undo!.simulate('click'); return Promise.resolve(); }); @@ -389,11 +375,11 @@ for _ in range(50): runMountedTest('Dispose test', async () => { // tslint:disable-next-line:no-any - const interactiveWindow = await getOrCreateInteractiveWindow(); + const interactiveWindow = await getOrCreateInteractiveWindow(ioc); await interactiveWindow.show(); // Have to wait for the load to finish await interactiveWindow.dispose(); // tslint:disable-next-line:no-any - const h2 = await getOrCreateInteractiveWindow(); + const h2 = await getOrCreateInteractiveWindow(ioc); // Check equal and then dispose so the test goes away const equal = Object.is(interactiveWindow, h2); await h2.show(); @@ -407,7 +393,7 @@ for _ in range(50): assert.equal(ioc.getContext(EditorContexts.HaveRedoableCells), false, 'Should not have redoable before starting'); // Verify we can send different commands to the UI and it will respond - const interactiveWindow = await getOrCreateInteractiveWindow(); + const interactiveWindow = await getOrCreateInteractiveWindow(ioc); // Get an update promise so we can wait for the add code const updatePromise = waitForUpdate(wrapper, InteractivePanel); @@ -464,12 +450,12 @@ for _ in range(50): runMountedTest('Simple input', async (wrapper) => { // Create an interactive window so that it listens to the results. - const interactiveWindow = await getOrCreateInteractiveWindow(); + const interactiveWindow = await getOrCreateInteractiveWindow(ioc); await interactiveWindow.show(); // Then enter some code. - await enterInput(wrapper, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); + await enterInput(wrapper, InteractivePanel, 'a=1\na', 'InteractiveCell'); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); }, () => { return ioc; }); runMountedTest('Copy to source input', async (wrapper) => { @@ -480,13 +466,13 @@ for _ in range(50): editor.setRevealCallback(() => showedEditor.resolve()); // Create an interactive window so that it listens to the results. - const interactiveWindow = await getOrCreateInteractiveWindow(); + const interactiveWindow = await getOrCreateInteractiveWindow(ioc); await interactiveWindow.show(); // Then enter some code. - await enterInput(wrapper, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); - const ImageButtons = getLastOutputCell(wrapper).find(ImageButton); + await enterInput(wrapper, InteractivePanel, 'a=1\na', 'InteractiveCell'); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); + const ImageButtons = getLastOutputCell(wrapper, 'InteractiveCell').find(ImageButton); assert.equal(ImageButtons.length, 4, 'Cell buttons not found'); const copyToSource = ImageButtons.at(2); @@ -499,40 +485,40 @@ for _ in range(50): runMountedTest('Multiple input', async (wrapper) => { // Create an interactive window so that it listens to the results. - const interactiveWindow = await getOrCreateInteractiveWindow(); + const interactiveWindow = await getOrCreateInteractiveWindow(ioc); await interactiveWindow.show(); // Then enter some code. - await enterInput(wrapper, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); + await enterInput(wrapper, InteractivePanel, 'a=1\na', 'InteractiveCell'); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); // Then delete the node - const lastCell = getLastOutputCell(wrapper); + const lastCell = getLastOutputCell(wrapper, 'InteractiveCell'); const ImageButtons = lastCell.find(ImageButton); assert.equal(ImageButtons.length, 4, 'Cell buttons not found'); const deleteButton = ImageButtons.at(3); // Make sure delete works - const afterDelete = await getCellResults(wrapper, 1, async () => { + const afterDelete = await getInteractiveCellResults(wrapper, 1, async () => { deleteButton.simulate('click'); return Promise.resolve(); }); assert.equal(afterDelete.length, 1, `Delete should remove a cell`); // Should be able to enter again - await enterInput(wrapper, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); + await enterInput(wrapper, InteractivePanel, 'a=1\na', 'InteractiveCell'); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); // Try a 3rd time with some new input addMockData(ioc, 'print("hello")', 'hello'); - await enterInput(wrapper, 'print("hello")'); - verifyHtmlOnCell(wrapper, 'hello', CellPosition.Last); + await enterInput(wrapper, InteractivePanel, 'print("hello")', 'InteractiveCell'); + verifyHtmlOnCell(wrapper, 'InteractiveCell', 'hello', CellPosition.Last); }, () => { return ioc; }); runMountedTest('Restart with session failure', async (wrapper) => { // Prime the pump - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); + await addCode(ioc, wrapper, 'a=1\na'); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); // Then something that could possibly timeout addContinuousMockData(ioc, 'import time\r\ntime.sleep(1000)', (_c) => { @@ -548,7 +534,7 @@ for _ in range(50): } // Then try executing our long running cell and restarting in the middle - const interactiveWindow = await getOrCreateInteractiveWindow(); + const interactiveWindow = await getOrCreateInteractiveWindow(ioc); const executed = createDeferred(); // We have to wait until the execute goes through before we reset. interactiveWindow.onExecutedCode(() => executed.resolve()); @@ -559,7 +545,7 @@ for _ in range(50): // Now see if our wrapper still works. Interactive window should have forced a restart await interactiveWindow.addCode('a=1\na', 'foo', 0); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); }, () => { return ioc; }); @@ -576,11 +562,11 @@ for _ in range(50): assert.equal(cells.length, 2, 'Not enough cells generated'); // Run the first cell - await addCode(getOrCreateInteractiveWindow, wrapper, concatMultilineString(cells[0].data.source), 4); + await addCode(ioc, wrapper, concatMultilineString(cells[0].data.source), 4); // Last cell should generate a series of updates. Verify we end up with a single image - await addCode(getOrCreateInteractiveWindow, wrapper, concatMultilineString(cells[1].data.source), 10); - const cell = getLastOutputCell(wrapper); + await addCode(ioc, wrapper, concatMultilineString(cells[1].data.source), 10); + const cell = getLastOutputCell(wrapper, 'InteractiveCell'); const output = cell!.find('div.cell-output'); assert.ok(output.length > 0, 'No output cell found'); @@ -598,9 +584,9 @@ for _ in range(50): ioc.getSettings().datascience.enableGather = true; // Enter some code. const code = '#%%\na=1\na'; - await addCode(getOrCreateInteractiveWindow, wrapper, code); + await addCode(ioc, wrapper, code); addMockData(ioc, code, undefined); - const ImageButtons = getLastOutputCell(wrapper).find(ImageButton); // This isn't rendering correctly + const ImageButtons = getLastOutputCell(wrapper, 'InteractiveCell').find(ImageButton); // This isn't rendering correctly assert.equal(ImageButtons.length, 4, 'Cell buttons not found'); const gatherCode = ImageButtons.at(0); @@ -616,13 +602,13 @@ for _ in range(50): runMountedTest('Gather code run from input box', async (wrapper) => { ioc.getSettings().datascience.enableGather = true; // Create an interactive window so that it listens to the results. - const interactiveWindow = await getOrCreateInteractiveWindow(); + const interactiveWindow = await getOrCreateInteractiveWindow(ioc); await interactiveWindow.show(); // Then enter some code. - await enterInput(wrapper, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); - const ImageButtons = getLastOutputCell(wrapper).find(ImageButton); + await enterInput(wrapper, InteractivePanel, 'a=1\na', 'InteractiveCell'); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); + const ImageButtons = getLastOutputCell(wrapper, 'InteractiveCell').find(ImageButton); assert.equal(ImageButtons.length, 4, 'Cell buttons not found'); const gatherCode = ImageButtons.at(0); @@ -639,7 +625,7 @@ for _ in range(50): ioc.addDocument(`#%%${os.EOL}print("bar")`, 'foo.py'); const docManager = ioc.get<IDocumentManager>(IDocumentManager); docManager.showTextDocument(docManager.textDocuments[0]); - const window = await getOrCreateInteractiveWindow() as InteractiveWindow; + const window = await getOrCreateInteractiveWindow(ioc) as InteractiveWindow; window.copyCode({source: 'print("baz")'}); assert.equal(docManager.textDocuments[0].getText(), `#%%${os.EOL}print("baz")${os.EOL}#%%${os.EOL}print("bar")`, 'Text not inserted'); const activeEditor = docManager.activeTextEditor as MockEditor; @@ -654,8 +640,8 @@ for _ in range(50): // Output should be trimmed to just two lines of output const code = `print("hello\\nworld\\nhow\\nare\\nyou")`; addMockData(ioc, code, 'are\nyou\n'); - await addCode(getOrCreateInteractiveWindow, wrapper, code, 4); + await addCode(ioc, wrapper, code, 4); - verifyHtmlOnCell(wrapper, '>are\nyou', CellPosition.Last); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '>are\nyou', CellPosition.Last); }, () => { return ioc; }); }); diff --git a/src/test/datascience/interactiveWindowTestHelpers.tsx b/src/test/datascience/interactiveWindowTestHelpers.tsx index 4de9bfdbf52d..05bf07e76361 100644 --- a/src/test/datascience/interactiveWindowTestHelpers.tsx +++ b/src/test/datascience/interactiveWindowTestHelpers.tsx @@ -2,33 +2,22 @@ // Licensed under the MIT License. 'use strict'; import * as assert from 'assert'; -import { mount, ReactWrapper } from 'enzyme'; -import { min } from 'lodash'; -import * as path from 'path'; +import { ReactWrapper } from 'enzyme'; import * as React from 'react'; -import { CancellationToken } from 'vscode'; -import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; -import { IDataScienceSettings } from '../../client/common/types'; -import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; -import { IInteractiveWindow, IJupyterExecution } from '../../client/datascience/types'; +import { IInteractiveWindow, IInteractiveWindowProvider, IJupyterExecution } from '../../client/datascience/types'; import { InteractivePanel } from '../../datascience-ui/history-react/interactivePanel'; -import { ImageButton } from '../../datascience-ui/react-common/imageButton'; -import { updateSettings } from '../../datascience-ui/react-common/settingsReactSide'; import { DataScienceIocContainer } from './dataScienceIocContainer'; -import { createInputEvent, createKeyboardEvent, waitForUpdate } from './reactHelpers'; +import { addMockData, getCellResults, mountWebView } from './testHelpers'; -//tslint:disable:trailing-comma no-any no-multiline-string -export enum CellInputState { - Hidden, - Visible, - Collapsed, - Expanded +// tslint:disable-next-line: no-any +export function getInteractiveCellResults(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, expectedRenders: number, updater: () => Promise<void>): Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { + return getCellResults(wrapper, InteractivePanel, 'InteractiveCell', expectedRenders, updater); } -export enum CellPosition { - First = 'first', - Last = 'last' +export function getOrCreateInteractiveWindow(ioc: DataScienceIocContainer): Promise<IInteractiveWindow> { + const interactiveWindowProvider = ioc.get<IInteractiveWindowProvider>(IInteractiveWindowProvider); + return interactiveWindowProvider.getOrCreateActive(); } // tslint:disable-next-line:no-any @@ -47,329 +36,19 @@ export function runMountedTest(name: string, testFunc: (wrapper: ReactWrapper<an }); } -//export async function getOrCreateHistory(ioc: DataScienceIocContainer): Promise<IInteractiveWindow> { - //const interactiveWindowProvider = ioc.get<IInteractiveWindowProvider>(IInteractiveWindowProvider); - //const result = await interactiveWindowProvider.getOrCreateActive(); - - //// During testing the MainPanel sends the init message before our history is created. - //// Pretend like it's happening now - //const listener = ((result as any).messageListener) as InteractiveWindowMessageListener; - //listener.onMessage(InteractiveWindowMessages.Started, {}); - - //return result; -//} - -export function mountWebView(ioc: DataScienceIocContainer, node: React.ReactElement): ReactWrapper<any, Readonly<{}>, React.Component> { - // Setup our webview panel - ioc.createWebView(() => mount(node)); - return ioc.wrapper!; -} - -export function addMockData(ioc: DataScienceIocContainer, code: string, result: string | number | undefined, mimeType?: string, cellType?: string) { - if (ioc.mockJupyter) { - if (cellType && cellType === 'error') { - ioc.mockJupyter.addError(code, result ? result.toString() : ''); - } else { - if (result) { - ioc.mockJupyter.addCell(code, result, mimeType); - } else { - ioc.mockJupyter.addCell(code); - } - } - } -} - -export function addContinuousMockData(ioc: DataScienceIocContainer, code: string, resultGenerator: (c: CancellationToken) => Promise<{ result: string; haveMore: boolean }>) { - if (ioc.mockJupyter) { - ioc.mockJupyter.addContinuousOutputCell(code, resultGenerator); - } -} - -export function getLastOutputCell(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>): ReactWrapper<any, Readonly<{}>, React.Component> { - // Skip the edit cell - const foundResult = wrapper.find('InteractiveCell'); - assert.ok(foundResult.length >= 2, 'Didn\'t find any cells being rendered'); - return foundResult.at(foundResult.length - 2); -} - -export function verifyHtmlOnCell(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, html: string | undefined, cellIndex: number | CellPosition) { - const foundResult = wrapper.find('InteractiveCell'); - assert.ok(foundResult.length >= 1, 'Didn\'t find any cells being rendered'); - - let targetCell: ReactWrapper; - // Get the correct result that we are dealing with - if (typeof cellIndex === 'number') { - if (cellIndex >= 0 && cellIndex <= (foundResult.length - 1)) { - targetCell = foundResult.at(cellIndex); - } - } else if (typeof cellIndex === 'string') { - switch (cellIndex) { - case CellPosition.First: - targetCell = foundResult.first(); - break; - - case CellPosition.Last: - // Skip the input cell on these checks. - targetCell = getLastOutputCell(wrapper); - break; - - default: - // Fall through, targetCell check will fail out - break; - } - } - - // ! is ok here to get rid of undefined type check as we want a fail here if we have not initialized targetCell - assert.ok(targetCell!, 'Target cell doesn\'t exist'); - - // If html is specified, check it - if (html) { - // Extract only the first 100 chars from the input string - const sliced = html.substr(0, min([html.length, 100])); - const output = targetCell!.find('div.cell-output'); - assert.ok(output.length > 0, 'No output cell found'); - const outHtml = output.html(); - assert.ok(outHtml.includes(sliced), `${outHtml} does not contain ${sliced}`); - } else { - const output = targetCell!.find('div.cell-output'); - const outputHtml = output.length > 0 ? output.html() : 'empty'; - // html not specified, look for an empty render - assert.ok(targetCell!.isEmptyRender(), `Target cell is not empty render, got this instead: ${outputHtml}`); - } -} - -export function verifyLastCellInputState(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, state: CellInputState) { - - const lastCell = getLastOutputCell(wrapper); - assert.ok(lastCell, 'Last call doesn\'t exist'); - - const inputBlock = lastCell.find('div.cell-input'); - const toggleButton = lastCell.find('polygon.collapse-input-svg'); - - switch (state) { - case CellInputState.Hidden: - assert.ok(inputBlock.length === 0, 'Cell input not hidden'); - break; - - case CellInputState.Visible: - assert.ok(inputBlock.length === 1, 'Cell input not visible'); - break; - - case CellInputState.Expanded: - assert.ok(toggleButton.html().includes('collapse-input-svg-rotate'), 'Cell input toggle not expanded'); - break; - - case CellInputState.Collapsed: - assert.ok(!toggleButton.html().includes('collapse-input-svg-rotate'), 'Cell input toggle not collapsed'); - break; - - default: - assert.fail('Unknown cellInputStat'); - break; - } -} - -export async function getCellResults(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, expectedRenders: number, updater: () => Promise<void>): Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { - - // Get a render promise with the expected number of renders - const renderPromise = waitForUpdate(wrapper, InteractivePanel, expectedRenders); - - // Call our function to update the react control - await updater(); - - // Wait for all of the renders to go through - await renderPromise; - - // Return the result - return wrapper.find('InteractiveCell'); -} - -export async function addCode(interactiveWindowProvider: () => Promise<IInteractiveWindow>, wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, code: string, expectedRenderCount: number = 4, expectError: boolean = false): Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { +// tslint:disable-next-line: no-any +export async function addCode(ioc: DataScienceIocContainer, wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, code: string, expectedRenderCount: number = 4, expectError: boolean = false): Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { // Adding code should cause 5 renders to happen. // 1) Input // 2) Status ready // 3) Execute_Input message // 4) Output message (if there's only one) // 5) Status finished - return getCellResults(wrapper, expectedRenderCount, async () => { - const history = await interactiveWindowProvider(); + return getInteractiveCellResults(wrapper, expectedRenderCount, async () => { + const history = await getOrCreateInteractiveWindow(ioc); const success = await history.addCode(code, 'foo.py', 2); if (expectError) { assert.equal(success, false, `${code} did not produce an error`); } }); } - -function simulateKey(domNode: HTMLTextAreaElement, key: string, shiftDown?: boolean) { - // Submit a keypress into the textarea. Simulate doesn't work here because the keydown - // handler is not registered in any react code. It's being handled with DOM events - - // According to this: - // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Usage_notes - // The normal events are - // 1) keydown - // 2) keypress - // 3) keyup - let event = createKeyboardEvent('keydown', { key, code: key, shiftKey: shiftDown }); - - // Dispatch. Result can be swallowed. If so skip the next event. - let result = domNode.dispatchEvent(event); - if (result) { - event = createKeyboardEvent('keypress', { key, code: key, shiftKey: shiftDown }); - result = domNode.dispatchEvent(event); - if (result) { - event = createKeyboardEvent('keyup', { key, code: key, shiftKey: shiftDown }); - domNode.dispatchEvent(event); - - // Update our value. This will reset selection to zero. - domNode.value = domNode.value + key; - - // Tell the dom node its selection start has changed. Monaco - // reads this to determine where the character went. - domNode.selectionEnd = domNode.value.length; - domNode.selectionStart = domNode.value.length; - - // Dispatch an input event so we update the textarea - domNode.dispatchEvent(createInputEvent()); - } - } - -} - -async function submitInput(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, textArea: HTMLTextAreaElement): Promise<void> { - // Get a render promise with the expected number of renders (how many updates a the shift + enter will cause) - // Should be 6 - 1 for the shift+enter and 5 for the new cell. - const renderPromise = waitForUpdate(wrapper, InteractivePanel, 6); - - // Submit a keypress into the textarea - simulateKey(textArea, 'Enter', true); - - return renderPromise; -} - -function enterKey(_wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, textArea: HTMLTextAreaElement, key: string) { - // Simulate a key press - simulateKey(textArea, key); -} - -export function getEditor(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) : ReactWrapper<any, Readonly<{}>, React.Component> { - // Find the last cell. It should have a monacoEditor object - const cells = wrapper.find('InteractiveCell'); - const lastCell = cells.last(); - return lastCell.find('MonacoEditor'); -} - -export function typeCode(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, code: string) : HTMLTextAreaElement | null { - - // Find the last cell. It should have a monacoEditor object. We need to search - // through its DOM to find the actual textarea to send input to - // (we can't actually find it with the enzyme wrappers because they only search - // React accessible nodes and the monaco html is not react) - const editorControl = getEditor(wrapper); - const ecDom = editorControl.getDOMNode(); - assert.ok(ecDom, 'ec DOM object not found'); - const textArea = ecDom!.querySelector('.overflow-guard')!.querySelector('textarea'); - assert.ok(textArea!, 'Cannot find the textarea inside the monaco editor'); - textArea!.focus(); - - // Now simulate entering all of the keys - for (let i = 0; i < code.length; i += 1) { - enterKey(wrapper, textArea!, code.charAt(i)); - } - - return textArea; -} - -export async function enterInput(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, code: string): Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { - - // First we have to type the code into the input box - const textArea = typeCode(wrapper, code); - - // Now simulate a shift enter. This should cause a new cell to be added - await submitInput(wrapper, textArea!); - - // Return the result - return wrapper.find('InteractiveCell'); -} - -export function findButton(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, index: number): ReactWrapper<any, Readonly<{}>, React.Component> | undefined { - const mainObj = wrapper.find(InteractivePanel); - if (mainObj) { - const buttons = mainObj.find(ImageButton); - if (buttons) { - return buttons.at(index); - } - } -} - -// The default base set of data science settings to use -export function defaultDataScienceSettings(): IDataScienceSettings { - return { - allowImportFromNotebook: true, - jupyterLaunchTimeout: 10, - jupyterLaunchRetries: 3, - enabled: true, - jupyterServerURI: 'local', - notebookFileRoot: 'WORKSPACE', - changeDirOnImportExport: true, - useDefaultConfigForJupyter: true, - jupyterInterruptTimeout: 10000, - searchForJupyter: true, - showCellInputCode: true, - collapseCellInputCodeByDefault: true, - allowInput: true, - maxOutputSize: 400, - errorBackgroundColor: '#FFFFFF', - sendSelectionToInteractiveWindow: false, - showJupyterVariableExplorer: true, - variableExplorerExclude: 'module;function;builtin_function_or_method', - codeRegularExpression: '^(#\\s*%%|#\\s*\\<codecell\\>|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])', - markdownRegularExpression: '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\<markdowncell\\>)', - enablePlotViewer: true, - runStartupCommands: '', - debugJustMyCode: true - }; -} - -// Set initial data science settings to use for a test (initially loaded via settingsReactSide.ts) -export function initialDataScienceSettings(newSettings: IDataScienceSettings) { - const settingsString = JSON.stringify(newSettings); - updateSettings(settingsString); -} - -export function getMainPanel(wrapper: ReactWrapper<any, Readonly<{}>>): InteractivePanel | undefined { - const mainObj = wrapper.find(InteractivePanel); - if (mainObj) { - return mainObj.instance() as InteractivePanel; - } - - return undefined; -} - -// Update data science settings while running (goes through the UpdateSettings channel) -export function updateDataScienceSettings(wrapper: ReactWrapper<any, Readonly<{}>>, newSettings: IDataScienceSettings) { - const settingsString = JSON.stringify(newSettings); - const mainPanel = getMainPanel(wrapper); - if (mainPanel) { - mainPanel.stateController.handleMessage(InteractiveWindowMessages.UpdateSettings, settingsString); - } - wrapper.update(); -} - -export function toggleCellExpansion(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) { - // Find the last cell added - const lastCell = getLastOutputCell(wrapper); - assert.ok(lastCell, 'Last call doesn\'t exist'); - - const toggleButton = lastCell.find('button.collapse-input'); - assert.ok(toggleButton); - toggleButton.simulate('click'); -} - -export function escapePath(p: string) { - return p.replace(/\\/g, '\\\\'); -} - -export function srcDirectory() { - return path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); -} diff --git a/src/test/datascience/liveshare.functional.test.tsx b/src/test/datascience/liveshare.functional.test.tsx index 4ef8016cc773..27ea390bac19 100644 --- a/src/test/datascience/liveshare.functional.test.tsx +++ b/src/test/datascience/liveshare.functional.test.tsx @@ -28,8 +28,8 @@ import { InteractivePanel } from '../../datascience-ui/history-react/interactive import { asyncDump } from '../common/asyncDump'; import { DataScienceIocContainer } from './dataScienceIocContainer'; import { createDocument } from './editor-integration/helpers'; -import { addMockData, CellPosition, verifyHtmlOnCell } from './interactiveWindowTestHelpers'; import { waitForUpdate } from './reactHelpers'; +import { addMockData, CellPosition, verifyHtmlOnCell } from './testHelpers'; //tslint:disable:trailing-comma no-any no-multiline-string @@ -180,7 +180,7 @@ suite('DataScience LiveShare tests', () => { // Just run some code in the host const wrapper = await addCodeToRole(vsls.Role.Host, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); }); test('Host & Guest Simple', async () => { @@ -195,11 +195,11 @@ suite('DataScience LiveShare tests', () => { // Send code through the host const wrapper = await addCodeToRole(vsls.Role.Host, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); // Verify it ended up on the guest too assert.ok(guestContainer.wrapper, 'Guest wrapper not created'); - verifyHtmlOnCell(guestContainer.wrapper!, '<span>1</span>', CellPosition.Last); + verifyHtmlOnCell(guestContainer.wrapper!, 'InteractiveCell', '<span>1</span>', CellPosition.Last); }); test('Host Shutdown and Run', async () => { @@ -212,14 +212,14 @@ suite('DataScience LiveShare tests', () => { // Send code through the host let wrapper = await addCodeToRole(vsls.Role.Host, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); // Stop the session await stopSession(vsls.Role.Host); // Send code again. It should still work. wrapper = await addCodeToRole(vsls.Role.Host, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); }); test('Host startup and guest restart', async () => { @@ -232,7 +232,7 @@ suite('DataScience LiveShare tests', () => { // Send code through the host let wrapper = await addCodeToRole(vsls.Role.Host, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); // Shutdown the host await host.dispose(); @@ -240,10 +240,10 @@ suite('DataScience LiveShare tests', () => { // Startup a guest and run some code. await startSession(vsls.Role.Guest); wrapper = await addCodeToRole(vsls.Role.Guest, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); assert.ok(hostContainer.wrapper, 'Host wrapper not created'); - verifyHtmlOnCell(hostContainer.wrapper!, '<span>1</span>', CellPosition.Last); + verifyHtmlOnCell(hostContainer.wrapper!, 'InteractiveCell', '<span>1</span>', CellPosition.Last); }); test('Going through codewatcher', async () => { @@ -270,9 +270,9 @@ suite('DataScience LiveShare tests', () => { assert.ok(both, 'Expected both guest and host to be used'); await codeWatcher.runAllCells(); }); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); assert.ok(hostContainer.wrapper, 'Host wrapper not created for some reason'); - verifyHtmlOnCell(hostContainer.wrapper!, '<span>1</span>', CellPosition.Last); + verifyHtmlOnCell(hostContainer.wrapper!, 'InteractiveCell', '<span>1</span>', CellPosition.Last); }); test('Export from guest', async () => { @@ -322,7 +322,7 @@ suite('DataScience LiveShare tests', () => { // Start just the host and verify it works await startSession(vsls.Role.Host); let wrapper = await addCodeToRole(vsls.Role.Host, '#%%\na=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); // Disable guest checking on the guest (same as if the guest doesn't have the python extension) await startSession(vsls.Role.Guest); @@ -331,7 +331,7 @@ suite('DataScience LiveShare tests', () => { // Host should now be in a state that if any code runs, the session should end. However // the code should still run wrapper = await addCodeToRole(vsls.Role.Host, '#%%\na=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); assert.equal(isSessionStarted(vsls.Role.Host), false, 'Host should have exited session'); assert.equal(isSessionStarted(vsls.Role.Guest), false, 'Guest should have exited session'); assert.ok(lastErrorMessage, 'Error was not set during session shutdown'); diff --git a/src/test/datascience/mockJupyterManager.ts b/src/test/datascience/mockJupyterManager.ts index a11a3c68eff1..d2671fd8d382 100644 --- a/src/test/datascience/mockJupyterManager.ts +++ b/src/test/datascience/mockJupyterManager.ts @@ -164,7 +164,7 @@ export class MockJupyterManager implements IJupyterSessionManager { output_type: 'error', ename: message, evalue: message, - traceback: [] + traceback: [message] }; this.addCell(code, result); diff --git a/src/test/datascience/nativeEditor.functional.test.tsx b/src/test/datascience/nativeEditor.functional.test.tsx new file mode 100644 index 000000000000..b7b558b986c6 --- /dev/null +++ b/src/test/datascience/nativeEditor.functional.test.tsx @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as assert from 'assert'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { Disposable, TextDocument, TextEditor, Uri } from 'vscode'; + +import { IApplicationShell, IDocumentManager } from '../../client/common/application/types'; +import { createDeferred } from '../../client/common/utils/async'; +import { noop } from '../../client/common/utils/misc'; +import { INotebookEditor, INotebookEditorProvider } from '../../client/datascience/types'; +import { NativeEditor } from '../../datascience-ui/native-editor/nativeEditor'; +import { ImageButton } from '../../datascience-ui/react-common/imageButton'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { addCell, getNativeCellResults, runMountedTest } from './nativeEditorTestHelpers'; +import { + addContinuousMockData, + addMockData, + CellPosition, + escapePath, + findButton, + getLastOutputCell, + srcDirectory, + verifyHtmlOnCell +} from './testHelpers'; + +//import { asyncDump } from '../common/asyncDump'; +// tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string +suite('DataScience Native Editor tests', () => { + const disposables: Disposable[] = []; + let ioc: DataScienceIocContainer; + + setup(() => { + ioc = new DataScienceIocContainer(); + ioc.registerDataScienceTypes(); + }); + + teardown(async () => { + for (const disposable of disposables) { + if (!disposable) { + continue; + } + // tslint:disable-next-line:no-any + const promise = disposable.dispose() as Promise<any>; + if (promise) { + await promise; + } + } + await ioc.dispose(); + }); + + // Uncomment this to debug hangs on exit + // suiteTeardown(() => { + // asyncDump(); + // }); + + async function getOrCreateNativeEditor(uri?: Uri, contents?: string): Promise<INotebookEditor> { + const notebookProvider = ioc.get<INotebookEditorProvider>(INotebookEditorProvider); + if (uri && contents) { + return notebookProvider.open(uri, contents); + } else { + return notebookProvider.createNew(); + } + } + + async function waitForMessageResponse(action: () => void): Promise<void> { + ioc.wrapperCreatedPromise = createDeferred<boolean>(); + action(); + await ioc.wrapperCreatedPromise.promise; + ioc.wrapperCreatedPromise = undefined; + } + + function createNewEditor(): Promise<INotebookEditor> { + return getOrCreateNativeEditor(); + } + + runMountedTest('Simple text', async (wrapper) => { + // Create an editor so something is listening to messages + await createNewEditor(); + + // Add a cell into the UI and wait for it to render + await addCell(wrapper, 'a=1\na'); + + verifyHtmlOnCell(wrapper, 'NativeCell', '<span>1</span>', CellPosition.Last); + }, () => { return ioc; }); + + runMountedTest('Mime Types', async (wrapper) => { + // Create an editor so something is listening to messages + await createNewEditor(); + + const badPanda = `import pandas as pd +df = pd.read("${escapePath(path.join(srcDirectory(), 'DefaultSalesReport.csv'))}") +df.head()`; + const goodPanda = `import pandas as pd +df = pd.read_csv("${escapePath(path.join(srcDirectory(), 'DefaultSalesReport.csv'))}") +df.head()`; + const matPlotLib = 'import matplotlib.pyplot as plt\r\nimport numpy as np\r\nx = np.linspace(0,20,100)\r\nplt.plot(x, np.sin(x))\r\nplt.show()'; + const matPlotLibResults = 'img'; + const spinningCursor = `import sys +import time + +def spinning_cursor(): + while True: + for cursor in '|/-\\\\': + yield cursor + +spinner = spinning_cursor() +for _ in range(50): + sys.stdout.write(next(spinner)) + sys.stdout.flush() + time.sleep(0.1) + sys.stdout.write('\\r')`; + + addMockData(ioc, badPanda, `pandas has no attribute 'read'`, 'text/html', 'error'); + addMockData(ioc, goodPanda, `<td>A table</td>`, 'text/html'); + addMockData(ioc, matPlotLib, matPlotLibResults, 'text/html'); + const cursors = ['|', '/', '-', '\\']; + let cursorPos = 0; + let loops = 3; + addContinuousMockData(ioc, spinningCursor, async (_c) => { + const result = `${cursors[cursorPos]}\r`; + cursorPos += 1; + if (cursorPos >= cursors.length) { + cursorPos = 0; + loops -= 1; + } + return Promise.resolve({ result: result, haveMore: loops > 0 }); + }); + + await addCell(wrapper, badPanda, 5); + verifyHtmlOnCell(wrapper, 'NativeCell', `has no attribute 'read'`, CellPosition.Last); + + await addCell(wrapper, goodPanda, 5); + verifyHtmlOnCell(wrapper, 'NativeCell', `<td>`, CellPosition.Last); + + await addCell(wrapper, matPlotLib, 5); + verifyHtmlOnCell(wrapper, 'NativeCell', matPlotLibResults, CellPosition.Last); + + await addCell(wrapper, spinningCursor, 4 + (ioc.mockJupyter ? (cursors.length * 3) : 0)); + verifyHtmlOnCell(wrapper, 'NativeCell', '<div>', CellPosition.Last); + }, () => { return ioc; }); + + runMountedTest('Click buttons', async (wrapper) => { + // Goto source should cause the visible editor to be picked as long as its filename matches + const showedEditor = createDeferred(); + const textEditors: TextEditor[] = []; + const docManager = TypeMoq.Mock.ofType<IDocumentManager>(); + const visibleEditor = TypeMoq.Mock.ofType<TextEditor>(); + const dummyDocument = TypeMoq.Mock.ofType<TextDocument>(); + dummyDocument.setup(d => d.fileName).returns(() => 'foo.py'); + visibleEditor.setup(v => v.show()).returns(() => showedEditor.resolve()); + visibleEditor.setup(v => v.revealRange(TypeMoq.It.isAny())).returns(noop); + visibleEditor.setup(v => v.document).returns(() => dummyDocument.object); + textEditors.push(visibleEditor.object); + docManager.setup(a => a.visibleTextEditors).returns(() => textEditors); + ioc.serviceManager.rebindInstance<IDocumentManager>(IDocumentManager, docManager.object); + // Create an editor so something is listening to messages + await createNewEditor(); + + // Get a cell into the list + await addCell(wrapper, 'a=1\na'); + + // find the buttons on the cell itself + const cell = getLastOutputCell(wrapper, 'NativeCell'); + const ImageButtons = cell.find(ImageButton); + assert.equal(ImageButtons.length, 7, 'Cell buttons not found'); + const deleteButton = ImageButtons.at(6); + + // Make sure delete works + const afterDelete = await getNativeCellResults(wrapper, 1, async () => { + deleteButton.simulate('click'); + return Promise.resolve(); + }); + assert.equal(afterDelete.length, 0, `Delete should remove a cell`); + }, () => { return ioc; }); + + runMountedTest('Export', async (wrapper) => { + // Export should cause the export dialog to come up. Remap appshell so we can check + const dummyDisposable = { + dispose: () => { return; } + }; + let exportCalled = false; + const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + appShell.setup(a => a.showErrorMessage(TypeMoq.It.isAnyString())).returns((e) => { throw e; }); + appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('')); + appShell.setup(a => a.showSaveDialog(TypeMoq.It.isAny())).returns(() => { + exportCalled = true; + return Promise.resolve(undefined); + }); + appShell.setup(a => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); + ioc.serviceManager.rebindInstance<IApplicationShell>(IApplicationShell, appShell.object); + + // Make sure to create the interactive window after the rebind or it gets the wrong application shell. + await createNewEditor(); + await addCell(wrapper, 'a=1\na'); + + // Export should cause exportCalled to change to true + const exportButton = findButton(wrapper, NativeEditor, 5); + await waitForMessageResponse(() => exportButton!.simulate('click')); + assert.equal(exportCalled, true, 'Export should have been called'); + }, () => { return ioc; }); +}); diff --git a/src/test/datascience/nativeEditorTestHelpers.tsx b/src/test/datascience/nativeEditorTestHelpers.tsx new file mode 100644 index 000000000000..4ff9d05f2eb0 --- /dev/null +++ b/src/test/datascience/nativeEditorTestHelpers.tsx @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as assert from 'assert'; +import { ReactWrapper } from 'enzyme'; +import * as React from 'react'; + +import { IJupyterExecution } from '../../client/datascience/types'; +import { NativeEditor } from '../../datascience-ui/native-editor/nativeEditor'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { waitForUpdate } from './reactHelpers'; +import { addMockData, getCellResults, getMainPanel, mountWebView } from './testHelpers'; + +// tslint:disable-next-line: no-any +export function getNativeCellResults(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, expectedRenders: number, updater: () => Promise<void>): Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { + return getCellResults(wrapper, NativeEditor, 'NativeCell', expectedRenders, updater); +} + +// tslint:disable-next-line:no-any +export function runMountedTest(name: string, testFunc: (wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) => Promise<void>, getIOC: () => DataScienceIocContainer) { + test(name, async () => { + const ioc = getIOC(); + const jupyterExecution = ioc.get<IJupyterExecution>(IJupyterExecution); + if (await jupyterExecution.isNotebookSupported()) { + addMockData(ioc, 'a=1\na', 1); + const wrapper = mountWebView(ioc, <NativeEditor baseTheme='vscode-light' codeTheme='light_vs' testMode={true} skipDefault={true} />); + await testFunc(wrapper); + } else { + // tslint:disable-next-line:no-console + console.log(`${name} skipped, no Jupyter installed.`); + } + }); +} + +// tslint:disable-next-line: no-any +export async function addCell(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, code: string, expectedSubmitRenderCount: number = 6): Promise<void> { + // First get the stateController on the main panel. That's how we'll add a new cell. + const reactEditor = getMainPanel<NativeEditor>(wrapper, NativeEditor); + assert.ok(reactEditor, 'Cannot find the main panel during adding a cell'); + let update = waitForUpdate(wrapper, NativeEditor, 1); + const vm = reactEditor!.stateController.addNewCell(); + + // Then use that cell to stick new input. + assert.ok(vm, 'Did not add a new cell to the main panel'); + await update; + + update = waitForUpdate(wrapper, NativeEditor, expectedSubmitRenderCount); + reactEditor!.stateController.submitInput(code, vm!); + return update; +} diff --git a/src/test/datascience/reactHelpers.ts b/src/test/datascience/reactHelpers.ts index b9a168a027ad..d2b96ce4419f 100644 --- a/src/test/datascience/reactHelpers.ts +++ b/src/test/datascience/reactHelpers.ts @@ -557,12 +557,12 @@ const keyMap: { [key: string]: { code: number; shift: boolean } } = { }; export function createMessageEvent(data: any): MessageEvent { - const domWindow = window as DOMWindow; + const domWindow = (window as any) as DOMWindow; return new domWindow.MessageEvent('message', { data }); } export function createKeyboardEvent(type: string, options: KeyboardEventInit): KeyboardEvent { - const domWindow = window as DOMWindow; + const domWindow = (window as any) as DOMWindow; options.bubbles = true; options.cancelable = true; @@ -580,13 +580,13 @@ export function createKeyboardEvent(type: string, options: KeyboardEventInit): K } export function createInputEvent(): Event { - const domWindow = window as DOMWindow; + const domWindow = (window as any) as DOMWindow; return new domWindow.Event('input', { bubbles: true, cancelable: false }); } export function blurWindow() { // blur isn't implemented. We just need to dispatch the blur event - const domWindow = window as DOMWindow; + const domWindow = (window as any) as DOMWindow; const blurEvent = new domWindow.Event('blur', { bubbles: true }); domWindow.dispatchEvent(blurEvent); } diff --git a/src/test/datascience/testHelpers.tsx b/src/test/datascience/testHelpers.tsx new file mode 100644 index 000000000000..3c94710dbb34 --- /dev/null +++ b/src/test/datascience/testHelpers.tsx @@ -0,0 +1,329 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as assert from 'assert'; +import { mount, ReactWrapper } from 'enzyme'; +import { min } from 'lodash'; +import * as path from 'path'; +import * as React from 'react'; +import { CancellationToken } from 'vscode'; + +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import { IDataScienceSettings } from '../../client/common/types'; +import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { ImageButton } from '../../datascience-ui/react-common/imageButton'; +import { updateSettings } from '../../datascience-ui/react-common/settingsReactSide'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { createInputEvent, createKeyboardEvent, waitForUpdate } from './reactHelpers'; + +//tslint:disable:trailing-comma no-any no-multiline-string +export enum CellInputState { + Hidden, + Visible, + Collapsed, + Expanded +} + +export enum CellPosition { + First = 'first', + Last = 'last' +} + +export function mountWebView(ioc: DataScienceIocContainer, node: React.ReactElement): ReactWrapper<any, Readonly<{}>, React.Component> { + // Setup our webview panel + ioc.createWebView(() => mount(node)); + return ioc.wrapper!; +} + +export function addMockData(ioc: DataScienceIocContainer, code: string, result: string | number | undefined, mimeType?: string, cellType?: string) { + if (ioc.mockJupyter) { + if (cellType && cellType === 'error') { + ioc.mockJupyter.addError(code, result ? result.toString() : ''); + } else { + if (result) { + ioc.mockJupyter.addCell(code, result, mimeType); + } else { + ioc.mockJupyter.addCell(code); + } + } + } +} + +export function addContinuousMockData(ioc: DataScienceIocContainer, code: string, resultGenerator: (c: CancellationToken) => Promise<{ result: string; haveMore: boolean }>) { + if (ioc.mockJupyter) { + ioc.mockJupyter.addContinuousOutputCell(code, resultGenerator); + } +} +export function getLastOutputCell(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, cellType: string): ReactWrapper<any, Readonly<{}>, React.Component> { + // Skip the edit cell if in the interactive window + const count = cellType === 'InteractiveCell' ? 2 : 1; + const foundResult = wrapper.find(cellType); + assert.ok(foundResult.length >= count, 'Didn\'t find any cells being rendered'); + return foundResult.at(foundResult.length - count); +} + +export function verifyHtmlOnCell(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, cellType: string, html: string | undefined, cellIndex: number | CellPosition) { + const foundResult = wrapper.find(cellType); + assert.ok(foundResult.length >= 1, 'Didn\'t find any cells being rendered'); + + let targetCell: ReactWrapper; + // Get the correct result that we are dealing with + if (typeof cellIndex === 'number') { + if (cellIndex >= 0 && cellIndex <= (foundResult.length - 1)) { + targetCell = foundResult.at(cellIndex); + } + } else if (typeof cellIndex === 'string') { + switch (cellIndex) { + case CellPosition.First: + targetCell = foundResult.first(); + break; + + case CellPosition.Last: + // Skip the input cell on these checks. + targetCell = getLastOutputCell(wrapper, cellType); + break; + + default: + // Fall through, targetCell check will fail out + break; + } + } + + // ! is ok here to get rid of undefined type check as we want a fail here if we have not initialized targetCell + assert.ok(targetCell!, 'Target cell doesn\'t exist'); + + // If html is specified, check it + if (html) { + // Extract only the first 100 chars from the input string + const sliced = html.substr(0, min([html.length, 100])); + const output = targetCell!.find('div.cell-output'); + assert.ok(output.length > 0, 'No output cell found'); + const outHtml = output.html(); + assert.ok(outHtml.includes(sliced), `${outHtml} does not contain ${sliced}`); + } else { + const output = targetCell!.find('div.cell-output'); + const outputHtml = output.length > 0 ? output.html() : 'empty'; + // html not specified, look for an empty render + assert.ok(targetCell!.isEmptyRender(), `Target cell is not empty render, got this instead: ${outputHtml}`); + } +} + +export function verifyLastCellInputState(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, cellType: string, state: CellInputState) { + + const lastCell = getLastOutputCell(wrapper, cellType); + assert.ok(lastCell, 'Last call doesn\'t exist'); + + const inputBlock = lastCell.find('div.cell-input'); + const toggleButton = lastCell.find('polygon.collapse-input-svg'); + + switch (state) { + case CellInputState.Hidden: + assert.ok(inputBlock.length === 0, 'Cell input not hidden'); + break; + + case CellInputState.Visible: + assert.ok(inputBlock.length === 1, 'Cell input not visible'); + break; + + case CellInputState.Expanded: + assert.ok(toggleButton.html().includes('collapse-input-svg-rotate'), 'Cell input toggle not expanded'); + break; + + case CellInputState.Collapsed: + assert.ok(!toggleButton.html().includes('collapse-input-svg-rotate'), 'Cell input toggle not collapsed'); + break; + + default: + assert.fail('Unknown cellInputStat'); + break; + } +} + +export async function getCellResults(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, mainClass: React.ComponentClass<any>, cellType: string, expectedRenders: number, updater: () => Promise<void>): Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { + + // Get a render promise with the expected number of renders + const renderPromise = waitForUpdate(wrapper, mainClass, expectedRenders); + + // Call our function to update the react control + await updater(); + + // Wait for all of the renders to go through + await renderPromise; + + // Return the result + return wrapper.find(cellType); +} + +function simulateKey(domNode: HTMLTextAreaElement, key: string, shiftDown?: boolean) { + // Submit a keypress into the textarea. Simulate doesn't work here because the keydown + // handler is not registered in any react code. It's being handled with DOM events + + // According to this: + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Usage_notes + // The normal events are + // 1) keydown + // 2) keypress + // 3) keyup + let event = createKeyboardEvent('keydown', { key, code: key, shiftKey: shiftDown }); + + // Dispatch. Result can be swallowed. If so skip the next event. + let result = domNode.dispatchEvent(event); + if (result) { + event = createKeyboardEvent('keypress', { key, code: key, shiftKey: shiftDown }); + result = domNode.dispatchEvent(event); + if (result) { + event = createKeyboardEvent('keyup', { key, code: key, shiftKey: shiftDown }); + domNode.dispatchEvent(event); + + // Update our value. This will reset selection to zero. + domNode.value = domNode.value + key; + + // Tell the dom node its selection start has changed. Monaco + // reads this to determine where the character went. + domNode.selectionEnd = domNode.value.length; + domNode.selectionStart = domNode.value.length; + + // Dispatch an input event so we update the textarea + domNode.dispatchEvent(createInputEvent()); + } + } + +} + +async function submitInput(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, mainClass: React.ComponentClass<any>, textArea: HTMLTextAreaElement): Promise<void> { + // Get a render promise with the expected number of renders (how many updates a the shift + enter will cause) + // Should be 6 - 1 for the shift+enter and 5 for the new cell. + const renderPromise = waitForUpdate(wrapper, mainClass, 6); + + // Submit a keypress into the textarea + simulateKey(textArea, 'Enter', true); + + return renderPromise; +} + +function enterKey(_wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, textArea: HTMLTextAreaElement, key: string) { + // Simulate a key press + simulateKey(textArea, key); +} + +export function getEditor(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) : ReactWrapper<any, Readonly<{}>, React.Component> { + // Find the last cell. It should have a monacoEditor object + const cells = wrapper.find('InteractiveCell'); + const lastCell = cells.last(); + return lastCell.find('MonacoEditor'); +} + +export function typeCode(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, code: string) : HTMLTextAreaElement | null { + + // Find the last cell. It should have a monacoEditor object. We need to search + // through its DOM to find the actual textarea to send input to + // (we can't actually find it with the enzyme wrappers because they only search + // React accessible nodes and the monaco html is not react) + const editorControl = getEditor(wrapper); + const ecDom = editorControl.getDOMNode(); + assert.ok(ecDom, 'ec DOM object not found'); + const textArea = ecDom!.querySelector('.overflow-guard')!.querySelector('textarea'); + assert.ok(textArea!, 'Cannot find the textarea inside the monaco editor'); + textArea!.focus(); + + // Now simulate entering all of the keys + for (let i = 0; i < code.length; i += 1) { + enterKey(wrapper, textArea!, code.charAt(i)); + } + + return textArea; +} + +export async function enterInput(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, mainClass: React.ComponentClass<any>, code: string, resultClass: string): Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { + + // First we have to type the code into the input box + const textArea = typeCode(wrapper, code); + + // Now simulate a shift enter. This should cause a new cell to be added + await submitInput(wrapper, mainClass, textArea!); + + // Return the result + return wrapper.find(resultClass); +} + +export function findButton(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, mainClass: React.ComponentClass<any>, index: number): ReactWrapper<any, Readonly<{}>, React.Component> | undefined { + const mainObj = wrapper.find(mainClass); + if (mainObj) { + const buttons = mainObj.find(ImageButton); + if (buttons) { + return buttons.at(index); + } + } +} + +// The default base set of data science settings to use +export function defaultDataScienceSettings(): IDataScienceSettings { + return { + allowImportFromNotebook: true, + jupyterLaunchTimeout: 10, + jupyterLaunchRetries: 3, + enabled: true, + jupyterServerURI: 'local', + notebookFileRoot: 'WORKSPACE', + changeDirOnImportExport: true, + useDefaultConfigForJupyter: true, + jupyterInterruptTimeout: 10000, + searchForJupyter: true, + showCellInputCode: true, + collapseCellInputCodeByDefault: true, + allowInput: true, + maxOutputSize: 400, + errorBackgroundColor: '#FFFFFF', + sendSelectionToInteractiveWindow: false, + showJupyterVariableExplorer: true, + variableExplorerExclude: 'module;function;builtin_function_or_method', + codeRegularExpression: '^(#\\s*%%|#\\s*\\<codecell\\>|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])', + markdownRegularExpression: '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\<markdowncell\\>)', + enablePlotViewer: true, + runStartupCommands: '', + debugJustMyCode: true + }; +} + +// Set initial data science settings to use for a test (initially loaded via settingsReactSide.ts) +export function initialDataScienceSettings(newSettings: IDataScienceSettings) { + const settingsString = JSON.stringify(newSettings); + updateSettings(settingsString); +} + +export function getMainPanel<P>(wrapper: ReactWrapper<any, Readonly<{}>>, mainClass: React.ComponentClass<any>): P | undefined { + const mainObj = wrapper.find(mainClass); + if (mainObj) { + return (mainObj.instance() as any) as P; + } + + return undefined; +} + +// Update data science settings while running (goes through the UpdateSettings channel) +export function updateDataScienceSettings(wrapper: ReactWrapper<any, Readonly<{}>>, mainClass: React.ComponentClass<any>, newSettings: IDataScienceSettings) { + const settingsString = JSON.stringify(newSettings); + const mainPanel = getMainPanel(wrapper, mainClass) as any; + if (mainPanel) { + mainPanel.stateController.handleMessage(InteractiveWindowMessages.UpdateSettings, settingsString); + } + wrapper.update(); +} + +export function toggleCellExpansion(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, cellType: string) { + // Find the last cell added + const lastCell = getLastOutputCell(wrapper, cellType); + assert.ok(lastCell, 'Last call doesn\'t exist'); + + const toggleButton = lastCell.find('button.collapse-input'); + assert.ok(toggleButton); + toggleButton.simulate('click'); +} + +export function escapePath(p: string) { + return p.replace(/\\/g, '\\\\'); +} + +export function srcDirectory() { + return path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); +} diff --git a/src/test/datascience/testInteractiveWindowProvider.ts b/src/test/datascience/testInteractiveWindowProvider.ts new file mode 100644 index 000000000000..4da8eae3436f --- /dev/null +++ b/src/test/datascience/testInteractiveWindowProvider.ts @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import { Event } from 'vscode'; + +import { ILiveShareApi } from '../../client/common/application/types'; +import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry } from '../../client/common/types'; +import { + InteractiveWindowMessageListener +} from '../../client/datascience/interactive-common/interactiveWindowMessageListener'; +import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { InteractiveWindow } from '../../client/datascience/interactive-window/interactiveWindow'; +import { InteractiveWindowProvider } from '../../client/datascience/interactive-window/interactiveWindowProvider'; +import { IInteractiveWindow, IInteractiveWindowProvider, INotebookServerOptions } from '../../client/datascience/types'; +import { IServiceContainer } from '../../client/ioc/types'; + +@injectable() +export class TestInteractiveWindowProvider implements IInteractiveWindowProvider { + + private realProvider: InteractiveWindowProvider; + constructor( + @inject(ILiveShareApi) liveShare: ILiveShareApi, + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry, + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(IConfigurationService) configService: IConfigurationService + ) { + this.realProvider = new InteractiveWindowProvider(liveShare, serviceContainer, asyncRegistry, disposables, configService); + + // During a test, the 'create' function will end up being called during a live share. We need to hook its result too + // so just hook the 'create' function to fix all callers. + // tslint:disable-next-line: no-any + const fungible = this.realProvider as any; + const origCreate = fungible.create.bind(fungible); + fungible.create = async () => { + const result = await origCreate(); + // During testing the MainPanel sends the init message before our interactive window is created. + // Pretend like it's happening now + // tslint:disable-next-line: no-any + const listener = ((result as any).messageListener) as InteractiveWindowMessageListener; + listener.onMessage(InteractiveWindowMessages.Started, {}); + + // Also need the css request so that other messages can go through + const webHost = result as InteractiveWindow; + webHost.setTheme(false); + + return result; + }; + } + + public getActive(): IInteractiveWindow | undefined { + return this.realProvider.getActive(); + } + + public get onExecutedCode(): Event<string> { + return this.realProvider.onExecutedCode; + } + + public async getOrCreateActive(): Promise<IInteractiveWindow> { + return this.realProvider.getOrCreateActive(); + } + + public getNotebookOptions(): Promise<INotebookServerOptions> { + return this.realProvider.getNotebookOptions(); + } +} diff --git a/src/test/datascience/testNativeEditorProvider.ts b/src/test/datascience/testNativeEditorProvider.ts new file mode 100644 index 000000000000..9ffcd6133d28 --- /dev/null +++ b/src/test/datascience/testNativeEditorProvider.ts @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; + +import { IWorkspaceService } from '../../client/common/application/types'; +import { IFileSystem } from '../../client/common/platform/types'; +import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry } from '../../client/common/types'; +import { + InteractiveWindowMessageListener +} from '../../client/datascience/interactive-common/interactiveWindowMessageListener'; +import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { NativeEditor } from '../../client/datascience/interactive-ipynb/nativeEditor'; +import { NativeEditorProvider } from '../../client/datascience/interactive-ipynb/nativeEditorProvider'; +import { INotebookEditor, INotebookEditorProvider, INotebookServerOptions } from '../../client/datascience/types'; +import { IServiceContainer } from '../../client/ioc/types'; + +@injectable() +export class TestNativeEditorProvider implements INotebookEditorProvider { + private realProvider: NativeEditorProvider; + + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry, + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(IWorkspaceService) workspace: IWorkspaceService, + @inject(IConfigurationService) configuration: IConfigurationService, + @inject(IFileSystem) fileSystem: IFileSystem + ) { + this.realProvider = new NativeEditorProvider(serviceContainer, asyncRegistry, disposables, workspace, configuration, fileSystem); + } + + public get activeEditor(): INotebookEditor | undefined { + return this.realProvider.activeEditor; + } + + public get editors(): INotebookEditor[] { + return this.realProvider.editors; + } + + public async open(file: Uri, contents: string): Promise<INotebookEditor> { + const result = await this.realProvider.open(file, contents); + + // During testing the MainPanel sends the init message before our interactive window is created. + // Pretend like it's happening now + // tslint:disable-next-line: no-any + const listener = ((result as any).messageListener) as InteractiveWindowMessageListener; + listener.onMessage(InteractiveWindowMessages.Started, {}); + + // Also need the css request so that other messages can go through + const webHost = result as NativeEditor; + webHost.setTheme(false); + + return result; + } + + public show(file: Uri): Promise<INotebookEditor | undefined> { + return this.realProvider.show(file); + } + + public async createNew(): Promise<INotebookEditor> { + const result = await this.realProvider.createNew(); + + // During testing the MainPanel sends the init message before our interactive window is created. + // Pretend like it's happening now + // tslint:disable-next-line: no-any + const listener = ((result as any).messageListener) as InteractiveWindowMessageListener; + listener.onMessage(InteractiveWindowMessages.Started, {}); + + // Also need the css request so that other messages can go through + const webHost = result as NativeEditor; + webHost.setTheme(false); + + return result; + } + + public async getNotebookOptions(): Promise<INotebookServerOptions> { + return this.realProvider.getNotebookOptions(); + } +} diff --git a/src/test/datascience/variableexplorer.functional.test.tsx b/src/test/datascience/variableexplorer.functional.test.tsx index 6eadb7b1524b..4e71be91c543 100644 --- a/src/test/datascience/variableexplorer.functional.test.tsx +++ b/src/test/datascience/variableexplorer.functional.test.tsx @@ -8,11 +8,7 @@ import { parse } from 'node-html-parser'; import * as React from 'react'; import { Disposable } from 'vscode'; -import { - InteractiveWindowMessageListener -} from '../../client/datascience/interactive-common/interactiveWindowMessageListener'; -import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; -import { IInteractiveWindow, IInteractiveWindowProvider, IJupyterVariable } from '../../client/datascience/types'; +import { IJupyterVariable } from '../../client/datascience/types'; import { InteractivePanel } from '../../datascience-ui/history-react/interactivePanel'; import { VariableExplorer } from '../../datascience-ui/interactive-common/variableExplorer'; import { DataScienceIocContainer } from './dataScienceIocContainer'; @@ -54,18 +50,6 @@ suite('DataScience Interactive Window variable explorer tests', () => { await ioc.dispose(); }); - async function getOrCreateInteractiveWindow(): Promise<IInteractiveWindow> { - const interactiveWindowProvider = ioc.get<IInteractiveWindowProvider>(IInteractiveWindowProvider); - const result = await interactiveWindowProvider.getOrCreateActive(); - - // During testing the MainPanel sends the init message before our interactive window is created. - // Pretend like it's happening now - const listener = ((result as any).messageListener) as InteractiveWindowMessageListener; - listener.onMessage(InteractiveWindowMessages.Started, {}); - - return result; - } - runMountedTest('Variable explorer - Exclude', async (wrapper) => { const basicCode: string = `import numpy as np import pandas as pd @@ -74,8 +58,8 @@ value = 'hello world'`; openVariableExplorer(wrapper); - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - await addCode(getOrCreateInteractiveWindow, wrapper, basicCode, 4); + await addCode(ioc, wrapper, 'a=1\na'); + await addCode(ioc, wrapper, basicCode, 4); await waitForUpdate(wrapper, VariableExplorer, 3); // We should show a string and show an int, the modules should be hidden @@ -90,7 +74,7 @@ value = 'hello world'`; ioc.getSettings().datascience.variableExplorerExclude = `${ioc.getSettings().datascience.variableExplorerExclude};str`; // Add another string and check our vars, strings should be hidden - await addCode(getOrCreateInteractiveWindow, wrapper, basicCode2, 4); + await addCode(ioc, wrapper, basicCode2, 4); await waitForUpdate(wrapper, VariableExplorer, 2); targetVariables = [ @@ -105,7 +89,7 @@ value = 'hello world'`; openVariableExplorer(wrapper); - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); + await addCode(ioc, wrapper, 'a=1\na'); await waitForUpdate(wrapper, VariableExplorer, 2); // Check that we have just the 'a' variable @@ -115,7 +99,7 @@ value = 'hello world'`; verifyVariables(wrapper, targetVariables); // Add another variable and check it - await addCode(getOrCreateInteractiveWindow, wrapper, basicCode, 4); + await addCode(ioc, wrapper, basicCode, 4); await waitForUpdate(wrapper, VariableExplorer, 3); targetVariables = [ @@ -126,7 +110,7 @@ value = 'hello world'`; verifyVariables(wrapper, targetVariables); // Add a second variable and check it - await addCode(getOrCreateInteractiveWindow, wrapper, basicCode2, 4); + await addCode(ioc, wrapper, basicCode2, 4); await waitForUpdate(wrapper, VariableExplorer, 4); targetVariables = [ @@ -144,8 +128,8 @@ value = 'hello world'`; openVariableExplorer(wrapper); - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - await addCode(getOrCreateInteractiveWindow, wrapper, basicCode, 4); + await addCode(ioc, wrapper, 'a=1\na'); + await addCode(ioc, wrapper, basicCode, 4); // Here we are only going to wait for two renders instead of the needed three // a should have the value updated, but value should still be loading @@ -176,8 +160,8 @@ myDict = {'a': 1}`; openVariableExplorer(wrapper); - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - await addCode(getOrCreateInteractiveWindow, wrapper, basicCode, 4); + await addCode(ioc, wrapper, 'a=1\na'); + await addCode(ioc, wrapper, basicCode, 4); // Verify that we actually update the variable explorer // Count here is our main render + a render for each variable row as they come in @@ -208,8 +192,8 @@ myTuple = 1,2,3,4,5,6,7,8,9 openVariableExplorer(wrapper); - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - await addCode(getOrCreateInteractiveWindow, wrapper, basicCode, 4); + await addCode(ioc, wrapper, 'a=1\na'); + await addCode(ioc, wrapper, basicCode, 4); // Verify that we actually update the variable explorer // Count here is our main render + a render for each variable row as they come in @@ -244,8 +228,8 @@ strc = 'c'`; openVariableExplorer(wrapper); - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - await addCode(getOrCreateInteractiveWindow, wrapper, basicCode, 4); + await addCode(ioc, wrapper, 'a=1\na'); + await addCode(ioc, wrapper, basicCode, 4); await waitForUpdate(wrapper, VariableExplorer, 7);