jsoneditor.spec.ts 12 KB

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