UNPKG

angular-ui-mention

Version:

Facebook-like @mentions for text inputs built around composability

561 lines (454 loc) 17.6 kB
describe('uiMention', () => { let $scope, $attrs, $q, $timeout, $document, createController, ngModelController; beforeEach(() => { module('ui.mention'); inject(($injector, $controller) => { $scope = $injector.get('$rootScope').$new(); $q = $injector.get('$q'); $timeout = $injector.get('$timeout'); $document = $injector.get('$document'); $attrs = {}; createController = (el) => { return $controller('uiMention', { $scope: $scope, $attrs: $attrs, $element: el, $q: $q, $timeout: $timeout, $document: $document }); }; }); }); context('on invocation', () => { let ctrlInstance, $element; beforeEach(() => { $element = angular.element('<span ui-mention ng-model="xyz"></span>'); ctrlInstance = createController($element); }); it('exposes a delimiter', () => { expect(ctrlInstance.delimiter).to.eql('@'); }); it('exposes a searchPattern', () => { expect(ctrlInstance.searchPattern).to.eql(/(?:\s+|^)@(\w+(?: \w+)?)$/); }); it('exposes a decodePattern', () => { expect(ctrlInstance.decodePattern).to.eql(/@[[\s\w]+:[0-9a-z-]+]/gi); }); it('exposes the given $element', () => { expect(ctrlInstance.$element).to.eq($element); }); it('exposes an array of choices', () => { expect(ctrlInstance.choices).to.eql([]); }); it('exposes an array of mentions', () => { expect(ctrlInstance.mentions).to.eql([]); }); }); context('public API', function () { let ctrlInstance, $element; beforeEach(function () { $scope.model = 'bar'; $element = angular.element('<span ui-mention ng-model="model"></span><span></span>'); ctrlInstance = createController($element); $scope.$digest(); }); [ 'init', 'render', 'renderElement', 'highlight', 'decode', 'label', 'encode', 'replace', 'select', 'up', 'down', 'search', 'findChoices', 'cancel', 'autogrow' ].forEach((fn) => { it(fn + ' is a public API method on ' + ctrlInstance, () => { expect(ctrlInstance).to.have.property(fn).that.is.a('function'); }); }); context('.init()', () => { let ngModel, mentions; beforeEach(() => { ngModel = { $parsers: [], $formatters: [] }; mentions = [{ id: 1, first: 'foo', last: 'bar' }, { id: 2, first: 'k', last: 'v' }]; ctrlInstance.init(ngModel); ctrlInstance.mentions = mentions; }); it('sets $attrs.ngTrim to false', () => { expect($attrs.ngTrim).to.eq('false'); }); context('ngModel.$parsers', () => { let $parsers, mentionParser; beforeEach(() => { $parsers = ngModel.$parsers; mentionParser = $parsers[0]; }); it('received a new $parser', () => { expect($parsers.length).to.eq(1); }); it('sets up a mentions property on the controller instance', () => { mentionParser(''); expect(ctrlInstance).to.have.property('mentions').that.is.an('array'); }); it('returns the given value', () => { expect(mentionParser('foo bar')).to.eq('@[foo bar:1]'); }); it('filters out non matching mentions', () => { mentionParser('foo bar'); expect(ctrlInstance.mentions).to.eql(mentions.slice(0,1)); }); it('updates the HTML content of the adjacent DOM element', () => { mentionParser('foo bar'); expect($element.next().html()).to.eq('<span>foo bar</span>'); }); }); context('ngModel.$formatters', () => { let $formatters, formatter; beforeEach(() => { $formatters = ngModel.$formatters; formatter = $formatters[0]; }); it('received a new $formatter', () => { expect($formatters.length).to.eq(1); }); it('returns an empty string by default', () => { expect(formatter()).to.eq(''); }); it('casts any non-string argument to a string', () => { expect(formatter(123)).to.eq('123'); expect(formatter(false)).to.eq('false'); expect(formatter(true)).to.eq('true'); expect(formatter({})).to.eq('[object Object]'); expect(formatter([])).to.eq(''); }); it('filters out non matching mentions', () => { expect(ctrlInstance.mentions).to.include(mentions[1]); formatter('@[foo bar:1]'); expect(ctrlInstance.mentions).to.not.include(mentions[1]); }); it('returns an encoded version of the passed value', () => { expect(formatter('@[foo bar:1]')).to.eq('foo bar'); }); }); context('ngModel.$render', () => { it('sets the val property of $element to ngModel.$viewValue', () => { ngModel.$viewValue = 'wat'; ngModel.$render(); expect($element.val()).to.eq('wat'); }); it('defaults to an empty string', () => { ngModel.$render(); expect($element.val()).to.eq(''); }); it('updates the HTML content of the adjacent DOM element', () => { let testCases = [ { $modelValue: '@[foo bar:1]', expected: '<span>foo bar</span>' }, { $modelValue: '@[foo bar:1] @[foo bar:1]', expected: '<span>foo bar</span> <span>foo bar</span>' }, { $modelValue: '@[foo bar:1] @[k v:2] @[foo bar:1]', expected: '<span>foo bar</span> <span>k v</span> <span>foo bar</span>' }, { $modelValue: '@[foo bar:1] @[k v:2] @[foo bar:1] @[k v:2]', expected: '<span>foo bar</span> <span>k v</span> <span>foo bar</span> <span>k v</span>' } ]; testCases.forEach(testCase => { ngModel.$modelValue = testCase.$modelValue; ngModel.$render(); expect($element.next().html()).to.eq(testCase.expected); }); }); }); }); context('.render()', () => { let ngModel, mentions; beforeEach(() => { ngModel = { $parsers: [], $formatters: [] }; mentions = [{ id: 1, first: 'foo', last: 'bar' }, { id: 2, first: 'k', last: 'v' }]; $element = angular.element('<span ui-mention ng-model="model"></span><span></span>'); ctrlInstance = createController($element); $scope.$digest(); ctrlInstance.init(ngModel); ctrlInstance.mentions = mentions; }); it('the default argument is ngModel.$modelValue if no other was passed', () => { ngModel.$modelValue = 'nope'; expect(ctrlInstance.render()).to.eq('nope'); }); it('casts the given argument to a string', () => { expect(ctrlInstance.render(123)).to.eq('123'); }); it('converts a syntax encoded string to HTML', () => { let testCases = [ { $modelValue: '@[foo bar:1] @[k v:2]', expected: '<span>foo bar</span> <span>k v</span>' }, { $modelValue: '@[foo bar:1] @[foo bar:1]', expected: '<span>foo bar</span> <span>foo bar</span>' }, { $modelValue: '@[foo bar:1] @[k v:2] @[foo bar:1]', expected: '<span>foo bar</span> <span>k v</span> <span>foo bar</span>' }, { $modelValue: '@[foo bar:1] @[k v:2] @[foo bar:1] @[k v:2]', expected: '<span>foo bar</span> <span>k v</span> <span>foo bar</span> <span>k v</span>' } ]; testCases.forEach(testCase => { ngModel.$modelValue = testCase.$modelValue; expect(ctrlInstance.render()).to.eq(testCase.expected); }); }); it('does not convert non-mentions', () => { ngModel.$modelValue = '@[wat nope:123]'; expect(ctrlInstance.render()).to.not.eq('<span>wat nope</span>'); }); it('checks the render html element', () => { expect(ctrlInstance.renderElement()).to.have.property('html'); }); it('replaces the html of $element.next with the converted value', () => { ngModel.$modelValue = '@[foo bar:1] @[k v:2] @[foo bar:1]'; ctrlInstance.render(); expect(ctrlInstance.renderElement().html()).to.eq('<span>foo bar</span> <span>k v</span> <span>foo bar</span>') }); }); context('.highlight()', () => { let choice; beforeEach(() => { choice = { first: 'x', last: 'y' }; }); it('returns an HTML formatted version of the given argument', () => { expect(ctrlInstance.highlight(choice)).to.eq('<span>x y</span>'); }); }); context.skip('.decode()', () => { /** Untested - NOT CURRENTLY USED **/ }); context('.label()', () => { it('converts the given object to a readable string', () => { expect(ctrlInstance.label({ first: 0, last: 1 })).to.eq('0 1'); }); }); context('.encode()', () => { it('encodes the given object to a syntax encoded string', () => { let choice = { first: 'x', last: 'y', id: 123 }; expect(ctrlInstance.encode(choice)).to.eq('@[x y:123]'); }); }); context.skip('.replace()', () => { /** Untested - marked with @TODO's **/ }); context('.select()', () => { let ngModel, mentions; beforeEach(() => { ngModel = { $parsers: [], $formatters: [], $setViewValue: sinon.stub() }; ctrlInstance.init(ngModel); ctrlInstance.searching = ['']; ngModel.$viewValue = 'foo'; }); it('adds a mention to the current mentions', () => { expect(ctrlInstance.mentions.length).to.eq(0); ctrlInstance.select({ id: 1, first: 'foo', last: 'bar' }); expect(ctrlInstance.mentions[0]).to.eql({ id: 1, first: 'foo', last: 'bar' }); ctrlInstance.select({ id: 1, first: 'foo', last: 'bar' }); expect(ctrlInstance.mentions.length).to.eq(1); ctrlInstance.select({ id: 2, first: 'k', last: 'v' }); expect(ctrlInstance.mentions.length).to.eq(2); expect(ctrlInstance.mentions[1]).to.eql({ id: 2, first: 'k', last: 'v' }); }); it('clears the controller choices', () => { ctrlInstance.select({ first: 'foo', last: 'bar' }); expect(ctrlInstance.choices).to.eql([]); }); it('sets the searching regex to null', () => { ctrlInstance.select({ first: 'foo', last: 'bar' }); expect(ctrlInstance.searching).to.eq(null); }); it('returns nothing', () => { expect(ctrlInstance.select({ first: 'foo', last: 'bar' })).to.eq(undefined); }); }); context('.up()', () => { it('moves the activeChoice up in the choices collection', () => { let choices = [{ id: 1 }, { id: 2 }, { id: 3 }]; ctrlInstance.choices = choices; ctrlInstance.activeChoice = choices[1]; ctrlInstance.up(); expect(ctrlInstance.activeChoice).to.eq(choices[0]); ctrlInstance.up(); expect(ctrlInstance.activeChoice).to.eq(choices[2]); ctrlInstance.up(); expect(ctrlInstance.activeChoice).to.eq(choices[1]); }); }); context('.down()', () => { it('moves the activeChoice down in the choices collection', () => { let choices = [{ id: 1 }, { id: 2 }, { id: 3 }]; ctrlInstance.choices = choices; ctrlInstance.activeChoice = choices[1]; ctrlInstance.down(); expect(ctrlInstance.activeChoice).to.eq(choices[2]); ctrlInstance.down(); expect(ctrlInstance.activeChoice).to.eq(choices[0]); ctrlInstance.down(); expect(ctrlInstance.activeChoice).to.eq(choices[1]); }); }); context('.search()', () => { it('sets the controller searching property to the passed argument', () => { ctrlInstance.search('foo'); expect(ctrlInstance.searching).to.eq('foo'); }); it('returns a promise', () => { expect(ctrlInstance.search('')).to.have.property('$$state'); }); it('resolves with the possible choices', () => { function fn () { return ctrlInstance.search(''); } fn().then(function (res) { expect(res).to.be.an('array'); }); $timeout.flush(); }); }); context('.findChoices', () => { it('returns an array', () => { expect(ctrlInstance.findChoices()).to.be.an('array'); }); }); context('.cancel()', () => { it('clears the controller choices', () => { ctrlInstance.choices = [{}, {}]; ctrlInstance.cancel(); expect(ctrlInstance.choices).to.eql([]); }); it('sets the searching regex to null', () => { ctrlInstance.searching = /x/.exec('y'); ctrlInstance.cancel(); expect(ctrlInstance.searching).to.eq(null); }); }); context('.autogrow()', () => { it('sets the $element height to 0', () => { ctrlInstance.autogrow(); expect($element[0].style.height).to.eq('0px'); }); it('sets the $element height to scrollHeight if box-sizing is borderBox', () => { $element[0].style.boxSizing = 'border-box'; ctrlInstance.autogrow(); expect($element[0].style.height).to.eq($element[0].scrollHeight + 'px'); }); }); }); context('DOM listeners', () => { let ctrlInstance, $element; beforeEach(function () { $scope.model = 'bar'; $element = angular.element('<span ui-mention ng-model="model"></span>'); ctrlInstance = createController($element); $scope.$digest(); }); ['keyup', 'click', 'focus'].forEach((ev) => { context('on ' + ev, () => { it('sets moved to false if moved is truthy', () => { ctrlInstance.moved = true; trigger($element, ev); expect(ctrlInstance.moved).to.eq(false); }); it('does nothing if the selectionStart does not match selectionEnd', () => { let spy = sinon.spy($scope, '$apply'); $element[0].selectionStart = 0; $element[0].selectionEnd = 1; trigger($element, ev); expect(spy).to.not.have.been.calledOnce; }); it('searches if there is a match', () => { let spy = sinon.spy(ctrlInstance, 'search'); ctrlInstance.searchPattern = /foo/; $element.val('@foo'); $element[0].selectionStart = $element[0].selectionEnd = 4; trigger($element, ev); expect(spy).to.have.been.calledOnce; }); it('cancels if there is no match', () => { let spy = sinon.spy(ctrlInstance, 'cancel'); ctrlInstance.searchPattern = /foo/; $element.val('@bar'); $element[0].selectionStart = $element[0].selectionEnd = 4; trigger($element, ev); expect(spy).to.have.been.calledOnce; }); it('triggers scope.$apply regardless', () => { let spy = sinon.spy($scope, '$apply'); $element[0].selectionStart = $element[0].selectionEnd = 0; $element.val(''); trigger($element, ev); expect(spy).to.have.been.calledOnce; }); }); }); /** * TODO: Get ev.keyCode working. * QT5 ain't cool with KeyBoardEvent constructors. */ context.skip('on keydown', () => { let ev = 'keydown'; it('does nothing if not searching', () => { let spy = sinon.spy($scope, '$apply'); trigger($element, ev); expect(spy).to.not.have.been.calledOnce; }); it('selects if keycode 13 (return)', () => { let spy = sinon.spy(ctrlInstance, 'select'); ctrlInstance.searching = true; trigger($element, ev, 13); expect(spy).to.have.been.calledOnce; }); it('goes up if keycode 38 (up)', () => { let spy = sinon.spy(ctrlInstance, 'up'); ctrlInstance.searching = true; trigger($element, ev, 38); expect(spy).to.have.been.calledOnce; }); it('goes down if keycode 40 (down)', () => { let spy = sinon.spy(ctrlInstance, 'down'); ctrlInstance.searching = true; trigger($element, ev, 40); expect(spy).to.have.been.calledOnce; }); context('if keycode is either 13, 38 or 40', () => { it('sets moved to true ', () => { ctrlInstance.searching = true; trigger($element, ev, 13); expect(ctrlInstance.moved).to.eq(true); }); it('cancels the default of event', () => { ctrlInstance.searching = true; let evt = trigger($element, ev, 13); let spy = sinon.spy(evt, 'preventDefault'); expect(spy).to.have.been.calledOnce; }); it('triggers scope.$apply', () => { ctrlInstance.searching = true; trigger($element, ev, 13); let spy = sinon.spy($scope, '$apply'); expect(spy).to.have.been.calledOnce; }); }); }); function trigger (el, ev, code) { let evt; evt = $document[0].createEvent('KeyboardEvent'); evt.initKeyboardEvent(ev, true, true); evt.keyCode = code; el[0].dispatchEvent(evt); return evt; } }); });