actions.spec.ts 55 KB

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