jsoneditor.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import expect = require('expect.js');
  4. import {
  5. CodeMirrorEditorFactory
  6. } from '@jupyterlab/codemirror';
  7. import {
  8. ObservableJSON
  9. } from '@jupyterlab/coreutils';
  10. import {
  11. JSONEditor
  12. } from '@jupyterlab/codeeditor';
  13. import {
  14. Message
  15. } from '@phosphor/messaging';
  16. import {
  17. Widget
  18. } from '@phosphor/widgets';
  19. import {
  20. simulate
  21. } from 'simulate-event';
  22. class LogEditor extends JSONEditor {
  23. methods: string[] = [];
  24. events: string[] = [];
  25. handleEvent(event: Event): void {
  26. super.handleEvent(event);
  27. this.events.push(event.type);
  28. }
  29. protected onAfterAttach(msg: Message): void {
  30. super.onAfterAttach(msg);
  31. this.methods.push('onAfterAttach');
  32. }
  33. protected onAfterShow(msg: Message): void {
  34. super.onAfterShow(msg);
  35. this.methods.push('onAfterShow');
  36. }
  37. protected onUpdateRequest(msg: Message): void {
  38. super.onUpdateRequest(msg);
  39. this.methods.push('onUpdateRequest');
  40. }
  41. protected onBeforeDetach(msg: Message): void {
  42. super.onBeforeDetach(msg);
  43. this.methods.push('onBeforeDetach');
  44. }
  45. }
  46. describe('apputils', () => {
  47. describe('JSONEditor', () => {
  48. let editor: LogEditor;
  49. let editorServices = new CodeMirrorEditorFactory();
  50. const editorFactory = editorServices.newInlineEditor.bind(editorServices);
  51. beforeEach(() => {
  52. editor = new LogEditor({ editorFactory });
  53. });
  54. afterEach(() => {
  55. editor.dispose();
  56. });
  57. describe('#constructor', () => {
  58. it('should create a new metadata editor', () => {
  59. let newEditor = new JSONEditor({ editorFactory });
  60. expect(newEditor).to.be.a(JSONEditor);
  61. });
  62. });
  63. describe('#collapsible', () => {
  64. it('should default to false', () => {
  65. expect(editor.collapsible).to.be(false);
  66. });
  67. it ('should be settable in the constructor', () => {
  68. let newEditor = new JSONEditor({ editorFactory, collapsible: true });
  69. expect(newEditor.collapsible).to.be(true);
  70. });
  71. });
  72. describe('#editorTitle', () => {
  73. it('should default to empty string', () => {
  74. expect(editor.editorTitle).to.be('');
  75. });
  76. it ('should be settable in the constructor', () => {
  77. let newEditor = new JSONEditor({ editorFactory, title: 'foo' });
  78. expect(newEditor.editorTitle).to.be('foo');
  79. });
  80. it('should be settable', () => {
  81. editor.editorTitle = 'foo';
  82. expect(editor.editorTitle).to.be('foo');
  83. });
  84. });
  85. describe('#headerNode', () => {
  86. it('should be the header node used by the editor', () => {
  87. expect(editor.headerNode.classList).to.contain('jp-JSONEditor-header');
  88. });
  89. });
  90. describe('#titleNode', () => {
  91. it('should be the title node used by the editor', () => {
  92. expect(editor.titleNode.classList).to.contain('jp-JSONEditor-title');
  93. });
  94. });
  95. describe('#collapserNode', () => {
  96. it('should be the collapser node used by the editor', () => {
  97. expect(editor.collapserNode.classList)
  98. .to.contain('jp-JSONEditor-collapser');
  99. });
  100. });
  101. describe('#editorHostNode', () => {
  102. it('should be the editor host node used by the editor', () => {
  103. expect(editor.editorHostNode.classList)
  104. .to.contain('jp-JSONEditor-host');
  105. });
  106. });
  107. describe('#revertButtonNode', () => {
  108. it('should be the revert button node used by the editor', () => {
  109. expect(editor.revertButtonNode.classList)
  110. .to.contain('jp-JSONEditor-revertButton');
  111. });
  112. });
  113. describe('#commitButtonNode', () => {
  114. it('should be the commit button node used by the editor', () => {
  115. expect(editor.commitButtonNode.classList)
  116. .to.contain('jp-JSONEditor-commitButton');
  117. });
  118. });
  119. describe('#source', () => {
  120. it('should be the source of the metadata', () => {
  121. expect(editor.source).to.be(null);
  122. });
  123. it('should be settable', () => {
  124. let source = new ObservableJSON();
  125. editor.source = source;
  126. expect(editor.source).to.be(source);
  127. });
  128. it('should update the text area value', () => {
  129. let model = editor.model;
  130. expect(model.value.text).to.be('No data!');
  131. editor.source = new ObservableJSON();
  132. expect(model.value.text).to.be('{}');
  133. });
  134. });
  135. describe('#isDirty', () => {
  136. it('should test whether the editor value is dirty', () => {
  137. expect(editor.isDirty).to.be(false);
  138. Widget.attach(editor, document.body);
  139. editor.model.value.text = 'a';
  140. expect(editor.isDirty).to.be(true);
  141. });
  142. it('should be dirty if the value changes while focused', () => {
  143. editor.source = new ObservableJSON();
  144. Widget.attach(editor, document.body);
  145. editor.editor.focus();
  146. expect(editor.isDirty).to.be(false);
  147. editor.source.set('foo', 1);
  148. expect(editor.isDirty).to.be(true);
  149. });
  150. it('should not be set if not focused', () => {
  151. editor.source = new ObservableJSON();
  152. Widget.attach(editor, document.body);
  153. expect(editor.isDirty).to.be(false);
  154. editor.source.set('foo', 1);
  155. expect(editor.isDirty).to.be(false);
  156. });
  157. });
  158. context('model.value.changed', () => {
  159. it('should add the error flag if invalid JSON', () => {
  160. editor.model.value.text = 'foo';
  161. expect(editor.hasClass('jp-mod-error')).to.be(true);
  162. });
  163. it('should show the commit button if the value has changed', () => {
  164. editor.model.value.text = '{"foo": 2}';
  165. editor.model.value.text = '{"foo": 1}';
  166. expect(editor.commitButtonNode.hidden).to.be(false);
  167. });
  168. it('should not show the commit button if the value is invalid', () => {
  169. editor.model.value.text = 'foo';
  170. expect(editor.commitButtonNode.hidden).to.be(true);
  171. });
  172. it('should show the revert button if the value has changed', () => {
  173. editor.model.value.text = 'foo';
  174. expect(editor.revertButtonNode.hidden).to.be(false);
  175. });
  176. });
  177. describe('#handleEvent()', () => {
  178. beforeEach(() => {
  179. Widget.attach(editor, document.body);
  180. });
  181. context('blur', () => {
  182. it('should handle blur events on the host node', () => {
  183. editor.editor.focus();
  184. simulate(editor.editorHostNode, 'blur');
  185. expect(editor.events).to.contain('blur');
  186. });
  187. it('should revert to current data if there was no change', () => {
  188. editor.source = new ObservableJSON();
  189. editor.editor.focus();
  190. editor.source.set('foo', 1);
  191. let model = editor.model;
  192. expect(model.value.text).to.be('{}');
  193. simulate(editor.editorHostNode, 'blur');
  194. expect(model.value.text).to.be('{\n "foo": 1\n}');
  195. });
  196. it('should not revert to current data if there was a change', () => {
  197. editor.source = new ObservableJSON();
  198. editor.model.value.text = 'foo';
  199. editor.source.set('foo', 1);
  200. let model = editor.model;
  201. expect(model.value.text).to.be('foo');
  202. simulate(editor.editorHostNode, 'blur');
  203. expect(model.value.text).to.be('foo');
  204. expect(editor.commitButtonNode.hidden).to.be(true);
  205. expect(editor.revertButtonNode.hidden).to.be(false);
  206. });
  207. });
  208. context('click', () => {
  209. it('should handle click events on the revert button', () => {
  210. simulate(editor.revertButtonNode, 'click');
  211. expect(editor.events).to.contain('click');
  212. });
  213. it('should revert the current data', () => {
  214. editor.source = new ObservableJSON();
  215. editor.model.value.text = 'foo';
  216. simulate(editor.revertButtonNode, 'click');
  217. expect(editor.model.value.text).to.be('{}');
  218. });
  219. it('should handle programmatic changes', () => {
  220. editor.source = new ObservableJSON();
  221. editor.model.value.text = 'foo';
  222. editor.source.set('foo', 1);
  223. simulate(editor.revertButtonNode, 'click');
  224. expect(editor.model.value.text).to.be('{\n "foo": 1\n}');
  225. });
  226. it('should handle click events on the commit button', () => {
  227. simulate(editor.commitButtonNode, 'click');
  228. expect(editor.events).to.contain('click');
  229. });
  230. it('should bail if it is not valid JSON', () => {
  231. editor.source = new ObservableJSON();
  232. editor.model.value.text = 'foo';
  233. editor.source.set('foo', 1);
  234. simulate(editor.commitButtonNode, 'click');
  235. expect(editor.model.value.text).to.be('foo');
  236. });
  237. it('should override a key that was set programmatically', () => {
  238. editor.source = new ObservableJSON();
  239. editor.model.value.text = '{"foo": 2}';
  240. editor.source.set('foo', 1);
  241. simulate(editor.commitButtonNode, 'click');
  242. expect(editor.model.value.text).to.be('{\n "foo": 2\n}');
  243. });
  244. it('should allow a programmatic key to update', () => {
  245. editor.source = new ObservableJSON();
  246. editor.source.set('foo', 1);
  247. editor.source.set('bar', 1);
  248. editor.model.value.text = '{"foo":1, "bar": 2}';
  249. editor.source.set('foo', 2);
  250. simulate(editor.commitButtonNode, 'click');
  251. let expected = '{\n "foo": 2,\n "bar": 2\n}';
  252. expect(editor.model.value.text).to.be(expected);
  253. });
  254. it('should allow a key to be added by the user', () => {
  255. editor.source = new ObservableJSON();
  256. editor.source.set('foo', 1);
  257. editor.source.set('bar', 1);
  258. editor.model.value.text = '{"foo":1, "bar": 2, "baz": 3}';
  259. editor.source.set('foo', 2);
  260. simulate(editor.commitButtonNode, 'click');
  261. let value = '{\n "foo": 2,\n "bar": 2,\n "baz": 3\n}';
  262. expect(editor.model.value.text).to.be(value);
  263. });
  264. it('should allow a key to be removed by the user', () => {
  265. editor.source = new ObservableJSON();
  266. editor.source.set('foo', 1);
  267. editor.source.set('bar', 1);
  268. editor.model.value.text = '{"foo": 1}';
  269. simulate(editor.commitButtonNode, 'click');
  270. expect(editor.model.value.text).to.be('{\n "foo": 1\n}');
  271. });
  272. it('should allow a key to be removed programmatically that was not set by the user', () => {
  273. editor.source = new ObservableJSON();
  274. editor.source.set('foo', 1);
  275. editor.source.set('bar', 1);
  276. editor.model.value.text = '{"foo": 1, "bar": 3}';
  277. editor.source.delete('foo');
  278. simulate(editor.commitButtonNode, 'click');
  279. expect(editor.model.value.text).to.be('{\n "bar": 3\n}');
  280. });
  281. it('should keep a key that was removed programmatically that was changed by the user', () => {
  282. editor.source = new ObservableJSON();
  283. editor.source.set('foo', 1);
  284. editor.source.set('bar', 1);
  285. editor.model.value.text = '{"foo": 2, "bar": 3}';
  286. editor.source.set('foo', null);
  287. simulate(editor.commitButtonNode, 'click');
  288. let expected = '{\n "foo": 2,\n "bar": 3\n}';
  289. expect(editor.model.value.text).to.be(expected);
  290. });
  291. it('should collapse the editor', () => {
  292. editor.dispose();
  293. editor = new LogEditor({ editorFactory, collapsible: true });
  294. Widget.attach(editor, document.body);
  295. simulate(editor.titleNode, 'click');
  296. expect(editor.editorHostNode.classList).to.contain('jp-mod-collapsed');
  297. });
  298. it('should have no effect if the editor is not collapsible', () => {
  299. simulate(editor.titleNode, 'click');
  300. expect(editor.editorHostNode.classList).to.not.contain('jp-mod-collapsed');
  301. });
  302. });
  303. });
  304. describe('#onAfterAttach()', () => {
  305. it('should add event listeners', () => {
  306. Widget.attach(editor, document.body);
  307. expect(editor.methods).to.contain('onAfterAttach');
  308. editor.editor.focus();
  309. simulate(editor.editorHostNode, 'blur');
  310. simulate(editor.revertButtonNode, 'click');
  311. simulate(editor.commitButtonNode, 'click');
  312. expect(editor.events).to.eql(['blur', 'click', 'click']);
  313. });
  314. });
  315. describe('#onAfterShow()', () => {
  316. it('should update the editor', done => {
  317. editor.hide();
  318. Widget.attach(editor, document.body);
  319. editor.show();
  320. requestAnimationFrame(() => {
  321. expect(editor.methods).to.contain('onUpdateRequest');
  322. done();
  323. });
  324. });
  325. });
  326. describe('#onBeforeDetach()', () => {
  327. it('should remove event listeners', () => {
  328. Widget.attach(editor, document.body);
  329. Widget.detach(editor);
  330. expect(editor.methods).to.contain('onBeforeDetach');
  331. editor.editor.focus();
  332. simulate(editor.editorHostNode, 'blur');
  333. simulate(editor.revertButtonNode, 'click');
  334. simulate(editor.commitButtonNode, 'click');
  335. expect(editor.events).to.eql([]);
  336. });
  337. });
  338. context('#source.changed', () => {
  339. it('should update the value', () => {
  340. editor.source = new ObservableJSON();
  341. editor.source.set('foo', 1);
  342. expect(editor.model.value.text).to.be('{\n "foo": 1\n}');
  343. });
  344. it('should bail if the input is dirty', () => {
  345. Widget.attach(editor, document.body);
  346. editor.source = new ObservableJSON();
  347. editor.model.value.text = 'ha';
  348. editor.source.set('foo', 2);
  349. expect(editor.model.value.text).to.be('ha');
  350. });
  351. it('should bail if the input is focused', () => {
  352. Widget.attach(editor, document.body);
  353. editor.model.value.text = '{}';
  354. editor.source = new ObservableJSON();
  355. editor.editor.focus();
  356. editor.source.set('foo', 2);
  357. expect(editor.model.value.text).to.be('{}');
  358. });
  359. });
  360. });
  361. });