widget.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. // Copyright (c) Jupyter Development Team.
  2. import 'jest';
  3. import { SessionContext } from '@jupyterlab/apputils';
  4. import { KernelManager } from '@jupyterlab/services';
  5. import { Message } from '@lumino/messaging';
  6. import { Widget } from '@lumino/widgets';
  7. import {
  8. IOutputAreaModel,
  9. OutputAreaModel,
  10. OutputArea
  11. } from '@jupyterlab/outputarea';
  12. import {
  13. createSessionContext,
  14. defaultRenderMime,
  15. NBTestUtils,
  16. JupyterServer,
  17. flakyIt as it
  18. } from '@jupyterlab/testutils';
  19. /**
  20. * The default rendermime instance to use for testing.
  21. */
  22. const rendermime = defaultRenderMime();
  23. const CODE = 'print("hello")';
  24. class LogOutputArea extends OutputArea {
  25. methods: string[] = [];
  26. protected onUpdateRequest(msg: Message): void {
  27. super.onUpdateRequest(msg);
  28. this.methods.push('onUpdateRequest');
  29. }
  30. protected onModelChanged(
  31. sender: IOutputAreaModel,
  32. args: IOutputAreaModel.ChangedArgs
  33. ) {
  34. super.onModelChanged(sender, args);
  35. this.methods.push('onModelChanged');
  36. }
  37. }
  38. const server = new JupyterServer();
  39. beforeAll(async () => {
  40. jest.setTimeout(20000);
  41. await server.start();
  42. });
  43. afterAll(async () => {
  44. await server.shutdown();
  45. });
  46. describe('outputarea/widget', () => {
  47. let widget: LogOutputArea;
  48. let model: OutputAreaModel;
  49. beforeEach(() => {
  50. model = new OutputAreaModel({
  51. values: NBTestUtils.DEFAULT_OUTPUTS,
  52. trusted: true
  53. });
  54. widget = new LogOutputArea({ rendermime, model });
  55. });
  56. afterEach(() => {
  57. model.dispose();
  58. widget.dispose();
  59. });
  60. describe('OutputArea', () => {
  61. describe('#constructor()', () => {
  62. it('should create an output area widget', () => {
  63. expect(widget).toBeInstanceOf(OutputArea);
  64. expect(widget.hasClass('jp-OutputArea')).toBe(true);
  65. });
  66. it('should take an optional contentFactory', () => {
  67. const contentFactory = Object.create(OutputArea.defaultContentFactory);
  68. const widget = new OutputArea({ rendermime, contentFactory, model });
  69. expect(widget.contentFactory).toBe(contentFactory);
  70. });
  71. });
  72. describe('#model', () => {
  73. it('should be the model used by the widget', () => {
  74. expect(widget.model).toBe(model);
  75. });
  76. });
  77. describe('#rendermime', () => {
  78. it('should be the rendermime instance used by the widget', () => {
  79. expect(widget.rendermime).toBe(rendermime);
  80. });
  81. });
  82. describe('#contentFactory', () => {
  83. it('should be the contentFactory used by the widget', () => {
  84. expect(widget.contentFactory).toBe(OutputArea.defaultContentFactory);
  85. });
  86. });
  87. describe('#widgets', () => {
  88. it('should get the child widget at the specified index', () => {
  89. expect(widget.widgets[0]).toBeInstanceOf(Widget);
  90. });
  91. it('should get the number of child widgets', () => {
  92. expect(widget.widgets.length).toBe(
  93. NBTestUtils.DEFAULT_OUTPUTS.length - 1
  94. );
  95. widget.model.clear();
  96. expect(widget.widgets.length).toBe(0);
  97. });
  98. });
  99. describe('#future', () => {
  100. let sessionContext: SessionContext;
  101. beforeEach(async () => {
  102. sessionContext = await createSessionContext();
  103. await sessionContext.initialize();
  104. await sessionContext.session?.kernel?.info;
  105. });
  106. afterEach(async () => {
  107. await sessionContext.shutdown();
  108. sessionContext.dispose();
  109. });
  110. it('should execute code on a kernel and send outputs to the model', async () => {
  111. const future = sessionContext.session?.kernel?.requestExecute({
  112. code: CODE
  113. })!;
  114. widget.future = future;
  115. const reply = await future.done;
  116. expect(reply!.content.execution_count).toBeTruthy();
  117. expect(reply!.content.status).toBe('ok');
  118. expect(model.length).toBe(1);
  119. });
  120. it('should clear existing outputs', async () => {
  121. widget.model.fromJSON(NBTestUtils.DEFAULT_OUTPUTS);
  122. const future = sessionContext.session?.kernel?.requestExecute({
  123. code: CODE
  124. })!;
  125. widget.future = future;
  126. const reply = await future.done;
  127. expect(reply!.content.execution_count).toBeTruthy();
  128. expect(model.length).toBe(1);
  129. });
  130. });
  131. describe('#onModelChanged()', () => {
  132. it('should handle an added output', () => {
  133. widget.model.clear();
  134. widget.methods = [];
  135. widget.model.add(NBTestUtils.DEFAULT_OUTPUTS[0]);
  136. expect(widget.methods).toEqual(
  137. expect.arrayContaining(['onModelChanged'])
  138. );
  139. expect(widget.widgets.length).toBe(1);
  140. });
  141. it('should handle a clear', () => {
  142. widget.model.fromJSON(NBTestUtils.DEFAULT_OUTPUTS);
  143. widget.methods = [];
  144. widget.model.clear();
  145. expect(widget.methods).toEqual(
  146. expect.arrayContaining(['onModelChanged'])
  147. );
  148. expect(widget.widgets.length).toBe(0);
  149. });
  150. it('should handle a set', () => {
  151. widget.model.clear();
  152. widget.model.add(NBTestUtils.DEFAULT_OUTPUTS[0]);
  153. widget.methods = [];
  154. widget.model.add(NBTestUtils.DEFAULT_OUTPUTS[0]);
  155. expect(widget.methods).toEqual(
  156. expect.arrayContaining(['onModelChanged'])
  157. );
  158. expect(widget.widgets.length).toBe(1);
  159. });
  160. it('should rerender when preferred mimetype changes', () => {
  161. // Add output with both safe and unsafe types
  162. widget.model.clear();
  163. widget.model.add({
  164. output_type: 'display_data',
  165. data: {
  166. 'image/svg+xml':
  167. '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"></svg>',
  168. 'text/plain': 'hello, world'
  169. },
  170. metadata: {}
  171. });
  172. expect(widget.node.innerHTML).toContain('<img src="data:image/svg+xml');
  173. widget.model.trusted = !widget.model.trusted;
  174. expect(widget.node.innerHTML).toEqual(
  175. expect.not.arrayContaining(['<img src="data:image/svg+xml'])
  176. );
  177. widget.model.trusted = !widget.model.trusted;
  178. expect(widget.node.innerHTML).toContain('<img src="data:image/svg+xml');
  179. });
  180. it('should rerender when isolation changes', () => {
  181. // Add output with both safe and unsafe types
  182. widget.model.clear();
  183. widget.model.add({
  184. output_type: 'display_data',
  185. data: {
  186. 'text/plain': 'hello, world'
  187. }
  188. });
  189. expect(widget.node.innerHTML).toEqual(
  190. expect.not.arrayContaining(['<iframe'])
  191. );
  192. widget.model.set(0, {
  193. output_type: 'display_data',
  194. data: {
  195. 'text/plain': 'hello, world'
  196. },
  197. metadata: {
  198. isolated: true
  199. }
  200. });
  201. expect(widget.node.innerHTML).toContain('<iframe');
  202. widget.model.set(0, {
  203. output_type: 'display_data',
  204. data: {
  205. 'text/plain': 'hello, world'
  206. }
  207. });
  208. expect(widget.node.innerHTML).not.toContain('<iframe');
  209. });
  210. });
  211. describe('.execute()', () => {
  212. let sessionContext: SessionContext;
  213. beforeEach(async () => {
  214. sessionContext = await createSessionContext();
  215. await sessionContext.initialize();
  216. await sessionContext.session?.kernel?.info;
  217. });
  218. afterEach(async () => {
  219. await sessionContext.shutdown();
  220. sessionContext.dispose();
  221. });
  222. it('should execute code on a kernel and send outputs to the model', async () => {
  223. const reply = await OutputArea.execute(CODE, widget, sessionContext);
  224. expect(reply!.content.execution_count).toBeTruthy();
  225. expect(reply!.content.status).toBe('ok');
  226. expect(model.length).toBe(1);
  227. });
  228. it('should clear existing outputs', async () => {
  229. widget.model.fromJSON(NBTestUtils.DEFAULT_OUTPUTS);
  230. const reply = await OutputArea.execute(CODE, widget, sessionContext);
  231. expect(reply!.content.execution_count).toBeTruthy();
  232. expect(model.length).toBe(1);
  233. });
  234. it('should handle routing of display messages', async () => {
  235. const model0 = new OutputAreaModel({ trusted: true });
  236. const widget0 = new LogOutputArea({ rendermime, model: model0 });
  237. const model1 = new OutputAreaModel({ trusted: true });
  238. const widget1 = new LogOutputArea({ rendermime, model: model1 });
  239. const model2 = new OutputAreaModel({ trusted: true });
  240. const widget2 = new LogOutputArea({ rendermime, model: model2 });
  241. const code0 = [
  242. 'ip = get_ipython()',
  243. 'from IPython.display import display',
  244. 'def display_with_id(obj, display_id, update=False):',
  245. ' iopub = ip.kernel.iopub_socket',
  246. ' session = get_ipython().kernel.session',
  247. ' data, md = ip.display_formatter.format(obj)',
  248. ' transient = {"display_id": display_id}',
  249. ' content = {"data": data, "metadata": md, "transient": transient}',
  250. ' msg_type = "update_display_data" if update else "display_data"',
  251. ' session.send(iopub, msg_type, content, parent=ip.parent_header)'
  252. ].join('\n');
  253. const code1 = [
  254. 'display("above")',
  255. 'display_with_id(1, "here")',
  256. 'display("below")'
  257. ].join('\n');
  258. const code2 = [
  259. 'display_with_id(2, "here")',
  260. 'display_with_id(3, "there")',
  261. 'display_with_id(4, "here")'
  262. ].join('\n');
  263. let ipySessionContext: SessionContext;
  264. ipySessionContext = await createSessionContext({
  265. kernelPreference: { name: 'ipython' }
  266. });
  267. await ipySessionContext.initialize();
  268. const promise0 = OutputArea.execute(code0, widget0, ipySessionContext);
  269. const promise1 = OutputArea.execute(code1, widget1, ipySessionContext);
  270. await Promise.all([promise0, promise1]);
  271. expect(model1.length).toBe(3);
  272. expect(model1.toJSON()[1].data).toEqual({ 'text/plain': '1' });
  273. await OutputArea.execute(code2, widget2, ipySessionContext);
  274. expect(model1.length).toBe(3);
  275. expect(model1.toJSON()[1].data).toEqual({ 'text/plain': '4' });
  276. expect(model2.length).toBe(3);
  277. const outputs = model2.toJSON();
  278. expect(outputs[0].data).toEqual({ 'text/plain': '4' });
  279. expect(outputs[1].data).toEqual({ 'text/plain': '3' });
  280. expect(outputs[2].data).toEqual({ 'text/plain': '4' });
  281. await ipySessionContext.shutdown();
  282. });
  283. it('should stop on an error', async () => {
  284. let ipySessionContext: SessionContext;
  285. ipySessionContext = await createSessionContext({
  286. kernelPreference: { name: 'ipython' }
  287. });
  288. await ipySessionContext.initialize();
  289. const widget1 = new LogOutputArea({ rendermime, model });
  290. const future1 = OutputArea.execute('a++1', widget, ipySessionContext);
  291. const future2 = OutputArea.execute('a=1', widget1, ipySessionContext);
  292. const reply = await future1;
  293. const reply2 = await future2;
  294. expect(reply!.content.status).toBe('error');
  295. expect(reply2!.content.status).toBe('aborted');
  296. expect(model.length).toBe(1);
  297. widget1.dispose();
  298. await ipySessionContext.shutdown();
  299. });
  300. it('should allow an error given "raises-exception" metadata tag', async () => {
  301. let ipySessionContext: SessionContext;
  302. ipySessionContext = await createSessionContext({
  303. kernelPreference: { name: 'ipython' }
  304. });
  305. await ipySessionContext.initialize();
  306. const widget1 = new LogOutputArea({ rendermime, model });
  307. const metadata = { tags: ['raises-exception'] };
  308. const future1 = OutputArea.execute(
  309. 'a++1',
  310. widget,
  311. ipySessionContext,
  312. metadata
  313. );
  314. const future2 = OutputArea.execute('a=1', widget1, ipySessionContext);
  315. const reply = await future1;
  316. const reply2 = await future2;
  317. expect(reply!.content.status).toBe('error');
  318. expect(reply2!.content.status).toBe('ok');
  319. widget1.dispose();
  320. await ipySessionContext.shutdown();
  321. });
  322. });
  323. describe('.ContentFactory', () => {
  324. describe('#createOutputPrompt()', () => {
  325. it('should create an output prompt', () => {
  326. const factory = new OutputArea.ContentFactory();
  327. expect(factory.createOutputPrompt().executionCount).toBeNull();
  328. });
  329. });
  330. describe('#createStdin()', () => {
  331. it('should create a stdin widget', async () => {
  332. const manager = new KernelManager();
  333. const kernel = await manager.startNew();
  334. const factory = new OutputArea.ContentFactory();
  335. const future = kernel.requestExecute({ code: CODE });
  336. const options = {
  337. prompt: 'hello',
  338. password: false,
  339. future
  340. };
  341. expect(factory.createStdin(options)).toBeInstanceOf(Widget);
  342. await kernel.shutdown();
  343. kernel.dispose();
  344. });
  345. });
  346. });
  347. describe('.defaultContentFactory', () => {
  348. it('should be a `contentFactory` instance', () => {
  349. expect(OutputArea.defaultContentFactory).toBeInstanceOf(
  350. OutputArea.ContentFactory
  351. );
  352. });
  353. });
  354. });
  355. });