diff --git a/news/2 Fixes/11711.md b/news/2 Fixes/11711.md new file mode 100644 index 000000000000..925b2acebf8c --- /dev/null +++ b/news/2 Fixes/11711.md @@ -0,0 +1 @@ +Fix saving during close and auto backup to actually save a notebook. \ No newline at end of file diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index cb491620eb41..60e1173dccb9 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -195,8 +195,8 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu [DSCommands.SetJupyterKernel]: [KernelConnectionMetadata, Uri, undefined | Uri]; [DSCommands.SwitchJupyterKernel]: [ISwitchKernelOptions | undefined]; [DSCommands.SelectJupyterCommandLine]: [undefined | Uri]; - [DSCommands.SaveNotebookNonCustomEditor]: [Uri]; - [DSCommands.SaveAsNotebookNonCustomEditor]: [Uri, Uri]; + [DSCommands.SaveNotebookNonCustomEditor]: [INotebookModel]; + [DSCommands.SaveAsNotebookNonCustomEditor]: [INotebookModel, Uri]; [DSCommands.OpenNotebookNonCustomEditor]: [Uri]; [DSCommands.GatherQuality]: [string]; [DSCommands.LatestExtension]: [string]; diff --git a/src/client/datascience/gather/gatherLogger.ts b/src/client/datascience/gather/gatherLogger.ts index c947f9e3d6e6..e5b625d26a2f 100644 --- a/src/client/datascience/gather/gatherLogger.ts +++ b/src/client/datascience/gather/gatherLogger.ts @@ -3,7 +3,7 @@ import { inject, injectable } from 'inversify'; import cloneDeep = require('lodash/cloneDeep'); import { extensions } from 'vscode'; import { concatMultilineString } from '../../../datascience-ui/common'; -import { traceError } from '../../common/logger'; +import { traceError, traceInfo } from '../../common/logger'; import { IConfigurationService } from '../../common/types'; import { noop } from '../../common/utils/misc'; import { sendTelemetryEvent } from '../../telemetry'; @@ -69,7 +69,11 @@ export class GatherLogger implements IGatherLogger { } } const api = ext.exports; - this.gather = api.getGatherProvider(); + try { + this.gather = api.getGatherProvider(); + } catch { + traceInfo(`Gather not installed`); + } } } } diff --git a/src/client/datascience/interactive-ipynb/nativeEditorOldWebView.ts b/src/client/datascience/interactive-ipynb/nativeEditorOldWebView.ts index 2510048c538f..17103448e995 100644 --- a/src/client/datascience/interactive-ipynb/nativeEditorOldWebView.ts +++ b/src/client/datascience/interactive-ipynb/nativeEditorOldWebView.ts @@ -269,7 +269,7 @@ export class NativeEditorOldWebView extends NativeEditor { } try { if (!this.isUntitled) { - await this.commandManager.executeCommand(Commands.SaveNotebookNonCustomEditor, this.model?.file); + await this.commandManager.executeCommand(Commands.SaveNotebookNonCustomEditor, this.model); this.savedEvent.fire(this); return; } @@ -295,7 +295,7 @@ export class NativeEditorOldWebView extends NativeEditor { if (fileToSaveTo) { await this.commandManager.executeCommand( Commands.SaveAsNotebookNonCustomEditor, - this.model.file, + this.model, fileToSaveTo ); this.savedEvent.fire(this); diff --git a/src/client/datascience/interactive-ipynb/nativeEditorProviderOld.ts b/src/client/datascience/interactive-ipynb/nativeEditorProviderOld.ts index c92dcc94ebaf..f22e117111a3 100644 --- a/src/client/datascience/interactive-ipynb/nativeEditorProviderOld.ts +++ b/src/client/datascience/interactive-ipynb/nativeEditorProviderOld.ts @@ -83,7 +83,7 @@ export class NativeEditorProviderOld extends NativeEditorProvider { @inject(IWorkspaceService) workspace: IWorkspaceService, @inject(IConfigurationService) configuration: IConfigurationService, @inject(ICustomEditorService) customEditorService: ICustomEditorService, - @inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem, + @inject(IDataScienceFileSystem) fs: IDataScienceFileSystem, @inject(IDocumentManager) private documentManager: IDocumentManager, @inject(ICommandManager) private readonly cmdManager: ICommandManager, @inject(IDataScienceErrorHandler) private dataScienceErrorHandler: IDataScienceErrorHandler, @@ -98,7 +98,8 @@ export class NativeEditorProviderOld extends NativeEditorProvider { configuration, customEditorService, storage, - notebookProvider + notebookProvider, + fs ); // No live share sync required as open document from vscode will give us our contents. @@ -107,21 +108,18 @@ export class NativeEditorProviderOld extends NativeEditorProvider { this.documentManager.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditorHandler.bind(this)) ); this.disposables.push( - this.cmdManager.registerCommand(Commands.SaveNotebookNonCustomEditor, async (resource: Uri) => { - const customDocument = this.customDocuments.get(resource.fsPath); - if (customDocument) { - await this.saveCustomDocument(customDocument, new CancellationTokenSource().token); - } + this.cmdManager.registerCommand(Commands.SaveNotebookNonCustomEditor, async (model: INotebookModel) => { + await this.storage.save(model, new CancellationTokenSource().token); }) ); this.disposables.push( this.cmdManager.registerCommand( Commands.SaveAsNotebookNonCustomEditor, - async (resource: Uri, targetResource: Uri) => { - const customDocument = this.customDocuments.get(resource.fsPath); + async (model: INotebookModel, targetResource: Uri) => { + await this.storage.saveAs(model, targetResource); + const customDocument = this.customDocuments.get(model.file.fsPath); if (customDocument) { - await this.saveCustomDocumentAs(customDocument, targetResource); - this.customDocuments.delete(resource.fsPath); + this.customDocuments.delete(model.file.fsPath); this.customDocuments.set(targetResource.fsPath, { ...customDocument, uri: targetResource }); } } diff --git a/src/client/datascience/notebookStorage/nativeEditorProvider.ts b/src/client/datascience/notebookStorage/nativeEditorProvider.ts index 984fe9006f59..4561d9f6e376 100644 --- a/src/client/datascience/notebookStorage/nativeEditorProvider.ts +++ b/src/client/datascience/notebookStorage/nativeEditorProvider.ts @@ -108,7 +108,8 @@ export class NativeEditorProvider implements INotebookEditorProvider, CustomEdit @inject(IConfigurationService) protected readonly configuration: IConfigurationService, @inject(ICustomEditorService) private customEditorService: ICustomEditorService, @inject(INotebookStorageProvider) protected readonly storage: INotebookStorageProvider, - @inject(INotebookProvider) private readonly notebookProvider: INotebookProvider + @inject(INotebookProvider) private readonly notebookProvider: INotebookProvider, + @inject(IDataScienceFileSystem) protected readonly fs: IDataScienceFileSystem ) { traceInfo(`id is ${this._id}`); @@ -214,14 +215,18 @@ export class NativeEditorProvider implements INotebookEditorProvider, CustomEdit public async loadModel(file: Uri, contents?: string, backupId?: string): Promise; // tslint:disable-next-line: no-any public async loadModel(file: Uri, contents?: string, options?: any): Promise { - // Every time we load a new untitled file, up the counter past the max value for this counter - this.untitledCounter = getNextUntitledCounter(file, this.untitledCounter); + // Get the model that may match this file + let model = [...this.models.values()].find((m) => this.fs.arePathsSame(m.file, file)); + if (!model) { + // Every time we load a new untitled file, up the counter past the max value for this counter + this.untitledCounter = getNextUntitledCounter(file, this.untitledCounter); - // Load our model from our storage object. - const model = await this.storage.getOrCreateModel(file, contents, options); + // Load our model from our storage object. + model = await this.storage.getOrCreateModel(file, contents, options); - // Make sure to listen to events on the model - this.trackModel(model); + // Make sure to listen to events on the model + this.trackModel(model); + } return model; } diff --git a/src/client/datascience/notebookStorage/nativeEditorStorage.ts b/src/client/datascience/notebookStorage/nativeEditorStorage.ts index a778943e4d9c..0ca96a90bf9e 100644 --- a/src/client/datascience/notebookStorage/nativeEditorStorage.ts +++ b/src/client/datascience/notebookStorage/nativeEditorStorage.ts @@ -26,7 +26,7 @@ import { import detectIndent = require('detect-indent'); import { VSCodeNotebookModel } from './vscNotebookModel'; -const KeyPrefix = 'notebook-storage-'; +export const KeyPrefix = 'notebook-storage-'; const NotebookTransferKey = 'notebook-transfered'; export function getNextUntitledCounter(file: Uri | undefined, currentValue: number): number { @@ -178,21 +178,29 @@ export class NativeEditorStorage implements INotebookStorage { // Keep track of the time when this data was saved. // This way when we retrieve the data we can compare it against last modified date of the file. const specialContents = contents ? JSON.stringify({ contents, lastModifiedTimeMs: Date.now() }) : undefined; - return this.writeToStorage(filePath, specialContents, cancelToken); + return this.writeToStorage(model.file, filePath, specialContents, cancelToken); } private async clearHotExit(file: Uri, backupId?: string): Promise { const key = backupId || this.getStaticStorageKey(file); const filePath = this.getHashedFileName(key); - await this.writeToStorage(filePath); + await this.writeToStorage(undefined, filePath); } - private async writeToStorage(filePath: string, contents?: string, cancelToken?: CancellationToken): Promise { + private async writeToStorage( + owningFile: Uri | undefined, + filePath: string, + contents?: string, + cancelToken?: CancellationToken + ): Promise { try { if (!cancelToken?.isCancellationRequested) { if (contents) { await this.fs.createLocalDirectory(path.dirname(filePath)); if (!cancelToken?.isCancellationRequested) { + if (owningFile) { + this.trustService.trustNotebook(owningFile, contents).ignoreErrors(); + } await this.fs.writeLocalFile(filePath, contents); } } else { @@ -374,6 +382,8 @@ export class NativeEditorStorage implements INotebookStorage { if (isNotebookTrusted) { model.trust(); } + } else { + model.trust(); } return model; @@ -407,9 +417,10 @@ export class NativeEditorStorage implements INotebookStorage { } private async getStoredContentsFromFile(file: Uri, key: string): Promise { + const filePath = this.getHashedFileName(key); try { // Use this to read from the extension global location - const contents = await this.fs.readLocalFile(file.fsPath); + const contents = await this.fs.readLocalFile(filePath); const data = JSON.parse(contents); // Check whether the file has been modified since the last time the contents were saved. if (data && data.lastModifiedTimeMs && file.scheme === 'file') { diff --git a/src/datascience-ui/interactive-common/redux/store.ts b/src/datascience-ui/interactive-common/redux/store.ts index 133e19bec405..345be932aa1f 100644 --- a/src/datascience-ui/interactive-common/redux/store.ts +++ b/src/datascience-ui/interactive-common/redux/store.ts @@ -31,6 +31,7 @@ import { forceLoad } from '../transforms'; import { isAllowedAction, isAllowedMessage, postActionToExtension } from './helpers'; import { generatePostOfficeSendReducer } from './postOffice'; import { generateMonacoReducer, IMonacoState } from './reducers/monaco'; +import { CommonActionType } from './reducers/types'; import { generateVariableReducer, IVariableState } from './reducers/variables'; function generateDefaultState( @@ -109,19 +110,21 @@ function createSendInfoMiddleware(): Redux.Middleware<{}, IStore> { } // If cell vm count changed or selected cell changed, send the message - const currentSelection = getSelectedAndFocusedInfo(afterState.main); - if ( - prevState.main.cellVMs.length !== afterState.main.cellVMs.length || - getSelectedAndFocusedInfo(prevState.main).selectedCellId !== currentSelection.selectedCellId || - prevState.main.undoStack.length !== afterState.main.undoStack.length || - prevState.main.redoStack.length !== afterState.main.redoStack.length - ) { - postActionToExtension({ queueAction: store.dispatch }, InteractiveWindowMessages.SendInfo, { - cellCount: afterState.main.cellVMs.length, - undoCount: afterState.main.undoStack.length, - redoCount: afterState.main.redoStack.length, - selectedCell: currentSelection.selectedCellId - }); + if (!action.type || action.type !== CommonActionType.UNMOUNT) { + const currentSelection = getSelectedAndFocusedInfo(afterState.main); + if ( + prevState.main.cellVMs.length !== afterState.main.cellVMs.length || + getSelectedAndFocusedInfo(prevState.main).selectedCellId !== currentSelection.selectedCellId || + prevState.main.undoStack.length !== afterState.main.undoStack.length || + prevState.main.redoStack.length !== afterState.main.redoStack.length + ) { + postActionToExtension({ queueAction: store.dispatch }, InteractiveWindowMessages.SendInfo, { + cellCount: afterState.main.cellVMs.length, + undoCount: afterState.main.undoStack.length, + redoCount: afterState.main.redoStack.length, + selectedCell: currentSelection.selectedCellId + }); + } } return res; }; @@ -159,21 +162,26 @@ function createTestMiddleware(): Redux.Middleware<{}, IStore> { }); }; - // Special case for focusing a cell - const previousSelection = getSelectedAndFocusedInfo(prevState.main); - const currentSelection = getSelectedAndFocusedInfo(afterState.main); - if (previousSelection.focusedCellId !== currentSelection.focusedCellId && currentSelection.focusedCellId) { - // Send async so happens after render state changes (so our enzyme wrapper is up to date) - sendMessage(InteractiveWindowMessages.FocusedCellEditor, { cellId: action.payload.cellId }); - } - if (previousSelection.selectedCellId !== currentSelection.selectedCellId && currentSelection.selectedCellId) { - // Send async so happens after render state changes (so our enzyme wrapper is up to date) - sendMessage(InteractiveWindowMessages.SelectedCell, { cellId: action.payload.cellId }); - } - // Special case for unfocusing a cell - if (previousSelection.focusedCellId !== currentSelection.focusedCellId && !currentSelection.focusedCellId) { - // Send async so happens after render state changes (so our enzyme wrapper is up to date) - sendMessage(InteractiveWindowMessages.UnfocusedCellEditor); + if (!action.type || action.type !== CommonActionType.UNMOUNT) { + // Special case for focusing a cell + const previousSelection = getSelectedAndFocusedInfo(prevState.main); + const currentSelection = getSelectedAndFocusedInfo(afterState.main); + if (previousSelection.focusedCellId !== currentSelection.focusedCellId && currentSelection.focusedCellId) { + // Send async so happens after render state changes (so our enzyme wrapper is up to date) + sendMessage(InteractiveWindowMessages.FocusedCellEditor, { cellId: action.payload.cellId }); + } + if ( + previousSelection.selectedCellId !== currentSelection.selectedCellId && + currentSelection.selectedCellId + ) { + // Send async so happens after render state changes (so our enzyme wrapper is up to date) + sendMessage(InteractiveWindowMessages.SelectedCell, { cellId: action.payload.cellId }); + } + // Special case for unfocusing a cell + if (previousSelection.focusedCellId !== currentSelection.focusedCellId && !currentSelection.focusedCellId) { + // Send async so happens after render state changes (so our enzyme wrapper is up to date) + sendMessage(InteractiveWindowMessages.UnfocusedCellEditor); + } } // Indicate settings updates @@ -218,7 +226,10 @@ function createTestMiddleware(): Redux.Middleware<{}, IStore> { sendMessage(InteractiveWindowMessages.ExecutionRendered); } - if (!action.type || action.type !== InteractiveWindowMessages.FinishCell) { + if ( + !action.type || + (action.type !== InteractiveWindowMessages.FinishCell && action.type !== CommonActionType.UNMOUNT) + ) { // Might be a non finish but still update cells (like an undo or a delete) const prevFinished = prevState.main.cellVMs .filter((c) => c.cell.state === CellState.finished || c.cell.state === CellState.error) diff --git a/src/test/datascience/interactive-ipynb/nativeEditorStorage.unit.test.ts b/src/test/datascience/interactive-ipynb/nativeEditorStorage.unit.test.ts index d0416e2fd2c3..1406af6445d5 100644 --- a/src/test/datascience/interactive-ipynb/nativeEditorStorage.unit.test.ts +++ b/src/test/datascience/interactive-ipynb/nativeEditorStorage.unit.test.ts @@ -283,7 +283,9 @@ suite('DataScience - Native Editor Storage', () => { when(executionProvider.serverStarted).thenReturn(serverStartedEvent.event); when(trustService.isNotebookTrusted(anything(), anything())).thenReturn(Promise.resolve(true)); - when(trustService.trustNotebook(anything(), anything())).thenReturn(Promise.resolve()); + when(trustService.trustNotebook(anything(), anything())).thenCall(() => { + return Promise.resolve(); + }); testIndex += 1; when(crypto.createHash(anything(), 'string')).thenReturn(`${testIndex}`); @@ -351,7 +353,7 @@ suite('DataScience - Native Editor Storage', () => { context.object, globalMemento, localMemento, - trustService, + instance(trustService), new NotebookModelFactory(false) ); diff --git a/src/test/datascience/mountedWebView.ts b/src/test/datascience/mountedWebView.ts index 6da22b1a0693..e5519624bff2 100644 --- a/src/test/datascience/mountedWebView.ts +++ b/src/test/datascience/mountedWebView.ts @@ -7,7 +7,7 @@ import { IWebPanelOptions, WebPanelMessage } from '../../client/common/application/types'; -import { traceInfo } from '../../client/common/logger'; +import { traceError, traceInfo } from '../../client/common/logger'; import { IDisposable } from '../../client/common/types'; import { createDeferred } from '../../client/common/utils/async'; import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; @@ -236,6 +236,9 @@ export class MountedWebView implements IMountedWebView, IDisposable { } } private postMessageToWebPanel(msg: any) { + if (this.disposed && !msg.type.startsWith(`DISPATCHED`)) { + traceError(`Posting to disposed mount.`); + } if (this.webPanelListener) { this.webPanelListener.onMessage(msg.type, msg.payload); } else { diff --git a/src/test/datascience/nativeEditor.functional.test.tsx b/src/test/datascience/nativeEditor.functional.test.tsx index 5df7a46d0b2d..041b71198905 100644 --- a/src/test/datascience/nativeEditor.functional.test.tsx +++ b/src/test/datascience/nativeEditor.functional.test.tsx @@ -13,7 +13,8 @@ import * as path from 'path'; import * as sinon from 'sinon'; import { anything, objectContaining, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; -import { Disposable, TextDocument, TextEditor, Uri, WindowState } from 'vscode'; +import { CustomEditorProvider, Disposable, TextDocument, TextEditor, Uri, WindowState } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; import { IApplicationShell, ICommandManager, @@ -22,12 +23,14 @@ import { IWorkspaceService } from '../../client/common/application/types'; import { LocalZMQKernel } from '../../client/common/experiments/groups'; +import { ICryptoUtils, IExtensionContext } from '../../client/common/types'; import { createDeferred, sleep, waitForPromise } from '../../client/common/utils/async'; import { noop } from '../../client/common/utils/misc'; import { Commands, Identifiers } from '../../client/datascience/constants'; import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; import { NativeEditor as NativeEditorWebView } from '../../client/datascience/interactive-ipynb/nativeEditor'; import { IKernelSpecQuickPickItem } from '../../client/datascience/jupyter/kernels/types'; +import { KeyPrefix } from '../../client/datascience/notebookStorage/nativeEditorStorage'; import { ICell, IDataScienceErrorHandler, @@ -79,8 +82,10 @@ import { srcDirectory, typeCode, verifyCellIndex, + verifyCellSource, verifyHtmlOnCell } from './testHelpers'; +import { ITestNativeEditorProvider } from './testNativeEditorProvider'; use(chaiAsPromised); @@ -238,6 +243,137 @@ suite('DataScience Native Editor', () => { } }); + runMountedTest('Save on close', async (_context) => { + // Close should cause the save as to come up. Remap appshell so we can check + const dummyDisposable = { + dispose: () => { + return; + } + }; + const appShell = TypeMoq.Mock.ofType(); + appShell + .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())) + .returns((e) => { + throw e; + }); + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns((_a1, _a2, a3, _a4) => Promise.resolve(a3)); + appShell + .setup((a) => a.showSaveDialog(TypeMoq.It.isAny())) + .returns(() => { + return Promise.resolve(Uri.file(tempNotebookFile.filePath)); + }); + appShell.setup((a) => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); + ioc.serviceManager.rebindInstance(IApplicationShell, appShell.object); + + // Create an editor + const ne = await createNewEditor(ioc); + + // Add a cell + await addCell(ne.mount, 'a=1\na'); + + // Close the editor. It should ask for save as (if not custom editor) + if (useCustomEditorApi) { + // For custom editor do what VS code would do on close + const notebookEditorProvider = ioc.get(INotebookEditorProvider); + const customDoc = notebookEditorProvider.getCustomDocument(ne.editor.file); + assert.ok(customDoc, 'No custom document for new notebook'); + const customEditorProvider = (notebookEditorProvider as any) as CustomEditorProvider; + await customEditorProvider.saveCustomDocumentAs( + customDoc!, + Uri.file(tempNotebookFile.filePath), + CancellationToken.None + ); + } + await ne.editor.dispose(); + + // Open the temp file to make sure it has the new cell + const opened = await openEditor(ioc, '', tempNotebookFile.filePath); + + verifyCellSource(opened.mount.wrapper, 'NativeCell', 'a=1\na', CellPosition.Last); + }); + + function getHashedFileName(file: Uri): string { + const crypto = ioc.get(ICryptoUtils); + const context = ioc.get(IExtensionContext); + const key = `${KeyPrefix}${file.toString()}`; + const name = `${crypto.createHash(key, 'string')}.ipynb`; + return path.join(context.globalStoragePath, name); + } + + runMountedTest('Save on shutdown', async (context) => { + // Skip this test is using custom editor. VS code handles this situation + if (useCustomEditorApi) { + context.skip(); + } else { + // When we dispose act like user wasn't able to hit anything + const appShell = TypeMoq.Mock.ofType(); + appShell + .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())) + .returns((e) => { + throw e; + }); + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns((_a1, _a2, _a3, _a4) => Promise.resolve(undefined)); + appShell + .setup((a) => a.showSaveDialog(TypeMoq.It.isAny())) + .returns(() => { + return Promise.resolve(Uri.file(tempNotebookFile.filePath)); + }); + ioc.serviceManager.rebindInstance(IApplicationShell, appShell.object); + + // Turn off auto save so that backup works. + await updateFileConfig(ioc, 'autoSave', 'off'); + + // Create an editor with a specific path + const ne = await openEditor(ioc, '', tempNotebookFile.filePath); + + // Figure out the backup file name + const deferred = createDeferred(); + const backupFileName = getHashedFileName(Uri.file(tempNotebookFile.filePath)); + fs.watchFile(backupFileName, (c, p) => { + if (p.mtime < c.mtime) { + deferred.resolve(true); + } + }); + + try { + // Add a cell + await addCell(ne.mount, 'a=1\na'); + + // Wait for write. It should have written to backup + const result = await waitForPromise(deferred.promise, 5000); + assert.ok(result, 'Backup file did not write'); + + // Prevent reopen (we want to act like shutdown) + (ne.editor as any).reopen = noop; + await closeNotebook(ioc, ne.editor); + } finally { + fs.unwatchFile(backupFileName); + } + + // Reopen and verify + const opened = await openEditor(ioc, '', tempNotebookFile.filePath); + verifyCellSource(opened.mount.wrapper, 'NativeCell', 'a=1\na', CellPosition.Last); + } + }); + runMountedTest('Invalid kernel still runs', async (context) => { if (ioc.mockJupyter) { const kernelDesc = { diff --git a/src/test/datascience/testHelpers.tsx b/src/test/datascience/testHelpers.tsx index ebc746d39bb7..d8c07d1236b6 100644 --- a/src/test/datascience/testHelpers.tsx +++ b/src/test/datascience/testHelpers.tsx @@ -178,6 +178,49 @@ export function getLastOutputCell( return getOutputCell(wrapper, cellType, foundResult.length - count)!; } +export function verifyCellSource( + wrapper: ReactWrapper, React.Component>, + cellType: 'NativeCell' | 'InteractiveCell', + source: string, + cellIndex: number | CellPosition +) { + wrapper.update(); + + const foundResult = wrapper.find(cellType); + assert.ok(foundResult.length >= 1, "Didn't find any cells being rendered"); + let targetCell: ReactWrapper; + let index = 0; + // 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); + index = foundResult.length - 1; + 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"); + + const editor = cellType === 'InteractiveCell' ? getInteractiveEditor(wrapper) : getNativeEditor(wrapper, index); + const inst = editor!.instance() as MonacoEditor; + assert.deepStrictEqual(inst.state.model?.getValue(), source, 'Source does not match on cell'); +} + export function verifyHtmlOnCell( wrapper: ReactWrapper, React.Component>, cellType: 'NativeCell' | 'InteractiveCell', diff --git a/src/test/datascience/testNativeEditorProvider.ts b/src/test/datascience/testNativeEditorProvider.ts index 4a4031709e2f..2b85c3ca2f6f 100644 --- a/src/test/datascience/testNativeEditorProvider.ts +++ b/src/test/datascience/testNativeEditorProvider.ts @@ -3,7 +3,7 @@ 'use strict'; import { inject, injectable } from 'inversify'; import * as uuid from 'uuid/v4'; -import { Uri, WebviewPanel } from 'vscode'; +import { CustomDocument, Uri, WebviewPanel } from 'vscode'; import { ICommandManager, @@ -35,6 +35,7 @@ import { mountConnectedMainPanel } from './testHelpers'; export interface ITestNativeEditorProvider extends INotebookEditorProvider { getMountedWebView(window: INotebookEditor | undefined): IMountedWebView; waitForMessage(file: Uri | undefined, message: string, options?: WaitForMessageOptions): Promise; + getCustomDocument(file: Uri): CustomDocument | undefined; } // Mixin class to provide common functionality between the two different native editor providers. @@ -70,6 +71,10 @@ function TestNativeEditorProviderMixin return this.pendingMessageWaits[this.pendingMessageWaits.length - 1].deferred.promise; } + public getCustomDocument(file: Uri) { + return this.customDocuments.get(file.fsPath); + } + protected createNotebookEditor(model: INotebookModel, panel?: WebviewPanel): NativeEditor { // Generate the mount wrapper using a custom id const id = uuid(); @@ -125,7 +130,8 @@ export class TestNativeEditorProvider extends TestNativeEditorProviderMixin(Nati @inject(IConfigurationService) configuration: IConfigurationService, @inject(ICustomEditorService) customEditorService: ICustomEditorService, @inject(INotebookStorageProvider) storage: INotebookStorageProvider, - @inject(INotebookProvider) notebookProvider: INotebookProvider + @inject(INotebookProvider) notebookProvider: INotebookProvider, + @inject(IDataScienceFileSystem) fs: IDataScienceFileSystem ) { super( serviceContainer, @@ -135,7 +141,8 @@ export class TestNativeEditorProvider extends TestNativeEditorProviderMixin(Nati configuration, customEditorService, storage, - notebookProvider + notebookProvider, + fs ); } }