actions.spec.ts 56 KB

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