| suite('saneKeyboardEvents', function() { | |
| var el; | |
| var Event = jQuery.Event | |
| function supportsSelectionAPI() { | |
| return 'selectionStart' in el[0]; | |
| } | |
| setup(function() { | |
| el = $('<textarea>').appendTo('#mock'); | |
| }); | |
| teardown(function() { | |
| el.remove(); | |
| }); | |
| test('normal keys', function(done) { | |
| var counter = 0; | |
| saneKeyboardEvents(el, { | |
| keystroke: noop, | |
| typedText: function(text, keydown, keypress) { | |
| counter += 1; | |
| assert.ok(counter <= 1, 'callback is only called once'); | |
| assert.equal(text, 'a', 'text comes back as a'); | |
| assert.equal(el.val(), '', 'the textarea remains empty'); | |
| done(); | |
| } | |
| }); | |
| el.trigger(Event('keydown', { which: 97 })); | |
| el.trigger(Event('keypress', { which: 97 })); | |
| el.val('a'); | |
| }); | |
| test('one keydown only', function(done) { | |
| var counter = 0; | |
| saneKeyboardEvents(el, { | |
| keystroke: function(key, evt) { | |
| counter += 1; | |
| assert.ok(counter <= 1, 'callback is called only once'); | |
| assert.equal(key, 'Backspace', 'key is correctly set'); | |
| done(); | |
| } | |
| }); | |
| el.trigger(Event('keydown', { which: 8 })); | |
| }); | |
| test('a series of keydowns only', function(done) { | |
| var counter = 0; | |
| saneKeyboardEvents(el, { | |
| keystroke: function(key, keydown) { | |
| counter += 1; | |
| assert.ok(counter <= 3, 'callback is called at most 3 times'); | |
| assert.ok(keydown); | |
| assert.equal(key, 'Left'); | |
| if (counter === 3) done(); | |
| } | |
| }); | |
| el.trigger(Event('keydown', { which: 37 })); | |
| el.trigger(Event('keydown', { which: 37 })); | |
| el.trigger(Event('keydown', { which: 37 })); | |
| }); | |
| test('one keydown and a series of keypresses', function(done) { | |
| var counter = 0; | |
| saneKeyboardEvents(el, { | |
| keystroke: function(key, keydown) { | |
| counter += 1; | |
| assert.ok(counter <= 3, 'callback is called at most 3 times'); | |
| assert.ok(keydown); | |
| assert.equal(key, 'Backspace'); | |
| if (counter === 3) done(); | |
| } | |
| }); | |
| el.trigger(Event('keydown', { which: 8 })); | |
| el.trigger(Event('keypress', { which: 8 })); | |
| el.trigger(Event('keypress', { which: 8 })); | |
| el.trigger(Event('keypress', { which: 8 })); | |
| }); | |
| suite('select', function() { | |
| test('select populates the textarea but doesn\'t call .typedText()', function() { | |
| var shim = saneKeyboardEvents(el, { keystroke: noop }); | |
| shim.select('foobar'); | |
| assert.equal(el.val(), 'foobar'); | |
| el.trigger('keydown'); | |
| assert.equal(el.val(), 'foobar', 'value remains after keydown'); | |
| if (supportsSelectionAPI()) { | |
| el.trigger('keypress'); | |
| assert.equal(el.val(), 'foobar', 'value remains after keypress'); | |
| el.trigger('input'); | |
| assert.equal(el.val(), 'foobar', 'value remains after flush after keypress'); | |
| } | |
| }); | |
| test('select populates the textarea but doesn\'t call text' + | |
| ' on keydown, even when the selection is not properly' + | |
| ' detectable', function() { | |
| var shim = saneKeyboardEvents(el, { keystroke: noop }); | |
| shim.select('foobar'); | |
| // monkey-patch the dom-level selection so that hasSelection() | |
| // returns false, as in IE < 9. | |
| el[0].selectionStart = el[0].selectionEnd = 0; | |
| el.trigger('keydown'); | |
| assert.equal(el.val(), 'foobar', 'value remains after keydown'); | |
| }); | |
| test('blurring', function() { | |
| var shim = saneKeyboardEvents(el, { keystroke: noop }); | |
| shim.select('foobar'); | |
| el.trigger('blur'); | |
| el.focus(); | |
| // IE < 9 doesn't support selection{Start,End} | |
| if (supportsSelectionAPI()) { | |
| assert.equal(el[0].selectionStart, 0, 'it\'s selected from the start'); | |
| assert.equal(el[0].selectionEnd, 6, 'it\'s selected to the end'); | |
| } | |
| assert.equal(el.val(), 'foobar', 'it still has content'); | |
| }); | |
| test('blur then empty selection', function() { | |
| var shim = saneKeyboardEvents(el, { keystroke: noop }); | |
| shim.select('foobar'); | |
| el.blur(); | |
| shim.select(''); | |
| assert.ok(document.activeElement !== el[0], 'textarea remains blurred'); | |
| }); | |
| test('blur in keystroke handler', function(done) { | |
| if (!document.hasFocus()) { | |
| console.warn( | |
| 'The test "blur in keystroke handler" needs the document to have ' + | |
| 'focus. Only when the document has focus does .select() on an ' + | |
| 'element also focus it, which is part of the problematic behavior ' + | |
| 'we are testing robustness against. (Specifically, erroneously ' + | |
| 'calling .select() in a timeout after the textarea has blurred, ' + | |
| '"stealing back" focus.)\n' + | |
| 'Normally, the page being open and focused is enough to have focus, ' + | |
| 'but with the Developer Tools open, it depends on whether you last ' + | |
| 'clicked on something in the Developer Tools or on the page itself. ' + | |
| 'Click the page, or close the Developer Tools, and Refresh.' | |
| ); | |
| el.remove(); // LOL next line skips teardown https://git.io/vaUWq | |
| this.skip(); | |
| } | |
| var shim = saneKeyboardEvents(el, { | |
| keystroke: function(key) { | |
| assert.equal(key, 'Left'); | |
| el[0].blur(); | |
| } | |
| }); | |
| shim.select('foobar'); | |
| assert.ok(document.activeElement === el[0], 'textarea focused'); | |
| el.trigger(Event('keydown', { which: 37 })); | |
| assert.ok(document.activeElement !== el[0], 'textarea blurred'); | |
| setTimeout(function() { | |
| assert.ok(document.activeElement !== el[0], 'textarea remains blurred'); | |
| done(); | |
| }); | |
| }); | |
| suite('selected text after keypress or paste doesn\'t get mistaken' + | |
| ' for inputted text', function() { | |
| test('select() immediately after paste', function() { | |
| var pastedText; | |
| var onPaste = function(text) { pastedText = text; }; | |
| var shim = saneKeyboardEvents(el, { | |
| paste: function(text) { onPaste(text); } | |
| }); | |
| el.trigger('paste').val('$x^2+1$'); | |
| shim.select('$\\frac{x^2+1}{2}$'); | |
| assert.equal(pastedText, '$x^2+1$'); | |
| assert.equal(el.val(), '$\\frac{x^2+1}{2}$'); | |
| onPaste = null; | |
| shim.select('$2$'); | |
| assert.equal(el.val(), '$2$'); | |
| }); | |
| test('select() after paste/input', function() { | |
| var pastedText; | |
| var onPaste = function(text) { pastedText = text; }; | |
| var shim = saneKeyboardEvents(el, { | |
| paste: function(text) { onPaste(text); } | |
| }); | |
| el.trigger('paste').val('$x^2+1$'); | |
| el.trigger('input'); | |
| assert.equal(pastedText, '$x^2+1$'); | |
| assert.equal(el.val(), ''); | |
| onPaste = null; | |
| shim.select('$\\frac{x^2+1}{2}$'); | |
| assert.equal(el.val(), '$\\frac{x^2+1}{2}$'); | |
| shim.select('$2$'); | |
| assert.equal(el.val(), '$2$'); | |
| }); | |
| test('select() immediately after keydown/keypress', function() { | |
| var typedText; | |
| var onText = function(text) { typedText = text; }; | |
| var shim = saneKeyboardEvents(el, { | |
| keystroke: noop, | |
| typedText: function(text) { onText(text); } | |
| }); | |
| el.trigger(Event('keydown', { which: 97 })); | |
| el.trigger(Event('keypress', { which: 97 })); | |
| el.val('a'); | |
| shim.select('$\\frac{a}{2}$'); | |
| assert.equal(typedText, 'a'); | |
| assert.equal(el.val(), '$\\frac{a}{2}$'); | |
| onText = null; | |
| shim.select('$2$'); | |
| assert.equal(el.val(), '$2$'); | |
| }); | |
| test('select() after keydown/keypress/input', function() { | |
| var typedText; | |
| var onText = function(text) { typedText = text; }; | |
| var shim = saneKeyboardEvents(el, { | |
| keystroke: noop, | |
| typedText: function(text) { onText(text); } | |
| }); | |
| el.trigger(Event('keydown', { which: 97 })); | |
| el.trigger(Event('keypress', { which: 97 })); | |
| el.val('a'); | |
| el.trigger('input'); | |
| assert.equal(typedText, 'a'); | |
| onText = null; | |
| shim.select('$\\frac{a}{2}$'); | |
| assert.equal(el.val(), '$\\frac{a}{2}$'); | |
| shim.select('$2$'); | |
| assert.equal(el.val(), '$2$'); | |
| }); | |
| suite('unrecognized keys that move cursor and clear selection', function() { | |
| test('without keypress', function() { | |
| var shim = saneKeyboardEvents(el, { keystroke: noop }); | |
| shim.select('a'); | |
| assert.equal(el.val(), 'a'); | |
| if (!supportsSelectionAPI()) return; | |
| el.trigger(Event('keydown', { which: 37, altKey: true })); | |
| el[0].selectionEnd = 0; | |
| el.trigger(Event('keyup', { which: 37, altKey: true })); | |
| assert.ok(el[0].selectionStart !== el[0].selectionEnd); | |
| el.blur(); | |
| shim.select(''); | |
| assert.ok(document.activeElement !== el[0], 'textarea remains blurred'); | |
| }); | |
| test('with keypress, many characters selected', function() { | |
| var shim = saneKeyboardEvents(el, { keystroke: noop }); | |
| shim.select('many characters'); | |
| assert.equal(el.val(), 'many characters'); | |
| if (!supportsSelectionAPI()) return; | |
| el.trigger(Event('keydown', { which: 37, altKey: true })); | |
| el.trigger(Event('keypress', { which: 37, altKey: true })); | |
| el[0].selectionEnd = 0; | |
| el.trigger('keyup'); | |
| assert.ok(el[0].selectionStart !== el[0].selectionEnd); | |
| el.blur(); | |
| shim.select(''); | |
| assert.ok(document.activeElement !== el[0], 'textarea remains blurred'); | |
| }); | |
| test('with keypress, only 1 character selected', function() { | |
| var count = 0; | |
| var shim = saneKeyboardEvents(el, { | |
| keystroke: noop, | |
| typedText: function(ch) { | |
| assert.equal(ch, 'a'); | |
| assert.equal(el.val(), ''); | |
| count += 1; | |
| } | |
| }); | |
| shim.select('a'); | |
| assert.equal(el.val(), 'a'); | |
| if (!supportsSelectionAPI()) return; | |
| el.trigger(Event('keydown', { which: 37, altKey: true })); | |
| el.trigger(Event('keypress', { which: 37, altKey: true })); | |
| el[0].selectionEnd = 0; | |
| el.trigger('keyup'); | |
| assert.equal(count, 1); | |
| el.blur(); | |
| shim.select(''); | |
| assert.ok(document.activeElement !== el[0], 'textarea remains blurred'); | |
| }); | |
| }); | |
| }); | |
| }); | |
| suite('paste', function() { | |
| test('paste event only', function(done) { | |
| saneKeyboardEvents(el, { | |
| paste: function(text) { | |
| assert.equal(text, '$x^2+1$'); | |
| done(); | |
| } | |
| }); | |
| el.trigger('paste'); | |
| el.val('$x^2+1$'); | |
| }); | |
| test('paste after keydown/keypress', function(done) { | |
| saneKeyboardEvents(el, { | |
| keystroke: noop, | |
| paste: function(text) { | |
| assert.equal(text, 'foobar'); | |
| done(); | |
| } | |
| }); | |
| // Ctrl-V in Firefox or Opera, according to unixpapa.com/js/key.html | |
| // without an `input` event | |
| el.trigger('keydown', { which: 86, ctrlKey: true }); | |
| el.trigger('keypress', { which: 118, ctrlKey: true }); | |
| el.trigger('paste'); | |
| el.val('foobar'); | |
| }); | |
| test('paste after keydown/keypress/input', function(done) { | |
| saneKeyboardEvents(el, { | |
| keystroke: noop, | |
| paste: function(text) { | |
| assert.equal(text, 'foobar'); | |
| done(); | |
| } | |
| }); | |
| // Ctrl-V in Firefox or Opera, according to unixpapa.com/js/key.html | |
| // with an `input` event | |
| el.trigger('keydown', { which: 86, ctrlKey: true }); | |
| el.trigger('keypress', { which: 118, ctrlKey: true }); | |
| el.trigger('paste'); | |
| el.val('foobar'); | |
| el.trigger('input'); | |
| }); | |
| test('keypress timeout happening before paste timeout', function(done) { | |
| saneKeyboardEvents(el, { | |
| keystroke: noop, | |
| paste: function(text) { | |
| assert.equal(text, 'foobar'); | |
| done(); | |
| } | |
| }); | |
| el.trigger('keydown', { which: 86, ctrlKey: true }); | |
| el.trigger('keypress', { which: 118, ctrlKey: true }); | |
| el.trigger('paste'); | |
| el.val('foobar'); | |
| // this synthesizes the keypress timeout calling handleText() | |
| // before the paste timeout happens. | |
| el.trigger('input'); | |
| }); | |
| }); | |
| }); |