11// Copyright (c) Microsoft Corporation. All rights reserved.
22// Licensed under the MIT License.
33'use strict' ;
4- import { assert , use } from 'chai' ;
4+ import { assert , expect , use } from 'chai' ;
55import * as chaiAsPromised from 'chai-as-promised' ;
66import { ReactWrapper } from 'enzyme' ;
77import * as fs from 'fs-extra' ;
88import { Disposable } from 'vscode' ;
99import { EnableTrustedNotebooks } from '../../client/common/experiments/groups' ;
1010import { noop } from '../../client/common/utils/misc' ;
1111import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes' ;
12- import { INotebookEditorProvider } from '../../client/datascience/types' ;
12+ import { INotebookEditor , INotebookEditorProvider , ITrustService } from '../../client/datascience/types' ;
13+ import { CommonActionType } from '../../datascience-ui/interactive-common/redux/reducers/types' ;
14+ import { TrustMessage } from '../../datascience-ui/interactive-common/trustMessage' ;
1315import { NativeCell } from '../../datascience-ui/native-editor/nativeCell' ;
1416import { NativeEditor } from '../../datascience-ui/native-editor/nativeEditor' ;
1517import { IKeyboardEvent } from '../../datascience-ui/react-common/event' ;
1618import { ImageButton } from '../../datascience-ui/react-common/imageButton' ;
1719import { MonacoEditor } from '../../datascience-ui/react-common/monacoEditor' ;
1820import { createTemporaryFile } from '../utils/fs' ;
1921import { DataScienceIocContainer } from './dataScienceIocContainer' ;
20- import { WaitForMessageOptions } from './mountedWebView' ;
21- import { openEditor } from './nativeEditorTestHelpers' ;
22+ import { IMountedWebView , WaitForMessageOptions } from './mountedWebView' ;
23+ import { closeNotebook , openEditor } from './nativeEditorTestHelpers' ;
2224import {
2325 addMockData ,
2426 enterEditorKey ,
2527 findButton ,
2628 getNativeFocusedEditor ,
2729 getOutputCell ,
2830 isCellFocused ,
31+ isCellMarkdown ,
32+ isCellSelected ,
2933 typeCode ,
3034 verifyHtmlOnCell
3135} from './testHelpers' ;
@@ -40,8 +44,9 @@ function waitForMessage(ioc: DataScienceIocContainer, message: string, options?:
4044 . waitForMessage ( message , options ) ;
4145}
4246// tslint:disable:no-any no-multiline-string
43- suite ( 'Untrusted notebooks ' , ( ) => {
47+ suite ( 'Notebook trust ' , ( ) => {
4448 let wrapper : ReactWrapper < any , Readonly < { } > , React . Component > ;
49+ let ne : { editor : INotebookEditor ; mount : IMountedWebView } ;
4550 const disposables : Disposable [ ] = [ ] ;
4651 let ioc : DataScienceIocContainer ;
4752 const baseFile = `
@@ -217,22 +222,31 @@ suite('Untrusted notebooks', () => {
217222 wrapper . update ( ) ;
218223 }
219224
220- async function setupFunction ( this : Mocha . Context , fileContents ?: any ) {
225+ async function setupFunction ( this : Mocha . Context ) {
221226 addMockData ( ioc , 'b=2\nb' , 2 ) ;
222227 addMockData ( ioc , 'c=3\nc' , 3 ) ;
223228 // Use a real file so we can save notebook to a file.
224229 // This is used in some tests (saving).
225230 notebookFile = await createTemporaryFile ( '.ipynb' ) ;
226- await fs . writeFile ( notebookFile . filePath , fileContents ? fileContents : baseFile ) ;
227- const ne = await openEditor ( ioc , fileContents ? fileContents : baseFile , notebookFile . filePath ) ;
231+ await fs . writeFile ( notebookFile . filePath , baseFile ) ;
232+ ne = await openEditor ( ioc , baseFile , notebookFile . filePath ) ;
228233 wrapper = ne . mount . wrapper ;
229234 }
235+
230236 function clickCell ( cellIndex : number ) {
231237 wrapper . update ( ) ;
232238 wrapper . find ( NativeCell ) . at ( cellIndex ) . simulate ( 'click' ) ;
233239 wrapper . update ( ) ;
234240 }
235241
242+ async function focusCell ( targetCellIndex : number ) {
243+ const update = waitForMessage ( ioc , InteractiveWindowMessages . FocusedCellEditor ) ;
244+ clickCell ( targetCellIndex ) ;
245+ simulateKeyPressOnCell ( targetCellIndex , { code : 'Enter' , editorInfo : undefined } ) ;
246+ await update ;
247+ assert . ok ( isCellFocused ( wrapper , 'NativeCell' , targetCellIndex ) ) ;
248+ }
249+
236250 teardown ( async ( ) => {
237251 for ( const disposable of disposables ) {
238252 if ( ! disposable ) {
@@ -258,47 +272,180 @@ suite('Untrusted notebooks', () => {
258272 await setupFunction . call ( this ) ;
259273 } ) ;
260274
261- test ( 'Outputs are not rendered' , ( ) => {
262- // No outputs should have rendered
263- assert . throws ( ( ) => verifyHtmlOnCell ( wrapper , 'NativeCell' , '<span>1</span>' , 0 ) ) ;
264- assert . throws ( ( ) => verifyHtmlOnCell ( wrapper , 'NativeCell' , '<span>2</span>' , 1 ) ) ;
265- assert . throws ( ( ) => verifyHtmlOnCell ( wrapper , 'NativeCell' , '<span>3</span>' , 2 ) ) ;
266- } ) ;
267- test ( 'Cannot insert cell into notebook' , async ( ) => {
268- // Click insert cell button
269- const insertCellButton = findButton ( wrapper , NativeEditor , 5 ) ;
270- insertCellButton ?. simulate ( 'click' ) ;
275+ suite ( 'Open an untrusted notebook' , async ( ) => {
276+ test ( 'Outputs are not rendered' , ( ) => {
277+ // No outputs should have rendered
278+ assert . throws ( ( ) => verifyHtmlOnCell ( wrapper , 'NativeCell' , '<span>1</span>' , 0 ) ) ;
279+ assert . throws ( ( ) => verifyHtmlOnCell ( wrapper , 'NativeCell' , '<span>2</span>' , 1 ) ) ;
280+ assert . throws ( ( ) => verifyHtmlOnCell ( wrapper , 'NativeCell' , '<span>3</span>' , 2 ) ) ;
281+ } ) ;
282+ test ( 'Cannot edit cell contents' , async ( ) => {
283+ await focusCell ( 0 ) ;
271284
272- // No cell should have been added
273- assert . equal ( wrapper . find ( 'NativeCell' ) . length , 3 , 'Cell added' ) ;
274- } ) ;
275- test ( 'Cannot edit cell contents' , async ( ) => {
276- // Set focus to a cell
277- let update = waitForMessage ( ioc , InteractiveWindowMessages . SelectedCell ) ;
278- clickCell ( 0 ) ;
279- await update ;
280- update = waitForMessage ( ioc , InteractiveWindowMessages . FocusedCellEditor ) ;
281- simulateKeyPressOnCell ( 0 , { code : 'Enter' } ) ;
282- await update ;
283- assert . ok ( isCellFocused ( wrapper , 'NativeCell' , 0 ) ) ;
285+ // Try to type code
286+ const editorEnzyme = getNativeFocusedEditor ( wrapper ) ;
287+ typeCode ( editorEnzyme , 'foo' ) ;
288+ const reactEditor = editorEnzyme ! . instance ( ) as MonacoEditor ;
289+ const editor = reactEditor . state . editor ;
290+ if ( editor ) {
291+ assert . notInclude ( editor . getModel ( ) ! . getValue ( ) , 'foo' , 'Was able to edit cell in untrusted notebook' ) ;
292+ }
293+ } ) ;
284294
285- // Then try to type code
286- const editorEnzyme = getNativeFocusedEditor ( wrapper ) ;
287- typeCode ( editorEnzyme , 'foo' ) ;
288- const reactEditor = editorEnzyme ! . instance ( ) as MonacoEditor ;
289- const editor = reactEditor . state . editor ;
290- if ( editor ) {
291- assert . notInclude ( editor . getModel ( ) ! . getValue ( ) , 'foo' , 'Was able to edit cell in untrusted notebook' ) ;
292- }
295+ suite ( 'Buttons are disabled' , async ( ) => {
296+ test ( 'Cannot run cell' , async ( ) => {
297+ // Click run cell button
298+ const cell = getOutputCell ( wrapper , 'NativeCell' , 1 ) ;
299+ const imageButtons = cell ! . find ( ImageButton ) ;
300+ const runButton = imageButtons . findWhere ( ( w ) => w . props ( ) . tooltip === 'Run cell' ) ;
301+ runButton . simulate ( 'click' ) ;
302+
303+ // Ensure cell was not executed
304+ assert . throws ( ( ) => verifyHtmlOnCell ( wrapper , 'NativeCell' , `2` , 1 ) ) ;
305+ } ) ;
306+ test ( 'Cannot switch to markdown' , async ( ) => {
307+ // Click switch to markdown button
308+ const cell = getOutputCell ( wrapper , 'NativeCell' , 1 ) ;
309+ const imageButtons = cell ! . find ( ImageButton ) ;
310+ const changeToMarkdown = imageButtons . findWhere ( ( w ) => w . props ( ) . tooltip === 'Change to markdown' ) ;
311+ changeToMarkdown . simulate ( 'click' ) ;
312+
313+ // Ensure cell is still code cell
314+ assert . isFalse ( isCellMarkdown ( wrapper , 'NativeCell' , 1 ) ) ;
315+ } ) ;
316+ test ( 'Cannot insert cell into notebook' , async ( ) => {
317+ // Click insert cell button
318+ const insertCellButton = findButton ( wrapper , NativeEditor , 5 ) ;
319+ insertCellButton ?. simulate ( 'click' ) ;
320+
321+ // No cell should have been added
322+ assert . equal ( wrapper . find ( 'NativeCell' ) . length , 3 , 'Cell added' ) ;
323+ } ) ;
324+ } ) ;
325+
326+ suite ( 'Jupyter shortcuts for editing notebook are disabled' , async ( ) => {
327+ test ( 'Ctrl+enter does not execute cell' , async ( ) => {
328+ const cellIndex = 1 ;
329+ await focusCell ( cellIndex ) ;
330+
331+ const promise = waitForMessage ( ioc , InteractiveWindowMessages . ExecutionRendered , { timeoutMs : 5_000 } ) ;
332+ simulateKeyPressOnCell ( cellIndex , { code : 'Enter' , ctrlKey : true , editorInfo : undefined } ) ;
333+
334+ // Waiting for an execution rendered message should timeout
335+ await expect ( promise ) . to . eventually . be . rejected ;
336+ // No output should have been rendered
337+ assert . throws ( ( ) => verifyHtmlOnCell ( wrapper , 'NativeCell' , '<span>2</span>' , cellIndex ) ) ;
338+ } ) ;
339+ test ( 'Shift+enter does not execute cell or advance to next cell' , async ( ) => {
340+ const cellIndex = 1 ;
341+ await focusCell ( cellIndex ) ;
342+
343+ const promise = waitForMessage ( ioc , InteractiveWindowMessages . ExecutionRendered , { timeoutMs : 5_000 } ) ;
344+ simulateKeyPressOnCell ( cellIndex , { code : 'Enter' , shiftKey : true , editorInfo : undefined } ) ;
345+
346+ // Waiting for an execution rendered message should timeout
347+ await expect ( promise ) . to . eventually . be . rejected ;
348+ // No output should have been rendered
349+ assert . throws ( ( ) => verifyHtmlOnCell ( wrapper , 'NativeCell' , '<span>2</span>' , cellIndex ) ) ;
350+ // 3rd cell should be neither selected nor focused
351+ assert . isFalse ( isCellSelected ( wrapper , 'NativeCell' , cellIndex + 1 ) ) ;
352+ assert . isFalse ( isCellFocused ( wrapper , 'NativeCell' , cellIndex + 1 ) ) ;
353+ } ) ;
354+ test ( 'Alt+enter does not execute cell or add a new cell below' , async ( ) => {
355+ assert . equal ( wrapper . find ( 'NativeCell' ) . length , 3 ) ;
356+ const cellIndex = 1 ;
357+ await focusCell ( cellIndex ) ;
358+
359+ const promise = waitForMessage ( ioc , InteractiveWindowMessages . ExecutionRendered , { timeoutMs : 5_000 } ) ;
360+ simulateKeyPressOnCell ( 1 , { code : 'Enter' , altKey : true , editorInfo : undefined } ) ;
361+
362+ // Waiting for an execution rendered message should timeout
363+ await expect ( promise ) . to . eventually . be . rejected ;
364+ // No output should have been rendered
365+ assert . throws ( ( ) => verifyHtmlOnCell ( wrapper , 'NativeCell' , '<span>2</span>' , cellIndex ) ) ;
366+ // No cell should have been added
367+ assert . equal ( wrapper . find ( 'NativeCell' ) . length , 3 , 'Cell added' ) ;
368+ } ) ;
369+ test ( '"a" does not add a cell' , async ( ) => {
370+ assert . equal ( wrapper . find ( 'NativeCell' ) . length , 3 ) ;
371+ const cellIndex = 0 ;
372+ await focusCell ( cellIndex ) ;
373+
374+ const addedCell = waitForMessage ( ioc , CommonActionType . INSERT_ABOVE_AND_FOCUS_NEW_CELL , {
375+ timeoutMs : 5_000
376+ } ) ;
377+ const update = waitForMessage ( ioc , InteractiveWindowMessages . SelectedCell , { timeoutMs : 5_000 } ) ;
378+ simulateKeyPressOnCell ( cellIndex , { code : 'a' } ) ;
379+
380+ await expect ( addedCell ) . to . eventually . be . rejected ;
381+ await expect ( update ) . to . eventually . be . rejected ;
382+ // There should still be 3 cells.
383+ assert . equal ( wrapper . find ( 'NativeCell' ) . length , 3 ) ;
384+ } ) ;
385+ test ( '"b" does not add a cell' , async ( ) => {
386+ assert . equal ( wrapper . find ( 'NativeCell' ) . length , 3 ) ;
387+ const cellIndex = 0 ;
388+ await focusCell ( cellIndex ) ;
389+
390+ const addedCell = waitForMessage ( ioc , CommonActionType . INSERT_BELOW_AND_FOCUS_NEW_CELL , {
391+ timeoutMs : 5_000
392+ } ) ;
393+ const update = waitForMessage ( ioc , InteractiveWindowMessages . SelectedCell , { timeoutMs : 5_000 } ) ;
394+ simulateKeyPressOnCell ( cellIndex , { code : 'b' } ) ;
395+
396+ await expect ( addedCell ) . to . eventually . be . rejected ;
397+ await expect ( update ) . to . eventually . be . rejected ;
398+ // There should still be 3 cells.
399+ assert . equal ( wrapper . find ( 'NativeCell' ) . length , 3 ) ;
400+ } ) ;
401+ test ( '"d" does not delete a cell' , async ( ) => {
402+ assert . equal ( wrapper . find ( 'NativeCell' ) . length , 3 ) ;
403+ const cellIndex = 2 ;
404+ await focusCell ( cellIndex ) ;
405+
406+ simulateKeyPressOnCell ( cellIndex , { code : 'd' } ) ;
407+ simulateKeyPressOnCell ( cellIndex , { code : 'd' } ) ;
408+
409+ // There should still be 3 cells.
410+ assert . equal ( wrapper . find ( 'NativeCell' ) . length , 3 ) ;
411+ } ) ;
412+ test ( '"m" does not change a code cell to markdown' , async ( ) => {
413+ const cellIndex = 2 ;
414+ await focusCell ( cellIndex ) ;
415+
416+ const update = waitForMessage ( ioc , CommonActionType . CHANGE_CELL_TYPE , {
417+ timeoutMs : 5_000
418+ } ) ;
419+ simulateKeyPressOnCell ( cellIndex , { code : 'm' } ) ;
420+
421+ await expect ( update ) . to . eventually . be . rejected ;
422+ assert . isFalse (
423+ isCellMarkdown ( wrapper , 'NativeCell' , cellIndex ) ,
424+ 'Code cell in untrusted notebook was changed to markdown'
425+ ) ;
426+ } ) ;
427+ } ) ;
293428 } ) ;
294- test ( 'Cannot run cell' , async ( ) => {
295- // Click run cell button
296- const cell = getOutputCell ( wrapper , 'NativeCell' , 1 ) ;
297- const imageButtons = cell ! . find ( ImageButton ) ;
298- const runButton = imageButtons . findWhere ( ( w ) => w . props ( ) . tooltip === 'Run cell' ) ;
299- runButton . simulate ( 'click' ) ;
300-
301- // Ensure cell was not executed
302- assert . throws ( ( ) => verifyHtmlOnCell ( wrapper , 'NativeCell' , `2` , 1 ) ) ;
429+
430+ suite ( 'Trust an untrusted notebook' , async ( ) => {
431+ test ( 'Trust persists when closed and reopened' , async ( ) => {
432+ const before = wrapper . find ( TrustMessage ) ;
433+ assert . equal ( before . text ( ) , 'Not Trusted' ) ;
434+
435+ // Trust notebook
436+ const trustService = ioc . get < ITrustService > ( ITrustService ) ;
437+ await trustService . trustNotebook ( ne . editor . model . file , ne . editor . model . getContent ( ) ) ;
438+
439+ // Close
440+ await closeNotebook ( ioc , ne . editor ) ;
441+
442+ // Reopen
443+ const newNativeEditor = await openEditor ( ioc , baseFile , notebookFile . filePath ) ;
444+ const newWrapper = newNativeEditor . mount . wrapper ;
445+
446+ // Verify notebook is now trusted
447+ const after = newWrapper . find ( TrustMessage ) ;
448+ assert . equal ( after . text ( ) , 'Trusted' ) ;
449+ } ) ;
303450 } ) ;
304451} ) ;
0 commit comments