From dafdee543484dd95a961f05b2db5255f32f6617b Mon Sep 17 00:00:00 2001 From: Cory Forsyth Date: Tue, 14 Jul 2015 13:17:28 -0400 Subject: [PATCH] Tests for creating/deleting sections * Tests for typing "\r\n" to create section, deleting sections * Refactor selectText to take optional start/end, more deletion tests * add helper `skipInPhantom` * test that when highlighting bold text, the bold button is active * Do not turn stickyToolbar on by default (this breaks Phantom tests) * remove extra `setTimeout` in mouseup listener --- src/js/editor/editor.js | 5 +- src/js/utils/selection-utils.js | 5 +- src/js/views/text-format-toolbar.js | 4 +- tests/acceptance/editor-commands-test.js | 113 +++++++++-------------- tests/acceptance/editor-sections-test.js | 102 ++++++++++++++++++++ tests/helpers/dom.js | 68 +++++++++----- tests/helpers/skip-in-phantom.js | 10 ++ tests/test-helpers.js | 4 +- 8 files changed, 210 insertions(+), 101 deletions(-) create mode 100644 tests/acceptance/editor-sections-test.js create mode 100644 tests/helpers/skip-in-phantom.js diff --git a/src/js/editor/editor.js b/src/js/editor/editor.js index 1f422cbda..83d9e82c5 100644 --- a/src/js/editor/editor.js +++ b/src/js/editor/editor.js @@ -36,7 +36,10 @@ var defaults = { autofocus: true, post: null, serverHost: '', - stickyToolbar: !!('ontouchstart' in window), + // FIXME PhantomJS has 'ontouchstart' in window, + // causing the stickyToolbar to accidentally be auto-activated + // in tests + stickyToolbar: false, // !!('ontouchstart' in window), textFormatCommands: [ new BoldCommand(), new ItalicCommand(), diff --git a/src/js/utils/selection-utils.js b/src/js/utils/selection-utils.js index 490c6daa6..961c05609 100644 --- a/src/js/utils/selection-utils.js +++ b/src/js/utils/selection-utils.js @@ -24,7 +24,10 @@ function getDirectionOfSelection(selection) { function getSelectionElement(selection) { selection = selection || window.getSelection(); - var node = getDirectionOfSelection(selection) === SelectionDirection.LEFT_TO_RIGHT ? selection.anchorNode : selection.focusNode; + // FIXME it used to return `anchorNode` when selection direction is `LEFT_TO_RIGHT`, + // but I think that was a bug. In Safari and Chrome the selection usually had the + // same anchorNode and focusNode when selecting text, so it didn't matter. + var node = getDirectionOfSelection(selection) === SelectionDirection.LEFT_TO_RIGHT ? selection.focusNode : selection.anchorNode; return node && (node.nodeType === 3 ? node.parentNode : node); } diff --git a/src/js/views/text-format-toolbar.js b/src/js/views/text-format-toolbar.js index 10600d171..e63f819a1 100644 --- a/src/js/views/text-format-toolbar.js +++ b/src/js/views/text-format-toolbar.js @@ -30,9 +30,7 @@ function TextFormatToolbar(options) { }); this.addEventListener(document, 'mouseup', () => { - setTimeout(function() { - handleTextSelection(toolbar); - }); + handleTextSelection(toolbar); }); this.addEventListener(document, 'keyup', (e) => { diff --git a/tests/acceptance/editor-commands-test.js b/tests/acceptance/editor-commands-test.js index 14d0235a1..084ce47c0 100644 --- a/tests/acceptance/editor-commands-test.js +++ b/tests/acceptance/editor-commands-test.js @@ -32,94 +32,67 @@ function clickToolbarButton(name, assert) { } test('when text is highlighted, shows toolbar', (assert) => { - let done = assert.async(); - - setTimeout(() => { - assert.hasElement('.ck-toolbar', 'displays toolbar'); - assert.hasElement('.ck-toolbar-btn', 'displays toolbar buttons'); - let boldBtnSelector = '.ck-toolbar-btn[title="bold"]'; - assert.hasElement(boldBtnSelector, 'has bold button'); - - done(); - }, 10); + assert.hasElement('.ck-toolbar', 'displays toolbar'); + assert.hasElement('.ck-toolbar-btn', 'displays toolbar buttons'); + let boldBtnSelector = '.ck-toolbar-btn[title="bold"]'; + assert.hasElement(boldBtnSelector, 'has bold button'); }); test('highlight text, click "bold" button bolds text', (assert) => { - let done = assert.async(); - - setTimeout(() => { - clickToolbarButton('bold', assert); - assert.hasElement('#editor b:contains(IS A)'); - - done(); - }, 10); + clickToolbarButton('bold', assert); + assert.hasElement('#editor b:contains(IS A)'); }); test('highlight text, click "italic" button italicizes text', (assert) => { - let done = assert.async(); + clickToolbarButton('italic', assert); + assert.hasElement('#editor i:contains(IS A)'); +}); - setTimeout(() => { - clickToolbarButton('italic', assert); - assert.hasElement('#editor i:contains(IS A)'); +test('highlight text, click "heading" button turns text into h2 header', (assert) => { + clickToolbarButton('heading', assert); + assert.hasElement('#editor h2:contains(THIS IS A TEST)'); +}); - done(); - }, 10); +test('highlight text, click "subheading" button turns text into h3 header', (assert) => { + clickToolbarButton('subheading', assert); + assert.hasElement('#editor h3:contains(THIS IS A TEST)'); }); -test('highlight text, click "heading" button turns text into h2 header', (assert) => { - let done = assert.async(); +test('highlight text, click "quote" button turns text into blockquote', (assert) => { + clickToolbarButton('quote', assert); + assert.hasElement('#editor blockquote:contains(THIS IS A TEST)'); +}); - setTimeout(() => { - clickToolbarButton('heading', assert); - assert.hasElement('#editor h2:contains(THIS IS A TEST)'); +// FIXME PhantomJS doesn't create keyboard events properly (they have no keyCode or which) +// see https://bugs.webkit.org/show_bug.cgi?id=36423 +Helpers.skipInPhantom('highlight text, click "link" button shows input for URL, makes link', (assert) => { + clickToolbarButton('link', assert); + let input = assert.hasElement('.ck-toolbar-prompt input'); + let url = 'http://google.com'; + $(input).val(url); + Helpers.dom.triggerKeyEvent(input[0], 'keyup'); - done(); - }, 10); + assert.hasElement(`#editor a[href="${url}"]:contains(${selectedText})`); }); -test('highlight text, click "subheading" button turns text into h3 header', (assert) => { - let done = assert.async(); +test('highlighting bold text shows bold button as active', (assert) => { + assert.hasNoElement(`.ck-toolbar-btn.active[title="bold"]`, + 'precond - bold button is not active'); + clickToolbarButton('bold', assert); - setTimeout(() => { - clickToolbarButton('subheading', assert); - assert.hasElement('#editor h3:contains(THIS IS A TEST)'); + assert.hasElement(`.ck-toolbar-btn.active[title="bold"]`, + 'bold button is active after clicking it'); - done(); - }, 10); -}); + Helpers.dom.clearSelection(); + Helpers.dom.triggerEvent(document, 'mouseup'); -test('highlight text, click "quote" button turns text into blockquote', (assert) => { - let done = assert.async(); + assert.hasNoElement('.ck-toolbar', 'toolbar is hidden'); - setTimeout(() => { - clickToolbarButton('quote', assert); - assert.hasElement('#editor blockquote:contains(THIS IS A TEST)'); + Helpers.dom.selectText(selectedText, editorElement); + Helpers.dom.triggerEvent(document, 'mouseup'); - done(); - }, 10); -}); + assert.hasElement('.ck-toolbar', 'toolbar is shown again'); -test('highlight text, click "link" button shows input for URL, makes link', (assert) => { - let done = assert.async(); - - setTimeout(() => { - // FIXME PhantomJS doesn't create keyboard events properly (they have no keyCode or which) - // see https://bugs.webkit.org/show_bug.cgi?id=36423 - let skippable = navigator.userAgent.indexOf('PhantomJS') !== -1; - if (skippable) { - assert.ok(true, 'Skipping test in phantomjs'); - done(); - return; - } - - clickToolbarButton('link', assert); - let input = assert.hasElement('.ck-toolbar-prompt input'); - let url = 'http://google.com'; - $(input).val(url); - Helpers.dom.triggerKeyEvent(input[0], 'keyup'); - - assert.hasElement(`#editor a[href="${url}"]:contains(${selectedText})`); - - done(); - }, 10); + assert.hasElement(`.ck-toolbar-btn.active[title="bold"]`, + 'bold button is active when selecting bold text'); }); diff --git a/tests/acceptance/editor-sections-test.js b/tests/acceptance/editor-sections-test.js new file mode 100644 index 000000000..172ecd0e4 --- /dev/null +++ b/tests/acceptance/editor-sections-test.js @@ -0,0 +1,102 @@ +import { Editor } from 'content-kit-editor'; +import Helpers from '../test-helpers'; + +const { test, module } = QUnit; + +const newline = '\r\n'; + +let fixture, editor, editorElement; +const mobileDocWith1Section = [ + [], + [ + [1, "P", [ + [[], 0, "only section"] + ]] + ] +]; +const mobileDocWith2Sections = [ + [], + [ + [1, "P", [ + [[], 0, "first section"] + ]], + [1, "P", [ + [[], 0, "second section"] + ]] + ] +]; +const mobileDocWith3Sections = [ + [], + [ + [1, "P", [ + [[], 0, "first section"] + ]], + [1, "P", [ + [[], 0, "second section"] + ]], + [1, "P", [ + [[], 0, "third section"] + ]] + ] +]; + +module('Acceptance: Editor sections', { + beforeEach() { + fixture = document.getElementById('qunit-fixture'); + editorElement = document.createElement('div'); + editorElement.setAttribute('id', 'editor'); + fixture.appendChild(editorElement); + }, + + afterEach() { + editor.destroy(); + } +}); + +test('typing inserts section', (assert) => { + editor = new Editor(editorElement, {mobiledoc: mobileDocWith1Section}); + assert.equal($('#editor p').length, 1, 'has 1 paragraph to start'); + + const text = 'new section'; + + Helpers.dom.moveCursorTo(editorElement); + document.execCommand('insertText', false, text + newline); + + assert.equal($('#editor p').length, 2, 'has 2 paragraphs after typing return'); + assert.hasElement(`#editor p:contains(${text})`, 'has first pargraph with "A"'); + assert.hasElement('#editor p:contains(only section)', 'has correct second paragraph text'); +}); + +test('deleting across 0 sections merges them', (assert) => { + editor = new Editor(editorElement, {mobiledoc: mobileDocWith2Sections}); + assert.equal($('#editor p').length, 2, 'precond - has 2 sections to start'); + + const p0 = $('#editor p:eq(0)')[0], + p1 = $('#editor p:eq(1)')[0]; + + Helpers.dom.selectText('tion', p0, 'sec', p1); + document.execCommand('delete', false); + + assert.equal($('#editor p').length, 1, 'has only 1 paragraph after deletion'); + assert.hasElement('#editor p:contains(first second section)', + 'remaining paragraph has correct text'); +}); + +test('deleting across 1 section removes it, joins the 2 boundary sections', (assert) => { + editor = new Editor(editorElement, {mobiledoc: mobileDocWith3Sections}); + assert.equal($('#editor p').length, 3, 'precond - has 3 paragraphs to start'); + + const p0 = $('#editor p:eq(0)')[0], + p1 = $('#editor p:eq(1)')[0], + p2 = $('#editor p:eq(2)')[0]; + assert.ok(p0 && p1 && p2, 'precond - paragraphs exist'); + + Helpers.dom.selectText('section', p0, 'third ', p2); + + document.execCommand('delete', false); + + + assert.equal($('#editor p').length, 1, 'has only 1 paragraph after deletion'); + assert.hasElement('#editor p:contains(first section)', + 'remaining paragraph has correct text'); +}); diff --git a/tests/helpers/dom.js b/tests/helpers/dom.js index 3ed2b525a..8b1f2031b 100644 --- a/tests/helpers/dom.js +++ b/tests/helpers/dom.js @@ -1,7 +1,12 @@ const TEXT_NODE = 3; -const ENTER_KEY_CODE = 13; +const ENTER_KEY = 13; +const LEFT_ARROW = 37; +const KEY_CODES = { + ENTER_KEY, + LEFT_ARROW +}; -function moveCursorTo(element, offset) { +function moveCursorTo(element, offset=0) { let range = document.createRange(); range.setStart(element, offset); range.setEnd(element, offset); @@ -11,6 +16,9 @@ function moveCursorTo(element, offset) { selection.addRange(range); } +function clearSelection() { + window.getSelection().removeAllRanges(); +} function walkDOMUntil(topNode, conditionFn=() => {}) { let stack = [topNode]; @@ -26,31 +34,39 @@ function walkDOMUntil(topNode, conditionFn=() => {}) { for (let i=0; i < currentElement.childNodes.length; i++) { stack.push(currentElement.childNodes[i]); } - if (currentElement.nextSibling) { - stack.push(currentElement.nextSibling); - } } } +function selectRange(startNode, startOffset, endNode, endOffset) { + const range = document.createRange(); + range.setStart(startNode, startOffset); + range.setEnd(endNode, endOffset); -function selectText(text, containingElement) { - let textNode = walkDOMUntil(containingElement, (el) => { - if (el.nodeType !== TEXT_NODE) { return; } + const selection = window.getSelection(); + if (selection.rangeCount > 0) { selection.removeAllRanges(); } + selection.addRange(range); +} - return el.textContent.indexOf(text) !== -1; - }); - if (!textNode) { - throw new Error(`Could not find a textNode containing "${text}"`); +function selectText(startText, + startContainingElement, + endText=startText, + endContainingElement=startContainingElement) { + const findTextNode = (text) => { + return (el) => el.nodeType === TEXT_NODE && el.textContent.indexOf(text) !== -1; + }; + const startTextNode = walkDOMUntil(startContainingElement, findTextNode(startText)); + const endTextNode = walkDOMUntil(endContainingElement, findTextNode(endText)); + + if (!startTextNode) { + throw new Error(`Could not find a starting textNode containing "${startText}"`); + } + if (!endTextNode) { + throw new Error(`Could not find an ending textNode containing "${endText}"`); } - let range = document.createRange(); - let startOffset = textNode.textContent.indexOf(text), - endOffset = startOffset + text.length; - range.setStart(textNode, startOffset); - range.setEnd(textNode, endOffset); - let selection = window.getSelection(); - if (selection.rangeCount > 0) { selection.removeAllRanges(); } - selection.addRange(range); + const startOffset = startTextNode.textContent.indexOf(startText), + endOffset = endTextNode.textContent.indexOf(endText) + endText.length; + selectRange(startTextNode, startOffset, endTextNode, endOffset); } function triggerEvent(node, eventType) { @@ -61,7 +77,7 @@ function triggerEvent(node, eventType) { node.dispatchEvent(clickEvent); } -function createKeyEvent(eventType, keyCode=ENTER_KEY_CODE) { +function createKeyEvent(eventType, keyCode) { let oEvent = document.createEvent('KeyboardEvent'); if (oEvent.initKeyboardEvent) { oEvent.initKeyboardEvent(eventType, true, true, window, 0, 0, 0, 0, 0, keyCode); @@ -71,8 +87,8 @@ function createKeyEvent(eventType, keyCode=ENTER_KEY_CODE) { // Hack for Chrome to force keyCode/which value try { - Object.defineProperty(oEvent, 'keyCode', {get: function() { return keyCode; }}); - Object.defineProperty(oEvent, 'which', {get: function() { return keyCode; }}); + Object.defineProperty(oEvent, 'keyCode', {get: function() { return keyCode; }}); + Object.defineProperty(oEvent, 'which', {get: function() { return keyCode; }}); } catch(e) { // FIXME // PhantomJS/webkit will throw an error "ERROR: Attempting to change access mechanism for an unconfigurable property" @@ -86,7 +102,7 @@ function createKeyEvent(eventType, keyCode=ENTER_KEY_CODE) { return oEvent; } -function triggerKeyEvent(node, eventType, keyCode=ENTER_KEY_CODE) { +function triggerKeyEvent(node, eventType, keyCode=KEY_CODES.ENTER_KEY) { let oEvent = createKeyEvent(eventType, keyCode); node.dispatchEvent(oEvent); } @@ -94,6 +110,8 @@ function triggerKeyEvent(node, eventType, keyCode=ENTER_KEY_CODE) { export default { moveCursorTo, selectText, + clearSelection, triggerEvent, - triggerKeyEvent + triggerKeyEvent, + KEY_CODES }; diff --git a/tests/helpers/skip-in-phantom.js b/tests/helpers/skip-in-phantom.js new file mode 100644 index 000000000..54e41c23c --- /dev/null +++ b/tests/helpers/skip-in-phantom.js @@ -0,0 +1,10 @@ +const { test } = QUnit; + +export default function(message, testFn) { + const isPhantom = navigator.userAgent.indexOf('PhantomJS') !== -1; + if (isPhantom) { + message = '[SKIPPED in PhantomJS] ' + message; + testFn = (assert) => assert.ok(true); + } + test(message, testFn); +} diff --git a/tests/test-helpers.js b/tests/test-helpers.js index 4c72d16ff..e4986cae2 100644 --- a/tests/test-helpers.js +++ b/tests/test-helpers.js @@ -2,7 +2,9 @@ import registerAssertions from './helpers/assertions'; registerAssertions(); import DOMHelpers from './helpers/dom'; +import skipInPhantom from './helpers/skip-in-phantom'; export default { - dom: DOMHelpers + dom: DOMHelpers, + skipInPhantom };