// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { CodeEditor } from '@jupyterlab/codeeditor'; import { Completer, CompleterModel, CompletionHandler } from '@jupyterlab/completer'; import { toArray } from '@lumino/algorithm'; import { JSONExt } from '@lumino/coreutils'; function makeState(text: string): Completer.ITextState { return { column: 0, lineHeight: 0, charWidth: 0, line: 0, coords: { left: 0, right: 0, top: 0, bottom: 0 } as CodeEditor.ICoordinate, text }; } describe('completer/model', () => { describe('CompleterModel', () => { describe('#constructor()', () => { it('should create a completer model', () => { const model = new CompleterModel(); expect(model).toBeInstanceOf(CompleterModel); expect(model.setCompletionItems).toBeDefined(); }); }); describe('#stateChanged', () => { it('should signal when model options have changed', () => { const model = new CompleterModel(); let called = 0; const listener = (sender: any, args: void) => { called++; }; model.stateChanged.connect(listener); expect(called).toBe(0); model.setOptions(['foo']); expect(called).toBe(1); model.setOptions(['foo'], { foo: 'instance' }); expect(called).toBe(2); }); it('should signal when model items have changed', () => { let model = new CompleterModel(); let called = 0; let listener = (sender: any, args: void) => { called++; }; model.stateChanged.connect(listener); expect(called).toBe(0); model.setCompletionItems!([{ label: 'foo' }]); expect(called).toBe(1); model.setCompletionItems!([{ label: 'foo' }]); model.setCompletionItems!([{ label: 'foo' }, { label: 'bar' }]); expect(called).toBe(2); }); it('should not signal when options have not changed', () => { const model = new CompleterModel(); let called = 0; const listener = (sender: any, args: void) => { called++; }; model.stateChanged.connect(listener); expect(called).toBe(0); model.setOptions(['foo']); model.setOptions(['foo']); expect(called).toBe(1); model.setOptions(['foo'], { foo: 'instance' }); model.setOptions(['foo'], { foo: 'instance' }); expect(called).toBe(2); model.setOptions([], {}); model.setOptions([], {}); expect(called).toBe(3); }); it('should not signal when items have not changed', () => { let model = new CompleterModel(); let called = 0; let listener = (sender: any, args: void) => { called++; }; model.stateChanged.connect(listener); expect(called).toBe(0); model.setCompletionItems!([{ label: 'foo' }]); model.setCompletionItems!([{ label: 'foo' }]); expect(called).toBe(1); model.setCompletionItems!([{ label: 'foo' }, { label: 'bar' }]); model.setCompletionItems!([{ label: 'foo' }, { label: 'bar' }]); expect(called).toBe(2); model.setCompletionItems!([]); model.setCompletionItems!([]); expect(called).toBe(3); }); it('should signal when original request changes', () => { const model = new CompleterModel(); let called = 0; const listener = (sender: any, args: void) => { called++; }; model.stateChanged.connect(listener); expect(called).toBe(0); model.original = makeState('foo'); expect(called).toBe(1); model.original = null; expect(called).toBe(2); }); it('should not signal when original request has not changed', () => { const model = new CompleterModel(); let called = 0; const listener = (sender: any, args: void) => { called++; }; model.stateChanged.connect(listener); expect(called).toBe(0); model.original = makeState('foo'); model.original = makeState('foo'); expect(called).toBe(1); model.original = null; model.original = null; expect(called).toBe(2); }); it('should signal when current text changes', () => { const model = new CompleterModel(); let called = 0; const currentValue = 'foo'; const newValue = 'foob'; const cursor: Completer.ICursorSpan = { start: 0, end: 0 }; const request = makeState(currentValue); const change = makeState(newValue); const listener = (sender: any, args: void) => { called++; }; model.stateChanged.connect(listener); expect(called).toBe(0); model.original = request; expect(called).toBe(1); model.cursor = cursor; model.current = change; expect(called).toBe(2); model.current = null; expect(called).toBe(3); }); it('should not signal when current text is unchanged', () => { const model = new CompleterModel(); let called = 0; const currentValue = 'foo'; const newValue = 'foob'; const cursor: Completer.ICursorSpan = { start: 0, end: 0 }; const request = makeState(currentValue); const change = makeState(newValue); const listener = (sender: any, args: void) => { called++; }; model.stateChanged.connect(listener); expect(called).toBe(0); model.original = request; expect(called).toBe(1); model.cursor = cursor; model.current = change; model.current = change; expect(called).toBe(2); model.current = null; model.current = null; expect(called).toBe(3); }); }); describe('#completionItems()', () => { it('should default to { items: [] }', () => { let model = new CompleterModel(); let want: CompletionHandler.ICompletionItems = []; expect(model.completionItems!()).toEqual(want); }); it('should return unmarked ICompletionItems if query is blank', () => { let model = new CompleterModel(); let want: CompletionHandler.ICompletionItems = [ { label: 'foo' }, { label: 'bar' }, { label: 'baz' } ]; model.setCompletionItems!([ { label: 'foo' }, { label: 'bar' }, { label: 'baz' } ]); expect(model.completionItems!()).toEqual(want); }); it('should return a marked list of items if query is set', () => { let model = new CompleterModel(); let want = 'foo'; model.setCompletionItems!([ { label: 'foo' }, { label: 'bar' }, { label: 'baz' } ]); model.query = 'f'; expect(model.completionItems!().length).toEqual(1); expect(model.completionItems!()[0].label).toEqual(want); }); it('should order list based on score', () => { const model = new CompleterModel(); const want: CompletionHandler.ICompletionItems = [ { insertText: 'qux', label: 'qux' }, { insertText: 'quux', label: 'quux' } ]; model.setCompletionItems!([ { label: 'foo' }, { label: 'bar' }, { label: 'baz' }, { label: 'quux' }, { label: 'qux' } ]); model.query = 'qux'; expect(model.completionItems!()).toEqual(want); }); it('should break ties in score by locale sort', () => { const model = new CompleterModel(); const want: CompletionHandler.ICompletionItems = [ { insertText: 'quux', label: 'quux' }, { insertText: 'qux', label: 'qux' } ]; model.setCompletionItems!([ { label: 'foo' }, { label: 'bar' }, { label: 'baz' }, { label: 'quux' }, { label: 'qux' } ]); model.query = 'qu'; expect(model.completionItems!()).toEqual(want); }); it('should return { items: [] } if reset', () => { let model = new CompleterModel(); let want: CompletionHandler.ICompletionItems = []; model.setCompletionItems!([ { label: 'foo' }, { label: 'bar' }, { label: 'baz' } ]); model.reset(); expect(model.completionItems!()).toEqual(want); }); }); describe('#items()', () => { it('should return an unfiltered list of items if query is blank', () => { const model = new CompleterModel(); const want: Completer.IItem[] = [ { raw: 'foo', text: 'foo' }, { raw: 'bar', text: 'bar' }, { raw: 'baz', text: 'baz' } ]; model.setOptions(['foo', 'bar', 'baz']); expect(toArray(model.items())).toEqual(want); }); it('should return a filtered list of items if query is set', () => { const model = new CompleterModel(); const want: Completer.IItem[] = [ { raw: 'foo', text: 'foo' } ]; model.setOptions(['foo', 'bar', 'baz']); model.query = 'f'; expect(toArray(model.items())).toEqual(want); }); it('should order list based on score', () => { const model = new CompleterModel(); const want: Completer.IItem[] = [ { raw: 'qux', text: 'qux' }, { raw: 'quux', text: 'quux' } ]; model.setOptions(['foo', 'bar', 'baz', 'quux', 'qux']); model.query = 'qux'; expect(toArray(model.items())).toEqual(want); }); it('should break ties in score by locale sort', () => { const model = new CompleterModel(); const want: Completer.IItem[] = [ { raw: 'quux', text: 'quux' }, { raw: 'qux', text: 'qux' } ]; model.setOptions(['foo', 'bar', 'baz', 'qux', 'quux']); model.query = 'qu'; expect(toArray(model.items())).toEqual(want); }); }); describe('#options()', () => { it('should default to an empty iterator', () => { const model = new CompleterModel(); expect(model.options().next()).toBeUndefined(); }); it('should return model options', () => { const model = new CompleterModel(); const options = ['foo']; model.setOptions(options, {}); expect(toArray(model.options())).not.toBe(options); expect(toArray(model.options())).toEqual(options); }); it('should return the typeMap', () => { const model = new CompleterModel(); const options = ['foo']; const typeMap = { foo: 'instance' }; model.setOptions(options, typeMap); expect(JSONExt.deepEqual(model.typeMap(), typeMap)).toBeTruthy(); }); }); describe('#original', () => { it('should default to null', () => { const model = new CompleterModel(); expect(model.original).toBeNull(); }); it('should return the original request', () => { const model = new CompleterModel(); const request = makeState('foo'); model.original = request; expect(model.original).toBe(request); }); }); describe('#current', () => { it('should default to null', () => { const model = new CompleterModel(); expect(model.current).toBeNull(); }); it('should initially equal the original request', () => { const model = new CompleterModel(); const request = makeState('foo'); model.original = request; expect(model.current).toBe(request); }); it('should not set if original request is nonexistent', () => { const model = new CompleterModel(); const currentValue = 'foo'; const newValue = 'foob'; const cursor: Completer.ICursorSpan = { start: 0, end: 0 }; const request = makeState(currentValue); const change = makeState(newValue); model.current = change; expect(model.current).toBeNull(); model.original = request; model.cursor = cursor; model.current = change; expect(model.current).toBe(change); }); it('should not set if cursor is nonexistent', () => { const model = new CompleterModel(); const currentValue = 'foo'; const newValue = 'foob'; const request = makeState(currentValue); const change = makeState(newValue); model.original = request; model.cursor = null; model.current = change; expect(model.current).not.toBe(change); }); it('should reset model if change is shorter than original', () => { const model = new CompleterModel(); const currentValue = 'foo'; const newValue = 'fo'; const cursor: Completer.ICursorSpan = { start: 0, end: 0 }; const request = makeState(currentValue); const change = makeState(newValue); model.original = request; model.cursor = cursor; model.current = change; expect(model.current).toBeNull(); expect(model.original).toBeNull(); expect(model.options().next()).toBeUndefined(); }); }); describe('#cursor', () => { it('should default to null', () => { const model = new CompleterModel(); expect(model.cursor).toBeNull(); }); it('should not set if original request is nonexistent', () => { const model = new CompleterModel(); const cursor: Completer.ICursorSpan = { start: 0, end: 0 }; const request = makeState('foo'); model.cursor = cursor; expect(model.cursor).toBeNull(); model.original = request; model.cursor = cursor; expect(model.cursor).toBe(cursor); }); }); describe('#isDisposed', () => { it('should be true if model has been disposed', () => { const model = new CompleterModel(); expect(model.isDisposed).toBe(false); model.dispose(); expect(model.isDisposed).toBe(true); }); }); describe('#dispose()', () => { it('should dispose of the model resources', () => { const model = new CompleterModel(); model.setOptions(['foo'], { foo: 'instance' }); expect(model.isDisposed).toBe(false); model.dispose(); expect(model.isDisposed).toBe(true); }); it('should be safe to call multiple times', () => { const model = new CompleterModel(); expect(model.isDisposed).toBe(false); model.dispose(); model.dispose(); expect(model.isDisposed).toBe(true); }); }); describe('#handleTextChange()', () => { it('should set current change value', () => { const model = new CompleterModel(); const currentValue = 'foo'; const newValue = 'foob'; const cursor: Completer.ICursorSpan = { start: 0, end: 0 }; const request = makeState(currentValue); const change = makeState(newValue); (change as any).column = 4; model.original = request; model.cursor = cursor; expect(model.current).toBe(request); model.handleTextChange(change); expect(model.current).toBe(change); }); it('should reset if last char is whitespace && column < original', () => { const model = new CompleterModel(); const currentValue = 'foo'; const newValue = 'foo '; const request = makeState(currentValue); (request as any).column = 3; const change = makeState(newValue); (change as any).column = 0; model.original = request; expect(model.original).toBe(request); model.handleTextChange(change); expect(model.original).toBeNull(); }); }); describe('#createPatch()', () => { it('should return a patch value', () => { const model = new CompleterModel(); const patch = 'foobar'; const want: Completer.IPatch = { start: 0, end: 3, value: patch }; const cursor: Completer.ICursorSpan = { start: 0, end: 3 }; model.original = makeState('foo'); model.cursor = cursor; expect(model.createPatch(patch)).toEqual(want); }); it('should return undefined if original request or cursor are null', () => { const model = new CompleterModel(); expect(model.createPatch('foo')).toBeUndefined(); }); it('should handle line breaks in original value', () => { const model = new CompleterModel(); const currentValue = 'foo\nbar'; const patch = 'barbaz'; const start = currentValue.length; const end = currentValue.length; const want: Completer.IPatch = { start, end, value: patch }; const cursor: Completer.ICursorSpan = { start, end }; model.original = makeState(currentValue); model.cursor = cursor; expect(model.createPatch(patch)).toEqual(want); }); }); }); });