widget.spec.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760
  1. // Copyright (c) Jupyter Development Team.
  2. import 'jest';
  3. import { MessageLoop, Message } from '@lumino/messaging';
  4. import { Panel } from '@lumino/widgets';
  5. import { Widget } from '@lumino/widgets';
  6. import { simulate } from 'simulate-event';
  7. import { CodeEditor, CodeEditorWrapper } from '@jupyterlab/codeeditor';
  8. import { CodeMirrorEditor } from '@jupyterlab/codemirror';
  9. import { Completer, CompleterModel } from '@jupyterlab/completer';
  10. import { framePromise, sleep } from '@jupyterlab/testutils';
  11. const TEST_ITEM_CLASS = 'jp-TestItem';
  12. const ITEM_CLASS = 'jp-Completer-item';
  13. const ACTIVE_CLASS = 'jp-mod-active';
  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 CustomRenderer extends Completer.Renderer {
  22. createItemNode(
  23. item: Completer.IItem,
  24. typeMap: Completer.TypeMap,
  25. orderedTypes: string[]
  26. ): HTMLLIElement {
  27. const li = super.createItemNode(item, typeMap, orderedTypes);
  28. li.classList.add(TEST_ITEM_CLASS);
  29. return li;
  30. }
  31. }
  32. class LogWidget extends Completer {
  33. events: string[] = [];
  34. methods: string[] = [];
  35. dispose(): void {
  36. super.dispose();
  37. this.events.length = 0;
  38. }
  39. handleEvent(event: Event): void {
  40. this.events.push(event.type);
  41. super.handleEvent(event);
  42. }
  43. protected onUpdateRequest(msg: Message): void {
  44. super.onUpdateRequest(msg);
  45. this.methods.push('onUpdateRequest');
  46. }
  47. }
  48. describe('completer/widget', () => {
  49. describe('Completer', () => {
  50. describe('#constructor()', () => {
  51. it('should create a completer widget', () => {
  52. const widget = new Completer({ editor: null });
  53. expect(widget).toBeInstanceOf(Completer);
  54. expect(Array.from(widget.node.classList)).toEqual(
  55. expect.arrayContaining(['jp-Completer'])
  56. );
  57. });
  58. it('should accept options with a model', () => {
  59. const options: Completer.IOptions = {
  60. editor: null,
  61. model: new CompleterModel()
  62. };
  63. const widget = new Completer(options);
  64. expect(widget).toBeInstanceOf(Completer);
  65. expect(widget.model).toBe(options.model);
  66. });
  67. it('should accept options with a renderer', () => {
  68. const options: Completer.IOptions = {
  69. editor: null,
  70. model: new CompleterModel(),
  71. renderer: new CustomRenderer()
  72. };
  73. options.model!.setOptions(['foo', 'bar']);
  74. const widget = new Completer(options);
  75. expect(widget).toBeInstanceOf(Completer);
  76. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  77. const items = widget.node.querySelectorAll(`.${ITEM_CLASS}`);
  78. expect(items).toHaveLength(2);
  79. expect(Array.from(items[0].classList)).toEqual(
  80. expect.arrayContaining([TEST_ITEM_CLASS])
  81. );
  82. });
  83. });
  84. describe('#selected', () => {
  85. it('should emit a signal when an item is selected', () => {
  86. const anchor = createEditorWidget();
  87. const options: Completer.IOptions = {
  88. editor: anchor.editor,
  89. model: new CompleterModel()
  90. };
  91. let value = '';
  92. const listener = (sender: any, selected: string) => {
  93. value = selected;
  94. };
  95. options.model!.setOptions(['foo', 'bar']);
  96. Widget.attach(anchor, document.body);
  97. const widget = new Completer(options);
  98. widget.selected.connect(listener);
  99. Widget.attach(widget, document.body);
  100. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  101. expect(value).toBe('');
  102. widget.selectActive();
  103. expect(value).toBe('foo');
  104. widget.dispose();
  105. anchor.dispose();
  106. });
  107. });
  108. describe('#visibilityChanged', () => {
  109. it('should emit a signal when completer visibility changes', async () => {
  110. const panel = new Panel();
  111. const code = createEditorWidget();
  112. const editor = code.editor;
  113. const model = new CompleterModel();
  114. let called = false;
  115. editor.model.value.text = 'a';
  116. panel.node.style.position = 'absolute';
  117. panel.node.style.top = '0px';
  118. panel.node.style.left = '0px';
  119. panel.node.style.height = '1000px';
  120. code.node.style.height = '900px';
  121. panel.addWidget(code);
  122. Widget.attach(panel, document.body);
  123. panel.node.scrollTop = 0;
  124. document.body.scrollTop = 0;
  125. const position = code.editor.getPositionAt(1)!;
  126. editor.setCursorPosition(position);
  127. const request: Completer.ITextState = {
  128. column: position.column,
  129. lineHeight: editor.lineHeight,
  130. charWidth: editor.charWidth,
  131. line: position.line,
  132. text: 'a'
  133. };
  134. model.original = request;
  135. model.cursor = { start: 0, end: 1 };
  136. model.setOptions(['abc', 'abd', 'abe', 'abi']);
  137. const widget = new Completer({ model, editor: code.editor });
  138. widget.hide();
  139. expect(called).toBe(false);
  140. widget.visibilityChanged.connect(() => {
  141. called = true;
  142. });
  143. Widget.attach(widget, document.body);
  144. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  145. await framePromise();
  146. expect(called).toBe(true);
  147. widget.dispose();
  148. code.dispose();
  149. panel.dispose();
  150. });
  151. });
  152. describe('#model', () => {
  153. it('should default to null', () => {
  154. const widget = new Completer({ editor: null });
  155. expect(widget.model).toBeNull();
  156. });
  157. it('should be settable', () => {
  158. const widget = new Completer({ editor: null });
  159. expect(widget.model).toBeNull();
  160. widget.model = new CompleterModel();
  161. expect(widget.model).toBeInstanceOf(CompleterModel);
  162. });
  163. it('should be safe to set multiple times', () => {
  164. const model = new CompleterModel();
  165. const widget = new Completer({ editor: null });
  166. widget.model = model;
  167. widget.model = model;
  168. expect(widget.model).toBe(model);
  169. });
  170. it('should be safe to reset', () => {
  171. const model = new CompleterModel();
  172. const widget = new Completer({
  173. editor: null,
  174. model: new CompleterModel()
  175. });
  176. expect(widget.model).not.toBe(model);
  177. widget.model = model;
  178. expect(widget.model).toBe(model);
  179. });
  180. });
  181. describe('#editor', () => {
  182. it('should default to null', () => {
  183. const widget = new Completer({ editor: null });
  184. expect(widget.editor).toBeNull();
  185. });
  186. it('should be settable', () => {
  187. const anchor = createEditorWidget();
  188. const widget = new Completer({ editor: null });
  189. expect(widget.editor).toBeNull();
  190. widget.editor = anchor.editor;
  191. expect(widget.editor).toBeTruthy();
  192. });
  193. });
  194. describe('#dispose()', () => {
  195. it('should dispose of the resources held by the widget', () => {
  196. const widget = new Completer({ editor: null });
  197. widget.dispose();
  198. expect(widget.isDisposed).toBe(true);
  199. });
  200. it('should be safe to call multiple times', () => {
  201. const widget = new Completer({ editor: null });
  202. widget.dispose();
  203. widget.dispose();
  204. expect(widget.isDisposed).toBe(true);
  205. });
  206. });
  207. describe('#reset()', () => {
  208. it('should reset the completer widget', () => {
  209. const anchor = createEditorWidget();
  210. const model = new CompleterModel();
  211. const options: Completer.IOptions = {
  212. editor: anchor.editor,
  213. model
  214. };
  215. model.setOptions(['foo', 'bar'], { foo: 'instance', bar: 'function' });
  216. Widget.attach(anchor, document.body);
  217. const widget = new Completer(options);
  218. Widget.attach(widget, document.body);
  219. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  220. expect(widget.isHidden).toBe(false);
  221. expect(model.options).toBeTruthy();
  222. widget.reset();
  223. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  224. expect(widget.isHidden).toBe(true);
  225. expect(model.options().next()).toBeUndefined();
  226. widget.dispose();
  227. anchor.dispose();
  228. });
  229. });
  230. describe('#handleEvent()', () => {
  231. it('should handle document keydown, mousedown, and scroll events', () => {
  232. const anchor = createEditorWidget();
  233. const widget = new LogWidget({ editor: anchor.editor });
  234. Widget.attach(anchor, document.body);
  235. Widget.attach(widget, document.body);
  236. ['keydown', 'mousedown', 'scroll'].forEach(type => {
  237. simulate(document.body, type);
  238. expect(widget.events).toEqual(expect.arrayContaining([type]));
  239. });
  240. widget.dispose();
  241. anchor.dispose();
  242. });
  243. describe('keydown', () => {
  244. it('should reset if keydown is outside anchor', () => {
  245. const model = new CompleterModel();
  246. const anchor = createEditorWidget();
  247. const options: Completer.IOptions = {
  248. editor: anchor.editor,
  249. model
  250. };
  251. model.setOptions(['foo', 'bar'], {
  252. foo: 'instance',
  253. bar: 'function'
  254. });
  255. Widget.attach(anchor, document.body);
  256. const widget = new Completer(options);
  257. Widget.attach(widget, document.body);
  258. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  259. expect(widget.isHidden).toBe(false);
  260. expect(model.options).toBeTruthy();
  261. simulate(document.body, 'keydown', { keyCode: 70 }); // F
  262. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  263. expect(widget.isHidden).toBe(true);
  264. expect(model.options().next()).toBeUndefined();
  265. widget.dispose();
  266. anchor.dispose();
  267. });
  268. it('should select the item below and not progress past last', () => {
  269. const anchor = createEditorWidget();
  270. const model = new CompleterModel();
  271. const options: Completer.IOptions = {
  272. editor: anchor.editor,
  273. model
  274. };
  275. model.setOptions(['foo', 'bar', 'baz'], {
  276. foo: 'instance',
  277. bar: 'function'
  278. });
  279. Widget.attach(anchor, document.body);
  280. const widget = new Completer(options);
  281. const target = document.createElement('div');
  282. anchor.node.appendChild(target);
  283. Widget.attach(widget, document.body);
  284. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  285. const items = widget.node.querySelectorAll(`.${ITEM_CLASS}`);
  286. expect(Array.from(items[0].classList)).toEqual(
  287. expect.arrayContaining([ACTIVE_CLASS])
  288. );
  289. expect(Array.from(items[1].classList)).toEqual(
  290. expect.not.arrayContaining([ACTIVE_CLASS])
  291. );
  292. expect(Array.from(items[2].classList)).toEqual(
  293. expect.not.arrayContaining([ACTIVE_CLASS])
  294. );
  295. simulate(target, 'keydown', { keyCode: 40 }); // Down
  296. expect(Array.from(items[0].classList)).toEqual(
  297. expect.not.arrayContaining([ACTIVE_CLASS])
  298. );
  299. expect(Array.from(items[1].classList)).toEqual(
  300. expect.arrayContaining([ACTIVE_CLASS])
  301. );
  302. expect(Array.from(items[2].classList)).toEqual(
  303. expect.not.arrayContaining([ACTIVE_CLASS])
  304. );
  305. simulate(target, 'keydown', { keyCode: 40 }); // Down
  306. expect(Array.from(items[0].classList)).toEqual(
  307. expect.not.arrayContaining([ACTIVE_CLASS])
  308. );
  309. expect(Array.from(items[1].classList)).toEqual(
  310. expect.not.arrayContaining([ACTIVE_CLASS])
  311. );
  312. expect(Array.from(items[2].classList)).toEqual(
  313. expect.arrayContaining([ACTIVE_CLASS])
  314. );
  315. simulate(target, 'keydown', { keyCode: 40 }); // Down
  316. expect(Array.from(items[0].classList)).toEqual(
  317. expect.not.arrayContaining([ACTIVE_CLASS])
  318. );
  319. expect(Array.from(items[1].classList)).toEqual(
  320. expect.not.arrayContaining([ACTIVE_CLASS])
  321. );
  322. expect(Array.from(items[2].classList)).toEqual(
  323. expect.arrayContaining([ACTIVE_CLASS])
  324. );
  325. widget.dispose();
  326. anchor.dispose();
  327. });
  328. it('should select the item above and not progress beyond first', () => {
  329. const anchor = createEditorWidget();
  330. const model = new CompleterModel();
  331. const options: Completer.IOptions = {
  332. editor: anchor.editor,
  333. model
  334. };
  335. model.setOptions(['foo', 'bar', 'baz'], {
  336. foo: 'instance',
  337. bar: 'function'
  338. });
  339. Widget.attach(anchor, document.body);
  340. const widget = new Completer(options);
  341. Widget.attach(widget, document.body);
  342. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  343. const items = widget.node.querySelectorAll(`.${ITEM_CLASS}`);
  344. expect(Array.from(items[0].classList)).toEqual(
  345. expect.arrayContaining([ACTIVE_CLASS])
  346. );
  347. expect(Array.from(items[1].classList)).toEqual(
  348. expect.not.arrayContaining([ACTIVE_CLASS])
  349. );
  350. expect(Array.from(items[2].classList)).toEqual(
  351. expect.not.arrayContaining([ACTIVE_CLASS])
  352. );
  353. simulate(anchor.node, 'keydown', { keyCode: 40 }); // Down
  354. expect(Array.from(items[0].classList)).toEqual(
  355. expect.not.arrayContaining([ACTIVE_CLASS])
  356. );
  357. expect(Array.from(items[1].classList)).toEqual(
  358. expect.arrayContaining([ACTIVE_CLASS])
  359. );
  360. expect(Array.from(items[2].classList)).toEqual(
  361. expect.not.arrayContaining([ACTIVE_CLASS])
  362. );
  363. simulate(anchor.node, 'keydown', { keyCode: 40 }); // Down
  364. expect(Array.from(items[0].classList)).toEqual(
  365. expect.not.arrayContaining([ACTIVE_CLASS])
  366. );
  367. expect(Array.from(items[1].classList)).toEqual(
  368. expect.not.arrayContaining([ACTIVE_CLASS])
  369. );
  370. expect(Array.from(items[2].classList)).toEqual(
  371. expect.arrayContaining([ACTIVE_CLASS])
  372. );
  373. simulate(anchor.node, 'keydown', { keyCode: 38 }); // Up
  374. expect(Array.from(items[0].classList)).toEqual(
  375. expect.not.arrayContaining([ACTIVE_CLASS])
  376. );
  377. expect(Array.from(items[1].classList)).toEqual(
  378. expect.arrayContaining([ACTIVE_CLASS])
  379. );
  380. expect(Array.from(items[2].classList)).toEqual(
  381. expect.not.arrayContaining([ACTIVE_CLASS])
  382. );
  383. simulate(anchor.node, 'keydown', { keyCode: 38 }); // Up
  384. expect(Array.from(items[0].classList)).toEqual(
  385. expect.arrayContaining([ACTIVE_CLASS])
  386. );
  387. expect(Array.from(items[1].classList)).toEqual(
  388. expect.not.arrayContaining([ACTIVE_CLASS])
  389. );
  390. expect(Array.from(items[2].classList)).toEqual(
  391. expect.not.arrayContaining([ACTIVE_CLASS])
  392. );
  393. simulate(anchor.node, 'keydown', { keyCode: 38 }); // Up
  394. expect(Array.from(items[0].classList)).toEqual(
  395. expect.arrayContaining([ACTIVE_CLASS])
  396. );
  397. expect(Array.from(items[1].classList)).toEqual(
  398. expect.not.arrayContaining([ACTIVE_CLASS])
  399. );
  400. expect(Array.from(items[2].classList)).toEqual(
  401. expect.not.arrayContaining([ACTIVE_CLASS])
  402. );
  403. widget.dispose();
  404. anchor.dispose();
  405. });
  406. it('should mark common subset on start and complete that subset on tab', async () => {
  407. const anchor = createEditorWidget();
  408. const model = new CompleterModel();
  409. const options: Completer.IOptions = {
  410. editor: anchor.editor,
  411. model
  412. };
  413. let value = '';
  414. const listener = (sender: any, selected: string) => {
  415. value = selected;
  416. };
  417. model.setOptions(['fo', 'foo', 'foo', 'fooo'], {
  418. foo: 'instance',
  419. bar: 'function'
  420. });
  421. Widget.attach(anchor, document.body);
  422. const widget = new Completer(options);
  423. widget.selected.connect(listener);
  424. Widget.attach(widget, document.body);
  425. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  426. await framePromise();
  427. const marked = widget.node.querySelectorAll(`.${ITEM_CLASS} mark`);
  428. expect(Object.keys(value)).toHaveLength(0);
  429. expect(marked).toHaveLength(4);
  430. expect(marked[0].textContent).toBe('fo');
  431. expect(marked[1].textContent).toBe('fo');
  432. expect(marked[2].textContent).toBe('fo');
  433. expect(marked[3].textContent).toBe('fo');
  434. simulate(anchor.node, 'keydown', { keyCode: 9 }); // Tab key
  435. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  436. expect(value).toBe('fo');
  437. widget.dispose();
  438. anchor.dispose();
  439. });
  440. });
  441. describe('mousedown', () => {
  442. it('should trigger a selected signal on mouse down', () => {
  443. const anchor = createEditorWidget();
  444. const model = new CompleterModel();
  445. const options: Completer.IOptions = {
  446. editor: anchor.editor,
  447. model
  448. };
  449. let value = '';
  450. const listener = (sender: any, selected: string) => {
  451. value = selected;
  452. };
  453. model.setOptions(['foo', 'bar', 'baz'], {
  454. foo: 'instance',
  455. bar: 'function'
  456. });
  457. model.query = 'b';
  458. Widget.attach(anchor, document.body);
  459. const widget = new Completer(options);
  460. widget.selected.connect(listener);
  461. Widget.attach(widget, document.body);
  462. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  463. const item = widget.node.querySelectorAll(`.${ITEM_CLASS} mark`)[1];
  464. simulate(anchor.node, 'keydown', { keyCode: 9 }); // Tab key
  465. expect(model.query).toBe('ba');
  466. simulate(item, 'mousedown');
  467. expect(value).toBe('baz');
  468. widget.dispose();
  469. anchor.dispose();
  470. });
  471. it('should ignore nonstandard mouse clicks (e.g., right click)', () => {
  472. const anchor = createEditorWidget();
  473. const model = new CompleterModel();
  474. const options: Completer.IOptions = {
  475. editor: anchor.editor,
  476. model
  477. };
  478. let value = '';
  479. const listener = (sender: any, selected: string) => {
  480. value = selected;
  481. };
  482. model.setOptions(['foo', 'bar']);
  483. Widget.attach(anchor, document.body);
  484. const widget = new Completer(options);
  485. widget.selected.connect(listener);
  486. Widget.attach(widget, document.body);
  487. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  488. expect(value).toBe('');
  489. simulate(widget.node, 'mousedown', { button: 1 });
  490. expect(value).toBe('');
  491. widget.dispose();
  492. anchor.dispose();
  493. });
  494. it('should ignore a mouse down that misses an item', () => {
  495. const anchor = createEditorWidget();
  496. const model = new CompleterModel();
  497. const options: Completer.IOptions = {
  498. editor: anchor.editor,
  499. model
  500. };
  501. let value = '';
  502. const listener = (sender: any, selected: string) => {
  503. value = selected;
  504. };
  505. model.setOptions(['foo', 'bar']);
  506. Widget.attach(anchor, document.body);
  507. const widget = new Completer(options);
  508. widget.selected.connect(listener);
  509. Widget.attach(widget, document.body);
  510. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  511. expect(value).toBe('');
  512. simulate(widget.node, 'mousedown');
  513. expect(value).toBe('');
  514. widget.dispose();
  515. anchor.dispose();
  516. });
  517. it('should hide widget if mouse down misses it', () => {
  518. const anchor = createEditorWidget();
  519. const model = new CompleterModel();
  520. const options: Completer.IOptions = {
  521. editor: anchor.editor,
  522. model
  523. };
  524. const listener = (sender: any, selected: string) => {
  525. // no op
  526. };
  527. model.setOptions(['foo', 'bar']);
  528. Widget.attach(anchor, document.body);
  529. const widget = new Completer(options);
  530. widget.selected.connect(listener);
  531. Widget.attach(widget, document.body);
  532. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  533. expect(widget.isHidden).toBe(false);
  534. simulate(anchor.node, 'mousedown');
  535. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  536. expect(widget.isHidden).toBe(true);
  537. widget.dispose();
  538. anchor.dispose();
  539. });
  540. });
  541. describe('scroll', () => {
  542. it.skip('should position itself according to the anchor', async () => {
  543. const panel = new Panel();
  544. const code = createEditorWidget();
  545. const editor = code.editor;
  546. const model = new CompleterModel();
  547. const text = '\n\n\n\n\n\na';
  548. code.node.style.height = '5000px';
  549. code.node.style.width = '400px';
  550. code.node.style.background = 'yellow';
  551. editor.model.value.text = text;
  552. panel.node.style.background = 'red';
  553. panel.node.style.height = '2000px';
  554. panel.node.style.width = '500px';
  555. panel.node.style.maxHeight = '500px';
  556. panel.node.style.overflow = 'auto';
  557. panel.node.style.position = 'absolute';
  558. panel.node.style.top = '0px';
  559. panel.node.style.left = '0px';
  560. panel.node.scrollTop = 10;
  561. panel.addWidget(code);
  562. Widget.attach(panel, document.body);
  563. editor.refresh();
  564. const position = code.editor.getPositionAt(text.length)!;
  565. const coords = code.editor.getCoordinateForPosition(position);
  566. editor.setCursorPosition(position);
  567. const request: Completer.ITextState = {
  568. column: position.column,
  569. lineHeight: editor.lineHeight,
  570. charWidth: editor.charWidth,
  571. line: position.line,
  572. text: 'a'
  573. };
  574. model.original = request;
  575. model.cursor = { start: text.length - 1, end: text.length };
  576. model.setOptions(['abc', 'abd', 'abe', 'abi']);
  577. const widget = new Completer({ model, editor: code.editor });
  578. Widget.attach(widget, document.body);
  579. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  580. simulate(document.body, 'scroll');
  581. // Because the scroll handling is asynchronous, this test uses a large
  582. // timeout (500ms) to guarantee the scroll handling has finished.
  583. await sleep(500);
  584. const top = parseInt(window.getComputedStyle(widget.node).top, 10);
  585. const bottom = Math.floor(coords.bottom);
  586. expect(top + panel.node.scrollTop).toBe(bottom);
  587. widget.dispose();
  588. code.dispose();
  589. panel.dispose();
  590. });
  591. });
  592. });
  593. describe('#onUpdateRequest()', () => {
  594. it('should emit a selection if there is only one match', () => {
  595. const anchor = createEditorWidget();
  596. const model = new CompleterModel();
  597. const coords = { left: 0, right: 0, top: 100, bottom: 120 };
  598. const request: Completer.ITextState = {
  599. column: 0,
  600. lineHeight: 0,
  601. charWidth: 0,
  602. line: 0,
  603. coords: coords as CodeEditor.ICoordinate,
  604. text: 'f'
  605. };
  606. let value = '';
  607. const options: Completer.IOptions = {
  608. editor: anchor.editor,
  609. model
  610. };
  611. const listener = (sender: any, selected: string) => {
  612. value = selected;
  613. };
  614. Widget.attach(anchor, document.body);
  615. model.original = request;
  616. model.setOptions(['foo']);
  617. const widget = new Completer(options);
  618. widget.selected.connect(listener);
  619. Widget.attach(widget, document.body);
  620. expect(value).toBe('');
  621. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  622. expect(value).toBe('foo');
  623. widget.dispose();
  624. anchor.dispose();
  625. });
  626. it('should do nothing if a model does not exist', () => {
  627. const widget = new LogWidget({ editor: null });
  628. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  629. expect(widget.methods).toEqual(
  630. expect.arrayContaining(['onUpdateRequest'])
  631. );
  632. });
  633. it('should un-hide widget if multiple options are available', () => {
  634. const anchor = createEditorWidget();
  635. const model = new CompleterModel();
  636. const coords = { left: 0, right: 0, top: 100, bottom: 120 };
  637. const request: Completer.ITextState = {
  638. column: 0,
  639. lineHeight: 0,
  640. charWidth: 0,
  641. line: 0,
  642. coords: coords as CodeEditor.ICoordinate,
  643. text: 'f'
  644. };
  645. const options: Completer.IOptions = {
  646. editor: anchor.editor,
  647. model
  648. };
  649. Widget.attach(anchor, document.body);
  650. model.original = request;
  651. model.setOptions(['foo', 'bar', 'baz']);
  652. const widget = new Completer(options);
  653. widget.hide();
  654. expect(widget.isHidden).toBe(true);
  655. Widget.attach(widget, document.body);
  656. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  657. expect(widget.isVisible).toBe(true);
  658. widget.dispose();
  659. anchor.dispose();
  660. });
  661. });
  662. });
  663. });