handler.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import 'jest';
  4. import { SessionContext, ISessionContext } from '@jupyterlab/apputils';
  5. import { CodeEditor, CodeEditorWrapper } from '@jupyterlab/codeeditor';
  6. import { CodeMirrorEditor } from '@jupyterlab/codemirror';
  7. import {
  8. Completer,
  9. CompletionHandler,
  10. CompleterModel,
  11. KernelConnector
  12. } from '@jupyterlab/completer';
  13. import { createSessionContext } from '@jupyterlab/testutils';
  14. function createEditorWidget(): CodeEditorWrapper {
  15. const model = new CodeEditor.Model();
  16. const factory = (options: CodeEditor.IOptions) => {
  17. return new CodeMirrorEditor(options);
  18. };
  19. return new CodeEditorWrapper({ factory, model });
  20. }
  21. class TestCompleterModel extends CompleterModel {
  22. methods: string[] = [];
  23. createPatch(patch: string): Completer.IPatch | undefined {
  24. this.methods.push('createPatch');
  25. return super.createPatch(patch);
  26. }
  27. handleTextChange(change: Completer.ITextState): void {
  28. this.methods.push('handleTextChange');
  29. super.handleTextChange(change);
  30. }
  31. }
  32. class TestCompletionHandler extends CompletionHandler {
  33. methods: string[] = [];
  34. onTextChanged(): void {
  35. super.onTextChanged();
  36. this.methods.push('onTextChanged');
  37. }
  38. onCompletionSelected(widget: Completer, value: string): void {
  39. super.onCompletionSelected(widget, value);
  40. this.methods.push('onCompletionSelected');
  41. }
  42. }
  43. describe('@jupyterlab/completer', () => {
  44. let connector: KernelConnector;
  45. let sessionContext: ISessionContext;
  46. beforeAll(async () => {
  47. sessionContext = await createSessionContext();
  48. await (sessionContext as SessionContext).initialize();
  49. connector = new KernelConnector({ session: sessionContext.session });
  50. });
  51. afterAll(() => sessionContext.shutdown());
  52. describe('CompletionHandler', () => {
  53. describe('#constructor()', () => {
  54. it('should create a completer handler', () => {
  55. const handler = new CompletionHandler({
  56. connector,
  57. completer: new Completer({ editor: null })
  58. });
  59. expect(handler).toBeInstanceOf(CompletionHandler);
  60. });
  61. });
  62. describe('#connector', () => {
  63. it('should be a data connector', () => {
  64. const handler = new CompletionHandler({
  65. connector,
  66. completer: new Completer({ editor: null })
  67. });
  68. expect(handler.connector).toHaveProperty('fetch');
  69. expect(handler.connector).toHaveProperty('remove');
  70. expect(handler.connector).toHaveProperty('save');
  71. });
  72. });
  73. describe('#editor', () => {
  74. it('should default to null', () => {
  75. const handler = new CompletionHandler({
  76. connector,
  77. completer: new Completer({ editor: null })
  78. });
  79. expect(handler.editor).toBeNull();
  80. });
  81. it('should be settable', () => {
  82. const handler = new CompletionHandler({
  83. connector,
  84. completer: new Completer({ editor: null })
  85. });
  86. const widget = createEditorWidget();
  87. expect(handler.editor).toBeNull();
  88. handler.editor = widget.editor;
  89. expect(handler.editor).toBe(widget.editor);
  90. });
  91. it('should be resettable', () => {
  92. const handler = new CompletionHandler({
  93. connector,
  94. completer: new Completer({ editor: null })
  95. });
  96. const one = createEditorWidget();
  97. const two = createEditorWidget();
  98. expect(handler.editor).toBeNull();
  99. handler.editor = one.editor;
  100. expect(handler.editor).toBe(one.editor);
  101. handler.editor = two.editor;
  102. expect(handler.editor).toBe(two.editor);
  103. });
  104. it('should remove the completer active and enabled classes of the old editor', () => {
  105. const handler = new CompletionHandler({
  106. connector,
  107. completer: new Completer({ editor: null })
  108. });
  109. const widget = createEditorWidget();
  110. handler.editor = widget.editor;
  111. widget.toggleClass('jp-mod-completer-enabled');
  112. widget.toggleClass('jp-mod-completer-active');
  113. handler.editor = null;
  114. expect(widget.hasClass('jp-mod-completer-enabled')).toBe(false);
  115. expect(widget.hasClass('jp-mod-completer-active')).toBe(false);
  116. });
  117. });
  118. describe('#isDisposed', () => {
  119. it('should be true if handler has been disposed', () => {
  120. const handler = new CompletionHandler({
  121. connector,
  122. completer: new Completer({ editor: null })
  123. });
  124. expect(handler.isDisposed).toBe(false);
  125. handler.dispose();
  126. expect(handler.isDisposed).toBe(true);
  127. });
  128. });
  129. describe('#dispose()', () => {
  130. it('should dispose of the handler resources', () => {
  131. const handler = new CompletionHandler({
  132. connector,
  133. completer: new Completer({ editor: null })
  134. });
  135. expect(handler.isDisposed).toBe(false);
  136. handler.dispose();
  137. expect(handler.isDisposed).toBe(true);
  138. });
  139. it('should be safe to call multiple times', () => {
  140. const handler = new CompletionHandler({
  141. connector,
  142. completer: new Completer({ editor: null })
  143. });
  144. expect(handler.isDisposed).toBe(false);
  145. handler.dispose();
  146. handler.dispose();
  147. expect(handler.isDisposed).toBe(true);
  148. });
  149. });
  150. describe('#onTextChanged()', () => {
  151. it('should fire when the active editor emits a text change', () => {
  152. const handler = new TestCompletionHandler({
  153. connector,
  154. completer: new Completer({ editor: null })
  155. });
  156. handler.editor = createEditorWidget().editor;
  157. expect(handler.methods).toEqual(
  158. expect.not.arrayContaining(['onTextChanged'])
  159. );
  160. handler.editor.model.value.text = 'foo';
  161. expect(handler.methods).toEqual(
  162. expect.arrayContaining(['onTextChanged'])
  163. );
  164. });
  165. it('should call model change handler if model exists', () => {
  166. const completer = new Completer({
  167. editor: null,
  168. model: new TestCompleterModel()
  169. });
  170. const handler = new TestCompletionHandler({ completer, connector });
  171. const editor = createEditorWidget().editor;
  172. const model = completer.model as TestCompleterModel;
  173. handler.editor = editor;
  174. expect(model.methods).toEqual(
  175. expect.not.arrayContaining(['handleTextChange'])
  176. );
  177. editor.model.value.text = 'bar';
  178. editor.setCursorPosition({ line: 0, column: 2 });
  179. // This signal is emitted (again) because the cursor position that
  180. // a natural user would create need to be recreated here.
  181. (editor.model.value.changed as any).emit({ type: 'set', value: 'bar' });
  182. expect(model.methods).toEqual(
  183. expect.arrayContaining(['handleTextChange'])
  184. );
  185. });
  186. });
  187. describe('#onCompletionSelected()', () => {
  188. it('should fire when the completer widget emits a signal', () => {
  189. const completer = new Completer({ editor: null });
  190. const handler = new TestCompletionHandler({ completer, connector });
  191. expect(handler.methods).toEqual(
  192. expect.not.arrayContaining(['onCompletionSelected'])
  193. );
  194. (completer.selected as any).emit('foo');
  195. expect(handler.methods).toEqual(
  196. expect.arrayContaining(['onCompletionSelected'])
  197. );
  198. });
  199. it('should call model create patch method if model exists', () => {
  200. const completer = new Completer({
  201. editor: null,
  202. model: new TestCompleterModel()
  203. });
  204. const handler = new TestCompletionHandler({ completer, connector });
  205. const model = completer.model as TestCompleterModel;
  206. handler.editor = createEditorWidget().editor;
  207. expect(model.methods).toEqual(
  208. expect.not.arrayContaining(['createPatch'])
  209. );
  210. (completer.selected as any).emit('foo');
  211. expect(model.methods).toEqual(expect.arrayContaining(['createPatch']));
  212. });
  213. it('should update cell if patch exists', () => {
  214. const model = new CompleterModel();
  215. const patch = 'foobar';
  216. const completer = new Completer({ editor: null, model });
  217. const handler = new TestCompletionHandler({ completer, connector });
  218. const editor = createEditorWidget().editor;
  219. const text = 'eggs\nfoo # comment\nbaz';
  220. const want = 'eggs\nfoobar # comment\nbaz';
  221. const line = 1;
  222. const column = 5;
  223. const request: Completer.ITextState = {
  224. column,
  225. line,
  226. lineHeight: 0,
  227. charWidth: 0,
  228. coords: null,
  229. text
  230. };
  231. handler.editor = editor;
  232. handler.editor.model.value.text = text;
  233. handler.editor.setCursorPosition({ line, column: column + 3 });
  234. model.original = request;
  235. model.cursor = { start: column, end: column + 3 };
  236. (completer.selected as any).emit(patch);
  237. expect(handler.editor.model.value.text).toBe(want);
  238. expect(handler.editor.getCursorPosition()).toEqual({
  239. line,
  240. column: column + 6
  241. });
  242. });
  243. it('should be undoable and redoable', () => {
  244. const model = new CompleterModel();
  245. const patch = 'foobar';
  246. const completer = new Completer({ editor: null, model });
  247. const handler = new TestCompletionHandler({ completer, connector });
  248. const editor = createEditorWidget().editor;
  249. const text = 'eggs\nfoo # comment\nbaz';
  250. const want = 'eggs\nfoobar # comment\nbaz';
  251. const line = 1;
  252. const column = 5;
  253. const request: Completer.ITextState = {
  254. column,
  255. line,
  256. lineHeight: 0,
  257. charWidth: 0,
  258. coords: null,
  259. text
  260. };
  261. handler.editor = editor;
  262. handler.editor.model.value.text = text;
  263. handler.editor.setCursorPosition({ line, column: column + 3 });
  264. model.original = request;
  265. model.cursor = { start: column, end: column + 3 };
  266. // Make the completion, check its value and cursor position.
  267. (completer.selected as any).emit(patch);
  268. expect(editor.model.value.text).toBe(want);
  269. expect(editor.getCursorPosition()).toEqual({
  270. line,
  271. column: column + 6
  272. });
  273. console.warn(editor.getCursorPosition());
  274. // Undo the completion, check its value and cursor position.
  275. editor.undo();
  276. expect(editor.model.value.text).toBe(text);
  277. expect(editor.getCursorPosition()).toEqual({
  278. line,
  279. column: column + 3
  280. });
  281. console.warn(editor.getCursorPosition());
  282. // Redo the completion, check its value and cursor position.
  283. editor.redo();
  284. expect(editor.model.value.text).toBe(want);
  285. expect(editor.getCursorPosition()).toEqual({
  286. line,
  287. column: column + 6
  288. });
  289. console.warn(editor.getCursorPosition());
  290. });
  291. });
  292. });
  293. });