jsoneditor.spec.ts 12 KB

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