Skip to content

Commit

Permalink
Merge pull request #20 from bustlelabs/editor-section-tests
Browse files Browse the repository at this point in the history
Editor section tests
  • Loading branch information
bantic committed Jul 16, 2015
2 parents 1e471f8 + dafdee5 commit 59610b1
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 101 deletions.
5 changes: 4 additions & 1 deletion src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
5 changes: 4 additions & 1 deletion src/js/utils/selection-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
4 changes: 1 addition & 3 deletions src/js/views/text-format-toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ function TextFormatToolbar(options) {
});

this.addEventListener(document, 'mouseup', () => {
setTimeout(function() {
handleTextSelection(toolbar);
});
handleTextSelection(toolbar);
});

this.addEventListener(document, 'keyup', (e) => {
Expand Down
113 changes: 43 additions & 70 deletions tests/acceptance/editor-commands-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
102 changes: 102 additions & 0 deletions tests/acceptance/editor-sections-test.js
Original file line number Diff line number Diff line change
@@ -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');
});
68 changes: 43 additions & 25 deletions tests/helpers/dom.js
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -11,6 +16,9 @@ function moveCursorTo(element, offset) {
selection.addRange(range);
}

function clearSelection() {
window.getSelection().removeAllRanges();
}

function walkDOMUntil(topNode, conditionFn=() => {}) {
let stack = [topNode];
Expand All @@ -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) {
Expand All @@ -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);
Expand All @@ -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"
Expand All @@ -86,14 +102,16 @@ 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);
}

export default {
moveCursorTo,
selectText,
clearSelection,
triggerEvent,
triggerKeyEvent
triggerKeyEvent,
KEY_CODES
};
Loading

0 comments on commit 59610b1

Please sign in to comment.