diff --git a/website/static/js/addProjectPlugin.js b/website/static/js/addProjectPlugin.js index 3a125ab338c..ba4e5c74c54 100644 --- a/website/static/js/addProjectPlugin.js +++ b/website/static/js/addProjectPlugin.js @@ -17,6 +17,7 @@ var sprintf = require('agh.sprintf').sprintf; var AddProject = { controller : function (options) { var self = this; + self.isComposing = false; self.defaults = { buttonTemplate : m('.btn.btn-primary[data-toggle="modal"][data-target="#addProjectModal"]', _('Create new project')), parentID : null, @@ -192,13 +193,24 @@ var AddProject = { m('.form-group.m-v-sm', [ m('label[for="projectName].f-w-lg.text-bigger', _('Title')), m('input[type="text"].form-control.project-name', { - onkeyup: function(ev){ + oncompositionstart: function () { + ctrl.isComposing = true; + }, + oncompositionend: function () { + ctrl.isComposing = false; + }, + oninput: function(ev) { var val = ev.target.value; ctrl.isValid(val.trim().length > 0); - if (ev.which === 13) { + ctrl.newProjectName(val); + }, + onkeydown: function(ev){ + var isComposing = ev.isComposing || ctrl.isComposing || ev.keyCode === 229; + if (ev.key === 'Enter' && !isComposing) { + ev.preventDefault(); + ev.stopPropagation(); ctrl.add(); } - ctrl.newProjectName(val); }, onchange: function(ev) { // This will not be reliably running! diff --git a/website/static/js/fangorn.js b/website/static/js/fangorn.js index 37f5a8c65ca..18f5d48d33d 100644 --- a/website/static/js/fangorn.js +++ b/website/static/js/fangorn.js @@ -2374,13 +2374,17 @@ var FGInput = { var id = args.id || ''; var helpTextId = args.helpTextId || ''; var oninput = args.oninput || noop; - var onkeypress = args.onkeypress || noop; + var onkeydown = args.onkeydown || noop; + var oncompositionstart = args.oncompositionstart || noop; + var oncompositionend = args.oncompositionend || noop; return m('span', [ m('input', { 'id' : id, className: 'pull-right form-control' + extraCSS, oninput: oninput, - onkeypress: onkeypress, + onkeydown: onkeydown, + oncompositionstart: oncompositionstart, + oncompositionend: oncompositionend, 'value': args.value || '', 'data-toggle': tooltipText ? 'tooltip' : '', 'title': tooltipText, @@ -2586,6 +2590,8 @@ var dismissToolbar = function(helpText){ var FGToolbar = { controller : function(args) { var self = this; + self.isComposingAddFolder = false; + self.isComposingRenameFolder = false; self.tb = args.treebeard; self.tb.toolbarMode = m.prop(toolbarModes.DEFAULT); self.items = args.treebeard.multiselected; @@ -2638,9 +2644,18 @@ var FGToolbar = { templates[toolbarModes.ADDFOLDER] = [ m('.col-xs-9', [ m.component(FGInput, { + oncompositionstart: function () { + ctrl.isComposingAddFolder = true; + }, + oncompositionend: function () { + ctrl.isComposingAddFolder = false; + }, oninput: m.withAttr('value', ctrl.nameData), - onkeypress: function (event) { - if (ctrl.tb.pressedKey === ENTER_KEY) { + onkeydown: function (event) { + const isComposing = event.isComposing || ctrl.isComposingAddFolder || event.keyCode === 229; + if (event.key === 'Enter' && !isComposing) { + event.preventDefault(); + event.stopPropagation(); ctrl.createFolder.call(ctrl.tb, event, ctrl.dismissToolbar); } }, @@ -2666,9 +2681,18 @@ var FGToolbar = { templates[toolbarModes.RENAME] = [ m('.col-xs-9', m.component(FGInput, { + oncompositionstart: function () { + ctrl.isComposingRenameFolder = true; + }, + oncompositionend: function () { + ctrl.isComposingRenameFolder = false; + }, oninput: m.withAttr('value', ctrl.renameData), - onkeypress: function (event) { - if (ctrl.tb.pressedKey === ENTER_KEY) { + onkeydown: function (event) { + var isComposing = event.isComposing || ctrl.isComposingRenameFolder || event.keyCode === 229; + if (event.key === 'Enter' && !isComposing) { + event.preventDefault(); + event.stopPropagation(); _renameEvent.call(ctrl.tb); } }, diff --git a/website/static/js/myProjects.js b/website/static/js/myProjects.js index 10db4bab6d7..d01a66dcc85 100644 --- a/website/static/js/myProjects.js +++ b/website/static/js/myProjects.js @@ -1205,6 +1205,8 @@ var MyProjects = { var Collections = { controller : function(args){ var self = this; + self.isComposingAdd = false; + self.isComposingRename = false; self.collections = args.collections; self.pageSize = args.collectionsPageSize; self.newCollectionName = m.prop(''); @@ -1539,15 +1541,26 @@ var Collections = { m('.form-group', [ m('label[for="addCollInput].f-w-lg.text-bigger', _('Collection name')), m('input[type="text"].form-control#addCollInput', { - onkeyup: function (ev){ - var val = $(this).val(); + oncompositionstart: function () { + ctrl.isComposingAdd = true; + }, + oncompositionend: function () { + ctrl.isComposingAdd = false; + }, + oninput: function(ev) { + var val = ev.target.value; ctrl.validateName(val); - if(ctrl.isValid()){ - if(ev.which === 13){ + ctrl.newCollectionName(val); + }, + onkeydown: function (ev){ + var isComposing = ev.isComposing || ctrl.isComposingAdd || ev.keyCode === 229; + if (ev.key === 'Enter' && !isComposing) { + ev.preventDefault(); + ev.stopPropagation(); + if (ctrl.isValid()) { ctrl.addCollection(); } } - ctrl.newCollectionName(val); }, onchange: function() { $osf.trackClick('myProjects', 'add-collection', 'type-collection-name'); @@ -1579,28 +1592,40 @@ var Collections = { $osf.trackClick('myProjects', 'edit-collection', 'click-close-rename-modal'); }}, [ m('span[aria-hidden="true"]','×') - ]), - m('h3.modal-title', _('Rename collection')) + ]), + m('h3.modal-title', _('Rename collection')) ]), body: m('.modal-body', [ m('.form-inline', [ m('.form-group', [ m('label[for="addCollInput]', _('Rename to: ')), m('input[type="text"].form-control.m-l-sm',{ - onkeyup: function(ev){ - var val = $(this).val(); + oncompositionstart: function () { + ctrl.isComposingRename = true; + }, + oncompositionend: function () { + ctrl.isComposingRename = false; + }, + oninput: function(ev) { + var val = ev.target.value; ctrl.validateName(val); - if(ctrl.isValid()) { - if (ev.which === 13) { // if enter is pressed + ctrl.collectionMenuObject().item.renamedLabel = val; + }, + onkeydown: function(ev){ + var isComposing = ev.isComposing || ctrl.isComposingRename || ev.keyCode === 229; + if (ev.key === 'Enter' && !isComposing) { + ev.preventDefault(); + ev.stopPropagation(); + if (ctrl.isValid()) { ctrl.renameCollection(); } } - ctrl.collectionMenuObject().item.renamedLabel = val; }, onchange: function() { $osf.trackClick('myProjects', 'edit-collection', 'type-rename-collection'); }, - value: ctrl.collectionMenuObject().item.renamedLabel}), + value: ctrl.collectionMenuObject().item.renamedLabel + }), m('span.help-block', ctrl.validationError()) ]) diff --git a/website/static/js/tests/MyProjects.test.js b/website/static/js/tests/MyProjects.test.js index 95c72950acc..cba0a69c50f 100644 --- a/website/static/js/tests/MyProjects.test.js +++ b/website/static/js/tests/MyProjects.test.js @@ -4,7 +4,7 @@ /*global describe, it, expect, example, before, after, beforeEach, afterEach, mocha, sinon*/ 'use strict'; var assert = require('chai').assert; - +var sinon = require('sinon'); var fb = require('js/myProjects.js'); var LinkObject = fb.LinkObject; @@ -36,4 +36,162 @@ describe('fileBrowser', function() { }); }); }); + + describe('Collections IME Keydown Handling', function() { + function makeMockCtrl(overrides) { + return Object.assign({ + isComposingAdd: false, + isComposingRename: false, + isValid: sinon.stub().returns(true), + validateName: sinon.stub(), + newCollectionName: sinon.stub(), + addCollection: sinon.stub(), + renameCollection: sinon.stub(), + collectionMenuObject: sinon.stub().returns({ item: { renamedLabel: '' } }), + }, overrides); + } + + function makeAddCollKeydownHandler(ctrl) { + return function(ev) { + var isComposing = ev.isComposing || ctrl.isComposingAdd || ev.keyCode === 229; + if (ev.key === 'Enter' && !isComposing) { + ev.preventDefault(); + ev.stopPropagation(); + if (ctrl.isValid()) { + ctrl.addCollection(); + } + } + }; + } + + function makeRenameCollKeydownHandler(ctrl) { + return function(ev) { + var isComposing = ev.isComposing || ctrl.isComposingRename || ev.keyCode === 229; + if (ev.key === 'Enter' && !isComposing) { + ev.preventDefault(); + ev.stopPropagation(); + if (ctrl.isValid()) { + ctrl.renameCollection(); + } + } + }; + } + + function makeEvent(overrides) { + return Object.assign({ + key: 'Enter', + isComposing: false, + keyCode: 13, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy(), + }, overrides); + } + + var ctrl; + beforeEach(function() { + ctrl = makeMockCtrl(); + }); + + describe('addCollection keydown', function() { + it('should call addCollection() on Enter when valid and not composing', function() { + var handler = makeAddCollKeydownHandler(ctrl); + handler(makeEvent()); + assert.ok(ctrl.addCollection.calledOnce, 'addCollection() should be called'); + }); + + it('should NOT call addCollection() during IME (event.isComposing=true)', function() { + var handler = makeAddCollKeydownHandler(ctrl); + handler(makeEvent({ isComposing: true })); + assert.ok(ctrl.addCollection.notCalled); + }); + + it('should NOT call addCollection() during Chrome IME race (ctrl.isComposing=true)', function() { + ctrl.isComposingAdd = true; + var handler = makeAddCollKeydownHandler(ctrl); + handler(makeEvent({ isComposing: false })); + assert.ok(ctrl.addCollection.notCalled, + 'ctrl.isComposing=true should block addCollection() even if event.isComposing=false'); + }); + + it('should NOT call addCollection() when keyCode is 229 (legacy IME)', function() { + var handler = makeAddCollKeydownHandler(ctrl); + handler(makeEvent({ isComposing: false, keyCode: 229 })); + assert.ok(ctrl.addCollection.notCalled); + }); + + it('should call preventDefault() on Enter even when form is invalid', function() { + ctrl.isValid.returns(false); + var handler = makeAddCollKeydownHandler(ctrl); + var ev = makeEvent(); + handler(ev); + assert.ok(ev.preventDefault.calledOnce, + '[BUG] preventDefault should be called on Enter regardless of validity'); + assert.ok(ctrl.addCollection.notCalled, 'addCollection should not be called when invalid'); + }); + + it('should NOT call addCollection() on non-Enter key', function() { + var handler = makeAddCollKeydownHandler(ctrl); + handler(makeEvent({ key: 'Escape', keyCode: 27 })); + assert.ok(ctrl.addCollection.notCalled); + }); + }); + + describe('renameCollection keydown', function() { + it('should call renameCollection() on Enter when valid and not composing', function() { + var handler = makeRenameCollKeydownHandler(ctrl); + handler(makeEvent()); + assert.ok(ctrl.renameCollection.calledOnce, 'renameCollection() should be called'); + }); + + it('should NOT call renameCollection() during IME (event.isComposing=true)', function() { + var handler = makeRenameCollKeydownHandler(ctrl); + handler(makeEvent({ isComposing: true })); + assert.ok(ctrl.renameCollection.notCalled); + }); + + it('should NOT call renameCollection() when ctrl.isComposing=true (Chrome race)', function() { + ctrl.isComposingRename = true; + var handler = makeRenameCollKeydownHandler(ctrl); + handler(makeEvent({ isComposing: false })); + assert.ok(ctrl.renameCollection.notCalled); + }); + + it('should NOT call renameCollection() when keyCode is 229', function() { + var handler = makeRenameCollKeydownHandler(ctrl); + handler(makeEvent({ keyCode: 229, isComposing: false })); + assert.ok(ctrl.renameCollection.notCalled); + }); + }); + + describe('ctrl.isComposing flag lifecycle', function() { + it('should be set true on compositionstart', function() { + ctrl.isComposing = false; + var onCompositionStart = function() { ctrl.isComposing = true; }; + onCompositionStart(); + assert.strictEqual(ctrl.isComposing, true); + }); + + it('should be set false on compositionend', function() { + ctrl.isComposing = true; + var onCompositionEnd = function() { ctrl.isComposing = false; }; + onCompositionEnd(); + assert.strictEqual(ctrl.isComposing, false); + }); + }); + + describe('oninput handler', function() { + it('should call validateName and newCollectionName with input value', function() { + var val = 'Test Collection'; + var ev = { target: { value: val } }; + var onInput = function(ev) { + var v = ev.target.value; + ctrl.validateName(v); + ctrl.newCollectionName(v); + }; + onInput(ev); + assert.ok(ctrl.validateName.calledWith(val)); + assert.ok(ctrl.newCollectionName.calledWith(val)); + }); + }); + }); }); \ No newline at end of file diff --git a/website/static/js/tests/addProjectPlugin.test.js b/website/static/js/tests/addProjectPlugin.test.js index d4b6c9c79c8..0f2ab7de026 100644 --- a/website/static/js/tests/addProjectPlugin.test.js +++ b/website/static/js/tests/addProjectPlugin.test.js @@ -1,6 +1,7 @@ /*global describe, it, expect, example, before, after, beforeEach, afterEach, mocha, sinon*/ 'use strict'; var assert = require('chai').assert; +var sinon = require('sinon'); var Raven = require('raven-js'); var $ = require('jquery'); var m = require('mithril'); @@ -35,4 +36,147 @@ describe('AddProjectPlugin', () => { assert.equal(project.newProjectCategory(), 'project'); assert.equal(project.project.newProjectInheritContribs(), false); }); + + describe('IME Keydown Handling (onkeydown)', () => { + var ctrl; + function makeMockCtrl(overrides) { + return Object.assign({ + isComposing: false, + isValid: sinon.stub().returns(true), + newProjectName: sinon.stub(), + add: sinon.stub(), + }, overrides); + } + function makeKeydownHandler(ctrl) { + return function(ev) { + var isComposing = ev.isComposing || ctrl.isComposing || ev.keyCode === 229; + if (ev.key === 'Enter' && !isComposing) { + ev.preventDefault(); + ev.stopPropagation(); + ctrl.add(); + } + }; + } + function makeEvent(overrides) { + return Object.assign({ + key: 'Enter', + isComposing: false, + keyCode: 13, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy(), + }, overrides); + } + + beforeEach(function() { + ctrl = makeMockCtrl(); + }); + + it('should call ctrl.add() when Enter is pressed without IME', () => { + var handler = makeKeydownHandler(ctrl); + var ev = makeEvent({ key: 'Enter', isComposing: false, keyCode: 13 }); + + handler(ev); + + assert.ok(ctrl.add.calledOnce, 'ctrl.add() should be called once'); + assert.ok(ev.preventDefault.calledOnce, 'preventDefault should be called'); + assert.ok(ev.stopPropagation.calledOnce, 'stopPropagation should be called'); + }); + + it('should NOT call ctrl.add() when a non-Enter key is pressed', () => { + var handler = makeKeydownHandler(ctrl); + var ev = makeEvent({ key: 'a', keyCode: 65 }); + + handler(ev); + + assert.ok(ctrl.add.notCalled, 'ctrl.add() should not be called for non-Enter key'); + assert.ok(ev.preventDefault.notCalled, 'preventDefault should not be called'); + }); + + it('should NOT call ctrl.add() when event.isComposing is true (standard IME)', () => { + var handler = makeKeydownHandler(ctrl); + var ev = makeEvent({ key: 'Enter', isComposing: true, keyCode: 13 }); + + handler(ev); + + assert.ok(ctrl.add.notCalled, 'ctrl.add() should not be called during IME composition'); + assert.ok(ev.preventDefault.notCalled, 'preventDefault should not be called during IME'); + }); + + it('should NOT call ctrl.add() when ctrl.isComposing is true (Chrome race condition)', () => { + ctrl.isComposing = true; + var handler = makeKeydownHandler(ctrl); + var ev = makeEvent({ key: 'Enter', isComposing: false, keyCode: 13 }); + + handler(ev); + + assert.ok(ctrl.add.notCalled, 'ctrl.add() should not be called when ctrl.isComposing is true'); + }); + + it('should NOT call ctrl.add() when keyCode is 229 (legacy IME indicator)', () => { + var handler = makeKeydownHandler(ctrl); + var ev = makeEvent({ key: 'Enter', isComposing: false, keyCode: 229 }); + + handler(ev); + + assert.ok(ctrl.add.notCalled, 'ctrl.add() should not be called when keyCode is 229'); + }); + + it('should set ctrl.isComposing to true on compositionstart', () => { + ctrl.isComposing = false; + var onCompositionStart = function() { ctrl.isComposing = true; }; + onCompositionStart(); + assert.strictEqual(ctrl.isComposing, true); + }); + + it('should set ctrl.isComposing to false on compositionend (synchronous)', () => { + ctrl.isComposing = true; + var onCompositionEnd = function() { ctrl.isComposing = false; }; + onCompositionEnd(); + assert.strictEqual(ctrl.isComposing, false); + }); + + it('[RECOMMENDED] ctrl.isComposing should still be true when keydown fires ' + + 'if compositionend uses setTimeout defer', function(done) { + ctrl.isComposing = true; + + var onCompositionEnd = function() { + setTimeout(function() { ctrl.isComposing = false; }, 0); + }; + onCompositionEnd(); + + assert.strictEqual(ctrl.isComposing, true, + 'isComposing should still be true synchronously after compositionend with setTimeout'); + + setTimeout(function() { + assert.strictEqual(ctrl.isComposing, false, + 'isComposing should be false after setTimeout resolves'); + done(); + }, 10); + }); + + it('should update newProjectName and isValid via oninput', () => { + var ev = { target: { value: 'My Project' } }; + var onInput = function(ev) { + var val = ev.target.value; + ctrl.isValid(val.trim().length > 0); + ctrl.newProjectName(val); + }; + + onInput(ev); + + assert.ok(ctrl.newProjectName.calledWith('My Project')); + assert.ok(ctrl.isValid.calledWith(true)); + }); + + it('should mark isValid false via oninput when input is whitespace only', () => { + var ev = { target: { value: ' ' } }; + var onInput = function(ev) { + var val = ev.target.value; + ctrl.isValid(val.trim().length > 0); + ctrl.newProjectName(val); + }; + onInput(ev); + assert.ok(ctrl.isValid.calledWith(false)); + }); + }); }); diff --git a/website/static/js/tests/fangorn.test.js b/website/static/js/tests/fangorn.test.js index 43afe7bc7b0..10a18c56f1e 100644 --- a/website/static/js/tests/fangorn.test.js +++ b/website/static/js/tests/fangorn.test.js @@ -6,6 +6,7 @@ var $osf = require('js/osfHelpers'); var Fangorn = require('js/fangorn'); var assert = require('chai').assert; +var sinon = require('sinon'); var utils = require('tests/utils'); var faker = require('faker'); var $ = require('jquery'); @@ -423,4 +424,166 @@ describe('fangorn', () => { }); }); }); + + describe('FGToolbar IME Keydown Handling', function() { + function makeMockCtrl(overrides) { + var tb = { + pressedKey: null, + }; + return Object.assign({ + isComposingAddFolder: false, + isComposingRenameFolder: false, + tb: tb, + nameData: sinon.stub(), + renameData: sinon.stub(), + createFolder: sinon.stub(), + dismissToolbar: sinon.stub(), + _renameEvent: sinon.stub(), + }, overrides); + } + + function makeAddFolderKeydownHandler(ctrl) { + return function(event) { + var isComposing = event.isComposing || ctrl.isComposingAddFolder || event.keyCode === 229; + if (event.key === 'Enter' && !isComposing) { + event.preventDefault(); + event.stopPropagation(); + ctrl.createFolder.call(ctrl.tb, event, ctrl.dismissToolbar); + } + }; + } + + function makeRenameKeydownHandler(ctrl, renameEvent) { + return function(event) { + var isComposing = event.isComposing || ctrl.isComposingRenameFolder || event.keyCode === 229; + if (event.key === 'Enter' && !isComposing) { + event.preventDefault(); + event.stopPropagation(); + renameEvent.call(ctrl.tb); + } + }; + } + + function makeEvent(overrides) { + return Object.assign({ + key: 'Enter', + isComposing: false, + keyCode: 13, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy(), + }, overrides); + } + + var ctrl; + beforeEach(function() { + ctrl = makeMockCtrl(); + }); + + describe('ADDFOLDER toolbar', function() { + it('should call createFolder on Enter when not composing', function() { + var handler = makeAddFolderKeydownHandler(ctrl); + var ev = makeEvent(); + handler(ev); + assert.ok(ctrl.createFolder.calledOnce, 'createFolder should be called'); + assert.ok(ev.preventDefault.calledOnce); + assert.ok(ev.stopPropagation.calledOnce); + }); + + it('should NOT call createFolder during IME (event.isComposing=true)', function() { + var handler = makeAddFolderKeydownHandler(ctrl); + handler(makeEvent({ isComposing: true })); + assert.ok(ctrl.createFolder.notCalled, + 'createFolder should not be called during IME composition'); + }); + + it('should NOT call createFolder when ctrl.isComposing=true (Chrome race condition)', function() { + ctrl.isComposingAddFolder = true; + var handler = makeAddFolderKeydownHandler(ctrl); + handler(makeEvent({ isComposing: false })); // Chrome: event.isComposing = false + assert.ok(ctrl.createFolder.notCalled, + 'createFolder should be blocked by ctrl.isComposing=true'); + }); + + it('should NOT call createFolder when keyCode is 229 (legacy IE)', function() { + var handler = makeAddFolderKeydownHandler(ctrl); + handler(makeEvent({ isComposing: false, keyCode: 229 })); + assert.ok(ctrl.createFolder.notCalled); + }); + + it('should NOT call createFolder on non-Enter key', function() { + var handler = makeAddFolderKeydownHandler(ctrl); + handler(makeEvent({ key: 'Tab', keyCode: 9 })); + assert.ok(ctrl.createFolder.notCalled); + }); + + it('should call createFolder with correct this context (ctrl.tb)', function() { + var handler = makeAddFolderKeydownHandler(ctrl); + var ev = makeEvent(); + handler(ev); + // createFolder.call(ctrl.tb, ...) → this = ctrl.tb + assert.ok(ctrl.createFolder.calledOn(ctrl.tb), + 'createFolder should be called with ctrl.tb as context'); + }); + }); + + describe('RENAME toolbar', function() { + var renameEventStub; + beforeEach(function() { + renameEventStub = sinon.stub(); + }); + + it('should call _renameEvent on Enter when not composing', function() { + var handler = makeRenameKeydownHandler(ctrl, renameEventStub); + var ev = makeEvent(); + handler(ev); + assert.ok(renameEventStub.calledOnce, '_renameEvent should be called'); + assert.ok(ev.preventDefault.calledOnce); + }); + + it('should NOT call _renameEvent during IME (event.isComposing=true)', function() { + var handler = makeRenameKeydownHandler(ctrl, renameEventStub); + handler(makeEvent({ isComposing: true })); + assert.ok(renameEventStub.notCalled); + }); + + it('should NOT call _renameEvent when ctrl.isComposing=true (Chrome race)', function() { + ctrl.isComposingRenameFolder = true; + var handler = makeRenameKeydownHandler(ctrl, renameEventStub); + handler(makeEvent({ isComposing: false })); + assert.ok(renameEventStub.notCalled); + }); + + it('should NOT call _renameEvent when keyCode is 229', function() { + var handler = makeRenameKeydownHandler(ctrl, renameEventStub); + handler(makeEvent({ keyCode: 229 })); + assert.ok(renameEventStub.notCalled); + }); + }); + + describe('FGToolbar isComposing flag', function() { + it('should start false (initialized)', function() { + assert.strictEqual(ctrl.isComposingAddFolder, false, + 'isComposingAddFolder should be initialized to false'); + + assert.strictEqual(ctrl.isComposingRenameFolder, false, + 'isComposingRenameFolder should be initialized to false'); + }); + + it('[RECOMMENDED] compositionend with setTimeout defer keeps flag true during keydown', function(done) { + ctrl.isComposing = true; + setTimeout(function() { ctrl.isComposing = false; }, 0); + + assert.strictEqual(ctrl.isComposing, true, + 'isComposing should still be true synchronously'); + + setTimeout(function() { + assert.strictEqual(ctrl.isComposing, false, + 'isComposing should be false after defer'); + done(); + }, 10); + }); + }); + }); }); + +