actions.spec.ts 58 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { ISessionContext, SessionContext } from '@jupyterlab/apputils';
  4. import { CodeCell, MarkdownCell, RawCell } from '@jupyterlab/cells';
  5. import { CellType, IMimeBundle } from '@jupyterlab/nbformat';
  6. import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
  7. import {
  8. acceptDialog,
  9. createSessionContext,
  10. dismissDialog,
  11. sleep
  12. } from '@jupyterlab/testutils';
  13. import { JupyterServer } from '@jupyterlab/testutils/lib/start_jupyter_server';
  14. import { each } from '@lumino/algorithm';
  15. import { JSONArray, JSONObject, UUID } from '@lumino/coreutils';
  16. import { KernelError, Notebook, NotebookActions, NotebookModel } from '..';
  17. import * as utils from './utils';
  18. const ERROR_INPUT = 'a = foo';
  19. const JUPYTER_CELL_MIME = 'application/vnd.jupyter.cells';
  20. const server = new JupyterServer();
  21. beforeAll(async () => {
  22. jest.setTimeout(20000);
  23. await server.start();
  24. });
  25. afterAll(async () => {
  26. await server.shutdown();
  27. });
  28. describe('@jupyterlab/notebook', () => {
  29. let rendermime: IRenderMimeRegistry;
  30. describe('NotebookActions', () => {
  31. let widget: Notebook;
  32. let sessionContext: ISessionContext;
  33. let ipySessionContext: ISessionContext;
  34. beforeAll(async function () {
  35. jest.setTimeout(20000);
  36. rendermime = utils.defaultRenderMime();
  37. async function createContext(options?: Partial<SessionContext.IOptions>) {
  38. const context = await createSessionContext(options);
  39. await context.initialize();
  40. await context.session?.kernel?.info;
  41. return context;
  42. }
  43. [sessionContext, ipySessionContext] = await Promise.all([
  44. createContext(),
  45. createContext({ kernelPreference: { name: 'ipython' } })
  46. ]);
  47. });
  48. beforeEach(() => {
  49. widget = new Notebook({
  50. rendermime,
  51. contentFactory: utils.createNotebookFactory(),
  52. mimeTypeService: utils.mimeTypeService
  53. });
  54. const model = new NotebookModel({
  55. disableDocumentWideUndoRedo: true
  56. });
  57. model.fromJSON(utils.DEFAULT_CONTENT);
  58. widget.model = model;
  59. model.sharedModel.clearUndoHistory();
  60. widget.activeCellIndex = 0;
  61. });
  62. afterEach(() => {
  63. widget.dispose();
  64. utils.clipboard.clear();
  65. });
  66. afterAll(async () => {
  67. await Promise.all([
  68. sessionContext.shutdown(),
  69. ipySessionContext.shutdown()
  70. ]);
  71. });
  72. describe('#executed', () => {
  73. it('should emit when Markdown and code cells are run', async () => {
  74. const cell = widget.activeCell as CodeCell;
  75. const next = widget.widgets[1] as MarkdownCell;
  76. let emitted = 0;
  77. let failed = 0;
  78. widget.select(next);
  79. cell.model.outputs.clear();
  80. next.rendered = false;
  81. NotebookActions.executed.connect((_, args) => {
  82. const { success } = args;
  83. emitted += 1;
  84. if (!success) {
  85. failed += 1;
  86. }
  87. });
  88. await NotebookActions.run(widget, sessionContext);
  89. expect(emitted).toBe(2);
  90. expect(failed).toBe(0);
  91. expect(next.rendered).toBe(true);
  92. });
  93. it('should emit an error for a cell execution failure.', async () => {
  94. let emitted = 0;
  95. let failed = 0;
  96. let cellError: KernelError | null | undefined = null;
  97. NotebookActions.executed.connect((_, args) => {
  98. const { success, error } = args;
  99. emitted += 1;
  100. if (!success) {
  101. failed += 1;
  102. cellError = error;
  103. }
  104. });
  105. const cell = widget.model!.contentFactory.createCodeCell({});
  106. cell.value.text = ERROR_INPUT;
  107. widget.model!.cells.push(cell);
  108. widget.select(widget.widgets[widget.widgets.length - 1]);
  109. const result = await NotebookActions.run(widget, ipySessionContext);
  110. expect(result).toBe(false);
  111. expect(emitted).toBe(2);
  112. expect(failed).toBe(1);
  113. expect(cellError).toBeInstanceOf(KernelError);
  114. expect(cellError!.errorName).toBe('NameError');
  115. expect(cellError!.errorValue).toBe("name 'foo' is not defined");
  116. expect(cellError!.traceback).not.toBeNull();
  117. await ipySessionContext.session!.kernel!.restart();
  118. });
  119. });
  120. describe('#executionScheduled', () => {
  121. it('should emit only when code cell execution is scheduled', async () => {
  122. const cell = widget.activeCell as CodeCell;
  123. const next = widget.widgets[1] as MarkdownCell;
  124. let emitted = 0;
  125. widget.activeCell!.model.value.text = "print('hello')";
  126. widget.select(next);
  127. cell.model.outputs.clear();
  128. next.rendered = false;
  129. NotebookActions.executionScheduled.connect(() => {
  130. emitted += 1;
  131. });
  132. await NotebookActions.run(widget, sessionContext);
  133. expect(emitted).toBe(1);
  134. expect(next.rendered).toBe(true);
  135. });
  136. });
  137. describe('#splitCell({})', () => {
  138. it('should split the active cell into two cells', () => {
  139. const cell = widget.activeCell!;
  140. const source = 'thisisasamplestringwithnospaces';
  141. cell.model.value.text = source;
  142. const index = widget.activeCellIndex;
  143. const editor = cell.editor;
  144. editor.setCursorPosition(editor.getPositionAt(10)!);
  145. NotebookActions.splitCell(widget);
  146. const cells = widget.model!.cells;
  147. const newSource =
  148. cells.get(index).value.text + cells.get(index + 1).value.text;
  149. expect(newSource).toBe(source);
  150. });
  151. it('should preserve leading white space in the second cell', () => {
  152. const cell = widget.activeCell!;
  153. const source = 'this\n\n is a test';
  154. cell.model.value.text = source;
  155. const editor = cell.editor;
  156. editor.setCursorPosition(editor.getPositionAt(4)!);
  157. NotebookActions.splitCell(widget);
  158. expect(widget.activeCell!.model.value.text).toBe(' is a test');
  159. });
  160. it('should clear the existing selection', () => {
  161. each(widget.widgets, child => {
  162. widget.select(child);
  163. });
  164. NotebookActions.splitCell(widget);
  165. for (let i = 0; i < widget.widgets.length; i++) {
  166. if (i === widget.activeCellIndex) {
  167. continue;
  168. }
  169. expect(widget.isSelected(widget.widgets[i])).toBe(false);
  170. }
  171. });
  172. it('should activate the second cell', () => {
  173. NotebookActions.splitCell(widget);
  174. expect(widget.activeCellIndex).toBe(1);
  175. });
  176. it('should preserve the types of each cell', () => {
  177. NotebookActions.changeCellType(widget, 'markdown');
  178. NotebookActions.splitCell(widget);
  179. expect(widget.activeCell).toBeInstanceOf(MarkdownCell);
  180. const prev = widget.widgets[0];
  181. expect(prev).toBeInstanceOf(MarkdownCell);
  182. });
  183. it('should create two empty cells if there is no content', () => {
  184. widget.activeCell!.model.value.text = '';
  185. NotebookActions.splitCell(widget);
  186. expect(widget.activeCell!.model.value.text).toBe('');
  187. const prev = widget.widgets[0];
  188. expect(prev.model.value.text).toBe('');
  189. });
  190. it('should be a no-op if there is no model', () => {
  191. widget.model = null;
  192. NotebookActions.splitCell(widget);
  193. expect(widget.activeCell).toBeUndefined();
  194. });
  195. it('should preserve the widget mode', () => {
  196. NotebookActions.splitCell(widget);
  197. expect(widget.mode).toBe('command');
  198. widget.mode = 'edit';
  199. NotebookActions.splitCell(widget);
  200. expect(widget.mode).toBe('edit');
  201. });
  202. it('should be undo-able', () => {
  203. const source = widget.activeCell!.model.value.text;
  204. const count = widget.widgets.length;
  205. NotebookActions.splitCell(widget);
  206. NotebookActions.undo(widget);
  207. expect(widget.widgets.length).toBe(count);
  208. const cell = widget.widgets[0];
  209. expect(cell.model.value.text).toBe(source);
  210. });
  211. });
  212. describe('#mergeCells', () => {
  213. it('should merge the selected cells', () => {
  214. let source = widget.activeCell!.model.value.text + '\n\n';
  215. let next = widget.widgets[1];
  216. widget.select(next);
  217. source += next.model.value.text + '\n\n';
  218. next = widget.widgets[2];
  219. widget.select(next);
  220. source += next.model.value.text;
  221. const count = widget.widgets.length;
  222. NotebookActions.mergeCells(widget);
  223. expect(widget.widgets.length).toBe(count - 2);
  224. expect(widget.activeCell!.model.value.text).toBe(source);
  225. });
  226. it('should be a no-op if there is no model', () => {
  227. widget.model = null;
  228. NotebookActions.mergeCells(widget);
  229. expect(widget.activeCell).toBeUndefined();
  230. });
  231. it('should select the next cell if there is only one cell selected', () => {
  232. let source = widget.activeCell!.model.value.text + '\n\n';
  233. const next = widget.widgets[1];
  234. source += next.model.value.text;
  235. NotebookActions.mergeCells(widget);
  236. expect(widget.activeCell!.model.value.text).toBe(source);
  237. });
  238. it('should select the previous cell if there is only one cell selected and mergeAbove is true', () => {
  239. widget.activeCellIndex = 1;
  240. let source = widget.activeCell!.model.value.text;
  241. const previous = widget.widgets[0];
  242. source = previous.model.value.text + '\n\n' + source;
  243. NotebookActions.mergeCells(widget, true);
  244. expect(widget.activeCell!.model.value.text).toBe(source);
  245. });
  246. it('should do nothing if first cell selected and mergeAbove is true', () => {
  247. let source = widget.activeCell!.model.value.text;
  248. const cellNumber = widget.widgets.length;
  249. NotebookActions.mergeCells(widget, true);
  250. expect(widget.widgets.length).toBe(cellNumber);
  251. expect(widget.activeCell!.model.value.text).toBe(source);
  252. });
  253. it('should clear the outputs of a code cell', () => {
  254. NotebookActions.mergeCells(widget);
  255. const cell = widget.activeCell as CodeCell;
  256. expect(cell.model.outputs.length).toBe(0);
  257. });
  258. it('should preserve the widget mode', () => {
  259. widget.mode = 'edit';
  260. NotebookActions.mergeCells(widget);
  261. expect(widget.mode).toBe('edit');
  262. widget.mode = 'command';
  263. NotebookActions.mergeCells(widget);
  264. expect(widget.mode).toBe('command');
  265. });
  266. it('should be undo-able', () => {
  267. const source = widget.activeCell!.model.value.text;
  268. const count = widget.widgets.length;
  269. NotebookActions.mergeCells(widget);
  270. NotebookActions.undo(widget);
  271. expect(widget.widgets.length).toBe(count);
  272. const cell = widget.widgets[0];
  273. expect(cell.model.value.text).toBe(source);
  274. });
  275. it('should unrender a markdown cell', () => {
  276. NotebookActions.changeCellType(widget, 'markdown');
  277. let cell = widget.activeCell as MarkdownCell;
  278. cell.rendered = true;
  279. NotebookActions.mergeCells(widget);
  280. cell = widget.activeCell as MarkdownCell;
  281. expect(cell.rendered).toBe(false);
  282. expect(widget.mode).toBe('command');
  283. });
  284. it('should preserve the cell type of the active cell', () => {
  285. NotebookActions.changeCellType(widget, 'raw');
  286. NotebookActions.mergeCells(widget);
  287. expect(widget.activeCell).toBeInstanceOf(RawCell);
  288. expect(widget.mode).toBe('command');
  289. });
  290. it.each(['raw', 'markdown'] as CellType[])(
  291. 'should merge attachments if the last selected cell is a %s cell',
  292. type => {
  293. for (let i = 0; i < 2; i++) {
  294. NotebookActions.changeCellType(widget, type);
  295. const markdownCell = widget.widgets[i] as MarkdownCell;
  296. const attachment: IMimeBundle = { 'text/plain': 'test' };
  297. markdownCell.model.attachments.set(UUID.uuid4(), attachment);
  298. widget.select(markdownCell);
  299. }
  300. NotebookActions.mergeCells(widget);
  301. const model = (widget.activeCell as MarkdownCell).model;
  302. expect(model.attachments.length).toBe(2);
  303. }
  304. );
  305. it('should drop attachments if the last selected cell is a code cell', () => {
  306. NotebookActions.changeCellType(widget, 'markdown');
  307. const markdownCell = widget.activeCell as MarkdownCell;
  308. const attachment: IMimeBundle = { 'text/plain': 'test' };
  309. markdownCell.model.attachments.set(UUID.uuid4(), attachment);
  310. const codeCell = widget.widgets[1];
  311. widget.select(codeCell);
  312. NotebookActions.changeCellType(widget, 'code');
  313. NotebookActions.deselectAll(widget);
  314. widget.select(markdownCell);
  315. widget.select(codeCell);
  316. NotebookActions.mergeCells(widget);
  317. const model = widget.activeCell!.model.toJSON();
  318. expect(model.cell_type).toEqual('code');
  319. expect(model.attachments).toBeUndefined();
  320. });
  321. });
  322. describe('#deleteCells()', () => {
  323. it('should delete the selected cells', () => {
  324. const next = widget.widgets[1];
  325. widget.select(next);
  326. const count = widget.widgets.length;
  327. NotebookActions.deleteCells(widget);
  328. expect(widget.widgets.length).toBe(count - 2);
  329. });
  330. it('should increment deletedCells model when cells deleted', () => {
  331. const next = widget.widgets[1];
  332. widget.select(next);
  333. const count = widget.model!.deletedCells.length;
  334. NotebookActions.deleteCells(widget);
  335. expect(widget.model!.deletedCells.length).toBe(count + 2);
  336. });
  337. it('should be a no-op if there is no model', () => {
  338. widget.model = null;
  339. NotebookActions.deleteCells(widget);
  340. expect(widget.activeCell).toBeUndefined();
  341. });
  342. it('should switch to command mode', () => {
  343. widget.mode = 'edit';
  344. NotebookActions.deleteCells(widget);
  345. expect(widget.mode).toBe('command');
  346. });
  347. it('should activate the cell after the last selected cell', () => {
  348. widget.activeCellIndex = 4;
  349. const prev = widget.widgets[2];
  350. widget.select(prev);
  351. NotebookActions.deleteCells(widget);
  352. expect(widget.activeCellIndex).toBe(3);
  353. });
  354. it('should select the previous cell if the last cell is deleted', () => {
  355. widget.select(widget.widgets[widget.widgets.length - 1]);
  356. NotebookActions.deleteCells(widget);
  357. expect(widget.activeCellIndex).toBe(widget.widgets.length - 1);
  358. });
  359. it('should add a code cell if all cells are deleted', async () => {
  360. for (let i = 0; i < widget.widgets.length; i++) {
  361. widget.select(widget.widgets[i]);
  362. }
  363. NotebookActions.deleteCells(widget);
  364. await sleep();
  365. expect(widget.widgets.length).toBe(1);
  366. expect(widget.activeCell).toBeInstanceOf(CodeCell);
  367. });
  368. it('should be undo-able', () => {
  369. const next = widget.widgets[1];
  370. widget.select(next);
  371. const source = widget.activeCell!.model.value.text;
  372. const count = widget.widgets.length;
  373. NotebookActions.deleteCells(widget);
  374. NotebookActions.undo(widget);
  375. expect(widget.widgets.length).toBe(count);
  376. const cell = widget.widgets[0];
  377. expect(cell.model.value.text).toBe(source);
  378. });
  379. it('should be undo-able if all the cells are deleted', () => {
  380. for (let i = 0; i < widget.widgets.length; i++) {
  381. widget.select(widget.widgets[i]);
  382. }
  383. const count = widget.widgets.length;
  384. const source = widget.widgets[1].model.value.text;
  385. NotebookActions.deleteCells(widget);
  386. NotebookActions.undo(widget);
  387. expect(widget.widgets.length).toBe(count);
  388. expect(widget.widgets[1].model.value.text).toBe(source);
  389. });
  390. });
  391. describe('#insertAbove()', () => {
  392. it('should insert a code cell above the active cell', () => {
  393. const count = widget.widgets.length;
  394. NotebookActions.insertAbove(widget);
  395. expect(widget.activeCellIndex).toBe(0);
  396. expect(widget.widgets.length).toBe(count + 1);
  397. expect(widget.activeCell).toBeInstanceOf(CodeCell);
  398. });
  399. it('should be a no-op if there is no model', () => {
  400. widget.model = null;
  401. NotebookActions.insertAbove(widget);
  402. expect(widget.activeCell).toBeUndefined();
  403. });
  404. it('should widget mode should be preserved', () => {
  405. NotebookActions.insertAbove(widget);
  406. expect(widget.mode).toBe('command');
  407. widget.mode = 'edit';
  408. NotebookActions.insertAbove(widget);
  409. expect(widget.mode).toBe('edit');
  410. });
  411. it('should be undo-able', () => {
  412. const count = widget.widgets.length;
  413. NotebookActions.insertAbove(widget);
  414. NotebookActions.undo(widget);
  415. expect(widget.widgets.length).toBe(count);
  416. });
  417. it('should clear the existing selection', () => {
  418. for (let i = 0; i < widget.widgets.length; i++) {
  419. widget.select(widget.widgets[i]);
  420. }
  421. NotebookActions.insertAbove(widget);
  422. for (let i = 0; i < widget.widgets.length; i++) {
  423. if (i === widget.activeCellIndex) {
  424. continue;
  425. }
  426. expect(widget.isSelected(widget.widgets[i])).toBe(false);
  427. }
  428. });
  429. it('should be the new active cell', () => {
  430. NotebookActions.insertAbove(widget);
  431. expect(widget.activeCell!.model.value.text).toBe('');
  432. });
  433. });
  434. describe('#insertBelow()', () => {
  435. it('should insert a code cell below the active cell', () => {
  436. const count = widget.widgets.length;
  437. NotebookActions.insertBelow(widget);
  438. expect(widget.activeCellIndex).toBe(1);
  439. expect(widget.widgets.length).toBe(count + 1);
  440. expect(widget.activeCell).toBeInstanceOf(CodeCell);
  441. });
  442. it('should be a no-op if there is no model', () => {
  443. widget.model = null;
  444. NotebookActions.insertBelow(widget);
  445. expect(widget.activeCell).toBeUndefined();
  446. });
  447. it('should widget mode should be preserved', () => {
  448. NotebookActions.insertBelow(widget);
  449. expect(widget.mode).toBe('command');
  450. widget.mode = 'edit';
  451. NotebookActions.insertBelow(widget);
  452. expect(widget.mode).toBe('edit');
  453. });
  454. it('should be undo-able', () => {
  455. const count = widget.widgets.length;
  456. NotebookActions.insertBelow(widget);
  457. NotebookActions.undo(widget);
  458. expect(widget.widgets.length).toBe(count);
  459. });
  460. it('should clear the existing selection', () => {
  461. for (let i = 0; i < widget.widgets.length; i++) {
  462. widget.select(widget.widgets[i]);
  463. }
  464. NotebookActions.insertBelow(widget);
  465. for (let i = 0; i < widget.widgets.length; i++) {
  466. if (i === widget.activeCellIndex) {
  467. continue;
  468. }
  469. expect(widget.isSelected(widget.widgets[i])).toBe(false);
  470. }
  471. });
  472. it('should be the new active cell', () => {
  473. NotebookActions.insertBelow(widget);
  474. expect(widget.activeCell!.model.value.text).toBe('');
  475. });
  476. });
  477. describe('#changeCellType()', () => {
  478. it('should change the selected cell type(s)', () => {
  479. let next = widget.widgets[1];
  480. widget.select(next);
  481. NotebookActions.changeCellType(widget, 'raw');
  482. expect(widget.activeCell).toBeInstanceOf(RawCell);
  483. next = widget.widgets[widget.activeCellIndex + 1];
  484. expect(next).toBeInstanceOf(RawCell);
  485. });
  486. it('should be a no-op if there is no model', () => {
  487. widget.model = null;
  488. NotebookActions.changeCellType(widget, 'code');
  489. expect(widget.activeCell).toBeUndefined();
  490. });
  491. it('should preserve the widget mode', () => {
  492. NotebookActions.changeCellType(widget, 'code');
  493. expect(widget.mode).toBe('command');
  494. widget.mode = 'edit';
  495. NotebookActions.changeCellType(widget, 'raw');
  496. expect(widget.mode).toBe('edit');
  497. });
  498. it('should be undo-able', () => {
  499. NotebookActions.changeCellType(widget, 'raw');
  500. NotebookActions.undo(widget);
  501. const cell = widget.widgets[0];
  502. expect(cell).toBeInstanceOf(CodeCell);
  503. });
  504. it('should clear the existing selection', () => {
  505. for (let i = 0; i < widget.widgets.length; i++) {
  506. widget.select(widget.widgets[i]);
  507. }
  508. NotebookActions.changeCellType(widget, 'raw');
  509. for (let i = 0; i < widget.widgets.length; i++) {
  510. if (i === widget.activeCellIndex) {
  511. continue;
  512. }
  513. expect(widget.isSelected(widget.widgets[i])).toBe(false);
  514. }
  515. });
  516. it('should unrender markdown cells', () => {
  517. NotebookActions.changeCellType(widget, 'markdown');
  518. const cell = widget.activeCell as MarkdownCell;
  519. expect(cell.rendered).toBe(false);
  520. });
  521. });
  522. describe('#run()', () => {
  523. it('should run the selected cells', async () => {
  524. let emitted = 0;
  525. NotebookActions.selectionExecuted.connect(() => {
  526. emitted += 1;
  527. });
  528. const next = widget.widgets[1] as MarkdownCell;
  529. widget.select(next);
  530. const cell = widget.activeCell as CodeCell;
  531. cell.model.outputs.clear();
  532. next.rendered = false;
  533. const result = await NotebookActions.run(widget, sessionContext);
  534. expect(result).toBe(true);
  535. expect(cell.model.outputs.length).toBeGreaterThan(0);
  536. expect(next.rendered).toBe(true);
  537. expect(emitted).toBe(1);
  538. });
  539. it('should delete deletedCells metadata when cell run', () => {
  540. const cell = widget.activeCell as CodeCell;
  541. let emitted = 0;
  542. NotebookActions.selectionExecuted.connect(() => {
  543. emitted += 1;
  544. });
  545. cell.model.outputs.clear();
  546. return NotebookActions.run(widget, sessionContext).then(result => {
  547. expect(result).toBe(true);
  548. expect(widget.model!.deletedCells.length).toBe(0);
  549. expect(emitted).toBe(1);
  550. });
  551. });
  552. it('should be a no-op if there is no model', async () => {
  553. widget.model = null;
  554. let emitted = 0;
  555. NotebookActions.selectionExecuted.connect(() => {
  556. emitted += 1;
  557. });
  558. const result = await NotebookActions.run(widget, sessionContext);
  559. expect(result).toBe(false);
  560. expect(emitted).toBe(0);
  561. });
  562. it('should activate the last selected cell', async () => {
  563. const other = widget.widgets[2];
  564. let emitted = 0;
  565. NotebookActions.selectionExecuted.connect(() => {
  566. emitted += 1;
  567. });
  568. widget.select(other);
  569. other.model.value.text = 'a = 1';
  570. const result = await NotebookActions.run(widget, sessionContext);
  571. expect(result).toBe(true);
  572. expect(widget.activeCell).toBe(other);
  573. expect(emitted).toBe(1);
  574. });
  575. it('should clear the selection', async () => {
  576. const next = widget.widgets[1];
  577. let emitted = 0;
  578. NotebookActions.selectionExecuted.connect(() => {
  579. emitted += 1;
  580. });
  581. widget.select(next);
  582. const result = await NotebookActions.run(widget, sessionContext);
  583. expect(result).toBe(true);
  584. expect(widget.isSelected(widget.widgets[0])).toBe(false);
  585. expect(emitted).toBe(1);
  586. });
  587. it('should change to command mode', async () => {
  588. widget.mode = 'edit';
  589. let emitted = 0;
  590. NotebookActions.selectionExecuted.connect(() => {
  591. emitted += 1;
  592. });
  593. const result = await NotebookActions.run(widget, sessionContext);
  594. expect(result).toBe(true);
  595. expect(widget.mode).toBe('command');
  596. expect(emitted).toBe(1);
  597. });
  598. it('should handle no session', async () => {
  599. let emitted = 0;
  600. NotebookActions.selectionExecuted.connect(() => {
  601. emitted += 1;
  602. });
  603. const result = await NotebookActions.run(widget, undefined);
  604. expect(result).toBe(true);
  605. const cell = widget.activeCell as CodeCell;
  606. expect(cell.model.executionCount).toBeNull();
  607. expect(emitted).toBe(1);
  608. });
  609. it('should stop executing code cells on an error', async () => {
  610. let emitted = 0;
  611. NotebookActions.selectionExecuted.connect(() => {
  612. emitted += 1;
  613. });
  614. let cell = widget.model!.contentFactory.createCodeCell({});
  615. cell.value.text = ERROR_INPUT;
  616. widget.model!.cells.insert(2, cell);
  617. widget.select(widget.widgets[2]);
  618. cell = widget.model!.contentFactory.createCodeCell({});
  619. widget.model!.cells.push(cell);
  620. widget.select(widget.widgets[widget.widgets.length - 1]);
  621. const result = await NotebookActions.run(widget, ipySessionContext);
  622. expect(result).toBe(false);
  623. expect(cell.executionCount).toBeNull();
  624. await ipySessionContext.session!.kernel!.restart();
  625. expect(emitted).toBe(1);
  626. });
  627. it('should render all markdown cells on an error', async () => {
  628. let emitted = 0;
  629. NotebookActions.selectionExecuted.connect(() => {
  630. emitted += 1;
  631. });
  632. const cell = widget.model!.contentFactory.createMarkdownCell({});
  633. widget.model!.cells.push(cell);
  634. const child = widget.widgets[widget.widgets.length - 1] as MarkdownCell;
  635. child.rendered = false;
  636. widget.select(child);
  637. widget.activeCell!.model.value.text = ERROR_INPUT;
  638. const result = await NotebookActions.run(widget, ipySessionContext);
  639. // Markdown rendering is asynchronous, but the cell
  640. // provides no way to hook into that. Sleep here
  641. // to make sure it finishes.
  642. await sleep(100);
  643. expect(result).toBe(false);
  644. expect(child.rendered).toBe(true);
  645. await ipySessionContext.session!.kernel!.restart();
  646. expect(emitted).toBe(1);
  647. });
  648. });
  649. describe('#runAndAdvance()', () => {
  650. it('should run the selected cells', async () => {
  651. const next = widget.widgets[1] as MarkdownCell;
  652. widget.select(next);
  653. const cell = widget.activeCell as CodeCell;
  654. cell.model.outputs.clear();
  655. next.rendered = false;
  656. const result = await NotebookActions.runAndAdvance(
  657. widget,
  658. sessionContext
  659. );
  660. expect(result).toBe(true);
  661. expect(cell.model.outputs.length).toBeGreaterThan(0);
  662. expect(next.rendered).toBe(true);
  663. });
  664. it('should be a no-op if there is no model', async () => {
  665. widget.model = null;
  666. const result = await NotebookActions.runAndAdvance(
  667. widget,
  668. sessionContext
  669. );
  670. expect(result).toBe(false);
  671. });
  672. it('should clear the existing selection', async () => {
  673. const next = widget.widgets[3];
  674. widget.select(next);
  675. const result = await NotebookActions.runAndAdvance(
  676. widget,
  677. ipySessionContext
  678. );
  679. expect(result).toBe(false);
  680. expect(widget.isSelected(widget.widgets[0])).toBe(false);
  681. await ipySessionContext.session!.kernel!.restart();
  682. });
  683. it('should change to command mode', async () => {
  684. widget.mode = 'edit';
  685. const result = await NotebookActions.runAndAdvance(
  686. widget,
  687. sessionContext
  688. );
  689. expect(result).toBe(true);
  690. expect(widget.mode).toBe('command');
  691. });
  692. it('should activate the cell after the last selected cell', async () => {
  693. const next = widget.widgets[3] as MarkdownCell;
  694. widget.select(next);
  695. const result = await NotebookActions.runAndAdvance(
  696. widget,
  697. sessionContext
  698. );
  699. expect(result).toBe(true);
  700. expect(widget.activeCellIndex).toBe(4);
  701. });
  702. it('should create a new code cell in edit mode if necessary', async () => {
  703. const count = widget.widgets.length;
  704. widget.activeCellIndex = count - 1;
  705. const result = await NotebookActions.runAndAdvance(
  706. widget,
  707. sessionContext
  708. );
  709. expect(result).toBe(true);
  710. expect(widget.widgets.length).toBe(count + 1);
  711. expect(widget.activeCell).toBeInstanceOf(CodeCell);
  712. expect(widget.mode).toBe('edit');
  713. });
  714. it('should allow an undo of the new cell', async () => {
  715. const count = widget.widgets.length;
  716. widget.activeCellIndex = count - 1;
  717. const result = await NotebookActions.runAndAdvance(
  718. widget,
  719. sessionContext
  720. );
  721. expect(result).toBe(true);
  722. NotebookActions.undo(widget);
  723. expect(widget.widgets.length).toBe(count);
  724. });
  725. it('should stop executing code cells on an error', async () => {
  726. widget.activeCell!.model.value.text = ERROR_INPUT;
  727. const cell = widget.model!.contentFactory.createCodeCell({});
  728. widget.model!.cells.push(cell);
  729. widget.select(widget.widgets[widget.widgets.length - 1]);
  730. const result = await NotebookActions.runAndAdvance(
  731. widget,
  732. ipySessionContext
  733. );
  734. expect(result).toBe(false);
  735. expect(cell.executionCount).toBeNull();
  736. await ipySessionContext.session!.kernel!.restart();
  737. });
  738. it('should render all markdown cells on an error', async () => {
  739. widget.activeCell!.model.value.text = ERROR_INPUT;
  740. const cell = widget.widgets[1] as MarkdownCell;
  741. cell.rendered = false;
  742. widget.select(cell);
  743. const result = await NotebookActions.runAndAdvance(
  744. widget,
  745. ipySessionContext
  746. );
  747. // Markdown rendering is asynchronous, but the cell
  748. // provides no way to hook into that. Sleep here
  749. // to make sure it finishes.
  750. await sleep(100);
  751. expect(result).toBe(false);
  752. expect(cell.rendered).toBe(true);
  753. expect(widget.activeCellIndex).toBe(2);
  754. await ipySessionContext.session!.kernel!.restart();
  755. });
  756. });
  757. describe('#runAndInsert()', () => {
  758. it('should run the selected cells', async () => {
  759. const next = widget.widgets[1] as MarkdownCell;
  760. widget.select(next);
  761. const cell = widget.activeCell as CodeCell;
  762. cell.model.outputs.clear();
  763. next.rendered = false;
  764. const result = await NotebookActions.runAndInsert(
  765. widget,
  766. sessionContext
  767. );
  768. expect(result).toBe(true);
  769. expect(cell.model.outputs.length).toBeGreaterThan(0);
  770. expect(next.rendered).toBe(true);
  771. });
  772. it('should be a no-op if there is no model', async () => {
  773. widget.model = null;
  774. const result = await NotebookActions.runAndInsert(
  775. widget,
  776. sessionContext
  777. );
  778. expect(result).toBe(false);
  779. });
  780. it('should clear the existing selection', async () => {
  781. const next = widget.widgets[1];
  782. widget.select(next);
  783. const result = await NotebookActions.runAndInsert(
  784. widget,
  785. sessionContext
  786. );
  787. expect(result).toBe(true);
  788. expect(widget.isSelected(widget.widgets[0])).toBe(false);
  789. });
  790. it('should insert a new code cell in edit mode after the last selected cell', async () => {
  791. const next = widget.widgets[2];
  792. widget.select(next);
  793. next.model.value.text = 'a = 1';
  794. const count = widget.widgets.length;
  795. const result = await NotebookActions.runAndInsert(
  796. widget,
  797. sessionContext
  798. );
  799. expect(result).toBe(true);
  800. expect(widget.activeCell).toBeInstanceOf(CodeCell);
  801. expect(widget.mode).toBe('edit');
  802. expect(widget.widgets.length).toBe(count + 1);
  803. });
  804. it('should allow an undo of the cell insert', async () => {
  805. const next = widget.widgets[2];
  806. widget.select(next);
  807. next.model.value.text = 'a = 1';
  808. const count = widget.widgets.length;
  809. const result = await NotebookActions.runAndInsert(
  810. widget,
  811. sessionContext
  812. );
  813. expect(result).toBe(true);
  814. NotebookActions.undo(widget);
  815. expect(widget.widgets.length).toBe(count);
  816. });
  817. it('should stop executing code cells on an error', async () => {
  818. widget.activeCell!.model.value.text = ERROR_INPUT;
  819. const cell = widget.model!.contentFactory.createCodeCell({});
  820. widget.model!.cells.push(cell);
  821. widget.select(widget.widgets[widget.widgets.length - 1]);
  822. const result = await NotebookActions.runAndInsert(
  823. widget,
  824. ipySessionContext
  825. );
  826. expect(result).toBe(false);
  827. expect(cell.executionCount).toBeNull();
  828. await ipySessionContext.session!.kernel!.restart();
  829. });
  830. it('should render all markdown cells on an error', async () => {
  831. widget.activeCell!.model.value.text = ERROR_INPUT;
  832. const cell = widget.widgets[1] as MarkdownCell;
  833. cell.rendered = false;
  834. widget.select(cell);
  835. const result = await NotebookActions.runAndInsert(
  836. widget,
  837. ipySessionContext
  838. );
  839. // Markdown rendering is asynchronous, but the cell
  840. // provides no way to hook into that. Sleep here
  841. // to make sure it finishes.
  842. await sleep(100);
  843. expect(result).toBe(false);
  844. expect(cell.rendered).toBe(true);
  845. expect(widget.activeCellIndex).toBe(2);
  846. await ipySessionContext.session!.kernel!.restart();
  847. });
  848. });
  849. describe('#runAll()', () => {
  850. beforeEach(() => {
  851. // Make sure all cells have valid code.
  852. widget.widgets[2].model.value.text = 'a = 1';
  853. });
  854. it('should run all of the cells in the notebook', async () => {
  855. const next = widget.widgets[1] as MarkdownCell;
  856. const cell = widget.activeCell as CodeCell;
  857. cell.model.outputs.clear();
  858. next.rendered = false;
  859. const result = await NotebookActions.runAll(widget, sessionContext);
  860. expect(result).toBe(true);
  861. expect(cell.model.outputs.length).toBeGreaterThan(0);
  862. expect(next.rendered).toBe(true);
  863. });
  864. it('should be a no-op if there is no model', async () => {
  865. widget.model = null;
  866. const result = await NotebookActions.runAll(widget, sessionContext);
  867. expect(result).toBe(false);
  868. });
  869. it('should change to command mode', async () => {
  870. widget.mode = 'edit';
  871. const result = await NotebookActions.runAll(widget, sessionContext);
  872. expect(result).toBe(true);
  873. expect(widget.mode).toBe('command');
  874. });
  875. it('should clear the existing selection', async () => {
  876. const next = widget.widgets[2];
  877. widget.select(next);
  878. const result = await NotebookActions.runAll(widget, sessionContext);
  879. expect(result).toBe(true);
  880. expect(widget.isSelected(widget.widgets[2])).toBe(false);
  881. });
  882. it('should activate the last cell', async () => {
  883. await NotebookActions.runAll(widget, sessionContext);
  884. expect(widget.activeCellIndex).toBe(widget.widgets.length - 1);
  885. });
  886. it('should stop executing code cells on an error', async () => {
  887. widget.activeCell!.model.value.text = ERROR_INPUT;
  888. const cell = widget.model!.contentFactory.createCodeCell({});
  889. widget.model!.cells.push(cell);
  890. const result = await NotebookActions.runAll(widget, ipySessionContext);
  891. expect(result).toBe(false);
  892. expect(cell.executionCount).toBeNull();
  893. expect(widget.activeCellIndex).toBe(widget.widgets.length - 1);
  894. await ipySessionContext.session!.kernel!.restart();
  895. });
  896. it('should render all markdown cells on an error', async () => {
  897. widget.activeCell!.model.value.text = ERROR_INPUT;
  898. const cell = widget.widgets[1] as MarkdownCell;
  899. cell.rendered = false;
  900. const result = await NotebookActions.runAll(widget, ipySessionContext);
  901. // Markdown rendering is asynchronous, but the cell
  902. // provides no way to hook into that. Sleep here
  903. // to make sure it finishes.
  904. await sleep(100);
  905. expect(result).toBe(false);
  906. expect(cell.rendered).toBe(true);
  907. await ipySessionContext.session!.kernel!.restart();
  908. });
  909. });
  910. describe('#selectAbove()', () => {
  911. it('should select the cell above the active cell', () => {
  912. widget.activeCellIndex = 1;
  913. NotebookActions.selectAbove(widget);
  914. expect(widget.activeCellIndex).toBe(0);
  915. });
  916. it('should be a no-op if there is no model', () => {
  917. widget.model = null;
  918. NotebookActions.selectAbove(widget);
  919. expect(widget.activeCellIndex).toBe(-1);
  920. });
  921. it('should not wrap around to the bottom', () => {
  922. NotebookActions.selectAbove(widget);
  923. expect(widget.activeCellIndex).toBe(0);
  924. });
  925. it('should preserve the mode', () => {
  926. widget.activeCellIndex = 2;
  927. NotebookActions.selectAbove(widget);
  928. expect(widget.mode).toBe('command');
  929. widget.mode = 'edit';
  930. NotebookActions.selectAbove(widget);
  931. expect(widget.mode).toBe('edit');
  932. });
  933. it('should skip collapsed cells in edit mode', () => {
  934. widget.activeCellIndex = 3;
  935. widget.mode = 'edit';
  936. widget.widgets[1].inputHidden = true;
  937. widget.widgets[2].inputHidden = true;
  938. widget.widgets[3].inputHidden = false;
  939. NotebookActions.selectAbove(widget);
  940. expect(widget.activeCellIndex).toBe(0);
  941. });
  942. });
  943. describe('#selectBelow()', () => {
  944. it('should select the cell below the active cell', () => {
  945. NotebookActions.selectBelow(widget);
  946. expect(widget.activeCellIndex).toBe(1);
  947. });
  948. it('should be a no-op if there is no model', () => {
  949. widget.model = null;
  950. NotebookActions.selectBelow(widget);
  951. expect(widget.activeCellIndex).toBe(-1);
  952. });
  953. it('should not wrap around to the top', () => {
  954. widget.activeCellIndex = widget.widgets.length - 1;
  955. NotebookActions.selectBelow(widget);
  956. expect(widget.activeCellIndex).not.toBe(0);
  957. });
  958. it('should preserve the mode', () => {
  959. widget.activeCellIndex = 2;
  960. NotebookActions.selectBelow(widget);
  961. expect(widget.mode).toBe('command');
  962. widget.mode = 'edit';
  963. NotebookActions.selectBelow(widget);
  964. expect(widget.mode).toBe('edit');
  965. });
  966. it('should not change if in edit mode and no non-collapsed cells below', () => {
  967. widget.activeCellIndex = widget.widgets.length - 2;
  968. widget.mode = 'edit';
  969. widget.widgets[widget.widgets.length - 1].inputHidden = true;
  970. NotebookActions.selectBelow(widget);
  971. expect(widget.activeCellIndex).toBe(widget.widgets.length - 2);
  972. });
  973. });
  974. describe('#extendSelectionAbove()', () => {
  975. it('should extend the selection to the cell above', () => {
  976. widget.activeCellIndex = 1;
  977. NotebookActions.extendSelectionAbove(widget);
  978. expect(widget.isSelected(widget.widgets[0])).toBe(true);
  979. });
  980. it('should extend the selection to the topmost cell', () => {
  981. widget.activeCellIndex = 1;
  982. NotebookActions.extendSelectionAbove(widget, true);
  983. for (let i = widget.activeCellIndex; i >= 0; i--) {
  984. expect(widget.isSelected(widget.widgets[i])).toBe(true);
  985. }
  986. });
  987. it('should be a no-op if there is no model', () => {
  988. widget.model = null;
  989. NotebookActions.extendSelectionAbove(widget);
  990. expect(widget.activeCellIndex).toBe(-1);
  991. });
  992. it('should change to command mode if there is a selection', () => {
  993. widget.mode = 'edit';
  994. widget.activeCellIndex = 1;
  995. NotebookActions.extendSelectionAbove(widget);
  996. expect(widget.mode).toBe('command');
  997. });
  998. it('should not wrap around to the bottom', () => {
  999. widget.mode = 'edit';
  1000. NotebookActions.extendSelectionAbove(widget);
  1001. expect(widget.activeCellIndex).toBe(0);
  1002. const last = widget.widgets[widget.widgets.length - 1];
  1003. expect(widget.isSelected(last)).toBe(false);
  1004. expect(widget.mode).toBe('edit');
  1005. });
  1006. it('should deselect the current cell if the cell above is selected', () => {
  1007. NotebookActions.extendSelectionBelow(widget);
  1008. NotebookActions.extendSelectionBelow(widget);
  1009. const cell = widget.activeCell!;
  1010. NotebookActions.extendSelectionAbove(widget);
  1011. expect(widget.isSelected(cell)).toBe(false);
  1012. });
  1013. it('should select only the first cell if we move from the second to first', () => {
  1014. NotebookActions.extendSelectionBelow(widget);
  1015. const cell = widget.activeCell!;
  1016. NotebookActions.extendSelectionAbove(widget);
  1017. expect(widget.isSelected(cell)).toBe(false);
  1018. expect(widget.activeCellIndex).toBe(0);
  1019. });
  1020. it('should activate the cell', () => {
  1021. widget.activeCellIndex = 1;
  1022. NotebookActions.extendSelectionAbove(widget);
  1023. expect(widget.activeCellIndex).toBe(0);
  1024. });
  1025. });
  1026. describe('#extendSelectionBelow()', () => {
  1027. it('should extend the selection to the cell below', () => {
  1028. NotebookActions.extendSelectionBelow(widget);
  1029. expect(widget.isSelected(widget.widgets[0])).toBe(true);
  1030. expect(widget.isSelected(widget.widgets[1])).toBe(true);
  1031. });
  1032. it('should extend the selection the bottom-most cell', () => {
  1033. NotebookActions.extendSelectionBelow(widget, true);
  1034. for (let i = widget.activeCellIndex; i < widget.widgets.length; i++) {
  1035. expect(widget.isSelected(widget.widgets[i])).toBe(true);
  1036. }
  1037. });
  1038. it('should be a no-op if there is no model', () => {
  1039. widget.model = null;
  1040. NotebookActions.extendSelectionBelow(widget);
  1041. expect(widget.activeCellIndex).toBe(-1);
  1042. });
  1043. it('should change to command mode if there is a selection', () => {
  1044. widget.mode = 'edit';
  1045. NotebookActions.extendSelectionBelow(widget);
  1046. expect(widget.mode).toBe('command');
  1047. });
  1048. it('should not wrap around to the top', () => {
  1049. const last = widget.widgets.length - 1;
  1050. widget.activeCellIndex = last;
  1051. widget.mode = 'edit';
  1052. NotebookActions.extendSelectionBelow(widget);
  1053. expect(widget.activeCellIndex).toBe(last);
  1054. expect(widget.isSelected(widget.widgets[0])).toBe(false);
  1055. expect(widget.mode).toBe('edit');
  1056. });
  1057. it('should deselect the current cell if the cell below is selected', () => {
  1058. const last = widget.widgets.length - 1;
  1059. widget.activeCellIndex = last;
  1060. NotebookActions.extendSelectionAbove(widget);
  1061. NotebookActions.extendSelectionAbove(widget);
  1062. const current = widget.activeCell!;
  1063. NotebookActions.extendSelectionBelow(widget);
  1064. expect(widget.isSelected(current)).toBe(false);
  1065. });
  1066. it('should select only the last cell if we move from the second last to last', () => {
  1067. const last = widget.widgets.length - 1;
  1068. widget.activeCellIndex = last;
  1069. NotebookActions.extendSelectionAbove(widget);
  1070. const current = widget.activeCell!;
  1071. NotebookActions.extendSelectionBelow(widget);
  1072. expect(widget.isSelected(current)).toBe(false);
  1073. expect(widget.activeCellIndex).toBe(last);
  1074. });
  1075. it('should activate the cell', () => {
  1076. NotebookActions.extendSelectionBelow(widget);
  1077. expect(widget.activeCellIndex).toBe(1);
  1078. });
  1079. });
  1080. describe('#moveUp()', () => {
  1081. it('should move the selected cells up', () => {
  1082. widget.activeCellIndex = 2;
  1083. NotebookActions.extendSelectionAbove(widget);
  1084. NotebookActions.moveUp(widget);
  1085. expect(widget.isSelected(widget.widgets[0])).toBe(true);
  1086. expect(widget.isSelected(widget.widgets[1])).toBe(true);
  1087. expect(widget.isSelected(widget.widgets[2])).toBe(false);
  1088. expect(widget.activeCellIndex).toBe(0);
  1089. });
  1090. it('should be a no-op if there is no model', () => {
  1091. widget.model = null;
  1092. NotebookActions.moveUp(widget);
  1093. expect(widget.activeCellIndex).toBe(-1);
  1094. });
  1095. it('should not wrap around to the bottom', () => {
  1096. expect(widget.activeCellIndex).toBe(0);
  1097. NotebookActions.moveUp(widget);
  1098. expect(widget.activeCellIndex).toBe(0);
  1099. });
  1100. it('should be undo-able', () => {
  1101. widget.activeCellIndex++;
  1102. const source = widget.activeCell!.model.value.text;
  1103. NotebookActions.moveUp(widget);
  1104. expect(widget.model!.cells.get(0).value.text).toBe(source);
  1105. NotebookActions.undo(widget);
  1106. expect(widget.model!.cells.get(1).value.text).toBe(source);
  1107. });
  1108. });
  1109. describe('#moveDown()', () => {
  1110. it('should move the selected cells down', () => {
  1111. NotebookActions.extendSelectionBelow(widget);
  1112. NotebookActions.moveDown(widget);
  1113. expect(widget.isSelected(widget.widgets[0])).toBe(false);
  1114. expect(widget.isSelected(widget.widgets[1])).toBe(true);
  1115. expect(widget.isSelected(widget.widgets[2])).toBe(true);
  1116. expect(widget.activeCellIndex).toBe(2);
  1117. });
  1118. it('should be a no-op if there is no model', () => {
  1119. widget.model = null;
  1120. NotebookActions.moveUp(widget);
  1121. expect(widget.activeCellIndex).toBe(-1);
  1122. });
  1123. it('should not wrap around to the top', () => {
  1124. widget.activeCellIndex = widget.widgets.length - 1;
  1125. NotebookActions.moveDown(widget);
  1126. expect(widget.activeCellIndex).toBe(widget.widgets.length - 1);
  1127. });
  1128. it('should be undo-able', () => {
  1129. const source = widget.activeCell!.model.value.text;
  1130. NotebookActions.moveDown(widget);
  1131. expect(widget.model!.cells.get(1).value.text).toBe(source);
  1132. NotebookActions.undo(widget);
  1133. expect(widget.model!.cells.get(0).value.text).toBe(source);
  1134. });
  1135. });
  1136. describe('#copy()', () => {
  1137. it('should copy the selected cells to a utils.clipboard', () => {
  1138. const next = widget.widgets[1];
  1139. widget.select(next);
  1140. NotebookActions.copy(widget);
  1141. expect(utils.clipboard.hasData(JUPYTER_CELL_MIME)).toBe(true);
  1142. const data = utils.clipboard.getData(JUPYTER_CELL_MIME);
  1143. expect(data.length).toBe(2);
  1144. });
  1145. it('should be a no-op if there is no model', () => {
  1146. widget.model = null;
  1147. NotebookActions.copy(widget);
  1148. expect(utils.clipboard.hasData(JUPYTER_CELL_MIME)).toBe(false);
  1149. });
  1150. it('should change to command mode', () => {
  1151. widget.mode = 'edit';
  1152. NotebookActions.copy(widget);
  1153. expect(widget.mode).toBe('command');
  1154. });
  1155. it('should delete metadata.deletable', () => {
  1156. const next = widget.widgets[1];
  1157. widget.select(next);
  1158. next.model.metadata.set('deletable', false);
  1159. NotebookActions.copy(widget);
  1160. const data = utils.clipboard.getData(JUPYTER_CELL_MIME) as JSONArray;
  1161. data.map(cell => {
  1162. expect(
  1163. ((cell as JSONObject).metadata as JSONObject).deletable
  1164. ).toBeUndefined();
  1165. });
  1166. });
  1167. });
  1168. describe('#cut()', () => {
  1169. it('should cut the selected cells to a utils.clipboard', () => {
  1170. const next = widget.widgets[1];
  1171. widget.select(next);
  1172. const count = widget.widgets.length;
  1173. NotebookActions.cut(widget);
  1174. expect(widget.widgets.length).toBe(count - 2);
  1175. });
  1176. it('should be a no-op if there is no model', () => {
  1177. widget.model = null;
  1178. NotebookActions.cut(widget);
  1179. expect(utils.clipboard.hasData(JUPYTER_CELL_MIME)).toBe(false);
  1180. });
  1181. it('should change to command mode', () => {
  1182. widget.mode = 'edit';
  1183. NotebookActions.cut(widget);
  1184. expect(widget.mode).toBe('command');
  1185. });
  1186. it('should be undo-able', () => {
  1187. const source = widget.activeCell!.model.value.text;
  1188. NotebookActions.cut(widget);
  1189. NotebookActions.undo(widget);
  1190. expect(widget.widgets[0].model.value.text).toBe(source);
  1191. });
  1192. it('should add a new code cell if all cells were cut', async () => {
  1193. for (let i = 0; i < widget.widgets.length; i++) {
  1194. widget.select(widget.widgets[i]);
  1195. }
  1196. NotebookActions.cut(widget);
  1197. await sleep();
  1198. expect(widget.widgets.length).toBe(1);
  1199. expect(widget.activeCell).toBeInstanceOf(CodeCell);
  1200. });
  1201. });
  1202. describe('#paste()', () => {
  1203. it('should paste cells from a utils.clipboard', () => {
  1204. const source = widget.activeCell!.model.value.text;
  1205. const next = widget.widgets[1];
  1206. widget.select(next);
  1207. const count = widget.widgets.length;
  1208. NotebookActions.cut(widget);
  1209. widget.activeCellIndex = 1;
  1210. NotebookActions.paste(widget);
  1211. expect(widget.widgets.length).toBe(count);
  1212. expect(widget.widgets[2].model.value.text).toBe(source);
  1213. expect(widget.activeCellIndex).toBe(3);
  1214. });
  1215. it('should be a no-op if there is no model', () => {
  1216. NotebookActions.copy(widget);
  1217. widget.model = null;
  1218. NotebookActions.paste(widget);
  1219. expect(widget.activeCellIndex).toBe(-1);
  1220. });
  1221. it('should be a no-op if there is no cell data on the utils.clipboard', () => {
  1222. const count = widget.widgets.length;
  1223. NotebookActions.paste(widget);
  1224. expect(widget.widgets.length).toBe(count);
  1225. });
  1226. it('should change to command mode', () => {
  1227. widget.mode = 'edit';
  1228. NotebookActions.cut(widget);
  1229. NotebookActions.paste(widget);
  1230. expect(widget.mode).toBe('command');
  1231. });
  1232. it('should be undo-able', () => {
  1233. const next = widget.widgets[1];
  1234. widget.select(next);
  1235. const count = widget.widgets.length;
  1236. NotebookActions.cut(widget);
  1237. widget.activeCellIndex = 1;
  1238. widget.model?.sharedModel.clearUndoHistory();
  1239. NotebookActions.paste(widget);
  1240. NotebookActions.undo(widget);
  1241. expect(widget.widgets.length).toBe(count - 2);
  1242. });
  1243. });
  1244. describe('#undo()', () => {
  1245. it('should undo a cell action', () => {
  1246. const count = widget.widgets.length;
  1247. const next = widget.widgets[1];
  1248. widget.select(next);
  1249. NotebookActions.deleteCells(widget);
  1250. NotebookActions.undo(widget);
  1251. expect(widget.widgets.length).toBe(count);
  1252. });
  1253. it('should switch the widget to command mode', () => {
  1254. widget.mode = 'edit';
  1255. NotebookActions.undo(widget);
  1256. expect(widget.mode).toBe('command');
  1257. });
  1258. it('should be a no-op if there is no model', () => {
  1259. widget.model = null;
  1260. NotebookActions.undo(widget);
  1261. expect(widget.activeCellIndex).toBe(-1);
  1262. });
  1263. it('should be a no-op if there are no cell actions to undo', () => {
  1264. const count = widget.widgets.length;
  1265. NotebookActions.deleteCells(widget);
  1266. widget.model!.cells.clearUndo();
  1267. NotebookActions.undo(widget);
  1268. expect(widget.widgets.length).toBe(count - 1);
  1269. });
  1270. });
  1271. describe('#redo()', () => {
  1272. it('should redo a cell action', () => {
  1273. const count = widget.widgets.length;
  1274. const next = widget.widgets[1];
  1275. widget.select(next);
  1276. NotebookActions.deleteCells(widget);
  1277. NotebookActions.undo(widget);
  1278. NotebookActions.redo(widget);
  1279. expect(widget.widgets.length).toBe(count - 2);
  1280. });
  1281. it('should switch the widget to command mode', () => {
  1282. NotebookActions.undo(widget);
  1283. widget.mode = 'edit';
  1284. NotebookActions.redo(widget);
  1285. expect(widget.mode).toBe('command');
  1286. });
  1287. it('should be a no-op if there is no model', () => {
  1288. NotebookActions.undo(widget);
  1289. widget.model = null;
  1290. NotebookActions.redo(widget);
  1291. expect(widget.activeCellIndex).toBe(-1);
  1292. });
  1293. it('should be a no-op if there are no cell actions to redo', () => {
  1294. const count = widget.widgets.length;
  1295. NotebookActions.redo(widget);
  1296. expect(widget.widgets.length).toBe(count);
  1297. });
  1298. });
  1299. describe('#toggleAllLineNumbers()', () => {
  1300. it('should toggle line numbers on all cells', () => {
  1301. const state = widget.activeCell!.editor.getOption('lineNumbers');
  1302. NotebookActions.toggleAllLineNumbers(widget);
  1303. for (let i = 0; i < widget.widgets.length; i++) {
  1304. const lineNumbers = widget.widgets[i].editor.getOption('lineNumbers');
  1305. expect(lineNumbers).toBe(!state);
  1306. }
  1307. });
  1308. it('should be based on the state of the active cell', () => {
  1309. const state = widget.activeCell!.editor.getOption('lineNumbers');
  1310. for (let i = 1; i < widget.widgets.length; i++) {
  1311. widget.widgets[i].editor.setOption('lineNumbers', !state);
  1312. }
  1313. NotebookActions.toggleAllLineNumbers(widget);
  1314. for (let i = 0; i < widget.widgets.length; i++) {
  1315. const lineNumbers = widget.widgets[i].editor.getOption('lineNumbers');
  1316. expect(lineNumbers).toBe(!state);
  1317. }
  1318. });
  1319. it('should preserve the widget mode', () => {
  1320. NotebookActions.toggleAllLineNumbers(widget);
  1321. expect(widget.mode).toBe('command');
  1322. widget.mode = 'edit';
  1323. NotebookActions.toggleAllLineNumbers(widget);
  1324. expect(widget.mode).toBe('edit');
  1325. });
  1326. it('should be a no-op if there is no model', () => {
  1327. widget.model = null;
  1328. NotebookActions.toggleAllLineNumbers(widget);
  1329. expect(widget.activeCellIndex).toBe(-1);
  1330. });
  1331. });
  1332. describe('#clearOutputs()', () => {
  1333. it('should clear the outputs on the selected cells', () => {
  1334. // Select the next code cell that has outputs.
  1335. let index = 0;
  1336. for (let i = 1; i < widget.widgets.length; i++) {
  1337. const cell = widget.widgets[i];
  1338. if (cell instanceof CodeCell && cell.model.outputs.length) {
  1339. widget.select(cell);
  1340. index = i;
  1341. break;
  1342. }
  1343. }
  1344. NotebookActions.clearOutputs(widget);
  1345. let cell = widget.widgets[0] as CodeCell;
  1346. expect(cell.model.outputs.length).toBe(0);
  1347. cell = widget.widgets[index] as CodeCell;
  1348. expect(cell.model.outputs.length).toBe(0);
  1349. });
  1350. it('should preserve the widget mode', () => {
  1351. NotebookActions.clearOutputs(widget);
  1352. expect(widget.mode).toBe('command');
  1353. widget.mode = 'edit';
  1354. NotebookActions.clearOutputs(widget);
  1355. expect(widget.mode).toBe('edit');
  1356. });
  1357. it('should be a no-op if there is no model', () => {
  1358. widget.model = null;
  1359. NotebookActions.clearOutputs(widget);
  1360. expect(widget.activeCellIndex).toBe(-1);
  1361. });
  1362. });
  1363. describe('#clearAllOutputs()', () => {
  1364. it('should clear the outputs on all cells', () => {
  1365. const next = widget.widgets[1];
  1366. widget.select(next);
  1367. NotebookActions.clearAllOutputs(widget);
  1368. for (let i = 0; i < widget.widgets.length; i++) {
  1369. const cell = widget.widgets[i];
  1370. if (cell instanceof CodeCell) {
  1371. expect(cell.model.outputs.length).toBe(0);
  1372. }
  1373. }
  1374. });
  1375. it('should preserve the widget mode', () => {
  1376. NotebookActions.clearAllOutputs(widget);
  1377. expect(widget.mode).toBe('command');
  1378. widget.mode = 'edit';
  1379. NotebookActions.clearAllOutputs(widget);
  1380. expect(widget.mode).toBe('edit');
  1381. });
  1382. it('should be a no-op if there is no model', () => {
  1383. widget.model = null;
  1384. NotebookActions.clearAllOutputs(widget);
  1385. expect(widget.activeCellIndex).toBe(-1);
  1386. });
  1387. });
  1388. describe('#setMarkdownHeader()', () => {
  1389. it('should set the markdown header level of selected cells', () => {
  1390. const next = widget.widgets[1];
  1391. widget.select(next);
  1392. NotebookActions.setMarkdownHeader(widget, 2);
  1393. expect(widget.activeCell!.model.value.text.slice(0, 3)).toBe('## ');
  1394. expect(next.model.value.text.slice(0, 3)).toBe('## ');
  1395. });
  1396. it('should convert the cells to markdown type', () => {
  1397. NotebookActions.setMarkdownHeader(widget, 2);
  1398. expect(widget.activeCell).toBeInstanceOf(MarkdownCell);
  1399. });
  1400. it('should be clamped between 1 and 6', () => {
  1401. NotebookActions.setMarkdownHeader(widget, -1);
  1402. expect(widget.activeCell!.model.value.text.slice(0, 2)).toBe('# ');
  1403. NotebookActions.setMarkdownHeader(widget, 10);
  1404. expect(widget.activeCell!.model.value.text.slice(0, 7)).toBe('###### ');
  1405. });
  1406. it('should be a no-op if there is no model', () => {
  1407. widget.model = null;
  1408. NotebookActions.setMarkdownHeader(widget, 1);
  1409. expect(widget.activeCellIndex).toBe(-1);
  1410. });
  1411. it('should replace an existing header', () => {
  1412. widget.activeCell!.model.value.text = '# foo';
  1413. NotebookActions.setMarkdownHeader(widget, 2);
  1414. expect(widget.activeCell!.model.value.text).toBe('## foo');
  1415. });
  1416. it('should replace leading white space', () => {
  1417. widget.activeCell!.model.value.text = ' foo';
  1418. NotebookActions.setMarkdownHeader(widget, 2);
  1419. expect(widget.activeCell!.model.value.text).toBe('## foo');
  1420. });
  1421. it('should unrender the cells', () => {
  1422. NotebookActions.setMarkdownHeader(widget, 1);
  1423. expect((widget.activeCell as MarkdownCell).rendered).toBe(false);
  1424. });
  1425. });
  1426. describe('#trust()', () => {
  1427. it('should trust the notebook cells if the user accepts', async () => {
  1428. const model = widget.model!;
  1429. model.fromJSON(utils.DEFAULT_CONTENT);
  1430. const cell = model.cells.get(0);
  1431. expect(cell.trusted).not.toBe(true);
  1432. const promise = NotebookActions.trust(widget);
  1433. await acceptDialog();
  1434. await promise;
  1435. expect(cell.trusted).toBe(true);
  1436. });
  1437. it('should not trust the notebook cells if the user aborts', async () => {
  1438. const model = widget.model!;
  1439. model.fromJSON(utils.DEFAULT_CONTENT);
  1440. const cell = model.cells.get(0);
  1441. expect(cell.trusted).not.toBe(true);
  1442. const promise = NotebookActions.trust(widget);
  1443. await dismissDialog();
  1444. await promise;
  1445. expect(cell.trusted).not.toBe(true);
  1446. });
  1447. it('should be a no-op if the model is `null`', async () => {
  1448. widget.model = null;
  1449. await NotebookActions.trust(widget);
  1450. });
  1451. it('should show a dialog if all cells are trusted', async () => {
  1452. const model = widget.model!;
  1453. model.fromJSON(utils.DEFAULT_CONTENT);
  1454. model.fromJSON(utils.DEFAULT_CONTENT);
  1455. for (let i = 0; i < model.cells.length; i++) {
  1456. const cell = model.cells.get(i);
  1457. cell.trusted = true;
  1458. }
  1459. const promise = NotebookActions.trust(widget);
  1460. await acceptDialog();
  1461. await promise;
  1462. });
  1463. });
  1464. });
  1465. });