diff --git a/news/3 Code Health/7372.md b/news/3 Code Health/7372.md new file mode 100644 index 000000000000..332da98db687 --- /dev/null +++ b/news/3 Code Health/7372.md @@ -0,0 +1 @@ +More functional tests for the notebook editor. diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index 6ebdb12a6423..ae7941cb95da 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -333,7 +333,7 @@ export class CodeWatcher implements ICodeWatcher { } this.sendPerceivedCellExecute(stopWatch); } catch (err) { - this.dataScienceErrorHandler.handleError(err).ignoreErrors(); + await this.dataScienceErrorHandler.handleError(err); } return result; @@ -347,7 +347,7 @@ export class CodeWatcher implements ICodeWatcher { const activeInteractiveWindow = await this.interactiveWindowProvider.getOrCreateActive(); return activeInteractiveWindow.addMessage(message); } catch (err) { - this.dataScienceErrorHandler.handleError(err).ignoreErrors(); + await this.dataScienceErrorHandler.handleError(err); } } } diff --git a/src/client/datascience/errorHandler/errorHandler.ts b/src/client/datascience/errorHandler/errorHandler.ts index 290af382c0b7..049d981ea6f4 100644 --- a/src/client/datascience/errorHandler/errorHandler.ts +++ b/src/client/datascience/errorHandler/errorHandler.ts @@ -20,34 +20,30 @@ export class DataScienceErrorHandler implements IDataScienceErrorHandler { public async handleError(err: Error): Promise { if (err instanceof JupyterInstallError) { - this.applicationShell.showInformationMessage( + const response = await this.applicationShell.showInformationMessage( err.message, localize.DataScience.jupyterInstall(), localize.DataScience.notebookCheckForImportNo(), - err.actionTitle) - .then(response => { - if (response === localize.DataScience.jupyterInstall()) { - return this.channels.getInstallationChannels() - .then(installers => { - if (installers) { - // If Conda is available, always pick it as the user must have a Conda Environment - const installer = installers.find(ins => ins.name === 'Conda'); - const product = ProductNames.get(Product.jupyter); + err.actionTitle); + if (response === localize.DataScience.jupyterInstall()) { + const installers = await this.channels.getInstallationChannels(); + if (installers) { + // If Conda is available, always pick it as the user must have a Conda Environment + const installer = installers.find(ins => ins.name === 'Conda'); + const product = ProductNames.get(Product.jupyter); - if (installer && product) { - installer.installModule(product) - .catch(e => this.applicationShell.showErrorMessage(e.message, localize.DataScience.pythonInteractiveHelpLink())); - } else if (installers[0] && product) { - installers[0].installModule(product) - .catch(e => this.applicationShell.showErrorMessage(e.message, localize.DataScience.pythonInteractiveHelpLink())); - } - } - }); - } else if (response === err.actionTitle) { - // This is a special error that shows a link to open for more help - this.applicationShell.openUrl(err.action); + if (installer && product) { + installer.installModule(product) + .catch(e => this.applicationShell.showErrorMessage(e.message, localize.DataScience.pythonInteractiveHelpLink())); + } else if (installers[0] && product) { + installers[0].installModule(product) + .catch(e => this.applicationShell.showErrorMessage(e.message, localize.DataScience.pythonInteractiveHelpLink())); } - }); + } + } else if (response === err.actionTitle) { + // This is a special error that shows a link to open for more help + this.applicationShell.openUrl(err.action); + } } else if (err instanceof JupyterSelfCertsError) { // Don't show the message for self cert errors noop(); diff --git a/src/client/datascience/interactive-common/interactiveBase.ts b/src/client/datascience/interactive-common/interactiveBase.ts index ebd865a2f22b..89118e43d2fd 100644 --- a/src/client/datascience/interactive-common/interactiveBase.ts +++ b/src/client/datascience/interactive-common/interactiveBase.ts @@ -647,7 +647,7 @@ export abstract class InteractiveBase extends WebViewHost { let activeInterpreter: PythonInterpreter | undefined; try { - activeInterpreter = await this.interpreterService.getActiveInterpreter(); - const usableInterpreter = await this.jupyterExecution.getUsableJupyterPython(); - if (usableInterpreter) { - // See if the usable interpreter is not our active one. If so, show a warning - // Only do this if not the guest in a liveshare session - const api = await this.liveShare.getApi(); - if (!api || (api.session && api.session.role !== vsls.Role.Guest)) { - const active = await this.interpreterService.getActiveInterpreter(); - const activeDisplayName = active ? active.displayName : undefined; - const activePath = active ? active.path : undefined; - const usableDisplayName = usableInterpreter ? usableInterpreter.displayName : undefined; - const usablePath = usableInterpreter ? usableInterpreter.path : undefined; - const notebookError = await this.jupyterExecution.getNotebookError(); - if (activePath && usablePath && !this.fileSystem.arePathsSame(activePath, usablePath) && activeDisplayName && usableDisplayName) { - this.applicationShell.showWarningMessage(localize.DataScience.jupyterKernelNotSupportedOnActive().format(activeDisplayName, usableDisplayName, notebookError)); + const options = await this.getNotebookOptions(); + if (options && !options.uri) { + activeInterpreter = await this.interpreterService.getActiveInterpreter(); + const usableInterpreter = await this.jupyterExecution.getUsableJupyterPython(); + if (usableInterpreter) { + // See if the usable interpreter is not our active one. If so, show a warning + // Only do this if not the guest in a liveshare session + const api = await this.liveShare.getApi(); + if (!api || (api.session && api.session.role !== vsls.Role.Guest)) { + const active = await this.interpreterService.getActiveInterpreter(); + const activeDisplayName = active ? active.displayName : undefined; + const activePath = active ? active.path : undefined; + const usableDisplayName = usableInterpreter ? usableInterpreter.displayName : undefined; + const usablePath = usableInterpreter ? usableInterpreter.path : undefined; + const notebookError = await this.jupyterExecution.getNotebookError(); + if (activePath && usablePath && !this.fileSystem.arePathsSame(activePath, usablePath) && activeDisplayName && usableDisplayName) { + this.applicationShell.showWarningMessage(localize.DataScience.jupyterKernelNotSupportedOnActive().format(activeDisplayName, usableDisplayName, notebookError)); + } } } + return usableInterpreter ? true : false; + } else { + return true; } - - return usableInterpreter ? true : false; - } catch (e) { // Can't find a usable interpreter, show the error. if (activeInterpreter) { diff --git a/src/client/datascience/interactive-ipynb/nativeEditor.ts b/src/client/datascience/interactive-ipynb/nativeEditor.ts index ee49ccfbc2c4..84442ab6127d 100644 --- a/src/client/datascience/interactive-ipynb/nativeEditor.ts +++ b/src/client/datascience/interactive-ipynb/nativeEditor.ts @@ -244,7 +244,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor { // If that works, send the cells to the web view return this.postMessage(InteractiveWindowMessages.LoadAllCells, { cells }); } catch (e) { - this.errorHandler.handleError(e).ignoreErrors(); + return this.errorHandler.handleError(e); } } @@ -280,10 +280,33 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor { this.shareMessage(InteractiveWindowMessages.RemoteReexecuteCode, { code: info.code, file: Identifiers.EmptyFileName, line: 0, id: info.id, originator: this.id, debug: false }); } } catch (exc) { - await this.errorHandler.handleError(exc); + // Make this error our cell output + this.sendCellsToWebView([ + { + data: { + source: info.code, + cell_type: 'code', + outputs: [{ + output_type: 'error', + evalue: exc.toString() + }], + metadata: {}, + execution_count: null + }, + id: info.id, + file: Identifiers.EmptyFileName, + line: 0, + state: CellState.error, + type: 'execute' + } + ]); // Tell the other side we restarted the kernel. This will stop all executions this.postMessage(InteractiveWindowMessages.RestartKernel).ignoreErrors(); + + // Handle an error + await this.errorHandler.handleError(exc); + } } diff --git a/src/client/datascience/interactive-ipynb/nativeEditorCommandListener.ts b/src/client/datascience/interactive-ipynb/nativeEditorCommandListener.ts index 97636f9366e2..91fd2edcf157 100644 --- a/src/client/datascience/interactive-ipynb/nativeEditorCommandListener.ts +++ b/src/client/datascience/interactive-ipynb/nativeEditorCommandListener.ts @@ -77,7 +77,7 @@ export class NativeEditorCommandListener implements IDataScienceCommandListener // Then take the contents and load it. await this.provider.open(file, contents); } catch (e) { - this.dataScienceErrorHandler.handleError(e).ignoreErrors(); + return this.dataScienceErrorHandler.handleError(e); } } } diff --git a/src/client/datascience/interactive-ipynb/nativeEditorProvider.ts b/src/client/datascience/interactive-ipynb/nativeEditorProvider.ts index 4a13e5ef9422..f1b46c22a73a 100644 --- a/src/client/datascience/interactive-ipynb/nativeEditorProvider.ts +++ b/src/client/datascience/interactive-ipynb/nativeEditorProvider.ts @@ -194,7 +194,7 @@ export class NativeEditorProvider implements INotebookEditorProvider, IAsyncDisp const command = 'workbench.action.closeActiveEditor'; await this.cmdManager.executeCommand(command); } catch (e) { - this.dataScienceErrorHandler.handleError(e).ignoreErrors(); + return this.dataScienceErrorHandler.handleError(e); } } } diff --git a/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts b/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts index 45db25118975..4933f6dde117 100644 --- a/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts +++ b/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts @@ -233,10 +233,10 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList } } } else { - this.dataScienceErrorHandler.handleError( + await this.dataScienceErrorHandler.handleError( new JupyterInstallError( localize.DataScience.jupyterNotSupported().format(await this.jupyterExecution.getNotebookError()), - localize.DataScience.pythonInteractiveHelpLink())).ignoreErrors(); + localize.DataScience.pythonInteractiveHelpLink())); } } diff --git a/src/client/datascience/jupyter/jupyterExecution.ts b/src/client/datascience/jupyter/jupyterExecution.ts index ebe024899fbd..2ac27ba96055 100644 --- a/src/client/datascience/jupyter/jupyterExecution.ts +++ b/src/client/datascience/jupyter/jupyterExecution.ts @@ -34,7 +34,7 @@ import { INotebookServerLaunchInfo, INotebookServerOptions } from '../types'; -import {IFindCommandResult, JupyterCommandFinder} from './jupyterCommandFinder'; +import { IFindCommandResult, JupyterCommandFinder } from './jupyterCommandFinder'; import { JupyterConnection, JupyterServerInfo } from './jupyterConnection'; import { JupyterInstallError } from './jupyterInstallError'; import { JupyterKernelSpec } from './jupyterKernelSpec'; @@ -94,7 +94,7 @@ export class JupyterExecutionBase implements IJupyterExecution { } public async getNotebookError(): Promise { - const notebook = await this.commandFinder.findBestCommand(JupyterCommands.NotebookCommand); + const notebook = await this.findBestCommand(JupyterCommands.NotebookCommand); return notebook.error ? notebook.error : localize.DataScience.notebookNotFound(); } @@ -200,7 +200,7 @@ export class JupyterExecutionBase implements IJupyterExecution { public async spawnNotebook(file: string): Promise { // First we find a way to start a notebook server - const notebookCommand = await this.findBestCommandTimed(JupyterCommands.NotebookCommand); + const notebookCommand = await this.findBestCommand(JupyterCommands.NotebookCommand); this.checkNotebookCommand(notebookCommand); const args: string[] = [`--NotebookApp.file_to_run=${file}`]; @@ -211,7 +211,7 @@ export class JupyterExecutionBase implements IJupyterExecution { public async importNotebook(file: string, template: string | undefined): Promise { // First we find a way to start a nbconvert - const convert = await this.findBestCommandTimed(JupyterCommands.ConvertCommand); + const convert = await this.findBestCommand(JupyterCommands.ConvertCommand); if (!convert.command) { throw new Error(localize.DataScience.jupyterNbConvertNotSupported()); } @@ -270,6 +270,10 @@ export class JupyterExecutionBase implements IJupyterExecution { } } + protected async findBestCommand(command: JupyterCommands, cancelToken?: CancellationToken): Promise { + return this.commandFinder.findBestCommand(command, cancelToken); + } + private checkNotebookCommand(notebook: IFindCommandResult) { if (!notebook.command) { const errorMessage = notebook.error ? notebook.error : localize.DataScience.notebookNotFound(); @@ -345,7 +349,7 @@ export class JupyterExecutionBase implements IJupyterExecution { @captureTelemetry(Telemetry.StartJupyter) private async startNotebookServer(useDefaultConfig: boolean, cancelToken?: CancellationToken): Promise<{ connection: IConnection; kernelSpec: IJupyterKernelSpec | undefined }> { // First we find a way to start a notebook server - const notebookCommand = await this.findBestCommandTimed(JupyterCommands.NotebookCommand, cancelToken); + const notebookCommand = await this.findBestCommand(JupyterCommands.NotebookCommand, cancelToken); this.checkNotebookCommand(notebookCommand); // Now actually launch it @@ -452,7 +456,7 @@ export class JupyterExecutionBase implements IJupyterExecution { private getUsableJupyterPythonImpl = async (cancelToken?: CancellationToken): Promise => { // This should be the best interpreter for notebooks - const found = await this.findBestCommandTimed(JupyterCommands.NotebookCommand, cancelToken); + const found = await this.findBestCommand(JupyterCommands.NotebookCommand, cancelToken); if (found && found.command) { return found.command.interpreter(); } @@ -490,7 +494,7 @@ export class JupyterExecutionBase implements IJupyterExecution { private async addMatchingSpec(bestInterpreter: PythonInterpreter, cancelToken?: CancellationToken): Promise { const displayName = localize.DataScience.historyTitle(); - const ipykernelCommand = await this.findBestCommandTimed(JupyterCommands.KernelCreateCommand, cancelToken); + const ipykernelCommand = await this.findBestCommand(JupyterCommands.KernelCreateCommand, cancelToken); // If this fails, then we just skip this spec try { @@ -571,7 +575,7 @@ export class JupyterExecutionBase implements IJupyterExecution { private isCommandSupported = async (command: JupyterCommands, cancelToken?: CancellationToken): Promise => { // See if we can find the command try { - const result = await this.findBestCommandTimed(command, cancelToken); + const result = await this.findBestCommand(command, cancelToken); return result.command !== undefined; } catch (err) { this.logger.logWarning(err); @@ -735,7 +739,7 @@ export class JupyterExecutionBase implements IJupyterExecution { private enumerateSpecs = async (_cancelToken?: CancellationToken): Promise<(JupyterKernelSpec | undefined)[]> => { if (await this.isKernelSpecSupported()) { - const kernelSpecCommand = await this.findBestCommandTimed(JupyterCommands.KernelSpecCommand); + const kernelSpecCommand = await this.findBestCommand(JupyterCommands.KernelSpecCommand); if (kernelSpecCommand.command) { try { @@ -768,8 +772,4 @@ export class JupyterExecutionBase implements IJupyterExecution { return []; } - - private async findBestCommandTimed(command: JupyterCommands, cancelToken?: CancellationToken): Promise { - return this.commandFinder.findBestCommand(command, cancelToken); - } } diff --git a/src/datascience-ui/native-editor/nativeEditorStateController.ts b/src/datascience-ui/native-editor/nativeEditorStateController.ts index 80433cf6dfee..7ed47506dd2c 100644 --- a/src/datascience-ui/native-editor/nativeEditorStateController.ts +++ b/src/datascience-ui/native-editor/nativeEditorStateController.ts @@ -50,6 +50,12 @@ export class NativeEditorStateController extends MainStateController { return super.handleMessage(msg, payload); } + // This method is used by tests to prepare this react control for loading again. + public reset() { + this.waitingForLoadRender = false; + this.setState({ busy: true }); + } + public canMoveUp = (cellId?: string) => { const index = this.getState().cellVMs.findIndex(cvm => cvm.cell.id === cellId); return (index > 0); diff --git a/src/test/datascience/nativeEditor.functional.test.tsx b/src/test/datascience/nativeEditor.functional.test.tsx index ddb2a1284e2d..8fcc845afb86 100644 --- a/src/test/datascience/nativeEditor.functional.test.tsx +++ b/src/test/datascience/nativeEditor.functional.test.tsx @@ -4,17 +4,27 @@ import * as assert from 'assert'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; -import { Disposable, TextDocument, TextEditor } from 'vscode'; +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 { Identifiers } from '../../client/datascience/constants'; -import { ICell, INotebookExporter } from '../../client/datascience/types'; +import { JupyterExecutionFactory } from '../../client/datascience/jupyter/jupyterExecutionFactory'; +import { ICell, IJupyterExecution, INotebookEditorProvider, INotebookExporter } from '../../client/datascience/types'; +import { PythonInterpreter } from '../../client/interpreter/contracts'; import { NativeEditor } from '../../datascience-ui/native-editor/nativeEditor'; import { ImageButton } from '../../datascience-ui/react-common/imageButton'; import { DataScienceIocContainer } from './dataScienceIocContainer'; -import { addCell, createNewEditor, getNativeCellResults, openEditor, runMountedTest } from './nativeEditorTestHelpers'; +import { + addCell, + closeNotebook, + createNewEditor, + getNativeCellResults, + mountNativeWebView, + openEditor, + runMountedTest +} from './nativeEditorTestHelpers'; import { waitForUpdate } from './reactHelpers'; import { addContinuousMockData, @@ -37,6 +47,15 @@ suite('DataScience Native Editor tests', () => { setup(() => { ioc = new DataScienceIocContainer(); ioc.registerDataScienceTypes(); + + const appShell = TypeMoq.Mock.ofType(); + appShell.setup(a => a.showErrorMessage(TypeMoq.It.isAnyString())).returns((_e) => Promise.resolve('')); + appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('')); + appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_a1: string, a2: string, _a3: string) => Promise.resolve(a2)); + appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_a1: string, _a2: any, _a3: string, a4: string) => Promise.resolve(a4)); + appShell.setup(a => a.showSaveDialog(TypeMoq.It.isAny())).returns(() => Promise.resolve(Uri.file('foo.ipynb'))); + ioc.serviceManager.rebindInstance(IApplicationShell, appShell.object); + }); teardown(async () => { @@ -227,4 +246,55 @@ for _ in range(50): verifyHtmlOnCell(wrapper, 'NativeCell', `2`, 1); verifyHtmlOnCell(wrapper, 'NativeCell', `3`, 2); }, () => { return ioc; }); + + runMountedTest('Startup and shutdown', async (wrapper) => { + addMockData(ioc, 'b=2\nb', 2); + addMockData(ioc, 'c=3\nc', 3); + + const baseFile = [ {id: 'NotebookImport#0', data: {source: 'a=1\na'}}, + {id: 'NotebookImport#1', data: {source: 'b=2\nb'}}, + {id: 'NotebookImport#2', data: {source: 'c=3\nc'}} ]; + const runAllCells = baseFile.map(cell => { + return createFileCell(cell, cell.data); + }); + const notebook = await ioc.get(INotebookExporter).translateToNotebook(runAllCells, undefined); + let editor = await openEditor(ioc, JSON.stringify(notebook)); + + // Run everything + let runAllButton = findButton(wrapper, NativeEditor, 3); + await waitForMessageResponse(ioc, () => runAllButton!.simulate('click')); + await waitForUpdate(wrapper, NativeEditor, 16); + + // Close editor. Should still have the server up + await closeNotebook(editor, wrapper); + const jupyterExecution = ioc.serviceManager.get(IJupyterExecution); + const editorProvider = ioc.serviceManager.get(INotebookEditorProvider); + const server = await jupyterExecution.getServer(await editorProvider.getNotebookOptions()); + assert.ok(server, 'Server was destroyed on notebook shutdown'); + + // Reopen, and rerun + editor = await openEditor(ioc, JSON.stringify(notebook)); + runAllButton = findButton(wrapper, NativeEditor, 3); + await waitForMessageResponse(ioc, () => runAllButton!.simulate('click')); + await waitForUpdate(wrapper, NativeEditor, 15); + verifyHtmlOnCell(wrapper, 'NativeCell', `1`, 0); + }, () => { return ioc; }); + + test('Failure', async () => { + // Make a dummy class that will fail during launch + class FailedProcess extends JupyterExecutionFactory { + public getUsableJupyterPython(): Promise { + return Promise.resolve(undefined); + } + } + ioc.serviceManager.rebind(IJupyterExecution, FailedProcess); + ioc.serviceManager.get(IJupyterExecution); + addMockData(ioc, 'a=1\na', 1); + const wrapper = mountNativeWebView(ioc); + await createNewEditor(ioc); + await addCell(wrapper, 'a=1\na', true, 2); + + // Cell should not have the output + verifyHtmlOnCell(wrapper, 'NativeCell', 'Jupyter cannot be started', CellPosition.Last); + }); }); diff --git a/src/test/datascience/nativeEditorTestHelpers.tsx b/src/test/datascience/nativeEditorTestHelpers.tsx index 12cf08779cef..37c4ce84511f 100644 --- a/src/test/datascience/nativeEditorTestHelpers.tsx +++ b/src/test/datascience/nativeEditorTestHelpers.tsx @@ -51,7 +51,7 @@ export function runMountedTest(name: string, testFunc: (wrapper: ReactWrapper(IJupyterExecution); if (await jupyterExecution.isNotebookSupported()) { addMockData(ioc, 'a=1\na', 1); - const wrapper = mountWebView(ioc, ); + const wrapper = mountNativeWebView(ioc); await testFunc(wrapper); } else { // tslint:disable-next-line:no-console @@ -60,6 +60,10 @@ export function runMountedTest(name: string, testFunc: (wrapper: ReactWrapper, React.Component> { + return mountWebView(ioc, ); +} + // tslint:disable-next-line: no-any export async function addCell(wrapper: ReactWrapper, React.Component>, code: string, submit: boolean = true, expectedSubmitRenderCount: number = 5): Promise { // First get the stateController on the main panel. That's how we'll add a new cell. @@ -81,3 +85,11 @@ export async function addCell(wrapper: ReactWrapper, React.Com return update; } } + +export function closeNotebook(editor: INotebookEditor, wrapper: ReactWrapper, React.Component>): Promise { + const reactEditor = getMainPanel(wrapper, NativeEditor); + if (reactEditor) { + reactEditor.stateController.reset(); + } + return editor.dispose(); +} diff --git a/src/test/datascience/testHelpers.tsx b/src/test/datascience/testHelpers.tsx index 157899499684..461a6aac039a 100644 --- a/src/test/datascience/testHelpers.tsx +++ b/src/test/datascience/testHelpers.tsx @@ -111,6 +111,8 @@ export function getLastOutputCell(wrapper: ReactWrapper, React } export function verifyHtmlOnCell(wrapper: ReactWrapper, React.Component>, cellType: string, html: string | undefined, cellIndex: number | CellPosition) { + wrapper.update(); + const foundResult = wrapper.find(cellType); assert.ok(foundResult.length >= 1, 'Didn\'t find any cells being rendered'); @@ -150,9 +152,9 @@ export function verifyHtmlOnCell(wrapper: ReactWrapper, React. 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'; + const outputHtml = output.length > 0 ? output.html() : undefined; // html not specified, look for an empty render - assert.ok(targetCell!.isEmptyRender(), `Target cell is not empty render, got this instead: ${outputHtml}`); + assert.ok(targetCell!.isEmptyRender() || outputHtml === undefined, `Target cell is not empty render, got this instead: ${outputHtml}`); } }