From 3170b4856f6937a63eb6906719519ef8a9035f29 Mon Sep 17 00:00:00 2001 From: Zahra Jabini Date: Tue, 3 Nov 2020 11:03:12 -0500 Subject: [PATCH] =?UTF-8?q?Typescriptify=20Phase=20VI=20=F0=9F=A5=83?= =?UTF-8?q?=F0=9F=A7=89=20(#749)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * MIGRATES TESTS!!! 🤯 * Don’t need these 🎲 --- .eslintrc.js | 3 +- package.json | 2 + src/js/editor/editor.ts | 38 +- src/js/editor/event-manager.ts | 15 +- src/js/models/atom-node.ts | 11 +- src/js/models/card-node.ts | 13 +- src/js/renderers/editor-dom.ts | 6 +- src/js/utils/keycodes.ts | 2 +- src/js/utils/object-utils.ts | 4 + src/js/utils/types.ts | 7 + tests/.eslintrc.js | 4 + tests/helpers/assertions.js | 330 ------ tests/helpers/assertions.ts | 322 ++++++ tests/helpers/browsers.js | 13 - tests/helpers/browsers.ts | 13 + tests/helpers/dom.js | 414 -------- tests/helpers/dom.ts | 436 ++++++++ tests/helpers/editor.js | 61 -- tests/helpers/editor.ts | 79 ++ tests/helpers/mobiledoc.js | 72 -- tests/helpers/mobiledoc.ts | 55 + tests/helpers/mock-editor.js | 24 - tests/helpers/mock-editor.ts | 21 + tests/helpers/post-abstract.js | 287 ------ tests/helpers/post-abstract.ts | 351 +++++++ tests/helpers/post-editor-run.js | 18 - tests/helpers/post-editor-run.ts | 19 + tests/helpers/render-built-abstract.js | 13 - tests/helpers/render-built-abstract.ts | 14 + tests/helpers/{sections.js => sections.ts} | 0 tests/helpers/wait.js | 5 - tests/helpers/wait.ts | 5 + tests/index.js | 4 +- tests/test-helpers.js | 86 -- tests/test-helpers.ts | 103 ++ tests/unit/utils/array-utils-test.js | 17 - tests/unit/utils/array-utils-test.ts | 17 + tests/unit/utils/assert-test.js | 18 - tests/unit/utils/assert-test.ts | 18 + tests/unit/utils/copy-test.js | 18 - tests/unit/utils/copy-test.ts | 17 + tests/unit/utils/cursor-position-test.js | 818 ++++++++------- tests/unit/utils/cursor-range-test.js | 503 +++++----- tests/unit/utils/fixed-queue-test.js | 40 - tests/unit/utils/fixed-queue-test.ts | 40 + tests/unit/utils/key-test.js | 128 ++- tests/unit/utils/linked-list-test.js | 1049 ++++++++++---------- tests/unit/utils/object-utils-test.js | 13 - tests/unit/utils/object-utils-test.ts | 13 + tests/unit/utils/parse-utils-test.js | 142 +-- tests/unit/utils/selection-utils-test.js | 85 -- tests/unit/utils/selection-utils-test.ts | 85 ++ tsconfig.json | 11 +- yarn.lock | 17 + 54 files changed, 2989 insertions(+), 2910 deletions(-) delete mode 100644 tests/helpers/assertions.js create mode 100644 tests/helpers/assertions.ts delete mode 100644 tests/helpers/browsers.js create mode 100644 tests/helpers/browsers.ts delete mode 100644 tests/helpers/dom.js create mode 100644 tests/helpers/dom.ts delete mode 100644 tests/helpers/editor.js create mode 100644 tests/helpers/editor.ts delete mode 100644 tests/helpers/mobiledoc.js create mode 100644 tests/helpers/mobiledoc.ts delete mode 100644 tests/helpers/mock-editor.js create mode 100644 tests/helpers/mock-editor.ts delete mode 100644 tests/helpers/post-abstract.js create mode 100644 tests/helpers/post-abstract.ts delete mode 100644 tests/helpers/post-editor-run.js create mode 100644 tests/helpers/post-editor-run.ts delete mode 100644 tests/helpers/render-built-abstract.js create mode 100644 tests/helpers/render-built-abstract.ts rename tests/helpers/{sections.js => sections.ts} (100%) delete mode 100644 tests/helpers/wait.js create mode 100644 tests/helpers/wait.ts delete mode 100644 tests/test-helpers.js create mode 100644 tests/test-helpers.ts delete mode 100644 tests/unit/utils/array-utils-test.js create mode 100644 tests/unit/utils/array-utils-test.ts delete mode 100644 tests/unit/utils/assert-test.js create mode 100644 tests/unit/utils/assert-test.ts delete mode 100644 tests/unit/utils/copy-test.js create mode 100644 tests/unit/utils/copy-test.ts delete mode 100644 tests/unit/utils/fixed-queue-test.js create mode 100644 tests/unit/utils/fixed-queue-test.ts delete mode 100644 tests/unit/utils/object-utils-test.js create mode 100644 tests/unit/utils/object-utils-test.ts delete mode 100644 tests/unit/utils/selection-utils-test.js create mode 100644 tests/unit/utils/selection-utils-test.ts diff --git a/.eslintrc.js b/.eslintrc.js index 05cb957b3..1deaf8dc5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,7 +11,7 @@ module.exports = { ], "globals": { "Atomics": "readonly", - "SharedArrayBuffer": "readonly" + "SharedArrayBuffer": "readonly", }, "plugins": ["@typescript-eslint"], "parser": "@typescript-eslint/parser", @@ -62,7 +62,6 @@ module.exports = { "error", "last" ], - "complexity": "error", "computed-property-spacing": [ "error", "never" diff --git a/package.json b/package.json index 999224565..1451b2f4b 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,8 @@ "@rollup/plugin-commonjs": "^15.0.0", "@rollup/plugin-node-resolve": "^9.0.0", "@rollup/plugin-typescript": "^5.0.2", + "@types/jquery": "^3.5.2", + "@types/qunit": "^2.9.5", "@typescript-eslint/eslint-plugin": "^3.6.1", "@typescript-eslint/parser": "^3.6.1", "conventional-changelog-cli": "^2.0.34", diff --git a/src/js/editor/editor.ts b/src/js/editor/editor.ts index b71ae17bf..22608c5f2 100644 --- a/src/js/editor/editor.ts +++ b/src/js/editor/editor.ts @@ -31,7 +31,7 @@ import Card, { CardMode, CardPayload } from '../models/card' import assert from '../utils/assert' import MutationHandler from '../editor/mutation-handler' import EditHistory from '../editor/edit-history' -import EventManager, { EventType, EventForType } from '../editor/event-manager' +import EventManager, { DOMEventType, DOMEventForType } from '../editor/event-manager' import EditState from '../editor/edit-state' import DOMRenderer from 'mobiledoc-dom-renderer' import TextRenderer from 'mobiledoc-text-renderer' @@ -56,27 +56,27 @@ import { TextInputHandlerListener } from './text-input-handler' export { EDITOR_ELEMENT_CLASS_NAME } from '../renderers/editor-dom' export interface EditorOptions { - parserPlugins: SectionParserPlugin[] - placeholder: string - spellcheck: boolean - autofocus: boolean - showLinkTooltips: boolean - undoDepth: number - undoBlockTimeout: number - cards: CardData[] - atoms: AtomData[] - cardOptions: {} - unknownCardHandler: CardRenderHook - unknownAtomHandler: CardRenderHook - mobiledoc: Option - html: Option - tooltipPlugin: TooltipPlugin + parserPlugins?: SectionParserPlugin[] + placeholder?: string + spellcheck?: boolean + autofocus?: boolean + showLinkTooltips?: boolean + undoDepth?: number + undoBlockTimeout?: number + cards?: CardData[] + atoms?: AtomData[] + cardOptions?: {} + unknownCardHandler?: CardRenderHook + unknownAtomHandler?: CardRenderHook + mobiledoc?: Option + html?: Option + tooltipPlugin?: TooltipPlugin /** @internal */ nodeType?: number } -const defaults: Partial = { +const defaults: EditorOptions = { placeholder: 'Write here...', spellcheck: true, autofocus: true, @@ -239,7 +239,7 @@ export default class Editor implements EditorOptions { * Set to 0 to disable undo/redo functionality. * @public */ - constructor(options: Partial = {}) { + constructor(options: EditorOptions = {}) { assert( 'editor create accepts an options object. For legacy usage passing an element for the first argument, consider the `html` option for loading DOM or HTML posts. For other cases call `editor.render(domNode)` after editor creation', options && !options.nodeType @@ -1304,7 +1304,7 @@ export default class Editor implements EditorOptions { } } - triggerEvent(context: HTMLElement, eventName: EventType, event: EventForType) { + triggerEvent(context: HTMLElement, eventName: DOMEventType, event: DOMEventForType) { this._eventManager._trigger(context, eventName, event) } diff --git a/src/js/editor/event-manager.ts b/src/js/editor/event-manager.ts index 6d6bfdb01..70c28c01b 100644 --- a/src/js/editor/event-manager.ts +++ b/src/js/editor/event-manager.ts @@ -28,8 +28,9 @@ declare global { } } -export type EventType = typeof ELEMENT_EVENT_TYPES[number] -export type EventForType = HTMLElementEventMap[T] +export type DOMEventType = typeof ELEMENT_EVENT_TYPES[number] +export type DOMEventForType = HTMLElementEventMap[T] +export type DOMEvent = HTMLElementEventMap[DOMEventType] interface ModifierKeys { shift: boolean @@ -37,7 +38,7 @@ interface ModifierKeys { type EventManagerListener = [ HTMLElement, - EventType, + DOMEventType, (event: CompositionEvent | KeyboardEvent | ClipboardEvent | DragEvent) => void ] @@ -100,10 +101,10 @@ export default class EventManager { this._textInputHandler = new TextInputHandler(this.editor) } - _addListener(context: HTMLElement, type: EventType) { + _addListener(context: HTMLElement, type: DOMEventType) { assert(`Missing listener for ${type}`, !!this[type]) - let listener: (event: EventForType) => void = event => this._handleEvent(type, event) + let listener: (event: DOMEventForType) => void = event => this._handleEvent(type, event) context.addEventListener(type, listener) this._listeners.push([context, type, listener]) } @@ -117,7 +118,7 @@ export default class EventManager { // This is primarily useful for programmatically simulating events on the // editor from the tests. - _trigger(context: HTMLElement, type: EventType, event: EventForType) { + _trigger(context: HTMLElement, type: DOMEventType, event: DOMEventForType) { forEach( filter(this._listeners, ([_context, _type]) => { return _context === context && _type === type @@ -134,7 +135,7 @@ export default class EventManager { this._removeListeners() } - _handleEvent(type: EventType, event: EventForType) { + _handleEvent(type: DOMEventType, event: DOMEventForType) { let { target: element } = event if (!this.started) { // abort handling this event diff --git a/src/js/models/atom-node.ts b/src/js/models/atom-node.ts index 68c8902c0..7d94297c5 100644 --- a/src/js/models/atom-node.ts +++ b/src/js/models/atom-node.ts @@ -1,17 +1,18 @@ -import assert from '../utils/assert' import Atom from './atom' +import assert from '../utils/assert' +import { JsonData, Dict, Maybe } from '../utils/types' -export interface AtomOptions {} +export type AtomOptions = Dict export type TeardownCallback = () => void export interface AtomRenderOptions { options: AtomOptions env: any value: unknown - payload: {} + payload: JsonData } -export type AtomRenderHook = (options: AtomRenderOptions) => Element +export type AtomRenderHook = (options: AtomRenderOptions) => Maybe export type AtomData = { name: string @@ -27,7 +28,7 @@ export default class AtomNode { atomOptions: AtomOptions _teardownCallback: TeardownCallback | null = null - _rendered: Node | null = null + _rendered: Maybe constructor(editor: any, atom: AtomData, model: Atom, element: Element, atomOptions: AtomOptions) { this.editor = editor diff --git a/src/js/models/card-node.ts b/src/js/models/card-node.ts index 08afad0d7..fc492fdd8 100644 --- a/src/js/models/card-node.ts +++ b/src/js/models/card-node.ts @@ -1,9 +1,10 @@ -import assert from '../utils/assert' import Card, { CardMode } from './card' +import assert from '../utils/assert' +import { Dict, Maybe } from '../utils/types' -export interface CardNodeOptions {} +export type CardNodeOptions = Dict -export type CardRenderHook = (...args: any[]) => Element +export type CardRenderHook = (...args: any[]) => Maybe type DidRenderCallback = null | (() => void) type TeardownCallback = null | (() => void) @@ -25,14 +26,14 @@ export default class CardNode { card: CardData section: Card element: Element - options: CardNodeOptions + options?: CardNodeOptions mode!: CardMode _rendered: Element | null = null _teardownCallback: TeardownCallback = null _didRenderCallback: DidRenderCallback = null - constructor(editor: any, card: CardData, section: Card, element: Element, options: CardNodeOptions) { + constructor(editor: any, card: CardData, section: Card, element: Element, options?: CardNodeOptions) { this.editor = editor this.card = card this.section = section @@ -112,7 +113,7 @@ export default class CardNode { this.editor.run((postEditor: any) => postEditor.removeSection(this.section)) } - _validateAndAppendRenderResult(rendered: Element | null) { + _validateAndAppendRenderResult(rendered: Maybe) { if (!rendered) { return } diff --git a/src/js/renderers/editor-dom.ts b/src/js/renderers/editor-dom.ts index c2c7d0cf0..017dd6334 100644 --- a/src/js/renderers/editor-dom.ts +++ b/src/js/renderers/editor-dom.ts @@ -14,7 +14,7 @@ import { Attributable } from '../models/_attributable' import { TagNameable } from '../models/_tag-nameable' import ListSection from '../models/list-section' import RenderNode from '../models/render-node' -import { Option, Maybe } from '../utils/types' +import { Option, Maybe, Dict } from '../utils/types' import Atom from '../models/atom' import Editor from '../editor/editor' import { hasChildSections } from '../models/_has-child-sections' @@ -306,7 +306,7 @@ class Visitor { unknownCardHandler: CardRenderHook unknownAtomHandler: AtomRenderHook - options: {} + options: Dict constructor( editor: Editor, @@ -314,7 +314,7 @@ class Visitor { atoms: AtomData[], unknownCardHandler: CardRenderHook, unknownAtomHandler: AtomRenderHook, - options: {} + options: Dict ) { this.editor = editor this.cards = validateCards(cards) diff --git a/src/js/utils/keycodes.ts b/src/js/utils/keycodes.ts index d79aaded7..46b378c72 100644 --- a/src/js/utils/keycodes.ts +++ b/src/js/utils/keycodes.ts @@ -1,4 +1,4 @@ -export default { +export default { BACKSPACE: 8, SPACE: 32, ENTER: 13, diff --git a/src/js/utils/object-utils.ts b/src/js/utils/object-utils.ts index 7acd93ecc..31fbe680d 100644 --- a/src/js/utils/object-utils.ts +++ b/src/js/utils/object-utils.ts @@ -9,3 +9,7 @@ export function entries(obj: T): (keyof T)[] { + return Object.keys(obj) +} diff --git a/src/js/utils/types.ts b/src/js/utils/types.ts index f03f3fd51..659782855 100644 --- a/src/js/utils/types.ts +++ b/src/js/utils/types.ts @@ -2,3 +2,10 @@ export type Option = T | null export type Maybe = T | null | undefined export type Dict = { [key: string]: T } + +export type ValueOf = T[keyof T] + +export type JsonPrimitive = string | number | boolean | null +export type JsonArray = JsonData[] +export type JsonObject = { [key: string]: JsonData } +export type JsonData = JsonPrimitive | JsonArray | JsonObject diff --git a/tests/.eslintrc.js b/tests/.eslintrc.js index dde6a3e56..cadac52be 100644 --- a/tests/.eslintrc.js +++ b/tests/.eslintrc.js @@ -1,8 +1,12 @@ module.exports = { + "globals": { + "$": "readonly" + }, "rules": { "dot-location": "off", "no-new": "off", "no-use-before-define": "off", "no-unused-vars": "off" } + }; diff --git a/tests/helpers/assertions.js b/tests/helpers/assertions.js deleted file mode 100644 index c29452143..000000000 --- a/tests/helpers/assertions.js +++ /dev/null @@ -1,330 +0,0 @@ -/* global QUnit, $ */ - -import DOMHelper from './dom'; -import mobiledocRenderers from 'mobiledoc-kit/renderers/mobiledoc'; -import { - MARKUP_SECTION_TYPE, - LIST_SECTION_TYPE, - MARKUP_TYPE, - MARKER_TYPE, - POST_TYPE, - LIST_ITEM_TYPE, - CARD_TYPE, - IMAGE_SECTION_TYPE, - ATOM_TYPE -} from 'mobiledoc-kit/models/types'; - -function compareMarkers(actual, expected, assert, path, deepCompare) { - if (actual.value !== expected.value) { - assert.equal(actual.value, expected.value, `wrong value at ${path}`); - } - if (actual.markups.length !== expected.markups.length) { - assert.equal(actual.markups.length, expected.markups.length, - `wrong markups at ${path}`); - } - if (deepCompare) { - actual.markups.forEach((markup, index) => { - comparePostNode(markup, expected.markups[index], - assert, `${path}:${index}`, deepCompare); - }); - } -} - -/* eslint-disable complexity */ -function comparePostNode(actual, expected, assert, path='root', deepCompare=false) { - if (!actual || !expected) { - assert.ok(!!actual, `missing actual post node at ${path}`); - assert.ok(!!expected, `missing expected post node at ${path}`); - return; - } - if (actual.type !== expected.type) { - assert.pushResult({ - result: false, - actual: actual.type, - expected: expected.type, - message: `wrong type at ${path}` - }); - } - - switch (actual.type) { - case POST_TYPE: - if (actual.sections.length !== expected.sections.length) { - assert.equal(actual.sections.length, expected.sections.length, - `wrong sections for post`); - } - if (deepCompare) { - actual.sections.forEach((section, index) => { - comparePostNode(section, expected.sections.objectAt(index), - assert, `${path}:${index}`, deepCompare); - }); - } - break; - case ATOM_TYPE: - if (actual.name !== expected.name) { - assert.equal(actual.name, expected.name, `wrong atom name at ${path}`); - } - compareMarkers(actual, expected, assert, path, deepCompare); - break; - case MARKER_TYPE: - compareMarkers(actual, expected, assert, path, deepCompare); - break; - case MARKUP_SECTION_TYPE: - case LIST_ITEM_TYPE: - if (actual.tagName !== expected.tagName) { - assert.equal(actual.tagName, expected.tagName, `wrong tagName at ${path}`); - } - if (actual.markers.length !== expected.markers.length) { - assert.equal(actual.markers.length, expected.markers.length, - `wrong markers at ${path}`); - } - if (deepCompare) { - actual.markers.forEach((marker, index) => { - comparePostNode(marker, expected.markers.objectAt(index), - assert, `${path}:${index}`, deepCompare); - }); - } - break; - case CARD_TYPE: - if (actual.name !== expected.name) { - assert.equal(actual.name, expected.name, `wrong card name at ${path}`); - } - if (!QUnit.equiv(actual.payload, expected.payload)) { - assert.deepEqual(actual.payload, expected.payload, - `wrong card payload at ${path}`); - } - break; - case LIST_SECTION_TYPE: - if (actual.items.length !== expected.items.length) { - assert.equal(actual.items.length, expected.items.length, - `wrong items at ${path}`); - } - if (deepCompare) { - actual.items.forEach((item, index) => { - comparePostNode(item, expected.items.objectAt(index), - assert, `${path}:${index}`, deepCompare); - }); - } - break; - case IMAGE_SECTION_TYPE: - if (actual.src !== expected.src) { - assert.equal(actual.src, expected.src, `wrong image src at ${path}`); - } - break; - case MARKUP_TYPE: - if (actual.tagName !== expected.tagName) { - assert.equal(actual.tagName, expected.tagName, - `wrong tagName at ${path}`); - } - if (!QUnit.equiv(actual.attributes, expected.attributes)) { - assert.deepEqual(actual.attributes, expected.attributes, - `wrong attributes at ${path}`); - } - break; - default: - throw new Error('wrong type :' + actual.type); - } -} -/* eslint-enable complexity */ - -export default function registerAssertions(QUnit) { - QUnit.assert.isBlank = function(val, message=`value is blank`) { - this.pushResult({ - result: val === null || val === undefined || val === '' || val === false, - actual: `${val} (typeof ${typeof val})`, - expected: `null|undefined|''|false`, - message - }); - }; - - QUnit.assert.hasElement = function(selector, - message=`hasElement "${selector}"`) { - let found = $('#qunit-fixture').find(selector); - this.pushResult({ - result: found.length > 0, - actual: `${found.length} matches for '${selector}'`, - expected: `>0 matches for '${selector}'`, - message: message - }); - return found; - }; - - QUnit.assert.hasNoElement = function(selector, - message=`hasNoElement "${selector}"`) { - let found = $(selector); - this.pushResult({ - result: found.length === 0, - actual: `${found.length} matches for '${selector}'`, - expected: `0 matches for '${selector}'`, - message: message - }); - return found; - }; - - QUnit.assert.hasClass = function(element, className, - message=`element has class "${className}"`) { - this.pushResult({ - result: element.classList.contains(className), - actual: element.classList, - expected: className, - message - }); - }; - - QUnit.assert.notHasClass = function(element, className, - message=`element has class "${className}"`) { - this.pushResult({ - result: !element.classList.contains(className), - actual: element.classList, - expected: className, - message - }); - }; - - QUnit.assert.selectedText = function(text, message=`selectedText "${text}"`) { - const selected = DOMHelper.getSelectedText(); - this.pushResult({ - result: selected === text, - actual: selected, - expected: text, - message: message - }); - }; - - QUnit.assert.inArray = function(element, array, - message=`has "${element}" in "${array}"`) { - QUnit.assert.ok(array.indexOf(element) !== -1, message); - }; - - QUnit.assert.postIsSimilar = function(post, expected, postName='post') { - comparePostNode(post, expected, this, postName, true); - let mobiledoc = mobiledocRenderers.render(post), - expectedMobiledoc = mobiledocRenderers.render(expected); - this.deepEqual(mobiledoc, expectedMobiledoc, - `${postName} is similar to expected`); - }; - - QUnit.assert.renderTreeIsEqual = function(renderTree, expectedPost) { - if (renderTree.rootNode.isDirty) { - this.ok(false, 'renderTree is dirty'); - return; - } - - expectedPost.sections.forEach((section, index) => { - let renderNode = renderTree.rootNode.childNodes.objectAt(index); - let path = `post:${index}`; - - let compareChildren = (parentPostNode, parentRenderNode, path) => { - let children = parentPostNode.markers || - parentPostNode.items || - []; - - if (children.length !== parentRenderNode.childNodes.length) { - this.equal(parentRenderNode.childNodes.length, children.length, - `wrong child render nodes at ${path}`); - return; - } - - children.forEach((child, index) => { - let renderNode = parentRenderNode.childNodes.objectAt(index); - - comparePostNode(child, renderNode && renderNode.postNode, - this, `${path}:${index}`, false); - compareChildren(child, renderNode, `${path}:${index}`); - }); - }; - - comparePostNode(section, renderNode.postNode, this, path, false); - compareChildren(section, renderNode, path); - }); - - this.ok(true, 'renderNode is similar'); - }; - - QUnit.assert.positionIsEqual = function(position, expected, - message=`position is equal`) { - if (position.section !== expected.section) { - this.pushResult({ - result: false, - actual: `${position.section.type}:${position.section.tagName}`, - expected: `${expected.section.type}:${expected.section.tagName}`, - message: `incorrect position section (${message})` - }); - } else if (position.offset !== expected.offset) { - this.pushResult({ - result: false, - actual: position.offset, - expected: expected.offset, - message: `incorrect position offset (${message})` - }); - } else { - this.pushResult({ - result: true, - actual: position, - expected: expected, - message: message - }); - } - }; - - QUnit.assert.rangeIsEqual = function(range, expected, - message=`range is equal`) { - let { head, tail, isCollapsed, direction } = range; - let { - head: expectedHead, - tail: expectedTail, - isCollapsed: expectedIsCollapsed, - direction: expectedDirection - } = expected; - - let failed = false; - - if (!head.isEqual(expectedHead)) { - failed = true; - this.pushResult({ - result: false, - actual: `${head.section.type}:${head.section.tagName}`, - expected: `${expectedHead.section.type}:${expectedHead.section.tagName}`, - message: 'incorrect head position' - }); - } - - if (!tail.isEqual(expectedTail)) { - failed = true; - this.pushResult({ - result: false, - actual: `${tail.section.type}:${tail.section.tagName}`, - expected: `${expectedTail.section.type}:${expectedTail.section.tagName}`, - message: 'incorrect tail position' - }); - } - - if (isCollapsed !== expectedIsCollapsed) { - failed = true; - this.pushResult({ - result: false, - actual: isCollapsed, - expected: expectedIsCollapsed, - message: 'wrong value for isCollapsed' - }); - } - - if (direction !== expectedDirection) { - failed = true; - this.pushResult({ - result: false, - actual: direction, - expected: expectedDirection, - message: 'wrong value for direction' - }); - } - - if (!failed) { - this.pushResult({ - result: true, - actual: range, - expected: expected, - message: message - }); - } - }; -} diff --git a/tests/helpers/assertions.ts b/tests/helpers/assertions.ts new file mode 100644 index 000000000..f98876579 --- /dev/null +++ b/tests/helpers/assertions.ts @@ -0,0 +1,322 @@ +/* global QUnit, $ */ + +import DOMHelper from './dom' +import mobiledocRenderers from '../../src/js/renderers/mobiledoc' +import Marker from '../../src/js/models/marker' +import { PostNode } from '../../src/js/models/post-node-builder' +import Markup from '../../src/js/models/markup' +import Post from '../../src/js/models/post' +import Atom from '../../src/js/models/atom' +import Markuperable from '../../src/js/utils/markuperable' +import ListItem from '../../src/js/models/list-item' +import Card from '../../src/js/models/card' +import ListSection from '../../src/js/models/list-section' +import Image from '../../src/js/models/image' +import RenderTree from '../../src/js/models/render-tree' +import { Position, Range } from '../../src/js' +import RenderNode from '../../src/js/models/render-node' +import Markerable from '../../src/js/models/_markerable' +import { TagNameable } from '../../src/js/models/_tag-nameable' +import Section from '../../src/js/models/_section' +import MarkupSection from 'mobiledoc-kit/models/markup-section' + +function compareMarkers(actual: Marker | Markuperable, expected: Marker, assert: Assert, path: string, deepCompare: boolean) { + if (actual.value !== expected.value) { + assert.equal(actual.value, expected.value, `wrong value at ${path}`) + } + if (actual.markups.length !== expected.markups.length) { + assert.equal(actual.markups.length, expected.markups.length, `wrong markups at ${path}`) + } + if (deepCompare) { + actual.markups.forEach((markup, index) => { + comparePostNode(markup, expected.markups[index], assert, `${path}:${index}`, deepCompare) + }) + } +} + +function comparePostNode(actual: T, expected: any, assert: Assert, path = 'root', deepCompare = false) { + if (!actual || !expected) { + assert.ok(!!actual, `missing actual post node at ${path}`) + assert.ok(!!expected, `missing expected post node at ${path}`) + return + } + if (actual.type !== expected.type) { + assert.pushResult({ + result: false, + actual: actual.type, + expected: expected.type, + message: `wrong type at ${path}`, + }) + } + + if (actual instanceof Post) { + if (actual.sections.length !== expected.sections.length) { + assert.equal(actual.sections.length, expected.sections.length, `wrong sections for post`) + } + if (deepCompare) { + actual.sections.forEach((section, index) => { + comparePostNode(section, expected.sections.objectAt(index), assert, `${path}:${index}`, deepCompare) + }) + } + } else if (actual instanceof Atom) { + if (actual.name !== expected.name) { + assert.equal(actual.name, expected.name, `wrong atom name at ${path}`) + } + compareMarkers(actual, expected, assert, path, deepCompare) + } else if (actual instanceof Marker) { + compareMarkers(actual, expected, assert, path, deepCompare) + } else if (actual instanceof MarkupSection || actual instanceof ListItem) { + if (actual.tagName !== expected.tagName) { + assert.equal(actual.tagName, expected.tagName, `wrong tagName at ${path}`) + } + if (actual.markers.length !== expected.markers.length) { + assert.equal(actual.markers.length, expected.markers.length, `wrong markers at ${path}`) + } + if (deepCompare) { + actual.markers.forEach((marker, index) => { + comparePostNode(marker, expected.markers.objectAt(index), assert, `${path}:${index}`, deepCompare) + }) + } + } else if (actual instanceof Card) { + if (actual.name !== expected.name) { + assert.equal(actual.name, expected.name, `wrong card name at ${path}`) + } + if (!QUnit.equiv(actual.payload, expected.payload)) { + assert.deepEqual(actual.payload, expected.payload, `wrong card payload at ${path}`) + } + } else if (actual instanceof ListSection) { + if (actual.items.length !== expected.items.length) { + assert.equal(actual.items.length, expected.items.length, `wrong items at ${path}`) + } + if (deepCompare) { + actual.items.forEach((item, index) => { + comparePostNode(item, expected.items.objectAt(index), assert, `${path}:${index}`, deepCompare) + }) + } + } else if (actual instanceof Image) { + if (actual.src !== expected.src) { + assert.equal(actual.src, expected.src, `wrong image src at ${path}`) + } + } else if (actual instanceof Markup) { + if (actual.tagName !== expected.tagName) { + assert.equal(actual.tagName, expected.tagName, `wrong tagName at ${path}`) + } + if (!QUnit.equiv(actual.attributes, expected.attributes)) { + assert.deepEqual(actual.attributes, expected.attributes, `wrong attributes at ${path}`) + } + } else { + throw new Error('wrong type :' + actual.type) + } +} + +declare global { + interface Assert { + isBlank(val: unknown, message: string): void + hasElement(selector: string, message: string): void + hasNoElement(selector: string, message: string): JQuery + hasClass(element: HTMLElement, className: string, message: string): void + notHasClass(element: HTMLElement, className: string, message: string): void + selectedText(text: string, message: string): void + inArray(element: T, array: T[], message: string): void + postIsSimilar(post: Post, expected: Post, postName: string): void + renderTreeIsEqual(renderTree: RenderTree, expectedPost: Post): void + positionIsEqual(position: Position, expected: Position, message: string): void + rangeIsEqual(range: Range, expected: Range, message: string): void + } +} + +export default function registerAssertions(QUnit: QUnit) { + QUnit.assert.isBlank = function (val, message = `value is blank`) { + this.pushResult({ + result: val === null || val === undefined || val === '' || val === false, + actual: `${val} (typeof ${typeof val})`, + expected: `null|undefined|''|false`, + message, + }) + } + + QUnit.assert.hasElement = function (selector, message = `hasElement "${selector}"`) { + let found = $('#qunit-fixture').find(selector) + this.pushResult({ + result: found.length > 0, + actual: `${found.length} matches for '${selector}'`, + expected: `>0 matches for '${selector}'`, + message: message, + }) + return found + } + + QUnit.assert.hasNoElement = function (selector, message = `hasNoElement "${selector}"`) { + let found = $(selector) + this.pushResult({ + result: found.length === 0, + actual: `${found.length} matches for '${selector}'`, + expected: `0 matches for '${selector}'`, + message: message, + }) + return found + } + + QUnit.assert.hasClass = function (element, className, message = `element has class "${className}"`) { + this.pushResult({ + result: element.classList.contains(className), + actual: element.classList, + expected: className, + message, + }) + } + + QUnit.assert.notHasClass = function (element, className, message = `element has class "${className}"`) { + this.pushResult({ + result: !element.classList.contains(className), + actual: element.classList, + expected: className, + message, + }) + } + + QUnit.assert.selectedText = function (text, message = `selectedText "${text}"`) { + const selected = DOMHelper.getSelectedText() + this.pushResult({ + result: selected === text, + actual: selected, + expected: text, + message: message, + }) + } + + QUnit.assert.inArray = function (element, array, message = `has "${element}" in "${array}"`) { + QUnit.assert.ok(array.indexOf(element) !== -1, message) + } + + QUnit.assert.postIsSimilar = function (post, expected, postName = 'post') { + comparePostNode(post, expected, this, postName, true) + let mobiledoc = mobiledocRenderers.render(post), + expectedMobiledoc = mobiledocRenderers.render(expected) + this.deepEqual(mobiledoc, expectedMobiledoc, `${postName} is similar to expected`) + } + + QUnit.assert.renderTreeIsEqual = function (renderTree, expectedPost) { + if (renderTree.rootNode.isDirty) { + this.ok(false, 'renderTree is dirty') + return + } + + expectedPost.sections.forEach((section, index) => { + let renderNode = renderTree.rootNode.childNodes.objectAt(index)! + let path = `post:${index}` + + let compareChildren = (parentPostNode: PostNode, parentRenderNode: RenderNode, path: string) => { + let children = (parentPostNode as Markerable).markers || (parentPostNode as ListSection).items || [] + + if (children.length !== parentRenderNode.childNodes.length) { + this.equal(parentRenderNode.childNodes.length, children.length, `wrong child render nodes at ${path}`) + return + } + + children.forEach((child, index) => { + let renderNode = parentRenderNode.childNodes.objectAt(index)! + + comparePostNode(child, renderNode && renderNode.postNode, this, `${path}:${index}`, false) + compareChildren(child, renderNode, `${path}:${index}`) + }) + } + + comparePostNode(section, renderNode.postNode, this, path, false) + compareChildren(section, renderNode, path) + }) + + this.ok(true, 'renderNode is similar') + } + + QUnit.assert.positionIsEqual = function (position, expected, message = `position is equal`) { + if (position.section !== expected.section) { + this.pushResult({ + result: false, + actual: formatPosition(position), + expected: formatPosition(expected), + message: `incorrect position section (${message})`, + }) + } else if (position.offset !== expected.offset) { + this.pushResult({ + result: false, + actual: position.offset, + expected: expected.offset, + message: `incorrect position offset (${message})`, + }) + } else { + this.pushResult({ + result: true, + actual: position, + expected: expected, + message: message, + }) + } + } + + QUnit.assert.rangeIsEqual = function (range, expected, message = `range is equal`) { + let { head, tail, isCollapsed, direction } = range + let { + head: expectedHead, + tail: expectedTail, + isCollapsed: expectedIsCollapsed, + direction: expectedDirection, + } = expected + + let failed = false + + if (!head.isEqual(expectedHead)) { + failed = true + this.pushResult({ + result: false, + actual: formatPosition(head), + expected: formatPosition(expectedHead), + message: 'incorrect head position', + }) + } + + if (!tail.isEqual(expectedTail)) { + failed = true + this.pushResult({ + result: false, + actual: formatPosition(tail), + expected: formatPosition(expectedTail), + message: 'incorrect tail position', + }) + } + + if (isCollapsed !== expectedIsCollapsed) { + failed = true + this.pushResult({ + result: false, + actual: isCollapsed, + expected: expectedIsCollapsed, + message: 'wrong value for isCollapsed', + }) + } + + if (direction !== expectedDirection) { + failed = true + this.pushResult({ + result: false, + actual: direction, + expected: expectedDirection, + message: 'wrong value for direction', + }) + } + + if (!failed) { + this.pushResult({ + result: true, + actual: range, + expected: expected, + message: message, + }) + } + } +} + +function formatPosition(position: Position): string { + const section = position.section! as Section & TagNameable + return `${section.type}:${section.tagName}` +} diff --git a/tests/helpers/browsers.js b/tests/helpers/browsers.js deleted file mode 100644 index e814ef478..000000000 --- a/tests/helpers/browsers.js +++ /dev/null @@ -1,13 +0,0 @@ -export function detectIE() { - let userAgent = navigator.userAgent; - return userAgent.indexOf("MSIE ") !== -1 || userAgent.indexOf("Trident/") !== -1 || userAgent.indexOf('Edge/') !== -1; -} - -export function detectIE11() { - return detectIE() && navigator.userAgent.indexOf("rv:11.0") !== -1; -} - -export function supportsSelectionExtend() { - let selection = window.getSelection(); - return !!selection.extend; -} diff --git a/tests/helpers/browsers.ts b/tests/helpers/browsers.ts new file mode 100644 index 000000000..61bf06187 --- /dev/null +++ b/tests/helpers/browsers.ts @@ -0,0 +1,13 @@ +export function detectIE() { + let userAgent = navigator.userAgent + return userAgent.indexOf('MSIE ') !== -1 || userAgent.indexOf('Trident/') !== -1 || userAgent.indexOf('Edge/') !== -1 +} + +export function detectIE11() { + return detectIE() && navigator.userAgent.indexOf('rv:11.0') !== -1 +} + +export function supportsSelectionExtend() { + let selection = window.getSelection()! + return !!selection.extend +} diff --git a/tests/helpers/dom.js b/tests/helpers/dom.js deleted file mode 100644 index ed471d13b..000000000 --- a/tests/helpers/dom.js +++ /dev/null @@ -1,414 +0,0 @@ -import { clearSelection } from 'mobiledoc-kit/utils/selection-utils'; -import { forEach, contains } from 'mobiledoc-kit/utils/array-utils'; -import KEY_CODES from 'mobiledoc-kit/utils/keycodes'; -import { DIRECTION, MODIFIERS } from 'mobiledoc-kit/utils/key'; -import { isTextNode } from 'mobiledoc-kit/utils/dom-utils'; -import { merge } from 'mobiledoc-kit/utils/merge'; -import { Editor } from 'mobiledoc-kit'; -import { - MIME_TEXT_PLAIN, - MIME_TEXT_HTML -} from 'mobiledoc-kit/utils/parse-utils'; -import { dasherize } from 'mobiledoc-kit/utils/string-utils'; - -function assertEditor(editor) { - if (!(editor instanceof Editor)) { - throw new Error('Must pass editor as first argument'); - } -} - -// walks DOWN the dom from node to childNodes, returning the element -// for which `conditionFn(element)` is true -function walkDOMUntil(topNode, conditionFn=() => {}) { - if (!topNode) { throw new Error('Cannot call walkDOMUntil without a node'); } - let stack = [topNode]; - let currentElement; - - while (stack.length) { - currentElement = stack.pop(); - - if (conditionFn(currentElement)) { - return currentElement; - } - - forEach(currentElement.childNodes, el => stack.push(el)); - } -} - -function findTextNode(parentElement, text) { - return walkDOMUntil(parentElement, node => { - return isTextNode(node) && node.textContent.indexOf(text) !== -1; - }); -} - -function selectRange(startNode, startOffset, endNode, endOffset) { - clearSelection(); - - const range = document.createRange(); - range.setStart(startNode, startOffset); - range.setEnd(endNode, endOffset); - - const selection = window.getSelection(); - selection.addRange(range); -} - -function selectText(editor, - startText, - startContainingElement=editor.element, - endText=startText, - endContainingElement=startContainingElement) { - - assertEditor(editor); - let startTextNode = findTextNode(startContainingElement, startText); - let endTextNode = findTextNode(endContainingElement, 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}"`); - } - - const startOffset = startTextNode.textContent.indexOf(startText), - endOffset = endTextNode.textContent.indexOf(endText) + endText.length; - selectRange(startTextNode, startOffset, endTextNode, endOffset); - editor._readRangeFromDOM(); -} - -function moveCursorWithoutNotifyingEditorTo(editor, node, offset=0, endNode=node, endOffset=offset) { - selectRange(node, offset, endNode, endOffset); -} - -function moveCursorTo(editor, node, offset=0, endNode=node, endOffset=offset) { - assertEditor(editor); - if (!node) { throw new Error('Cannot moveCursorTo node without node'); } - moveCursorWithoutNotifyingEditorTo(editor, node, offset, endNode, endOffset); - editor._readRangeFromDOM(); -} - -function triggerEvent(node, eventType) { - if (!node) { throw new Error(`Attempted to trigger event "${eventType}" on undefined node`); } - - let clickEvent = document.createEvent('MouseEvents'); - clickEvent.initEvent(eventType, true, true); - return node.dispatchEvent(clickEvent); -} - -function _triggerEditorEvent(editor, event) { - editor.triggerEvent(editor.element, event.type, event); -} - -function _buildDOM(tagName, attributes={}, children=[]) { - const el = document.createElement(tagName); - Object.keys(attributes).forEach(k => el.setAttribute(k, attributes[k])); - children.forEach(child => el.appendChild(child)); - return el; -} - -_buildDOM.text = (string) => { - return document.createTextNode(string); -}; - -/** - * Usage: - * build(t => - * t('div', attributes={}, children=[ - * t('b', {}, [ - * t.text('I am a bold text node') - * ]) - * ]) - * ); - */ -function build(tree) { - return tree(_buildDOM); -} - -function getSelectedText() { - const selection = window.getSelection(); - if (selection.rangeCount === 0) { - return null; - } else if (selection.rangeCount > 1) { - // FIXME? - throw new Error('Unable to get selected text for multiple ranges'); - } else { - return selection.toString(); - } -} - -// returns the node and the offset that the cursor is on -function getCursorPosition() { - const selection = window.getSelection(); - return { - node: selection.anchorNode, - offset: selection.anchorOffset - }; -} - -function createMockEvent(eventName, element, options={}) { - let event = { - type: eventName, - preventDefault() {}, - target: element - }; - merge(event, options); - return event; -} - -// options is merged into the mocked `KeyboardEvent` data. -// Useful for simulating modifier keys, eg: -// triggerDelete(editor, DIRECTION.BACKWARD, {altKey: true}) -function triggerDelete(editor, direction=DIRECTION.BACKWARD, options={}) { - assertEditor(editor); - const keyCode = direction === DIRECTION.BACKWARD ? KEY_CODES.BACKSPACE : - KEY_CODES.DELETE; - let eventOptions = merge({keyCode}, options); - let event = createMockEvent('keydown', editor.element, eventOptions); - _triggerEditorEvent(editor, event); -} - -function triggerForwardDelete(editor, options) { - return triggerDelete(editor, DIRECTION.FORWARD, options); -} - -function triggerEnter(editor) { - assertEditor(editor); - let event = createMockEvent('keydown', editor.element, { keyCode: KEY_CODES.ENTER}); - _triggerEditorEvent(editor, event); -} - -// keyCodes and charCodes are similar but not the same. -function keyCodeForChar(letter) { - let keyCode; - switch (letter) { - case '.': - keyCode = KEY_CODES['.']; - break; - case '\n': - keyCode = KEY_CODES.ENTER; - break; - default: - keyCode = letter.charCodeAt(0); - } - return keyCode; -} - -function insertText(editor, string) { - if (!string && editor) { throw new Error('Must pass `editor` to `insertText`'); } - - string.split('').forEach(letter => { - let stop = false; - let keyCode = keyCodeForChar(letter); - let charCode = letter.charCodeAt(0); - let preventDefault = () => stop = true; - let keydown = createMockEvent('keydown', editor.element, { - keyCode, - charCode, - preventDefault - }); - let keypress = createMockEvent('keypress', editor.element, { - keyCode, - charCode, - }); - let keyup = createMockEvent('keyup', editor.element, { - keyCode, - charCode, - preventDefault - }); - - _triggerEditorEvent(editor, keydown); - if (stop) { - return; - } - _triggerEditorEvent(editor, keypress); - if (stop) { - return; - } - _triggerEditorEvent(editor, keyup); - }); -} - -function triggerKeyEvent(editor, type, options) { - let event = createMockEvent(type, editor.element, options); - _triggerEditorEvent(editor, event); -} - -// triggers a key sequence like cmd-B on the editor, to test out -// registered keyCommands -function triggerKeyCommand(editor, string, modifiers=[]) { - if (typeof modifiers === "number") { - modifiers = [modifiers]; // convert singular to array - } - let charCode = (KEY_CODES[string] || string.toUpperCase().charCodeAt(0)); - let keyCode = charCode; - let keyEvent = createMockEvent('keydown', editor.element, { - charCode, - keyCode, - shiftKey: contains(modifiers, MODIFIERS.SHIFT), - metaKey: contains(modifiers, MODIFIERS.META), - ctrlKey: contains(modifiers, MODIFIERS.CTRL) - }); - _triggerEditorEvent(editor, keyEvent); -} - -function triggerRightArrowKey(editor, modifier) { - if (!(editor instanceof Editor)) { - throw new Error('Must pass editor to triggerRightArrowKey'); - } - let keydown = createMockEvent('keydown', editor.element, { - keyCode: KEY_CODES.RIGHT, - shiftKey: modifier === MODIFIERS.SHIFT - }); - let keyup = createMockEvent('keyup', editor.element, { - keyCode: KEY_CODES.RIGHT, - shiftKey: modifier === MODIFIERS.SHIFT - }); - _triggerEditorEvent(editor, keydown); - _triggerEditorEvent(editor, keyup); -} - -function triggerLeftArrowKey(editor, modifier) { - assertEditor(editor); - let keydown = createMockEvent('keydown', editor.element, { - keyCode: KEY_CODES.LEFT, - shiftKey: modifier === MODIFIERS.SHIFT - }); - let keyup = createMockEvent('keyup', editor.element, { - keyCode: KEY_CODES.LEFT, - shiftKey: modifier === MODIFIERS.SHIFT - }); - _triggerEditorEvent(editor, keydown); - _triggerEditorEvent(editor, keyup); -} - -// Allows our fake copy and paste events to communicate with each other. -const lastCopyData = {}; -function triggerCopyEvent(editor) { - let eventData = { - clipboardData: { - setData(type, value) { - lastCopyData[type] = value; - } - } - }; - - let event = createMockEvent('copy', editor.element, eventData); - _triggerEditorEvent(editor, event); -} - -function triggerCutEvent(editor) { - let event = createMockEvent('cut', editor.element, { - clipboardData: { - setData(type, value) { lastCopyData[type] = value; } - } - }); - _triggerEditorEvent(editor, event); -} - -function triggerPasteEvent(editor) { - let eventData = { - clipboardData: { - getData(type) { return lastCopyData[type]; } - } - }; - - let event = createMockEvent('paste', editor.element, eventData); - _triggerEditorEvent(editor, event); -} - -function triggerDropEvent(editor, {html, text, clientX, clientY}) { - if (!clientX || !clientY) { throw new Error('Must pass clientX, clientY'); } - let event = createMockEvent('drop', editor.element, { - clientX, - clientY, - dataTransfer: { - getData(mimeType) { - switch(mimeType) { - case MIME_TEXT_HTML: - return html; - case MIME_TEXT_PLAIN: - return text; - default: - throw new Error('invalid mime type ' + mimeType); - } - } - } - }); - - _triggerEditorEvent(editor, event); -} - -function getCopyData(type) { - return lastCopyData[type]; -} - -function setCopyData(type, value) { - lastCopyData[type] = value; -} - -function clearCopyData() { - Object.keys(lastCopyData).forEach(key => { - delete lastCopyData[key]; - }); -} - -function fromHTML(html) { - html = $.trim(html); - let div = document.createElement('div'); - div.innerHTML = html; - return div; -} - -/** - * Tests fail in IE when using `element.blur`, so remove focus by refocusing - * on another item instead of blurring the editor element - */ -function blur() { - let input = $(''); - input.appendTo('#qunit-fixture'); - input.focus(); -} - -function getData(element, name) { - if (element.dataset) { - return element.dataset[name]; - } else { - return element.getAttribute(dasherize(name)); - } -} - -const DOMHelper = { - moveCursorTo, - moveCursorWithoutNotifyingEditorTo, - selectRange, - selectText, - clearSelection, - triggerEvent, - build, - fromHTML, - KEY_CODES, - getCursorPosition, - getSelectedText, - triggerDelete, - triggerForwardDelete, - triggerEnter, - insertText, - triggerKeyEvent, - triggerKeyCommand, - triggerRightArrowKey, - triggerLeftArrowKey, - triggerCopyEvent, - triggerCutEvent, - triggerPasteEvent, - triggerDropEvent, - getCopyData, - setCopyData, - clearCopyData, - createMockEvent, - findTextNode, - blur, - getData -}; - -export { triggerEvent }; - -export default DOMHelper; diff --git a/tests/helpers/dom.ts b/tests/helpers/dom.ts new file mode 100644 index 000000000..296f5cdce --- /dev/null +++ b/tests/helpers/dom.ts @@ -0,0 +1,436 @@ +import { clearSelection } from 'mobiledoc-kit/utils/selection-utils' +import { forEach, contains } from 'mobiledoc-kit/utils/array-utils' +import KEY_CODES from 'mobiledoc-kit/utils/keycodes' +import { DIRECTION, MODIFIERS } from 'mobiledoc-kit/utils/key' +import { isTextNode } from 'mobiledoc-kit/utils/dom-utils' +import { merge } from 'mobiledoc-kit/utils/merge' +import { Editor } from 'mobiledoc-kit' +import { MIME_TEXT_PLAIN, MIME_TEXT_HTML } from 'mobiledoc-kit/utils/parse-utils' +import { dasherize } from 'mobiledoc-kit/utils/string-utils' +import { DOMEvent, DOMEventType } from 'mobiledoc-kit/editor/event-manager' +import { Dict } from 'mobiledoc-kit/utils/types' + +function assertEditor(editor: any): asserts editor is Editor { + if (!(editor instanceof Editor)) { + throw new Error('Must pass editor as first argument') + } +} + +// walks DOWN the dom from node to childNodes, returning the element +// for which `conditionFn(element)` is true +function walkDOMUntil(topNode: Node, conditionFn: (node: Node) => boolean = () => false) { + if (!topNode) { + throw new Error('Cannot call walkDOMUntil without a node') + } + let stack = [topNode] + let currentElement: Node + + while (stack.length) { + currentElement = stack.pop()! + + if (conditionFn(currentElement)) { + return currentElement + } + + forEach(currentElement.childNodes, el => stack.push(el)) + } +} + +function findTextNode(parentElement: Node, text: string) { + return walkDOMUntil(parentElement, node => { + return isTextNode(node) && node.textContent!.indexOf(text) !== -1 + }) +} + +function selectRange(startNode: Node, startOffset: number, endNode: Node, endOffset: number) { + clearSelection() + + const range = document.createRange() + range.setStart(startNode, startOffset) + range.setEnd(endNode, endOffset) + + const selection = window.getSelection()! + selection.addRange(range) +} + +function selectText( + editor: Editor, + startText: string, + startContainingElement = editor.element, + endText = startText, + endContainingElement = startContainingElement +) { + assertEditor(editor) + let startTextNode = findTextNode(startContainingElement, startText) + let endTextNode = findTextNode(endContainingElement, 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}"`) + } + + const startOffset = startTextNode.textContent!.indexOf(startText), + endOffset = endTextNode.textContent!.indexOf(endText) + endText.length + selectRange(startTextNode, startOffset, endTextNode, endOffset) + editor._readRangeFromDOM() +} + +function moveCursorWithoutNotifyingEditorTo( + _editor: Editor, + node: Node, + offset = 0, + endNode = node, + endOffset = offset +) { + selectRange(node, offset, endNode, endOffset) +} + +function moveCursorTo(editor: Editor, node: Node, offset = 0, endNode = node, endOffset = offset) { + assertEditor(editor) + if (!node) { + throw new Error('Cannot moveCursorTo node without node') + } + moveCursorWithoutNotifyingEditorTo(editor, node, offset, endNode, endOffset) + editor._readRangeFromDOM() +} + +function triggerEvent(node: Node, eventType: string) { + if (!node) { + throw new Error(`Attempted to trigger event "${eventType}" on undefined node`) + } + + let clickEvent = document.createEvent('MouseEvents') + clickEvent.initEvent(eventType, true, true) + return node.dispatchEvent(clickEvent) +} + +function _triggerEditorEvent(editor: Editor, event: DOMEvent) { + editor.triggerEvent(editor.element, event.type as DOMEventType, event) +} + +function _buildDOM(tagName: string, attributes: Dict = {}, children = []) { + const el = document.createElement(tagName) + Object.keys(attributes).forEach(k => el.setAttribute(k, attributes[k])) + children.forEach(child => el.appendChild(child)) + return el +} + +_buildDOM.text = (string: string) => { + return document.createTextNode(string) +} + +/** + * Usage: + * build(t => + * t('div', attributes={}, children=[ + * t('b', {}, [ + * t.text('I am a bold text node') + * ]) + * ]) + * ); + */ +function build(tree: (t: typeof _buildDOM) => HTMLElement) { + return tree(_buildDOM) +} + +function getSelectedText() { + const selection = window.getSelection()! + if (selection.rangeCount === 0) { + return null + } else if (selection.rangeCount > 1) { + // FIXME? + throw new Error('Unable to get selected text for multiple ranges') + } else { + return selection.toString() + } +} + +// returns the node and the offset that the cursor is on +function getCursorPosition() { + const selection = window.getSelection()! + return { + node: selection.anchorNode, + offset: selection.anchorOffset, + } +} + +function createMockEvent(eventName: string, element: HTMLElement, options = {}) { + let event = { + type: eventName, + preventDefault() {}, + target: element, + } + merge(event, options) + return (event as unknown) as DOMEvent +} + +// options is merged into the mocked `KeyboardEvent` data. +// Useful for simulating modifier keys, eg: +// triggerDelete(editor, DIRECTION.BACKWARD, {altKey: true}) +function triggerDelete(editor: Editor, direction = DIRECTION.BACKWARD, options = {}) { + assertEditor(editor) + const keyCode = direction === DIRECTION.BACKWARD ? KEY_CODES.BACKSPACE : KEY_CODES.DELETE + let eventOptions = merge({ keyCode }, options) + let event = createMockEvent('keydown', editor.element, eventOptions) + _triggerEditorEvent(editor, event) +} + +function triggerForwardDelete(editor: Editor, options: Dict) { + return triggerDelete(editor, DIRECTION.FORWARD, options) +} + +function triggerEnter(editor: Editor) { + assertEditor(editor) + let event = createMockEvent('keydown', editor.element, { keyCode: KEY_CODES.ENTER }) + _triggerEditorEvent(editor, event) +} + +// keyCodes and charCodes are similar but not the same. +function keyCodeForChar(letter: string) { + let keyCode + switch (letter) { + case '.': + keyCode = KEY_CODES['.'] + break + case '\n': + keyCode = KEY_CODES.ENTER + break + default: + keyCode = letter.charCodeAt(0) + } + return keyCode +} + +function insertText(editor: Editor, string: string) { + if (!string && editor) { + throw new Error('Must pass `editor` to `insertText`') + } + + string.split('').forEach(letter => { + let stop = false + let keyCode = keyCodeForChar(letter) + let charCode = letter.charCodeAt(0) + let preventDefault = () => (stop = true) + let keydown = createMockEvent('keydown', editor.element, { + keyCode, + charCode, + preventDefault, + }) + let keypress = createMockEvent('keypress', editor.element, { + keyCode, + charCode, + }) + let keyup = createMockEvent('keyup', editor.element, { + keyCode, + charCode, + preventDefault, + }) + + _triggerEditorEvent(editor, keydown) + if (stop) { + return + } + _triggerEditorEvent(editor, keypress) + if (stop) { + return + } + _triggerEditorEvent(editor, keyup) + }) +} + +function triggerKeyEvent(editor: Editor, type: string, options: Dict) { + let event = createMockEvent(type, editor.element, options) + _triggerEditorEvent(editor, event) +} + +// triggers a key sequence like cmd-B on the editor, to test out +// registered keyCommands +function triggerKeyCommand(editor: Editor, string: keyof typeof KEY_CODES, modifiers: number | number[] = []) { + if (typeof modifiers === 'number') { + modifiers = [modifiers] // convert singular to array + } + let charCode = KEY_CODES[string] || string.toUpperCase().charCodeAt(0) + let keyCode = charCode + let keyEvent = createMockEvent('keydown', editor.element, { + charCode, + keyCode, + shiftKey: contains(modifiers, MODIFIERS.SHIFT), + metaKey: contains(modifiers, MODIFIERS.META), + ctrlKey: contains(modifiers, MODIFIERS.CTRL), + }) + _triggerEditorEvent(editor, keyEvent) +} + +function triggerRightArrowKey(editor: Editor, modifier: number) { + if (!(editor instanceof Editor)) { + throw new Error('Must pass editor to triggerRightArrowKey') + } + let keydown = createMockEvent('keydown', editor.element, { + keyCode: KEY_CODES.RIGHT, + shiftKey: modifier === MODIFIERS.SHIFT, + }) + let keyup = createMockEvent('keyup', editor.element, { + keyCode: KEY_CODES.RIGHT, + shiftKey: modifier === MODIFIERS.SHIFT, + }) + _triggerEditorEvent(editor, keydown) + _triggerEditorEvent(editor, keyup) +} + +function triggerLeftArrowKey(editor: Editor, modifier: number) { + assertEditor(editor) + let keydown = createMockEvent('keydown', editor.element, { + keyCode: KEY_CODES.LEFT, + shiftKey: modifier === MODIFIERS.SHIFT, + }) + let keyup = createMockEvent('keyup', editor.element, { + keyCode: KEY_CODES.LEFT, + shiftKey: modifier === MODIFIERS.SHIFT, + }) + _triggerEditorEvent(editor, keydown) + _triggerEditorEvent(editor, keyup) +} + +// Allows our fake copy and paste events to communicate with each other. +const lastCopyData: Dict = {} +function triggerCopyEvent(editor: Editor) { + let eventData = { + clipboardData: { + setData(type: string, value: unknown) { + lastCopyData[type] = value + }, + }, + } + + let event = createMockEvent('copy', editor.element, eventData) + _triggerEditorEvent(editor, event) +} + +function triggerCutEvent(editor: Editor) { + let event = createMockEvent('cut', editor.element, { + clipboardData: { + setData(type: string, value: unknown) { + lastCopyData[type] = value + }, + }, + }) + _triggerEditorEvent(editor, event) +} + +function triggerPasteEvent(editor: Editor) { + let eventData = { + clipboardData: { + getData(type: string) { + return lastCopyData[type] + }, + }, + } + + let event = createMockEvent('paste', editor.element, eventData) + _triggerEditorEvent(editor, event) +} + +function triggerDropEvent( + editor: Editor, + { html, text, clientX, clientY }: { html: string; text: string; clientX: number; clientY: number } +) { + if (!clientX || !clientY) { + throw new Error('Must pass clientX, clientY') + } + let event = createMockEvent('drop', editor.element, { + clientX, + clientY, + dataTransfer: { + getData(mimeType: string) { + switch (mimeType) { + case MIME_TEXT_HTML: + return html + case MIME_TEXT_PLAIN: + return text + default: + throw new Error('invalid mime type ' + mimeType) + } + }, + }, + }) + + _triggerEditorEvent(editor, event) +} + +function getCopyData(type: string) { + return lastCopyData[type] +} + +function setCopyData(type: string, value: unknown) { + lastCopyData[type] = value +} + +function clearCopyData() { + Object.keys(lastCopyData).forEach(key => { + delete lastCopyData[key] + }) +} + +function fromHTML(html: string) { + html = $.trim(html) + let div = document.createElement('div') + div.innerHTML = html + return div +} + +/** + * Tests fail in IE when using `element.blur`, so remove focus by refocusing + * on another item instead of blurring the editor element + */ +function blur() { + let input = $('') + input.appendTo('#qunit-fixture') + input.focus() +} + +function getData(element: HTMLElement, name: string) { + if (element.dataset) { + return element.dataset[name] + } else { + return element.getAttribute(dasherize(name)) + } +} + +const DOMHelper = { + moveCursorTo, + moveCursorWithoutNotifyingEditorTo, + selectRange, + selectText, + clearSelection, + triggerEvent, + build, + fromHTML, + KEY_CODES, + getCursorPosition, + getSelectedText, + triggerDelete, + triggerForwardDelete, + triggerEnter, + insertText, + triggerKeyEvent, + triggerKeyCommand, + triggerRightArrowKey, + triggerLeftArrowKey, + triggerCopyEvent, + triggerCutEvent, + triggerPasteEvent, + triggerDropEvent, + getCopyData, + setCopyData, + clearCopyData, + createMockEvent, + findTextNode, + blur, + getData, +} + +export { triggerEvent } + +export default DOMHelper diff --git a/tests/helpers/editor.js b/tests/helpers/editor.js deleted file mode 100644 index e4628d554..000000000 --- a/tests/helpers/editor.js +++ /dev/null @@ -1,61 +0,0 @@ -import PostAbstractHelpers from './post-abstract'; -import Editor from 'mobiledoc-kit/editor/editor'; -import MobiledocRenderer from 'mobiledoc-kit/renderers/mobiledoc/0-3-1'; - -function retargetPosition(position, toPost) { - let fromPost = position.section.post; - let sectionIndex; - let retargetedPosition; - fromPost.walkAllLeafSections((section,index) => { - if (sectionIndex !== undefined) { return; } - if (section === position.section) { sectionIndex = index; } - }); - if (sectionIndex === undefined) { - throw new Error('`retargetPosition` could not find section index'); - } - toPost.walkAllLeafSections((section, index) => { - if (retargetedPosition) { return; } - if (index === sectionIndex) { - retargetedPosition = section.toPosition(position.offset); - } - }); - if (!retargetedPosition) { - throw new Error('`retargetPosition` could not find target section'); - } - return retargetedPosition; -} - -function retargetRange(range, toPost) { - let newHead = retargetPosition(range.head, toPost); - let newTail = retargetPosition(range.tail, toPost); - - return newHead.toRange(newTail); -} - -function buildFromText(texts, editorOptions={}) { - let renderElement = editorOptions.element; - delete editorOptions.element; - - let beforeRender = editorOptions.beforeRender || function() {}; - delete editorOptions.beforeRender; - - let {post, range} = PostAbstractHelpers.buildFromText(texts); - let mobiledoc = MobiledocRenderer.render(post); - editorOptions.mobiledoc = mobiledoc; - let editor = new Editor(editorOptions); - if (renderElement) { - beforeRender(editor); - editor.render(renderElement); - if (range) { - range = retargetRange(range, editor.post); - editor.selectRange(range); - } - } - return editor; -} - -export { - buildFromText, - retargetRange, - retargetPosition -}; diff --git a/tests/helpers/editor.ts b/tests/helpers/editor.ts new file mode 100644 index 000000000..d8a45444b --- /dev/null +++ b/tests/helpers/editor.ts @@ -0,0 +1,79 @@ +import PostAbstractHelpers from './post-abstract' +import Editor, { EditorOptions } from 'mobiledoc-kit/editor/editor' +import MobiledocRenderer from 'mobiledoc-kit/renderers/mobiledoc/0-3-1' +import Post from 'mobiledoc-kit/models/post' +import Position from 'mobiledoc-kit/utils/cursor/position' +import Range from 'mobiledoc-kit/utils/cursor/range' +import { unwrap } from 'mobiledoc-kit/utils/assert' +import { Maybe } from 'mobiledoc-kit/utils/types' + +function retargetPosition(position: Position, toPost: Post) { + let fromPost = unwrap(position.section!.post) + let sectionIndex: Maybe + let retargetedPosition: Maybe + + fromPost.walkAllLeafSections((section, index) => { + if (sectionIndex !== undefined) { + return + } + if (section === position.section) { + sectionIndex = index + } + }) + + if (sectionIndex === undefined) { + throw new Error('`retargetPosition` could not find section index') + } + + toPost.walkAllLeafSections((section, index) => { + if (retargetedPosition) { + return + } + if (index === sectionIndex) { + retargetedPosition = section.toPosition(position.offset) + } + }) + + if (!retargetedPosition) { + throw new Error('`retargetPosition` could not find target section') + } + + return retargetedPosition +} + +function retargetRange(range: Range, toPost: Post) { + let newHead = retargetPosition(range.head, toPost) + let newTail = retargetPosition(range.tail, toPost) + + return newHead.toRange(newTail) +} + +type BuildFromTextOptions = EditorOptions & { + element?: HTMLElement + beforeRender?: (editor: Editor) => void +} + +function buildFromText(texts: string | string[], editorOptions: BuildFromTextOptions = {}) { + let renderElement = editorOptions.element + delete editorOptions.element + + let beforeRender = editorOptions.beforeRender || function () {} + delete editorOptions.beforeRender + + let { post, range } = PostAbstractHelpers.buildFromText(texts) + let mobiledoc = MobiledocRenderer.render(post) + editorOptions.mobiledoc = mobiledoc + let editor = new Editor(editorOptions) + + if (renderElement) { + beforeRender(editor) + editor.render(renderElement) + if (range) { + range = retargetRange(range, editor.post) + editor.selectRange(range) + } + } + return editor +} + +export { buildFromText, retargetRange, retargetPosition } diff --git a/tests/helpers/mobiledoc.js b/tests/helpers/mobiledoc.js deleted file mode 100644 index 12ccb60ec..000000000 --- a/tests/helpers/mobiledoc.js +++ /dev/null @@ -1,72 +0,0 @@ -import PostAbstractHelpers from './post-abstract'; -import mobiledocRenderers from 'mobiledoc-kit/renderers/mobiledoc'; -import MobiledocRenderer_0_2, { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_2 } from 'mobiledoc-kit/renderers/mobiledoc/0-2'; -import MobiledocRenderer_0_3, { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_3 } from 'mobiledoc-kit/renderers/mobiledoc/0-3'; -import MobiledocRenderer_0_3_1, { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_3_1 } from 'mobiledoc-kit/renderers/mobiledoc/0-3-1'; -import MobiledocRenderer_0_3_2, { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_3_2 } from 'mobiledoc-kit/renderers/mobiledoc/0-3-2'; -import Editor from 'mobiledoc-kit/editor/editor'; -import Range from 'mobiledoc-kit/utils/cursor/range'; -import { mergeWithOptions } from 'mobiledoc-kit/utils/merge'; - -/* - * usage: - * build(({post, section, marker, markup}) => - * post([ - * section('P', [ - * marker('some text', [markup('B')]) - * ]) - * }) - * ) - * @return Mobiledoc - */ -function build(treeFn, version) { - let post = PostAbstractHelpers.build(treeFn); - switch (version) { - case MOBILEDOC_VERSION_0_2: - return MobiledocRenderer_0_2.render(post); - case MOBILEDOC_VERSION_0_3: - return MobiledocRenderer_0_3.render(post); - case MOBILEDOC_VERSION_0_3_1: - return MobiledocRenderer_0_3_1.render(post); - case MOBILEDOC_VERSION_0_3_2: - return MobiledocRenderer_0_3_2.render(post); - case undefined: - case null: - return mobiledocRenderers.render(post); - default: - throw new Error(`Unknown version of mobiledoc renderer requested: ${version}`); - } -} - -function renderPostInto(element, post, editorOptions={}) { - let mobiledoc = mobiledocRenderers.render(post); - mergeWithOptions(editorOptions, {mobiledoc}); - let editor = new Editor(editorOptions); - editor.render(element); - return editor; -} - -function renderInto(element, treeFn, editorOptions={}) { - let mobiledoc = build(treeFn); - mergeWithOptions(editorOptions, {mobiledoc}); - let editor = new Editor(editorOptions); - editor.render(element); - return editor; -} - -// In Firefox, if the window isn't active (which can happen when running tests -// at SauceLabs), the editor element won't have the selection. This helper method -// ensures that it has a cursor selection. -// See https://github.com/bustle/mobiledoc-kit/issues/388 -function renderIntoAndFocusTail(editorElement, treeFn, options={}) { - let editor = renderInto(editorElement, treeFn, options); - editor.selectRange(new Range(editor.post.tailPosition())); - return editor; -} - -export default { - build, - renderInto, - renderPostInto, - renderIntoAndFocusTail -}; diff --git a/tests/helpers/mobiledoc.ts b/tests/helpers/mobiledoc.ts new file mode 100644 index 000000000..ea1f7e753 --- /dev/null +++ b/tests/helpers/mobiledoc.ts @@ -0,0 +1,55 @@ +import PostAbstractHelpers, { BuildCallback } from './post-abstract' +import mobiledocRenderers, { MobiledocVersion } from 'mobiledoc-kit/renderers/mobiledoc' +import Editor, { EditorOptions } from 'mobiledoc-kit/editor/editor' +import Range from 'mobiledoc-kit/utils/cursor/range' +import { mergeWithOptions } from 'mobiledoc-kit/utils/merge' +import Post from 'mobiledoc-kit/models/post' + +/* + * usage: + * build(({post, section, marker, markup}) => + * post([ + * section('P', [ + * marker('some text', [markup('B')]) + * ]) + * }) + * ) + * @return Mobiledoc + */ +function build(treeFn: BuildCallback, version?: MobiledocVersion) { + let post = PostAbstractHelpers.build(treeFn) + return mobiledocRenderers.render(post, version) +} + +function renderPostInto(element: HTMLElement, post: Post, editorOptions = {}) { + let mobiledoc = mobiledocRenderers.render(post) + mergeWithOptions(editorOptions, { mobiledoc }) + let editor = new Editor(editorOptions) + editor.render(element) + return editor +} + +function renderInto(element: HTMLElement, treeFn: BuildCallback, editorOptions: EditorOptions = {}) { + let mobiledoc = build(treeFn) + mergeWithOptions(editorOptions, { mobiledoc }) + let editor = new Editor(editorOptions) + editor.render(element) + return editor +} + +// In Firefox, if the window isn't active (which can happen when running tests +// at SauceLabs), the editor element won't have the selection. This helper method +// ensures that it has a cursor selection. +// See https://github.com/bustle/mobiledoc-kit/issues/388 +function renderIntoAndFocusTail(editorElement: HTMLElement, treeFn: BuildCallback, options: EditorOptions = {}) { + let editor = renderInto(editorElement, treeFn, options) + editor.selectRange(new Range(editor.post.tailPosition())) + return editor +} + +export default { + build, + renderInto, + renderPostInto, + renderIntoAndFocusTail, +} diff --git a/tests/helpers/mock-editor.js b/tests/helpers/mock-editor.js deleted file mode 100644 index 2d8fbc7b4..000000000 --- a/tests/helpers/mock-editor.js +++ /dev/null @@ -1,24 +0,0 @@ -import PostEditor from 'mobiledoc-kit/editor/post'; -import Range from 'mobiledoc-kit/utils/cursor/range'; - -class MockEditor { - constructor(builder) { - this.builder = builder; - this.range = Range.blankRange(); - } - run(callback) { - let postEditor = new PostEditor(this); - postEditor.begin(); - let result = callback(postEditor); - postEditor.end(); - return result; - } - rerender() {} - _postDidChange() {} - selectRange(range) { - this._renderedRange = range; - } - _readRangeFromDOM() {} -} - -export default MockEditor; diff --git a/tests/helpers/mock-editor.ts b/tests/helpers/mock-editor.ts new file mode 100644 index 000000000..344f8ec84 --- /dev/null +++ b/tests/helpers/mock-editor.ts @@ -0,0 +1,21 @@ +import Range from 'mobiledoc-kit/utils/cursor/range' +import PostNodeBuilder from 'mobiledoc-kit/models/post-node-builder' + +export default class MockEditor { + builder: PostNodeBuilder + range: Range + _renderedRange!: Range + + constructor(builder: PostNodeBuilder) { + this.builder = builder + this.range = Range.blankRange() + } + + selectRange(range: Range) { + this._renderedRange = range + } + + rerender() {} + _postDidChange() {} + _readRangeFromDOM() {} +} diff --git a/tests/helpers/post-abstract.js b/tests/helpers/post-abstract.js deleted file mode 100644 index 73c3a9932..000000000 --- a/tests/helpers/post-abstract.js +++ /dev/null @@ -1,287 +0,0 @@ -import PostNodeBuilder from 'mobiledoc-kit/models/post-node-builder'; - -/* - * usage: - * Helpers.postAbstract.build(({post, section, marker, markup}) => - * post([ - * section('P', [ - * marker('some text', [markup('B')]) - * ]) - * }) - * ) - */ -function build(treeFn) { - let builder = new PostNodeBuilder(); - - const simpleBuilder = { - post : (...args) => builder.createPost(...args), - markupSection : (...args) => builder.createMarkupSection(...args), - markup : (...args) => builder.createMarkup(...args), - marker : (...args) => builder.createMarker(...args), - listSection : (...args) => builder.createListSection(...args), - listItem : (...args) => builder.createListItem(...args), - cardSection : (...args) => builder.createCardSection(...args), - imageSection : (...args) => builder.createImageSection(...args), - atom : (...args) => builder.createAtom(...args) - }; - - return treeFn(simpleBuilder); -} - -let cardRegex = /\[(.*)\]/; -let imageSectionRegex = /^\{(.*)\}/; -let markupRegex = /\*/g; -let listStartRegex = /^\* /; -let cursorRegex = /<|>|\|/g; - -function parsePositionOffsets(text) { - let offsets = {}; - - if (cardRegex.test(text)) { - [['|','solo'],['<','start'],['>','end']].forEach(([char, type]) => { - if (text.indexOf(char) !== -1) { - offsets[type] = text.indexOf(char) === 0 ? 0 : 1; - } - }); - } else { - if (listStartRegex.test(text)) { - text = text.replace(listStartRegex, ''); - } - text = text.replace(markupRegex,''); - if (text.indexOf('|') !== -1) { - offsets.solo = text.indexOf('|'); - } else if (text.indexOf('<') !== -1 || text.indexOf('>') !== -1) { - let hasStart = text.indexOf('<') !== -1; - let hasEnd = text.indexOf('>') !== -1; - if (hasStart) { - offsets.start = text.indexOf('<'); - text = text.replace(/'); - } - } - } - - return offsets; -} - -const DEFAULT_ATOM_NAME = 'some-atom'; -const DEFAULT_ATOM_VALUE = '@atom'; - -const MARKUP_CHARS = { - '*': 'b', - '_': 'em' -}; - -function parseTextIntoAtom(text, builder) { - let markers = []; - let atomIndex = text.indexOf('@'); - let afterAtomIndex = atomIndex + 1; - let atomName = DEFAULT_ATOM_NAME, - atomValue = DEFAULT_ATOM_VALUE, - atomPayload = {}; - - // If "@" is followed by "( ... json ... )", parse the json data - if (text[atomIndex+1] === "(") { - let jsonStartIndex = atomIndex+1; - let jsonEndIndex = text.indexOf(")",jsonStartIndex); - afterAtomIndex = jsonEndIndex + 1; - if (jsonEndIndex === -1) { - throw new Error('Atom JSON data had unmatched "(": ' + text); - } - let jsonString = text.slice(jsonStartIndex+1, jsonEndIndex); - jsonString = "{" + jsonString + "}"; - try { - let json = JSON.parse(jsonString); - if (json.name) { atomName = json.name; } - if (json.value) { atomValue = json.value; } - if (json.payload) { atomPayload = json.payload; } - } catch(e) { - throw new Error('Failed to parse atom JSON data string: ' + jsonString + ', ' + e); - } - } - - // create the atom - let atom = builder.atom(atomName, atomValue, atomPayload); - - // recursively parse the remaining text pieces - let pieces = [text.slice(0, atomIndex), atom, text.slice(afterAtomIndex)]; - - // join the markers together - pieces.forEach((piece, index) => { - if (index === 1) { // atom - markers.push(piece); - } else if (piece.length) { - markers = markers.concat(parseTextIntoMarkers(piece, builder)); - } - }); - - return markers; -} - -function parseTextWithMarkup(text, builder) { - let markers = []; - let markup, char; - Object.keys(MARKUP_CHARS).forEach(key => { - if (markup) { return; } - if (text.indexOf(key) !== -1) { - markup = builder.markup(MARKUP_CHARS[key]); - char = key; - } - }); - if (!markup) { throw new Error(`Failed to find markup in text: ${text}`); } - - let startIndex = text.indexOf(char); - let endIndex = text.indexOf(char, startIndex+1); - if (endIndex === -1) { throw new Error(`Malformed text: char ${char} do not match`); } - - let pieces = [text.slice(0, startIndex), - text.slice(startIndex+1, endIndex), - text.slice(endIndex+1)]; - pieces.forEach((piece, index) => { - if (index === 1) { // marked-up text - markers.push(builder.marker(piece, [markup])); - } else { - markers = markers.concat(parseTextIntoMarkers(piece, builder)); - } - }); - - return markers; -} - -function parseTextIntoMarkers(text, builder) { - text = text.replace(cursorRegex,''); - let markers = []; - - let hasAtom = text.indexOf('@') !== -1; - let hasMarkup = false; - Object.keys(MARKUP_CHARS).forEach(key => { - if (text.indexOf(key) !== -1) { hasMarkup = true; } - }); - - if (hasAtom) { - markers = markers.concat(parseTextIntoAtom(text, builder)); - } else if (hasMarkup) { - markers = markers.concat(parseTextWithMarkup(text, builder)); - } else if (text.length) { - markers.push(builder.marker(text)); - } - - return markers; -} - -function parseSingleText(text, builder) { - let section, positions = {}; - - let offsets = parsePositionOffsets(text); - - if (cardRegex.test(text)) { - section = builder.cardSection(cardRegex.exec(text)[1]); - } else if (imageSectionRegex.test(text)) { - section = builder.imageSection(imageSectionRegex.exec(text)[1]); - } else { - let type = 'p'; - if (listStartRegex.test(text)) { - text = text.replace(listStartRegex,''); - type = 'ul'; - } - - let markers = parseTextIntoMarkers(text, builder); - - switch (type) { - case 'p': - section = builder.markupSection('p', markers); - break; - case 'ul': - section = builder.listItem(markers); - break; - } - } - - ['start','end','solo'].forEach(type => { - if (offsets[type] !== undefined) { - positions[type] = section.toPosition(offsets[type]); - } - }); - - return { section, positions }; -} - -/** - * Shorthand to create a mobiledoc simply. - * Pass a string or an array of strings. - * - * Returns { post, range }, a post built from the mobiledoc and a range. - * - * Use "|" to indicate the cursor position or "<" and ">" to indicate a range. - * Use "[card-name]" to indicate a card - * Use asterisks to indicate bold text: "abc *bold* def" - * Use "@" to indicate an atom, default values for name,value,payload are DEFAULT_ATOM_NAME,DEFAULT_ATOM_VALUE,{} - * Use "@(name, value, payload)" to specify name,value and/or payload for an atom. The string from `(` to `)` is parsed as - * JSON, e.g.: '@("name": "my-atom", "value": "abc", "payload": {"foo": "bar"})' -> atom named "my-atom" with value 'abc', payload {foo: 'bar'} - * Use "* " at the start of the string to indicate a list item ("ul") - * - * Examples: - * buildFromText("abc") -> { post } with 1 markup section ("p") with text "abc" - * buildFromText(["abc","def"]) -> { post } with 2 markups sections ("p") with texts "abc" and "def" - * buildFromText("abc|def") -> { post, range } where range is collapsed at offset 3 (after the "c") - * buildFromText(["abcdef","[some-card]","def"]) -> { post } with [MarkupSection, Card, MarkupSection] sections - * buildFromText(["abc", "{def}", "def"]) -> { post } with [MarkupSection, ImageSection, MarkupSection] sections - * buildFromText(["* item 1", "* item 2"]) -> { post } with a ListSection with 2 ListItems - * buildFromText([""]) -> { post, range } where range is the entire post (before the "a" to after the "i") - */ -function buildFromText(texts) { - if (!Array.isArray(texts)) { texts = [texts]; } - let positions = {}; - - let post = build(builder => { - let sections = []; - let curList; - texts.forEach((text, index) => { - let { section, positions: _positions } = parseSingleText(text, builder); - let lastText = index === texts.length - 1; - - if (curList) { - if (section.isListItem) { - curList.items.append(section); - } else { - sections.push(curList); - sections.push(section); - curList = null; - } - } else if (section.isListItem) { - curList = builder.listSection('ul', [section]); - } else { - sections.push(section); - } - - if (lastText && curList) { - sections.push(curList); - } - - if (_positions.start) { positions.start = _positions.start; } - if (_positions.end) { positions.end = _positions.end; } - if (_positions.solo) { positions.solo = _positions.solo; } - }); - - return builder.post(sections); - }); - - let range; - if (positions.start) { - if (!positions.end) { throw new Error(`startPos but no endPos ${texts.join('\n')}`); } - range = positions.start.toRange(positions.end); - } else if (positions.solo) { - range = positions.solo.toRange(); - } - - return { post, range }; -} - - -export default { - build, - buildFromText, - DEFAULT_ATOM_NAME -}; diff --git a/tests/helpers/post-abstract.ts b/tests/helpers/post-abstract.ts new file mode 100644 index 000000000..836fb7d11 --- /dev/null +++ b/tests/helpers/post-abstract.ts @@ -0,0 +1,351 @@ +import PostNodeBuilder from 'mobiledoc-kit/models/post-node-builder' +import Post from 'mobiledoc-kit/models/post' +import { Dict, Maybe } from 'mobiledoc-kit/utils/types' +import Atom from 'mobiledoc-kit/models/atom' +import { keys } from 'mobiledoc-kit/utils/object-utils' +import Markup from 'mobiledoc-kit/models/markup' +import Markuperable from 'mobiledoc-kit/utils/markuperable' +import Section from 'mobiledoc-kit/models/_section' +import { unwrap } from 'mobiledoc-kit/utils/assert' +import Position from 'mobiledoc-kit/utils/cursor/position' +import ListSection from 'mobiledoc-kit/models/list-section' +import { isListItem } from 'mobiledoc-kit/models/list-item' +import { Cloneable } from 'mobiledoc-kit/models/_cloneable' + +type ProxyMethod any> = (...args: Parameters) => ReturnType + +export interface SimplePostBuilder { + post: ProxyMethod + markupSection: ProxyMethod + markup: ProxyMethod + marker: ProxyMethod + listSection: ProxyMethod + listItem: ProxyMethod + cardSection: ProxyMethod + imageSection: ProxyMethod + atom: ProxyMethod +} + +export type BuildCallback = (builder: SimplePostBuilder) => Post +/* + * usage: + * Helpers.postAbstract.build(({post, section, marker, markup}) => + * post([ + * section('P', [ + * marker('some text', [markup('B')]) + * ]) + * }) + * ) + */ +function build(treeFn: BuildCallback) { + let builder = new PostNodeBuilder() + + const simpleBuilder: SimplePostBuilder = { + post: (...args) => builder.createPost(...args), + markupSection: (...args) => builder.createMarkupSection(...args), + markup: (...args) => builder.createMarkup(...args), + marker: (...args) => builder.createMarker(...args), + listSection: (...args) => builder.createListSection(...args), + listItem: (...args) => builder.createListItem(...args), + cardSection: (...args) => builder.createCardSection(...args), + imageSection: (...args) => builder.createImageSection(...args), + atom: (...args) => builder.createAtom(...args), + } + + return treeFn(simpleBuilder) +} + +let cardRegex = /\[(.*)\]/ +let imageSectionRegex = /^\{(.*)\}/ +let markupRegex = /\*/g +let listStartRegex = /^\* / +let cursorRegex = /<|>|\|/g + +function parsePositionOffsets(text: string) { + let offsets: Dict = {} + + if (cardRegex.test(text)) { + ;[ + ['|', 'solo'], + ['<', 'start'], + ['>', 'end'], + ].forEach(([char, type]) => { + if (text.indexOf(char) !== -1) { + offsets[type] = text.indexOf(char) === 0 ? 0 : 1 + } + }) + } else { + if (listStartRegex.test(text)) { + text = text.replace(listStartRegex, '') + } + text = text.replace(markupRegex, '') + if (text.indexOf('|') !== -1) { + offsets.solo = text.indexOf('|') + } else if (text.indexOf('<') !== -1 || text.indexOf('>') !== -1) { + let hasStart = text.indexOf('<') !== -1 + let hasEnd = text.indexOf('>') !== -1 + if (hasStart) { + offsets.start = text.indexOf('<') + text = text.replace(/') + } + } + } + + return offsets +} + +const DEFAULT_ATOM_NAME = 'some-atom' +const DEFAULT_ATOM_VALUE = '@atom' + +const MARKUP_CHARS = { + '*': 'b', + _: 'em', +} + +function parseTextIntoAtom(text: string, builder: SimplePostBuilder) { + let markers: Markuperable[] = [] + let atomIndex = text.indexOf('@') + let afterAtomIndex = atomIndex + 1 + let atomName = DEFAULT_ATOM_NAME, + atomValue = DEFAULT_ATOM_VALUE, + atomPayload = {} + + // If "@" is followed by "( ... json ... )", parse the json data + if (text[atomIndex + 1] === '(') { + let jsonStartIndex = atomIndex + 1 + let jsonEndIndex = text.indexOf(')', jsonStartIndex) + afterAtomIndex = jsonEndIndex + 1 + if (jsonEndIndex === -1) { + throw new Error('Atom JSON data had unmatched "(": ' + text) + } + let jsonString = text.slice(jsonStartIndex + 1, jsonEndIndex) + jsonString = '{' + jsonString + '}' + try { + let json = JSON.parse(jsonString) + if (json.name) { + atomName = json.name + } + if (json.value) { + atomValue = json.value + } + if (json.payload) { + atomPayload = json.payload + } + } catch (e) { + throw new Error('Failed to parse atom JSON data string: ' + jsonString + ', ' + e) + } + } + + // create the atom + let atom = builder.atom(atomName, atomValue, atomPayload) + + // recursively parse the remaining text pieces + let pieces = [text.slice(0, atomIndex), atom, text.slice(afterAtomIndex)] + + // join the markers together + pieces.forEach((piece, index) => { + if (index === 1) { + // atom + markers.push(piece as Atom) + } else if (piece.length) { + markers = markers.concat(parseTextIntoMarkers(piece as string, builder)) + } + }) + + return markers +} + +function parseTextWithMarkup(text: string, builder: SimplePostBuilder) { + let markers: Markuperable[] = [] + let markup!: Markup + let char!: string + + keys(MARKUP_CHARS).forEach(key => { + if (markup) { + return + } + if (text.indexOf(key) !== -1) { + markup = builder.markup(MARKUP_CHARS[key]) + char = key + } + }) + + if (!markup) { + throw new Error(`Failed to find markup in text: ${text}`) + } + + let startIndex = text.indexOf(char) + let endIndex = text.indexOf(char, startIndex + 1) + if (endIndex === -1) { + throw new Error(`Malformed text: char ${char} do not match`) + } + + let pieces = [text.slice(0, startIndex), text.slice(startIndex + 1, endIndex), text.slice(endIndex + 1)] + pieces.forEach((piece, index) => { + if (index === 1) { + // marked-up text + markers.push(builder.marker(piece, [markup])) + } else { + markers = markers.concat(parseTextIntoMarkers(piece, builder)) + } + }) + + return markers +} + +function parseTextIntoMarkers(text: string, builder: SimplePostBuilder) { + text = text.replace(cursorRegex, '') + let markers: Markuperable[] = [] + + let hasAtom = text.indexOf('@') !== -1 + let hasMarkup = false + + Object.keys(MARKUP_CHARS).forEach(key => { + if (text.indexOf(key) !== -1) { + hasMarkup = true + } + }) + + if (hasAtom) { + markers = markers.concat(parseTextIntoAtom(text, builder)) + } else if (hasMarkup) { + markers = markers.concat(parseTextWithMarkup(text, builder)) + } else if (text.length) { + markers.push(builder.marker(text)) + } + + return markers +} + +function parseSingleText(text: string, builder: SimplePostBuilder) { + let section!: Cloneable
+ let positions: Positions = {} + + let offsets = parsePositionOffsets(text) + + if (cardRegex.test(text)) { + section = builder.cardSection(unwrap(cardRegex.exec(text))[1]) + } else if (imageSectionRegex.test(text)) { + section = builder.imageSection(unwrap(imageSectionRegex.exec(text))[1]) + } else { + let type = 'p' + if (listStartRegex.test(text)) { + text = text.replace(listStartRegex, '') + type = 'ul' + } + + let markers = parseTextIntoMarkers(text, builder) + + switch (type) { + case 'p': + section = builder.markupSection('p', markers) + break + case 'ul': + section = builder.listItem(markers) + break + } + } + + ;(['start', 'end', 'solo']).forEach(type => { + if (offsets[type] !== undefined) { + positions[type] = section.toPosition(offsets[type]) + } + }) + + return { section, positions } +} + +interface Positions { + start?: Position + end?: Position + solo?: Position +} + +/** + * Shorthand to create a mobiledoc simply. + * Pass a string or an array of strings. + * + * Returns { post, range }, a post built from the mobiledoc and a range. + * + * Use "|" to indicate the cursor position or "<" and ">" to indicate a range. + * Use "[card-name]" to indicate a card + * Use asterisks to indicate bold text: "abc *bold* def" + * Use "@" to indicate an atom, default values for name,value,payload are DEFAULT_ATOM_NAME,DEFAULT_ATOM_VALUE,{} + * Use "@(name, value, payload)" to specify name,value and/or payload for an atom. The string from `(` to `)` is parsed as + * JSON, e.g.: '@("name": "my-atom", "value": "abc", "payload": {"foo": "bar"})' -> atom named "my-atom" with value 'abc', payload {foo: 'bar'} + * Use "* " at the start of the string to indicate a list item ("ul") + * + * Examples: + * buildFromText("abc") -> { post } with 1 markup section ("p") with text "abc" + * buildFromText(["abc","def"]) -> { post } with 2 markups sections ("p") with texts "abc" and "def" + * buildFromText("abc|def") -> { post, range } where range is collapsed at offset 3 (after the "c") + * buildFromText(["abcdef","[some-card]","def"]) -> { post } with [MarkupSection, Card, MarkupSection] sections + * buildFromText(["abc", "{def}", "def"]) -> { post } with [MarkupSection, ImageSection, MarkupSection] sections + * buildFromText(["* item 1", "* item 2"]) -> { post } with a ListSection with 2 ListItems + * buildFromText([""]) -> { post, range } where range is the entire post (before the "a" to after the "i") + */ +function buildFromText(_texts: string | string[]) { + const texts = Array.isArray(_texts) ? _texts : [_texts] + const positions: Positions = {} + + let post = build(builder => { + let sections: Cloneable
[] = [] + let curList: Maybe + + texts.forEach((text, index) => { + let { section, positions: _positions } = parseSingleText(text, builder) + let lastText = index === texts.length - 1 + + if (curList) { + if (isListItem(section)) { + curList.items.append(section) + } else { + sections.push(curList) + sections.push(section) + curList = null + } + } else if (isListItem(section)) { + curList = builder.listSection('ul', [section]) + } else { + sections.push(section) + } + + if (lastText && curList) { + sections.push(curList) + } + + if (_positions.start) { + positions.start = _positions.start + } + if (_positions.end) { + positions.end = _positions.end + } + if (_positions.solo) { + positions.solo = _positions.solo + } + }) + + return builder.post(sections) + }) + + let range + if (positions.start) { + if (!positions.end) { + throw new Error(`startPos but no endPos ${texts.join('\n')}`) + } + range = positions.start.toRange(positions.end) + } else if (positions.solo) { + range = positions.solo.toRange() + } + + return { post, range } +} + +export default { + build, + buildFromText, + DEFAULT_ATOM_NAME, +} diff --git a/tests/helpers/post-editor-run.js b/tests/helpers/post-editor-run.js deleted file mode 100644 index 03563c82b..000000000 --- a/tests/helpers/post-editor-run.js +++ /dev/null @@ -1,18 +0,0 @@ -import PostNodeBuilder from 'mobiledoc-kit/models/post-node-builder'; -import PostEditor from 'mobiledoc-kit/editor/post'; -import MockEditor from './mock-editor'; -import renderBuiltAbstract from './render-built-abstract'; - -export default function run(post, callback) { - let builder = new PostNodeBuilder(); - let editor = new MockEditor(builder); - - renderBuiltAbstract(post, editor); - - let postEditor = new PostEditor(editor); - postEditor.begin(); - let result = callback(postEditor); - postEditor.complete(); - return result; -} - diff --git a/tests/helpers/post-editor-run.ts b/tests/helpers/post-editor-run.ts new file mode 100644 index 000000000..470ed9177 --- /dev/null +++ b/tests/helpers/post-editor-run.ts @@ -0,0 +1,19 @@ +import PostNodeBuilder from 'mobiledoc-kit/models/post-node-builder' +import PostEditor from 'mobiledoc-kit/editor/post' +import MockEditor from './mock-editor' +import renderBuiltAbstract from './render-built-abstract' +import Post from 'mobiledoc-kit/models/post' +import { Editor } from 'mobiledoc-kit' + +export default function run(post: Post, callback: (editor: PostEditor) => T): T { + let builder = new PostNodeBuilder() + let editor = new MockEditor(builder) + + renderBuiltAbstract(post, editor as unknown as Editor) + + let postEditor = new PostEditor(editor as unknown as Editor) + postEditor.begin() + let result = callback(postEditor) + postEditor.complete() + return result +} diff --git a/tests/helpers/render-built-abstract.js b/tests/helpers/render-built-abstract.js deleted file mode 100644 index 4377026ed..000000000 --- a/tests/helpers/render-built-abstract.js +++ /dev/null @@ -1,13 +0,0 @@ -import EditorDomRenderer from 'mobiledoc-kit/renderers/editor-dom'; -import RenderTree from 'mobiledoc-kit/models/render-tree'; - -export default function renderBuiltAbstract(post, editor) { - editor.post = post; - let unknownCardHandler = () => {}; - let unknownAtomHandler = () => {}; - let renderer = new EditorDomRenderer( - editor, [], [], unknownCardHandler, unknownAtomHandler); - let renderTree = new RenderTree(post); - renderer.render(renderTree); - return editor; -} diff --git a/tests/helpers/render-built-abstract.ts b/tests/helpers/render-built-abstract.ts new file mode 100644 index 000000000..9686baf58 --- /dev/null +++ b/tests/helpers/render-built-abstract.ts @@ -0,0 +1,14 @@ +import EditorDomRenderer from 'mobiledoc-kit/renderers/editor-dom' +import RenderTree from 'mobiledoc-kit/models/render-tree' +import Post from 'mobiledoc-kit/models/post' +import Editor from 'mobiledoc-kit/editor/editor' + +export default function renderBuiltAbstract(post: Post, editor: Editor) { + editor.post = post + let unknownCardHandler = () => null + let unknownAtomHandler = () => null + let renderer = new EditorDomRenderer(editor, [], [], unknownCardHandler, unknownAtomHandler, {}) + let renderTree = new RenderTree(post) + renderer.render(renderTree) + return editor +} diff --git a/tests/helpers/sections.js b/tests/helpers/sections.ts similarity index 100% rename from tests/helpers/sections.js rename to tests/helpers/sections.ts diff --git a/tests/helpers/wait.js b/tests/helpers/wait.js deleted file mode 100644 index 37b86fd99..000000000 --- a/tests/helpers/wait.js +++ /dev/null @@ -1,5 +0,0 @@ -let wait = (callback) => { - window.requestAnimationFrame(callback); -}; - -export default wait; diff --git a/tests/helpers/wait.ts b/tests/helpers/wait.ts new file mode 100644 index 000000000..da2290b22 --- /dev/null +++ b/tests/helpers/wait.ts @@ -0,0 +1,5 @@ +let wait = (callback: FrameRequestCallback) => { + window.requestAnimationFrame(callback) +} + +export default wait diff --git a/tests/index.js b/tests/index.js index 01a29dddc..2bae81de0 100644 --- a/tests/index.js +++ b/tests/index.js @@ -2,5 +2,5 @@ import $ from "jquery"; window.$ = $; import "qunit"; -import "./unit/**/*.js"; -import "./acceptance/**/*.js"; +import "./unit/**/*.{js,ts}"; +import "./acceptance/**/*.{js,ts}"; diff --git a/tests/test-helpers.js b/tests/test-helpers.js deleted file mode 100644 index a99f1517a..000000000 --- a/tests/test-helpers.js +++ /dev/null @@ -1,86 +0,0 @@ -/* global QUnit */ -import registerAssertions from './helpers/assertions'; -registerAssertions(QUnit); - -import DOMHelpers from './helpers/dom'; -import MobiledocHelpers from './helpers/mobiledoc'; -import PostAbstract from './helpers/post-abstract'; -import { detectIE11 } from './helpers/browsers'; -import wait from './helpers/wait'; -import MockEditor from './helpers/mock-editor'; -import renderBuiltAbstract from './helpers/render-built-abstract'; -import run from './helpers/post-editor-run'; -import * as EditorHelpers from './helpers/editor'; - -const { test:qunitTest, module, skip } = QUnit; - -QUnit.config.urlConfig.push({ - id: 'debugTest', - label: 'Debug Test' -}); - -const test = (msg, callback) => { - let originalCallback = callback; - callback = (...args) => { - if (QUnit.config.debugTest) { - // eslint-disable-next-line no-debugger - debugger; - } - originalCallback(...args); - }; - qunitTest(msg, callback); -}; - -const skipInIE11 = (msg, callback) => { - if (detectIE11()) { - skip('SKIPPED IN IE11: ' + msg, callback); - } else { - test(msg, callback); - } -}; - -QUnit.testStart(() => { - // The fixture is cleared between tests, clearing this - $('
').appendTo('#qunit-fixture'); -}); - -let sauceLog = []; - -QUnit.done(function (test_results) { - var tests = []; - for(var i = 0, len = sauceLog.length; i < len; i++) { - var details = sauceLog[i]; - tests.push({ - name: details.name, - result: details.result, - expected: details.expected, - actual: details.actual, - source: details.source - }); - } - test_results.tests = tests; - - window.global_test_results = test_results; -}); - -QUnit.testStart(function(testDetails){ - QUnit.log(function(details){ - if (!details.result) { - details.name = testDetails.name; - sauceLog.push(details); - } - }); -}); - -export default { - dom: DOMHelpers, - mobiledoc: MobiledocHelpers, - postAbstract: PostAbstract, - editor: EditorHelpers, - test, - module, - skipInIE11, - skip, - wait, - postEditor: { run, renderBuiltAbstract, MockEditor } -}; diff --git a/tests/test-helpers.ts b/tests/test-helpers.ts new file mode 100644 index 000000000..654688c36 --- /dev/null +++ b/tests/test-helpers.ts @@ -0,0 +1,103 @@ +import registerAssertions from './helpers/assertions' +registerAssertions(QUnit) + +import DOMHelpers from './helpers/dom' +import MobiledocHelpers from './helpers/mobiledoc' +import PostAbstract from './helpers/post-abstract' +import { detectIE11 } from './helpers/browsers' +import wait from './helpers/wait' +import MockEditor from './helpers/mock-editor' +import renderBuiltAbstract from './helpers/render-built-abstract' +import run from './helpers/post-editor-run' +import * as EditorHelpers from './helpers/editor' + +const { test: qunitTest, module, skip } = QUnit + +QUnit.config.urlConfig.push({ + id: 'debugTest', + label: 'Debug Test', +}) + +type TestCallback = (assert: Assert) => void | Promise + +declare global { + interface Config { + debugTest: boolean + } + + namespace QUnit { + interface DoneDetails { + tests: any + } + } + + interface Window { + global_test_results: QUnit.DoneDetails + } +} + +function test(msg: string, callback: TestCallback) { + let originalCallback = callback + callback = (...args) => { + if (QUnit.config.debugTest) { + // eslint-disable-next-line no-debugger + debugger + } + originalCallback(...args) + } + qunitTest(msg, callback) +} + +function skipInIE11(msg: string, callback: TestCallback) { + if (detectIE11()) { + skip('SKIPPED IN IE11: ' + msg, callback) + } else { + test(msg, callback) + } +} + +QUnit.testStart(() => { + // The fixture is cleared between tests, clearing this + $('
').appendTo('#qunit-fixture') +}) + +let sauceLog: QUnit.LogDetails[] = [] + +QUnit.done(testResults => { + var tests: Partial[] = [] + for (var i = 0, len = sauceLog.length; i < len; i++) { + var details = sauceLog[i] + tests.push({ + name: details.name, + result: details.result, + expected: details.expected, + actual: details.actual, + source: details.source, + }) + } + + testResults.tests = tests + window.global_test_results = testResults +}) + +QUnit.testStart(testDetails => { + QUnit.log(function (details) { + if (!details.result) { + details.name = testDetails.name + sauceLog.push(details) + } + }) +}) + +export default { + dom: DOMHelpers, + mobiledoc: MobiledocHelpers, + postAbstract: PostAbstract, + editor: EditorHelpers, + test, + module, + skipInIE11, + skip, + wait, + postEditor: { run, renderBuiltAbstract, MockEditor }, +} diff --git a/tests/unit/utils/array-utils-test.js b/tests/unit/utils/array-utils-test.js deleted file mode 100644 index dc5679f48..000000000 --- a/tests/unit/utils/array-utils-test.js +++ /dev/null @@ -1,17 +0,0 @@ -import Helpers from '../../test-helpers'; -import { kvArrayToObject, objectToSortedKVArray } from 'mobiledoc-kit/utils/array-utils'; - -const {module, test} = Helpers; - -module('Unit: Utils: Array Utils'); - -test('#objectToSortedKVArray works', (assert) => { - assert.deepEqual(objectToSortedKVArray({a: 1, b:2}), ['a', 1, 'b', 2]); - assert.deepEqual(objectToSortedKVArray({b: 1, a:2}), ['a', 2, 'b', 1]); - assert.deepEqual(objectToSortedKVArray({}), []); -}); - -test('#kvArrayToObject works', (assert) => { - assert.deepEqual(kvArrayToObject(['a', 1, 'b', 2]), {a:1, b:2}); - assert.deepEqual(kvArrayToObject([]), {}); -}); diff --git a/tests/unit/utils/array-utils-test.ts b/tests/unit/utils/array-utils-test.ts new file mode 100644 index 000000000..2eb64366f --- /dev/null +++ b/tests/unit/utils/array-utils-test.ts @@ -0,0 +1,17 @@ +import Helpers from '../../test-helpers' +import { kvArrayToObject, objectToSortedKVArray } from '../../../src/js/utils/array-utils' + +const { module, test } = Helpers + +module('Unit: Utils: Array Utils') + +test('#objectToSortedKVArray works', assert => { + assert.deepEqual(objectToSortedKVArray({ a: 1, b: 2 }), ['a', 1, 'b', 2]) + assert.deepEqual(objectToSortedKVArray({ b: 1, a: 2 }), ['a', 2, 'b', 1]) + assert.deepEqual(objectToSortedKVArray({}), []); +}) + +test('#kvArrayToObject works', assert => { + assert.deepEqual(kvArrayToObject(['a', 1, 'b', 2]), { a: 1, b: 2 }) + assert.deepEqual(kvArrayToObject([]), {}) +}) diff --git a/tests/unit/utils/assert-test.js b/tests/unit/utils/assert-test.js deleted file mode 100644 index 647844095..000000000 --- a/tests/unit/utils/assert-test.js +++ /dev/null @@ -1,18 +0,0 @@ -import Helpers from '../../test-helpers'; -import mobiledocAssert from 'mobiledoc-kit/utils/assert'; -import MobiledocError from 'mobiledoc-kit/utils/mobiledoc-error'; - -const {module, test} = Helpers; - -module('Unit: Utils: assert'); - -test('#throws a MobiledocError when conditional is false', (assert) => { - try { - mobiledocAssert('The message', false); - } catch (e) { - assert.ok(true, 'caught error'); - assert.equal(e.message, 'The message'); - assert.ok(e instanceof MobiledocError, 'e instanceof MobiledocError'); - } -}); - diff --git a/tests/unit/utils/assert-test.ts b/tests/unit/utils/assert-test.ts new file mode 100644 index 000000000..8dfbaffd9 --- /dev/null +++ b/tests/unit/utils/assert-test.ts @@ -0,0 +1,18 @@ +import Helpers from '../../test-helpers' +import mobiledocAssert from '../../../src/js/utils/assert' +import MobiledocError from '../../../src/js/utils/mobiledoc-error' + +const { module, test } = Helpers + +module('Unit: Utils: assert') + +test('#throws a MobiledocError when conditional is false', assert => { + try { + mobiledocAssert('The message', false) + } catch (e) { + assert.ok(true, 'caught error') + assert.equal(e.message, 'The message') + assert.ok(e instanceof MobiledocError, 'e instanceof MobiledocError') + } +}) + diff --git a/tests/unit/utils/copy-test.js b/tests/unit/utils/copy-test.js deleted file mode 100644 index 8119673a5..000000000 --- a/tests/unit/utils/copy-test.js +++ /dev/null @@ -1,18 +0,0 @@ -import Helpers from '../../test-helpers'; -import { shallowCopyObject } from 'mobiledoc-kit/utils/copy'; - -const {module, test} = Helpers; - -module('Unit: Utils: copy'); - -test('#shallowCopyObject breaks references', (assert) => { - let obj = {a: 1, b:'b'}; - let obj2 = shallowCopyObject(obj); - obj.a = 2; - obj.b = 'new b'; - - assert.ok(obj !== obj2, 'obj !== obj2'); - assert.equal(obj2.a, 1, 'obj2 "a" preserved'); - assert.equal(obj2.b, 'b', 'obj2 "b" preserved'); -}); - diff --git a/tests/unit/utils/copy-test.ts b/tests/unit/utils/copy-test.ts new file mode 100644 index 000000000..884b21176 --- /dev/null +++ b/tests/unit/utils/copy-test.ts @@ -0,0 +1,17 @@ +import Helpers from '../../test-helpers' +import { shallowCopyObject } from '../../../src/js/utils/copy' + +const { module, test } = Helpers + +module('Unit: Utils: copy') + +test('#shallowCopyObject breaks references', assert => { + let obj = { a: 1, b: 'b' } + let obj2 = shallowCopyObject(obj) + obj.a = 2 + obj.b = 'new b' + + assert.ok(obj !== obj2, 'obj !== obj2') + assert.equal(obj2.a, 1, 'obj2 "a" preserved') + assert.equal(obj2.b, 'b', 'obj2 "b" preserved') +}) diff --git a/tests/unit/utils/cursor-position-test.js b/tests/unit/utils/cursor-position-test.js index 5489f5eef..5a55d9cb1 100644 --- a/tests/unit/utils/cursor-position-test.js +++ b/tests/unit/utils/cursor-position-test.js @@ -1,198 +1,175 @@ -import Helpers from '../../test-helpers'; -import Position from 'mobiledoc-kit/utils/cursor/position'; -import { CARD_ELEMENT_CLASS_NAME, ZWNJ } from 'mobiledoc-kit/renderers/editor-dom'; -import { DIRECTION } from 'mobiledoc-kit/utils/key'; +import Helpers from '../../test-helpers' +import Position from '../../../src/js/utils/cursor/position' +import { CARD_ELEMENT_CLASS_NAME, ZWNJ } from '../../../src/js/renderers/editor-dom' +import { DIRECTION } from '../../../src/js/utils/key' -const { FORWARD, BACKWARD } = DIRECTION; +const { FORWARD, BACKWARD } = DIRECTION -const {module, test} = Helpers; +const { module, test } = Helpers -let editor, editorElement; +let editor +let editorElement module('Unit: Utils: Position', { beforeEach() { - editorElement = $('#editor')[0]; + editorElement = $('#editor')[0] }, afterEach() { if (editor) { - editor.destroy(); - editor = null; + editor.destroy() + editor = null } - } -}); - -test('#move moves forward and backward in markup section', (assert) => { - let post = Helpers.postAbstract.build(({post, markupSection, marker}) => { - return post([markupSection('p', [marker('abcd')])]); - }); - let position = post.sections.head.toPosition('ab'.length); - let rightPosition = post.sections.head.toPosition('abc'.length); - let leftPosition = post.sections.head.toPosition('a'.length); - - assert.positionIsEqual(position.move(FORWARD), rightPosition, 'right position'); - assert.positionIsEqual(position.move(BACKWARD), leftPosition, 'left position'); -}); - -test('#move is emoji-aware', (assert) => { - let emoji = '🙈'; - let post = Helpers.postAbstract.build(({post, markupSection, marker}) => { - return post([markupSection('p', [marker(`a${emoji}z`)])]); - }); - let marker = post.sections.head.markers.head; - assert.equal(marker.length, 'a'.length + 2 + 'z'.length); // precond - let position = post.sections.head.headPosition(); - - position = position.move(FORWARD); - assert.equal(position.offset, 1); - position = position.move(FORWARD); - assert.equal(position.offset, 3); // l-to-r across emoji - position = position.move(FORWARD); - assert.equal(position.offset, 4); - - position = position.move(BACKWARD); - assert.equal(position.offset, 3); - - position = position.move(BACKWARD); // r-to-l across emoji - assert.equal(position.offset, 1); - - position = position.move(BACKWARD); - assert.equal(position.offset, 0); -}); - -test('#move moves forward and backward between markup sections', (assert) => { - let post = Helpers.postAbstract.build(({post, markupSection, marker}) => { + }, +}) + +test('#move moves forward and backward in markup section', assert => { + let post = Helpers.postAbstract.build(({ post, markupSection, marker }) => { + return post([markupSection('p', [marker('abcd')])]) + }) + let position = post.sections.head.toPosition('ab'.length) + let rightPosition = post.sections.head.toPosition('abc'.length) + let leftPosition = post.sections.head.toPosition('a'.length) + + assert.positionIsEqual(position.move(FORWARD), rightPosition, 'right position') + assert.positionIsEqual(position.move(BACKWARD), leftPosition, 'left position') +}) + +test('#move is emoji-aware', assert => { + let emoji = '🙈' + let post = Helpers.postAbstract.build(({ post, markupSection, marker }) => { + return post([markupSection('p', [marker(`a${emoji}z`)])]) + }) + let marker = post.sections.head.markers.head + assert.equal(marker.length, 'a'.length + 2 + 'z'.length) // precond + let position = post.sections.head.headPosition() + + position = position.move(FORWARD) + assert.equal(position.offset, 1) + position = position.move(FORWARD) + assert.equal(position.offset, 3) // l-to-r across emoji + position = position.move(FORWARD) + assert.equal(position.offset, 4) + + position = position.move(BACKWARD) + assert.equal(position.offset, 3) + + position = position.move(BACKWARD) // r-to-l across emoji + assert.equal(position.offset, 1) + + position = position.move(BACKWARD) + assert.equal(position.offset, 0) +}) + +test('#move moves forward and backward between markup sections', assert => { + let post = Helpers.postAbstract.build(({ post, markupSection, marker }) => { return post([ markupSection('p', [marker('a')]), markupSection('p', [marker('b')]), - markupSection('p', [marker('c')]) - ]); - }); - let midHead = post.sections.objectAt(1).headPosition(); - let midTail = post.sections.objectAt(1).tailPosition(); - - let aTail = post.sections.head.tailPosition(); - let cHead = post.sections.tail.headPosition(); - - assert.positionIsEqual(midHead.move(BACKWARD), aTail, 'left to prev section'); - assert.positionIsEqual(midTail.move(FORWARD), cHead, 'right to next section'); -}); - -test('#move from one nested section to another', (assert) => { - let post = Helpers.postAbstract.build( - ({post, listSection, listItem, marker}) => { - return post([listSection('ul', [ - listItem([marker('a')]), - listItem([marker('b')]), - listItem([marker('c')]) - ])]); - }); - let midHead = post.sections.head.items.objectAt(1).headPosition(); - let midTail = post.sections.head.items.objectAt(1).tailPosition(); - - let aTail = post.sections.head.items.head.tailPosition(); - let cHead = post.sections.tail.items.tail.headPosition(); - - assert.positionIsEqual(midHead.move(BACKWARD), aTail, 'left to prev section'); - assert.positionIsEqual(midTail.move(FORWARD), cHead, 'right to next section'); -}); - -test('#move from last nested section to next un-nested section', (assert) => { - let post = Helpers.postAbstract.build( - ({post, listSection, listItem, markupSection, marker}) => { + markupSection('p', [marker('c')]), + ]) + }) + let midHead = post.sections.objectAt(1).headPosition() + let midTail = post.sections.objectAt(1).tailPosition() + + let aTail = post.sections.head.tailPosition() + let cHead = post.sections.tail.headPosition() + + assert.positionIsEqual(midHead.move(BACKWARD), aTail, 'left to prev section') + assert.positionIsEqual(midTail.move(FORWARD), cHead, 'right to next section') +}) + +test('#move from one nested section to another', assert => { + let post = Helpers.postAbstract.build(({ post, listSection, listItem, marker }) => { + return post([listSection('ul', [listItem([marker('a')]), listItem([marker('b')]), listItem([marker('c')])])]) + }) + let midHead = post.sections.head.items.objectAt(1).headPosition() + let midTail = post.sections.head.items.objectAt(1).tailPosition() + + let aTail = post.sections.head.items.head.tailPosition() + let cHead = post.sections.tail.items.tail.headPosition() + + assert.positionIsEqual(midHead.move(BACKWARD), aTail, 'left to prev section') + assert.positionIsEqual(midTail.move(FORWARD), cHead, 'right to next section') +}) + +test('#move from last nested section to next un-nested section', assert => { + let post = Helpers.postAbstract.build(({ post, listSection, listItem, markupSection, marker }) => { return post([ markupSection('p', [marker('a')]), listSection('ul', [listItem([marker('b')])]), - markupSection('p', [marker('c')]) - ]); - }); - let midHead = post.sections.objectAt(1).items.head.headPosition(); - let midTail = post.sections.objectAt(1).items.head.tailPosition(); - - let aTail = post.sections.head.tailPosition(); - let cHead = post.sections.tail.headPosition(); - - assert.positionIsEqual(midHead.move(BACKWARD), aTail, 'left to prev section'); - assert.positionIsEqual(midTail.move(FORWARD), cHead, 'right to next section'); -}); - -test('#move across and beyond card section', (assert) => { - let post = Helpers.postAbstract.build( - ({post, cardSection, markupSection, marker}) => { + markupSection('p', [marker('c')]), + ]) + }) + let midHead = post.sections.objectAt(1).items.head.headPosition() + let midTail = post.sections.objectAt(1).items.head.tailPosition() + + let aTail = post.sections.head.tailPosition() + let cHead = post.sections.tail.headPosition() + + assert.positionIsEqual(midHead.move(BACKWARD), aTail, 'left to prev section') + assert.positionIsEqual(midTail.move(FORWARD), cHead, 'right to next section') +}) + +test('#move across and beyond card section', assert => { + let post = Helpers.postAbstract.build(({ post, cardSection, markupSection, marker }) => { + return post([markupSection('p', [marker('a')]), cardSection('my-card'), markupSection('p', [marker('c')])]) + }) + let midHead = post.sections.objectAt(1).headPosition() + let midTail = post.sections.objectAt(1).tailPosition() + + let aTail = post.sections.head.tailPosition() + let cHead = post.sections.tail.headPosition() + + assert.positionIsEqual(midHead.move(BACKWARD), aTail, 'left to prev section') + assert.positionIsEqual(midTail.move(FORWARD), cHead, 'right to next section') + assert.positionIsEqual(midHead.move(FORWARD), midTail, 'move l-to-r across card') + assert.positionIsEqual(midTail.move(BACKWARD), midHead, 'move r-to-l across card') +}) + +test('#move across and beyond card section into list section', assert => { + let post = Helpers.postAbstract.build(({ post, cardSection, listSection, listItem, marker }) => { return post([ - markupSection('p', [marker('a')]), + listSection('ul', [listItem([marker('a1')]), listItem([marker('a2')])]), cardSection('my-card'), - markupSection('p', [marker('c')]) - ]); - }); - let midHead = post.sections.objectAt(1).headPosition(); - let midTail = post.sections.objectAt(1).tailPosition(); - - let aTail = post.sections.head.tailPosition(); - let cHead = post.sections.tail.headPosition(); - - assert.positionIsEqual(midHead.move(BACKWARD), aTail, 'left to prev section'); - assert.positionIsEqual(midTail.move(FORWARD), cHead, 'right to next section'); - assert.positionIsEqual(midHead.move(FORWARD), midTail, 'move l-to-r across card'); - assert.positionIsEqual(midTail.move(BACKWARD), midHead, 'move r-to-l across card'); -}); - -test('#move across and beyond card section into list section', (assert) => { - let post = Helpers.postAbstract.build( - ({post, cardSection, listSection, listItem, marker}) => { - return post([ - listSection('ul', [ - listItem([marker('a1')]), - listItem([marker('a2')]) - ]), - cardSection('my-card'), - listSection('ul', [ - listItem([marker('c1')]), - listItem([marker('c2')]) - ]) - ]); - }); - let midHead = post.sections.objectAt(1).headPosition(); - let midTail = post.sections.objectAt(1).tailPosition(); - - let aTail = post.sections.head.items.tail.tailPosition(); - let cHead = post.sections.tail.items.head.headPosition(); - - assert.positionIsEqual(midHead.move(BACKWARD), aTail, 'left to prev section'); - assert.positionIsEqual(midTail.move(FORWARD), cHead, 'right to next section'); -}); - -test('#move left at headPosition or right at tailPosition returns self', (assert) => { - let post = Helpers.postAbstract.build(({post, markupSection, marker}) => { - return post([ - markupSection('p', [marker('abc')]), - markupSection('p', [marker('def')]) - ]); - }); + listSection('ul', [listItem([marker('c1')]), listItem([marker('c2')])]), + ]) + }) + let midHead = post.sections.objectAt(1).headPosition() + let midTail = post.sections.objectAt(1).tailPosition() + + let aTail = post.sections.head.items.tail.tailPosition() + let cHead = post.sections.tail.items.head.headPosition() + + assert.positionIsEqual(midHead.move(BACKWARD), aTail, 'left to prev section') + assert.positionIsEqual(midTail.move(FORWARD), cHead, 'right to next section') +}) + +test('#move left at headPosition or right at tailPosition returns self', assert => { + let post = Helpers.postAbstract.build(({ post, markupSection, marker }) => { + return post([markupSection('p', [marker('abc')]), markupSection('p', [marker('def')])]) + }) let head = post.headPosition(), - tail = post.tailPosition(); - assert.positionIsEqual(head.move(BACKWARD), head, 'head move left is head'); - assert.positionIsEqual(tail.move(FORWARD), tail, 'tail move right is tail'); -}); + tail = post.tailPosition() + assert.positionIsEqual(head.move(BACKWARD), head, 'head move left is head') + assert.positionIsEqual(tail.move(FORWARD), tail, 'tail move right is tail') +}) -test('#move can move multiple units', (assert) => { - let post = Helpers.postAbstract.build(({post, markupSection, marker}) => { - return post([ - markupSection('p', [marker('abc')]), - markupSection('p', [marker('def')]) - ]); - }); +test('#move can move multiple units', assert => { + let post = Helpers.postAbstract.build(({ post, markupSection, marker }) => { + return post([markupSection('p', [marker('abc')]), markupSection('p', [marker('def')])]) + }) let head = post.headPosition(), - tail = post.tailPosition(); + tail = post.tailPosition() - assert.positionIsEqual(head.move(FORWARD * ('abc'.length + 1 + 'def'.length)), tail, 'head can move to tail'); - assert.positionIsEqual(tail.move(BACKWARD * ('abc'.length + 1 + 'def'.length)), head, 'tail can move to head'); + assert.positionIsEqual(head.move(FORWARD * ('abc'.length + 1 + 'def'.length)), tail, 'head can move to tail') + assert.positionIsEqual(tail.move(BACKWARD * ('abc'.length + 1 + 'def'.length)), head, 'tail can move to head') - assert.positionIsEqual(head.move(0), head, 'move(0) is no-op'); -}); + assert.positionIsEqual(head.move(0), head, 'move(0) is no-op') +}) -test('#moveWord in text (backward)', (assert) => { +test('#moveWord in text (backward)', assert => { let expectations = [ ['abc def|', 'abc |def'], ['abc d|ef', 'abc |def'], @@ -204,91 +181,84 @@ test('#moveWord in text (backward)', (assert) => { ['ab|c', '|abc'], ['|abc', '|abc'], ['abc |', '|abc'], - ['abcdéf|', '|abcdéf'] - ]; + ['abcdéf|', '|abcdéf'], + ] expectations.forEach(([before, after]) => { - let { post, range: { head: pos } } = Helpers.postAbstract.buildFromText(before); - let { range: { head: afterPos } } = Helpers.postAbstract.buildFromText(after); - - let expectedPos = post.sections.head.toPosition(afterPos.offset); - assert.positionIsEqual(pos.moveWord(BACKWARD), expectedPos, - `move word "${before}"->"${after}"`); - }); -}); - -test('#moveWord stops on word-separators', (assert) => { - let separators = ['-', '+', '=', '|']; + let { + post, + range: { head: pos }, + } = Helpers.postAbstract.buildFromText(before) + let { + range: { head: afterPos }, + } = Helpers.postAbstract.buildFromText(after) + + let expectedPos = post.sections.head.toPosition(afterPos.offset) + assert.positionIsEqual(pos.moveWord(BACKWARD), expectedPos, `move word "${before}"->"${after}"`) + }) +}) + +test('#moveWord stops on word-separators', assert => { + let separators = ['-', '+', '=', '|'] separators.forEach(sep => { - let text = `abc${sep}def`; - let post = Helpers.postAbstract.build(({post, markupSection, marker}) => { - return post([markupSection('p', [marker(text)])]); - }); - let pos = post.tailPosition(); - let expectedPos = post.sections.head.toPosition('abc '.length); - - assert.positionIsEqual(pos.moveWord(BACKWARD), expectedPos, `move word <- "${text}|"`); - }); -}); - -test('#moveWord does not stop on non-word-separators', (assert) => { - let nonSeparators = ['_', ':']; + let text = `abc${sep}def` + let post = Helpers.postAbstract.build(({ post, markupSection, marker }) => { + return post([markupSection('p', [marker(text)])]) + }) + let pos = post.tailPosition() + let expectedPos = post.sections.head.toPosition('abc '.length) + + assert.positionIsEqual(pos.moveWord(BACKWARD), expectedPos, `move word <- "${text}|"`) + }) +}) + +test('#moveWord does not stop on non-word-separators', assert => { + let nonSeparators = ['_', ':'] nonSeparators.forEach(sep => { - let text = `abc${sep}def`; + let text = `abc${sep}def` // Have to use `build` function here because "_" is a special char for `buildFromText` - let post = Helpers.postAbstract.build(({post, markupSection, marker}) => { - return post([markupSection('p', [marker(text)])]); - }); - let pos = post.tailPosition(); - let nextPos = post.headPosition(); - - assert.positionIsEqual(pos.moveWord(BACKWARD), nextPos, `move word <- "${text}|"`); - }); -}); - -test('#moveWord across markerable sections', (assert) => { - let { post } = Helpers.postAbstract.buildFromText(['abc def', '123 456']); - - let [first, second] = post.sections.toArray(); - let pos = (section, text) => section.toPosition(text.length); - let firstTail = first.tailPosition(); - let secondHead = second.headPosition(); - - assert.positionIsEqual(secondHead.moveWord(BACKWARD), pos(first, 'abc '), - 'secondHead <- "abc "'); - assert.positionIsEqual(firstTail.moveWord(FORWARD), pos(second, '123'), - 'firstTail <- "123"'); -}); - -test('#moveWord across markerable/non-markerable section boundaries', (assert) => { - let post = Helpers.postAbstract.build(({post, markupSection, cardSection, marker}) => { - return post([ - markupSection('p', [marker('abc')]), - cardSection('some-card'), - markupSection('p', [marker('def')]) - ]); - }); - - let [before, card, after] = post.sections.toArray(); - let cardHead = card.headPosition(); - let cardTail = card.tailPosition(); - let beforeTail = before.tailPosition(); - let afterHead = after.headPosition(); - - assert.positionIsEqual(cardHead.moveWord(BACKWARD), beforeTail, - 'cardHead <- beforeTail'); - assert.positionIsEqual(cardHead.moveWord(FORWARD), cardTail, - 'cardHead -> cardTail'); - assert.positionIsEqual(cardTail.moveWord(BACKWARD), cardHead, - 'cardTail <- cardHead'); - assert.positionIsEqual(afterHead.moveWord(BACKWARD), cardHead, - 'afterHead <- cardHead'); - assert.positionIsEqual(beforeTail.moveWord(FORWARD), cardTail, - 'beforeTail -> cardTail'); -}); - -test('#moveWord with atoms (backward)', (assert) => { + let post = Helpers.postAbstract.build(({ post, markupSection, marker }) => { + return post([markupSection('p', [marker(text)])]) + }) + let pos = post.tailPosition() + let nextPos = post.headPosition() + + assert.positionIsEqual(pos.moveWord(BACKWARD), nextPos, `move word <- "${text}|"`) + }) +}) + +test('#moveWord across markerable sections', assert => { + let { post } = Helpers.postAbstract.buildFromText(['abc def', '123 456']) + + let [first, second] = post.sections.toArray() + let pos = (section, text) => section.toPosition(text.length) + let firstTail = first.tailPosition() + let secondHead = second.headPosition() + + assert.positionIsEqual(secondHead.moveWord(BACKWARD), pos(first, 'abc '), 'secondHead <- "abc "') + assert.positionIsEqual(firstTail.moveWord(FORWARD), pos(second, '123'), 'firstTail <- "123"') +}) + +test('#moveWord across markerable/non-markerable section boundaries', assert => { + let post = Helpers.postAbstract.build(({ post, markupSection, cardSection, marker }) => { + return post([markupSection('p', [marker('abc')]), cardSection('some-card'), markupSection('p', [marker('def')])]) + }) + + let [before, card, after] = post.sections.toArray() + let cardHead = card.headPosition() + let cardTail = card.tailPosition() + let beforeTail = before.tailPosition() + let afterHead = after.headPosition() + + assert.positionIsEqual(cardHead.moveWord(BACKWARD), beforeTail, 'cardHead <- beforeTail') + assert.positionIsEqual(cardHead.moveWord(FORWARD), cardTail, 'cardHead -> cardTail') + assert.positionIsEqual(cardTail.moveWord(BACKWARD), cardHead, 'cardTail <- cardHead') + assert.positionIsEqual(afterHead.moveWord(BACKWARD), cardHead, 'afterHead <- cardHead') + assert.positionIsEqual(beforeTail.moveWord(FORWARD), cardTail, 'beforeTail -> cardTail') +}) + +test('#moveWord with atoms (backward)', assert => { let expectations = [ ['abc @|', 'abc |@'], ['abc |@', '|abc @'], @@ -296,21 +266,25 @@ test('#moveWord with atoms (backward)', (assert) => { ['@ |', '@| '], ['@@|', '@|@'], ['@|@', '|@@'], - ['|@@', '|@@'] - ]; + ['|@@', '|@@'], + ] expectations.forEach(([before, after]) => { - let { post, range: { head: pos } } = Helpers.postAbstract.buildFromText(before); - let { range: { head: nextPos } } = Helpers.postAbstract.buildFromText(after); - let section = post.sections.head; - nextPos = section.toPosition(nextPos.offset); - - assert.positionIsEqual(pos.moveWord(BACKWARD), nextPos, - `move word with atoms "${before}" -> "${after}"`); - }); -}); - -test('#moveWord in text (forward)', (assert) => { + let { + post, + range: { head: pos }, + } = Helpers.postAbstract.buildFromText(before) + let { + range: { head: nextPos }, + } = Helpers.postAbstract.buildFromText(after) + let section = post.sections.head + nextPos = section.toPosition(nextPos.offset) + + assert.positionIsEqual(pos.moveWord(BACKWARD), nextPos, `move word with atoms "${before}" -> "${after}"`) + }) +}) + +test('#moveWord in text (forward)', assert => { let expectations = [ ['|abc def', 'abc| def'], ['a|bc def', 'abc| def'], @@ -320,21 +294,25 @@ test('#moveWord in text (forward)', (assert) => { ['abc|', 'abc|'], ['ab|c', 'abc|'], ['|abc', 'abc|'], - ['| abc', ' abc|'] - ]; + ['| abc', ' abc|'], + ] expectations.forEach(([before, after]) => { - let { post, range: { head: pos } } = Helpers.postAbstract.buildFromText(before); - let { range: { head: nextPos } } = Helpers.postAbstract.buildFromText(after); - let section = post.sections.head; - nextPos = section.toPosition(nextPos.offset); // fix section - - assert.positionIsEqual(pos.moveWord(FORWARD), nextPos, - `move word "${before}"->"${after}"`); - }); -}); - -test('#moveWord with atoms (forward)', (assert) => { + let { + post, + range: { head: pos }, + } = Helpers.postAbstract.buildFromText(before) + let { + range: { head: nextPos }, + } = Helpers.postAbstract.buildFromText(after) + let section = post.sections.head + nextPos = section.toPosition(nextPos.offset) // fix section + + assert.positionIsEqual(pos.moveWord(FORWARD), nextPos, `move word "${before}"->"${after}"`) + }) +}) + +test('#moveWord with atoms (forward)', assert => { let expectations = [ ['|@', '@|'], ['@|', '@|'], @@ -343,76 +321,76 @@ test('#moveWord with atoms (forward)', (assert) => { ['abc| @', 'abc @|'], ['|@@', '@|@'], ['@|@', '@@|'], - ['@@|', '@@|'] - ]; + ['@@|', '@@|'], + ] expectations.forEach(([before, after]) => { - let { post, range: { head: pos } } = Helpers.postAbstract.buildFromText(before); - let { range: { head: nextPos } } = Helpers.postAbstract.buildFromText(after); - let section = post.sections.head; - nextPos = section.toPosition(nextPos.offset); + let { + post, + range: { head: pos }, + } = Helpers.postAbstract.buildFromText(before) + let { + range: { head: nextPos }, + } = Helpers.postAbstract.buildFromText(after) + let section = post.sections.head + nextPos = section.toPosition(nextPos.offset) - assert.positionIsEqual(pos.moveWord(FORWARD), nextPos, - `move word with atoms "${before}" -> "${after}"`); - }); -}); + assert.positionIsEqual(pos.moveWord(FORWARD), nextPos, `move word with atoms "${before}" -> "${after}"`) + }) +}) -test('#fromNode when node is marker text node', (assert) => { - editor = Helpers.mobiledoc.renderInto(editorElement, - ({post, markupSection, marker}) => { - return post([markupSection('p', [marker('abc'), marker('123')])]); - }); +test('#fromNode when node is marker text node', assert => { + editor = Helpers.mobiledoc.renderInto(editorElement, ({ post, markupSection, marker }) => { + return post([markupSection('p', [marker('abc'), marker('123')])]) + }) - let textNode = editorElement.firstChild // p - .lastChild; // textNode + let textNode = + editorElement.firstChild.lastChild // p // textNode - assert.equal(textNode.textContent, '123', 'precond - correct text node'); + assert.equal(textNode.textContent, '123', 'precond - correct text node') - let renderTree = editor._renderTree; - let position = Position.fromNode(renderTree, textNode, 2); + let renderTree = editor._renderTree + let position = Position.fromNode(renderTree, textNode, 2) - let section = editor.post.sections.head; - assert.positionIsEqual(position, section.toPosition('abc'.length + 2)); -}); + let section = editor.post.sections.head + assert.positionIsEqual(position, section.toPosition('abc'.length + 2)) +}) -test('#fromNode when node is section node with offset', (assert) => { - editor = Helpers.mobiledoc.renderInto(editorElement, - ({post, markupSection, marker}) => { - return post([markupSection('p', [marker('abc'), marker('123')])]); - }); +test('#fromNode when node is section node with offset', assert => { + editor = Helpers.mobiledoc.renderInto(editorElement, ({ post, markupSection, marker }) => { + return post([markupSection('p', [marker('abc'), marker('123')])]) + }) - let pNode = editorElement.firstChild; - assert.equal(pNode.tagName.toLowerCase(), 'p', 'precond - correct node'); + let pNode = editorElement.firstChild + assert.equal(pNode.tagName.toLowerCase(), 'p', 'precond - correct node') - let renderTree = editor._renderTree; - let position = Position.fromNode(renderTree, pNode, 0); + let renderTree = editor._renderTree + let position = Position.fromNode(renderTree, pNode, 0) - assert.positionIsEqual(position, editor.post.sections.head.headPosition()); -}); + assert.positionIsEqual(position, editor.post.sections.head.headPosition()) +}) -test('#fromNode when node is root element and offset is 0', (assert) => { - editor = Helpers.mobiledoc.renderInto(editorElement, - ({post, markupSection, marker}) => { - return post([markupSection('p', [marker('abc'), marker('123')])]); - }); +test('#fromNode when node is root element and offset is 0', assert => { + editor = Helpers.mobiledoc.renderInto(editorElement, ({ post, markupSection, marker }) => { + return post([markupSection('p', [marker('abc'), marker('123')])]) + }) - let renderTree = editor._renderTree; - let position = Position.fromNode(renderTree, editorElement, 0); + let renderTree = editor._renderTree + let position = Position.fromNode(renderTree, editorElement, 0) - assert.positionIsEqual(position, editor.post.headPosition()); -}); + assert.positionIsEqual(position, editor.post.headPosition()) +}) -test('#fromNode when node is root element and offset is > 0', (assert) => { - editor = Helpers.mobiledoc.renderInto(editorElement, - ({post, markupSection, marker}) => { - return post([markupSection('p', [marker('abc'), marker('123')])]); - }); +test('#fromNode when node is root element and offset is > 0', assert => { + editor = Helpers.mobiledoc.renderInto(editorElement, ({ post, markupSection, marker }) => { + return post([markupSection('p', [marker('abc'), marker('123')])]) + }) - let renderTree = editor._renderTree; - let position = Position.fromNode(renderTree, editorElement, 1); + let renderTree = editor._renderTree + let position = Position.fromNode(renderTree, editorElement, 1) - assert.positionIsEqual(position, editor.post.tailPosition()); -}); + assert.positionIsEqual(position, editor.post.tailPosition()) +}) /** * On Firefox, triple-clicking results in a different selection that on Chrome @@ -435,74 +413,68 @@ test('#fromNode when node is root element and offset is > 0', (assert) => { * So when getting the position for `focusNode`/`focusOffset`, we have to get * the tail of section. */ -test('#fromNode when offset refers to one past the number of child nodes of the node', function(assert) { - editor = Helpers.mobiledoc.renderInto(editorElement, - ({post, markupSection, marker}) => { - return post([markupSection('p', [marker('abc')])]); - }); - - let renderTree = editor._renderTree; - let elementNode = editorElement.firstChild; - let position = Position.fromNode(renderTree, elementNode, 1); - - assert.positionIsEqual(position, editor.post.tailPosition()); -}); - -test('#fromNode when node is card section element or next to it', (assert) => { - let editorOptions = { cards: [{ - name: 'some-card', - type: 'dom', - render() { - return $('
this is the card
')[0]; - } - }]}; - editor = Helpers.mobiledoc.renderInto(editorElement, - ({post, cardSection}) => { - return post([cardSection('some-card')]); - }, editorOptions); +test('#fromNode when offset refers to one past the number of child nodes of the node', function (assert) { + editor = Helpers.mobiledoc.renderInto(editorElement, ({ post, markupSection, marker }) => { + return post([markupSection('p', [marker('abc')])]) + }) + + let renderTree = editor._renderTree + let elementNode = editorElement.firstChild + let position = Position.fromNode(renderTree, elementNode, 1) + + assert.positionIsEqual(position, editor.post.tailPosition()) +}) + +test('#fromNode when node is card section element or next to it', assert => { + let editorOptions = { + cards: [ + { + name: 'some-card', + type: 'dom', + render() { + return $('
this is the card
')[0] + }, + }, + ], + } + editor = Helpers.mobiledoc.renderInto( + editorElement, + ({ post, cardSection }) => { + return post([cardSection('some-card')]) + }, + editorOptions + ) let nodes = { - wrapper: editorElement.firstChild, - leftCursor: editorElement.firstChild.firstChild, + wrapper: editorElement.firstChild, + leftCursor: editorElement.firstChild.firstChild, rightCursor: editorElement.firstChild.lastChild, - cardDiv: editorElement.firstChild.childNodes[1] - }; - - assert.ok(nodes.wrapper && nodes.leftCursor && nodes.rightCursor && - nodes.cardDiv, - 'precond - nodes'); - - assert.equal(nodes.wrapper.tagName.toLowerCase(), 'div', 'precond - wrapper'); - assert.equal(nodes.leftCursor.textContent, ZWNJ, 'precond - left cursor'); - assert.equal(nodes.rightCursor.textContent, ZWNJ, 'precond - right cursor'); - assert.ok(nodes.cardDiv.className.indexOf(CARD_ELEMENT_CLASS_NAME) !== -1, - 'precond -card div'); - - let renderTree = editor._renderTree; - let cardSection = editor.post.sections.head; - - let leftPos = cardSection.headPosition(); - let rightPos = cardSection.tailPosition(); - - assert.positionIsEqual(Position.fromNode(renderTree, nodes.wrapper, 0), - leftPos, 'wrapper offset 0'); - assert.positionIsEqual(Position.fromNode(renderTree, nodes.wrapper, 1), - leftPos, 'wrapper offset 1'); - assert.positionIsEqual(Position.fromNode(renderTree, nodes.wrapper, 2), - rightPos, 'wrapper offset 2'); - assert.positionIsEqual(Position.fromNode(renderTree, nodes.leftCursor, 0), - leftPos, 'left cursor offset 0'); - assert.positionIsEqual(Position.fromNode(renderTree, nodes.leftCursor, 1), - leftPos, 'left cursor offset 1'); - assert.positionIsEqual(Position.fromNode(renderTree, nodes.rightCursor, 0), - rightPos, 'right cursor offset 0'); - assert.positionIsEqual(Position.fromNode(renderTree, nodes.rightCursor, 1), - rightPos, 'right cursor offset 1'); - assert.positionIsEqual(Position.fromNode(renderTree, nodes.cardDiv, 0), - leftPos, 'card div offset 0'); - assert.positionIsEqual(Position.fromNode(renderTree, nodes.cardDiv, 1), - leftPos, 'card div offset 1'); -}); + cardDiv: editorElement.firstChild.childNodes[1], + } + + assert.ok(nodes.wrapper && nodes.leftCursor && nodes.rightCursor && nodes.cardDiv, 'precond - nodes') + + assert.equal(nodes.wrapper.tagName.toLowerCase(), 'div', 'precond - wrapper') + assert.equal(nodes.leftCursor.textContent, ZWNJ, 'precond - left cursor') + assert.equal(nodes.rightCursor.textContent, ZWNJ, 'precond - right cursor') + assert.ok(nodes.cardDiv.className.indexOf(CARD_ELEMENT_CLASS_NAME) !== -1, 'precond -card div') + + let renderTree = editor._renderTree + let cardSection = editor.post.sections.head + + let leftPos = cardSection.headPosition() + let rightPos = cardSection.tailPosition() + + assert.positionIsEqual(Position.fromNode(renderTree, nodes.wrapper, 0), leftPos, 'wrapper offset 0') + assert.positionIsEqual(Position.fromNode(renderTree, nodes.wrapper, 1), leftPos, 'wrapper offset 1') + assert.positionIsEqual(Position.fromNode(renderTree, nodes.wrapper, 2), rightPos, 'wrapper offset 2') + assert.positionIsEqual(Position.fromNode(renderTree, nodes.leftCursor, 0), leftPos, 'left cursor offset 0') + assert.positionIsEqual(Position.fromNode(renderTree, nodes.leftCursor, 1), leftPos, 'left cursor offset 1') + assert.positionIsEqual(Position.fromNode(renderTree, nodes.rightCursor, 0), rightPos, 'right cursor offset 0') + assert.positionIsEqual(Position.fromNode(renderTree, nodes.rightCursor, 1), rightPos, 'right cursor offset 1') + assert.positionIsEqual(Position.fromNode(renderTree, nodes.cardDiv, 0), leftPos, 'card div offset 0') + assert.positionIsEqual(Position.fromNode(renderTree, nodes.cardDiv, 1), leftPos, 'card div offset 1') +}) /** * When triple-clicking text in a disabled editor, some browsers will @@ -512,49 +484,49 @@ test('#fromNode when node is card section element or next to it', (assert) => { * Chrome and Safari appear to extend the selection to the next node in the document * that has a textNode in it. Firefox does not suffer from this issue. */ -test('#fromNode when selection is outside (after) the editor element', function(assert) { - let done = assert.async(); - let div$ = $('

AFTER

').insertAfter($(editorElement)); - let p = div$[0].firstChild; +test('#fromNode when selection is outside (after) the editor element', function (assert) { + let done = assert.async() + let div$ = $('

AFTER

').insertAfter($(editorElement)) + let p = div$[0].firstChild - editor = Helpers.mobiledoc.renderInto(editorElement, - ({post, markupSection, marker}) => post([markupSection('p', [marker('abcdef')])]) - ); + editor = Helpers.mobiledoc.renderInto(editorElement, ({ post, markupSection, marker }) => + post([markupSection('p', [marker('abcdef')])]) + ) // If the editor isn't disabled, some browsers will "fix" the selection range we are // about to add by constraining it within the contentEditable container div - editor.disableEditing(); + editor.disableEditing() - let anchorNode = $(editorElement).find('p:contains(abcdef)')[0].firstChild; - let focusNode = p; - Helpers.dom.selectRange(anchorNode, 0, focusNode, 0); + let anchorNode = $(editorElement).find('p:contains(abcdef)')[0].firstChild + let focusNode = p + Helpers.dom.selectRange(anchorNode, 0, focusNode, 0) Helpers.wait(() => { - assert.ok(window.getSelection().anchorNode === anchorNode, 'precond - anchor node'); - assert.ok(window.getSelection().focusNode === focusNode, 'precond - focus node'); - let range = editor.range; + assert.ok(window.getSelection().anchorNode === anchorNode, 'precond - anchor node') + assert.ok(window.getSelection().focusNode === focusNode, 'precond - focus node') + let range = editor.range - assert.positionIsEqual(range.head, editor.post.headPosition(), 'head'); - assert.positionIsEqual(range.tail, editor.post.tailPosition(), 'tail'); + assert.positionIsEqual(range.head, editor.post.headPosition(), 'head') + assert.positionIsEqual(range.tail, editor.post.tailPosition(), 'tail') - div$.remove(); - done(); - }); -}); + div$.remove() + done() + }) +}) -test('Position cannot be on list section', (assert) => { - let post = Helpers.postAbstract.build(({post, listSection, listItem}) => { - return post([listSection('ul', [listItem()])]); - }); +test('Position cannot be on list section', assert => { + let post = Helpers.postAbstract.build(({ post, listSection, listItem }) => { + return post([listSection('ul', [listItem()])]) + }) - let listSection = post.sections.head; - let listItem = listSection.items.head; + let listSection = post.sections.head + let listItem = listSection.items.head - let position; + let position assert.throws(() => { - position = listSection.toPosition(0); - }, /addressable by the cursor/); + position = listSection.toPosition(0) + }, /addressable by the cursor/) - position = listItem.toPosition(0); - assert.ok(position, 'position with list item is ok'); -}); + position = listItem.toPosition(0) + assert.ok(position, 'position with list item is ok') +}) diff --git a/tests/unit/utils/cursor-range-test.js b/tests/unit/utils/cursor-range-test.js index 4a09dab25..ba885e00e 100644 --- a/tests/unit/utils/cursor-range-test.js +++ b/tests/unit/utils/cursor-range-test.js @@ -1,287 +1,270 @@ -import Helpers from '../../test-helpers'; -import Range from 'mobiledoc-kit/utils/cursor/range'; -import { DIRECTION } from 'mobiledoc-kit/utils/key'; -import { detect } from 'mobiledoc-kit/utils/array-utils'; - -const { FORWARD, BACKWARD } = DIRECTION; -const {module, test} = Helpers; - -module('Unit: Utils: Range'); - -test('#trimTo(section) when range covers only one section', (assert) => { - const section = Helpers.postAbstract.build(({markupSection}) => markupSection()); - const range = Range.create(section, 0, section, 5); - - const newRange = range.trimTo(section); - assert.ok(newRange.head.section === section, 'head section is correct'); - assert.ok(newRange.tail.section === section, 'tail section is correct'); - assert.equal(newRange.head.offset, 0, 'head offset'); - assert.equal(newRange.tail.offset, 0, 'tail offset'); -}); - -test('#trimTo head section', (assert) => { - const text = 'abcdef'; - const section1 = Helpers.postAbstract.build( - ({markupSection, marker}) => markupSection('p', [marker(text)])); - const section2 = Helpers.postAbstract.build( - ({markupSection, marker}) => markupSection('p', [marker(text)])); - - const range = Range.create(section1, 0, section2, 5); - const newRange = range.trimTo(section1); - - assert.ok(newRange.head.section === section1, 'head section'); - assert.ok(newRange.tail.section === section1, 'tail section'); - assert.equal(newRange.head.offset, 0, 'head offset'); - assert.equal(newRange.tail.offset, text.length, 'tail offset'); -}); - -test('#trimTo tail section', (assert) => { - const text = 'abcdef'; - const section1 = Helpers.postAbstract.build( - ({markupSection, marker}) => markupSection('p', [marker(text)])); - const section2 = Helpers.postAbstract.build( - ({markupSection, marker}) => markupSection('p', [marker(text)])); - - const range = Range.create(section1, 0, section2, 5); - const newRange = range.trimTo(section2); - - assert.ok(newRange.head.section === section2, 'head section'); - assert.ok(newRange.tail.section === section2, 'tail section'); - assert.equal(newRange.head.offset, 0, 'head offset'); - assert.equal(newRange.tail.offset, 5, 'tail offset'); -}); - -test('#trimTo middle section', (assert) => { - const text = 'abcdef'; - const section1 = Helpers.postAbstract.build( - ({markupSection, marker}) => markupSection('p', [marker(text)])); - const section2 = Helpers.postAbstract.build( - ({markupSection, marker}) => markupSection('p', [marker(text)])); - const section3 = Helpers.postAbstract.build( - ({markupSection, marker}) => markupSection('p', [marker(text)])); - - const range = Range.create(section1, 0, section3, 5); - const newRange = range.trimTo(section2); - - assert.ok(newRange.head.section === section2, 'head section'); - assert.ok(newRange.tail.section === section2, 'tail section'); - assert.equal(newRange.head.offset, 0, 'head offset'); - assert.equal(newRange.tail.offset, section2.text.length, 'tail offset'); -}); - -test('#move moves collapsed range 1 character in direction', (assert) => { - let section = Helpers.postAbstract.build(({markupSection, marker}) => { - return markupSection('p', [marker('abc')]); - }); - let range = Range.create(section, 0); - let nextRange = Range.create(section, 1); - - assert.ok(range.isCollapsed, 'precond - range.isCollapsed'); - assert.rangeIsEqual(range.move(DIRECTION.FORWARD), nextRange, 'move forward'); - - assert.rangeIsEqual(nextRange.move(DIRECTION.BACKWARD), range, 'move backward'); -}); - -test('#move collapses non-collapsd range in direction', (assert) => { - let section = Helpers.postAbstract.build(({markupSection, marker}) => { - return markupSection('p', [marker('abcd')]); - }); - let range = Range.create(section, 1, section, 3); - let collapseForward = Range.create(section, 3); - let collapseBackward = Range.create(section, 1); - - assert.ok(!range.isCollapsed, 'precond - !range.isCollapsed'); - assert.rangeIsEqual(range.move(FORWARD), collapseForward, - 'collapse forward'); - assert.rangeIsEqual(range.move(BACKWARD), collapseBackward, - 'collapse forward'); -}); - -test('#extend expands range in direction', (assert) => { - let section = Helpers.postAbstract.build(({markupSection, marker}) => { - return markupSection('p', [marker('abcd')]); - }); - let collapsedRange = Range.create(section, 1); - let collapsedRangeForward = Range.create(section, 1, section, 2, FORWARD); - let collapsedRangeBackward = Range.create(section, 0, section, 1, BACKWARD); - - let nonCollapsedRange = Range.create(section, 1, section, 2); - let nonCollapsedRangeForward = Range.create(section, 1, section, 3, FORWARD); - let nonCollapsedRangeBackward = Range.create(section, 0, section, 2, BACKWARD); - - assert.ok(collapsedRange.isCollapsed, 'precond - collapsedRange.isCollapsed'); - assert.rangeIsEqual(collapsedRange.extend(FORWARD), collapsedRangeForward, - 'collapsedRange extend forward'); - assert.rangeIsEqual(collapsedRange.extend(BACKWARD), collapsedRangeBackward, - 'collapsedRange extend backward'); - - assert.ok(!nonCollapsedRange.isCollapsed, 'precond -nonCollapsedRange.isCollapsed'); - assert.rangeIsEqual(nonCollapsedRange.extend(FORWARD), nonCollapsedRangeForward, - 'nonCollapsedRange extend forward'); - assert.rangeIsEqual(nonCollapsedRange.extend(BACKWARD), nonCollapsedRangeBackward, - 'nonCollapsedRange extend backward'); -}); - -test('#extend expands range in multiple units in direction', (assert) => { - let post = Helpers.postAbstract.build(({post, markupSection, marker}) => { - return post([ - markupSection('p', [marker('abcd')]), - markupSection('p', [marker('1234')]) - ]); - }); - - let headSection = post.sections.head; - let tailSection = post.sections.tail; +import Helpers from '../../test-helpers' +import Range from 'mobiledoc-kit/utils/cursor/range' +import { DIRECTION } from 'mobiledoc-kit/utils/key' +import { detect } from 'mobiledoc-kit/utils/array-utils' + +const { FORWARD, BACKWARD } = DIRECTION +const { module, test } = Helpers + +module('Unit: Utils: Range') + +test('#trimTo(section) when range covers only one section', assert => { + const section = Helpers.postAbstract.build(({ markupSection }) => markupSection()) + const range = Range.create(section, 0, section, 5) + + const newRange = range.trimTo(section) + assert.ok(newRange.head.section === section, 'head section is correct') + assert.ok(newRange.tail.section === section, 'tail section is correct') + assert.equal(newRange.head.offset, 0, 'head offset') + assert.equal(newRange.tail.offset, 0, 'tail offset') +}) + +test('#trimTo head section', assert => { + const text = 'abcdef' + const section1 = Helpers.postAbstract.build(({ markupSection, marker }) => markupSection('p', [marker(text)])) + const section2 = Helpers.postAbstract.build(({ markupSection, marker }) => markupSection('p', [marker(text)])) + + const range = Range.create(section1, 0, section2, 5) + const newRange = range.trimTo(section1) + + assert.ok(newRange.head.section === section1, 'head section') + assert.ok(newRange.tail.section === section1, 'tail section') + assert.equal(newRange.head.offset, 0, 'head offset') + assert.equal(newRange.tail.offset, text.length, 'tail offset') +}) + +test('#trimTo tail section', assert => { + const text = 'abcdef' + const section1 = Helpers.postAbstract.build(({ markupSection, marker }) => markupSection('p', [marker(text)])) + const section2 = Helpers.postAbstract.build(({ markupSection, marker }) => markupSection('p', [marker(text)])) + + const range = Range.create(section1, 0, section2, 5) + const newRange = range.trimTo(section2) + + assert.ok(newRange.head.section === section2, 'head section') + assert.ok(newRange.tail.section === section2, 'tail section') + assert.equal(newRange.head.offset, 0, 'head offset') + assert.equal(newRange.tail.offset, 5, 'tail offset') +}) + +test('#trimTo middle section', assert => { + const text = 'abcdef' + const section1 = Helpers.postAbstract.build(({ markupSection, marker }) => markupSection('p', [marker(text)])) + const section2 = Helpers.postAbstract.build(({ markupSection, marker }) => markupSection('p', [marker(text)])) + const section3 = Helpers.postAbstract.build(({ markupSection, marker }) => markupSection('p', [marker(text)])) + + const range = Range.create(section1, 0, section3, 5) + const newRange = range.trimTo(section2) + + assert.ok(newRange.head.section === section2, 'head section') + assert.ok(newRange.tail.section === section2, 'tail section') + assert.equal(newRange.head.offset, 0, 'head offset') + assert.equal(newRange.tail.offset, section2.text.length, 'tail offset') +}) + +test('#move moves collapsed range 1 character in direction', assert => { + let section = Helpers.postAbstract.build(({ markupSection, marker }) => { + return markupSection('p', [marker('abc')]) + }) + let range = Range.create(section, 0) + let nextRange = Range.create(section, 1) + + assert.ok(range.isCollapsed, 'precond - range.isCollapsed') + assert.rangeIsEqual(range.move(DIRECTION.FORWARD), nextRange, 'move forward') + + assert.rangeIsEqual(nextRange.move(DIRECTION.BACKWARD), range, 'move backward') +}) + +test('#move collapses non-collapsd range in direction', assert => { + let section = Helpers.postAbstract.build(({ markupSection, marker }) => { + return markupSection('p', [marker('abcd')]) + }) + let range = Range.create(section, 1, section, 3) + let collapseForward = Range.create(section, 3) + let collapseBackward = Range.create(section, 1) + + assert.ok(!range.isCollapsed, 'precond - !range.isCollapsed') + assert.rangeIsEqual(range.move(FORWARD), collapseForward, 'collapse forward') + assert.rangeIsEqual(range.move(BACKWARD), collapseBackward, 'collapse forward') +}) + +test('#extend expands range in direction', assert => { + let section = Helpers.postAbstract.build(({ markupSection, marker }) => { + return markupSection('p', [marker('abcd')]) + }) + let collapsedRange = Range.create(section, 1) + let collapsedRangeForward = Range.create(section, 1, section, 2, FORWARD) + let collapsedRangeBackward = Range.create(section, 0, section, 1, BACKWARD) + + let nonCollapsedRange = Range.create(section, 1, section, 2) + let nonCollapsedRangeForward = Range.create(section, 1, section, 3, FORWARD) + let nonCollapsedRangeBackward = Range.create(section, 0, section, 2, BACKWARD) + + assert.ok(collapsedRange.isCollapsed, 'precond - collapsedRange.isCollapsed') + assert.rangeIsEqual(collapsedRange.extend(FORWARD), collapsedRangeForward, 'collapsedRange extend forward') + assert.rangeIsEqual(collapsedRange.extend(BACKWARD), collapsedRangeBackward, 'collapsedRange extend backward') + + assert.ok(!nonCollapsedRange.isCollapsed, 'precond -nonCollapsedRange.isCollapsed') + assert.rangeIsEqual(nonCollapsedRange.extend(FORWARD), nonCollapsedRangeForward, 'nonCollapsedRange extend forward') + assert.rangeIsEqual( + nonCollapsedRange.extend(BACKWARD), + nonCollapsedRangeBackward, + 'nonCollapsedRange extend backward' + ) +}) + +test('#extend expands range in multiple units in direction', assert => { + let post = Helpers.postAbstract.build(({ post, markupSection, marker }) => { + return post([markupSection('p', [marker('abcd')]), markupSection('p', [marker('1234')])]) + }) + + let headSection = post.sections.head + let tailSection = post.sections.tail // FORWARD - let collapsedRange = Range.create(headSection, 0); - let nonCollapsedRange = Range.create(headSection, 0, headSection, 1); - assert.rangeIsEqual(collapsedRange.extend(FORWARD*2), - Range.create(headSection, 0, headSection, 2, FORWARD), - 'extend forward 2'); - - assert.rangeIsEqual(collapsedRange.extend(FORWARD*('abcd12'.length+1)), - Range.create(headSection, 0, tailSection, 2, FORWARD), - 'extend forward across sections'); - - assert.rangeIsEqual(nonCollapsedRange.extend(FORWARD*2), - Range.create(headSection, 0, headSection, 3, FORWARD), - 'extend non-collapsed forward 2'); - - assert.rangeIsEqual(nonCollapsedRange.extend(FORWARD*('bcd12'.length+1)), - Range.create(headSection, 0, tailSection, 2, FORWARD), - 'extend non-collapsed across sections'); + let collapsedRange = Range.create(headSection, 0) + let nonCollapsedRange = Range.create(headSection, 0, headSection, 1) + assert.rangeIsEqual( + collapsedRange.extend(FORWARD * 2), + Range.create(headSection, 0, headSection, 2, FORWARD), + 'extend forward 2' + ) + + assert.rangeIsEqual( + collapsedRange.extend(FORWARD * ('abcd12'.length + 1)), + Range.create(headSection, 0, tailSection, 2, FORWARD), + 'extend forward across sections' + ) + + assert.rangeIsEqual( + nonCollapsedRange.extend(FORWARD * 2), + Range.create(headSection, 0, headSection, 3, FORWARD), + 'extend non-collapsed forward 2' + ) + + assert.rangeIsEqual( + nonCollapsedRange.extend(FORWARD * ('bcd12'.length + 1)), + Range.create(headSection, 0, tailSection, 2, FORWARD), + 'extend non-collapsed across sections' + ) // BACKWARD - collapsedRange = Range.create(tailSection, '1234'.length); - nonCollapsedRange = Range.create(tailSection, '12'.length, tailSection, '1234'.length); - assert.rangeIsEqual(collapsedRange.extend(BACKWARD*'12'.length), - Range.create(tailSection, '12'.length, tailSection, '1234'.length, BACKWARD), - 'extend backward 2'); - - assert.rangeIsEqual(collapsedRange.extend(BACKWARD*('1234cd'.length+1)), - Range.create(headSection, 'ab'.length, tailSection, '1234'.length, BACKWARD), - 'extend backward across sections'); - - assert.rangeIsEqual(nonCollapsedRange.extend(BACKWARD*2), - Range.create(tailSection, 0, tailSection, '1234'.length, BACKWARD), - 'extend non-collapsed backward 2'); - - assert.rangeIsEqual(nonCollapsedRange.extend(BACKWARD*('bcd12'.length+1)), - Range.create(headSection, 'a'.length, tailSection, '1234'.length, BACKWARD), - 'extend non-collapsed backward across sections'); -}); - -test('#extend(0) returns same range', (assert) => { - let post = Helpers.postAbstract.build(({post, markupSection, marker}) => { - return post([ - markupSection('p', [marker('abcd')]), - markupSection('p', [marker('1234')]) - ]); - }); - - let headSection = post.sections.head; - - let collapsedRange = Range.create(headSection, 0); - let nonCollapsedRange = Range.create(headSection, 0, headSection, 1); - - assert.rangeIsEqual(collapsedRange.extend(0), collapsedRange, 'extending collapsed range 0 is no-op'); - assert.rangeIsEqual(nonCollapsedRange.extend(0), nonCollapsedRange, 'extending non-collapsed range 0 is no-op'); -}); - -test('#expandByMarker processed markers in a callback and continues as long as the callback returns true', (assert) => { - let post = Helpers.postAbstract.build(({post, markupSection, marker, markup}) => { - let bold = markup('b'); - let italic = markup('i'); + collapsedRange = Range.create(tailSection, '1234'.length) + nonCollapsedRange = Range.create(tailSection, '12'.length, tailSection, '1234'.length) + assert.rangeIsEqual( + collapsedRange.extend(BACKWARD * '12'.length), + Range.create(tailSection, '12'.length, tailSection, '1234'.length, BACKWARD), + 'extend backward 2' + ) + + assert.rangeIsEqual( + collapsedRange.extend(BACKWARD * ('1234cd'.length + 1)), + Range.create(headSection, 'ab'.length, tailSection, '1234'.length, BACKWARD), + 'extend backward across sections' + ) + + assert.rangeIsEqual( + nonCollapsedRange.extend(BACKWARD * 2), + Range.create(tailSection, 0, tailSection, '1234'.length, BACKWARD), + 'extend non-collapsed backward 2' + ) + + assert.rangeIsEqual( + nonCollapsedRange.extend(BACKWARD * ('bcd12'.length + 1)), + Range.create(headSection, 'a'.length, tailSection, '1234'.length, BACKWARD), + 'extend non-collapsed backward across sections' + ) +}) + +test('#extend(0) returns same range', assert => { + let post = Helpers.postAbstract.build(({ post, markupSection, marker }) => { + return post([markupSection('p', [marker('abcd')]), markupSection('p', [marker('1234')])]) + }) + + let headSection = post.sections.head + + let collapsedRange = Range.create(headSection, 0) + let nonCollapsedRange = Range.create(headSection, 0, headSection, 1) + + assert.rangeIsEqual(collapsedRange.extend(0), collapsedRange, 'extending collapsed range 0 is no-op') + assert.rangeIsEqual(nonCollapsedRange.extend(0), nonCollapsedRange, 'extending non-collapsed range 0 is no-op') +}) + +test('#expandByMarker processed markers in a callback and continues as long as the callback returns true', assert => { + let post = Helpers.postAbstract.build(({ post, markupSection, marker, markup }) => { + let bold = markup('b') + let italic = markup('i') return post([ markupSection('p', [ marker('aiya', []), marker('biya', [bold, italic]), marker('ciya', [bold]), marker('diya', [bold]), - ]) - ]); - }); - - let section = post.sections.head; - let head = section.toPosition(9); // i in the third hiya - let tail = section.toPosition(15); // y in the last hiya - let range = head.toRange(tail); + ]), + ]) + }) + + let section = post.sections.head + let head = section.toPosition(9) // i in the third hiya + let tail = section.toPosition(15) // y in the last hiya + let range = head.toRange(tail) let expandedRange = range.expandByMarker(marker => { - return !!(detect(marker.markups, markup => markup.tagName === 'b')); - }); - - assert.positionIsEqual( - expandedRange.head, section.toPosition(4), - 'range head is start of second marker' - ); - assert.positionIsEqual( - expandedRange.tail, section.toPosition(16), - 'range tail did not change' - ); -}); + return !!detect(marker.markups, markup => markup.tagName === 'b') + }) + + assert.positionIsEqual(expandedRange.head, section.toPosition(4), 'range head is start of second marker') + assert.positionIsEqual(expandedRange.tail, section.toPosition(16), 'range tail did not change') +}) // https://github.com/bustle/mobiledoc-kit/issues/676 -test('#expandByMarker can expand to beginning of section with matching markups', (assert) => { - let post = Helpers.postAbstract.build(({post, markupSection, marker, markup}) => { - let bold = markup('b'); - let italic = markup('i'); +test('#expandByMarker can expand to beginning of section with matching markups', assert => { + let post = Helpers.postAbstract.build(({ post, markupSection, marker, markup }) => { + let bold = markup('b') + let italic = markup('i') return post([ markupSection('p', [ marker('aiya', [bold]), marker('biya', [bold, italic]), marker('ciya', [bold]), marker('diya', [bold]), - ]) - ]); - }); - - let section = post.sections.head; - let head = section.toPosition(14); // i in 4th hiya - let tail = section.toPosition(14); // i in 4th hiya - let range = head.toRange(tail); + ]), + ]) + }) + + let section = post.sections.head + let head = section.toPosition(14) // i in 4th hiya + let tail = section.toPosition(14) // i in 4th hiya + let range = head.toRange(tail) let expandedRange = range.expandByMarker(marker => { - return !!(detect(marker.markups, markup => markup.tagName === 'b')); - }); - - assert.positionIsEqual( - expandedRange.head, section.toPosition(0), - 'range head is start of first marker' - ); - assert.positionIsEqual( - expandedRange.tail, section.toPosition(16), - 'range tail is at end of last marker' - ); -}); - -test('#expandByMarker can expand to end of section with matching markups', (assert) => { - let post = Helpers.postAbstract.build(({post, markupSection, marker, markup}) => { - let bold = markup('b'); - let italic = markup('i'); + return !!detect(marker.markups, markup => markup.tagName === 'b') + }) + + assert.positionIsEqual(expandedRange.head, section.toPosition(0), 'range head is start of first marker') + assert.positionIsEqual(expandedRange.tail, section.toPosition(16), 'range tail is at end of last marker') +}) + +test('#expandByMarker can expand to end of section with matching markups', assert => { + let post = Helpers.postAbstract.build(({ post, markupSection, marker, markup }) => { + let bold = markup('b') + let italic = markup('i') return post([ markupSection('p', [ marker('aiya', [bold]), marker('biya', [bold, italic]), marker('ciya', [bold]), marker('diya', [bold]), - ]) - ]); - }); - - let section = post.sections.head; - let head = section.toPosition(2); // i in 4th hiya - let tail = section.toPosition(2); // i in 4th hiya - let range = head.toRange(tail); + ]), + ]) + }) + + let section = post.sections.head + let head = section.toPosition(2) // i in 4th hiya + let tail = section.toPosition(2) // i in 4th hiya + let range = head.toRange(tail) let expandedRange = range.expandByMarker(marker => { - return !!(detect(marker.markups, markup => markup.tagName === 'b')); - }); - - assert.positionIsEqual( - expandedRange.head, section.toPosition(0), - 'range head is start of first marker' - ); - assert.positionIsEqual( - expandedRange.tail, section.toPosition(16), - 'range tail is at end of last marker' - ); -}); + return !!detect(marker.markups, markup => markup.tagName === 'b') + }) + + assert.positionIsEqual(expandedRange.head, section.toPosition(0), 'range head is start of first marker') + assert.positionIsEqual(expandedRange.tail, section.toPosition(16), 'range tail is at end of last marker') +}) diff --git a/tests/unit/utils/fixed-queue-test.js b/tests/unit/utils/fixed-queue-test.js deleted file mode 100644 index 4032fe7df..000000000 --- a/tests/unit/utils/fixed-queue-test.js +++ /dev/null @@ -1,40 +0,0 @@ -import Helpers from '../../test-helpers'; -import FixedQueue from 'mobiledoc-kit/utils/fixed-queue'; - -const {module, test} = Helpers; - -module('Unit: Utils: FixedQueue'); - -test('basic implementation', (assert) => { - let queue = new FixedQueue(3); - for (let i=0; i < 3; i++) { - queue.push(i); - } - - assert.equal(queue.length, 3); - - let popped = []; - while (queue.length) { - popped.push(queue.pop()); - } - - assert.deepEqual(popped, [2,1,0]); -}); - -test('empty queue', (assert) => { - let queue = new FixedQueue(0); - assert.equal(queue.length, 0); - assert.equal(queue.pop(), undefined); - queue.push(1); - - assert.equal(queue.length, 0); - assert.deepEqual(queue.toArray(), []); -}); - -test('push onto full queue ejects first item', (assert) => { - let queue = new FixedQueue(1); - queue.push(0); - queue.push(1); - - assert.deepEqual(queue.toArray(), [1]); -}); diff --git a/tests/unit/utils/fixed-queue-test.ts b/tests/unit/utils/fixed-queue-test.ts new file mode 100644 index 000000000..00f1988d6 --- /dev/null +++ b/tests/unit/utils/fixed-queue-test.ts @@ -0,0 +1,40 @@ +import Helpers from '../../test-helpers' +import FixedQueue from '../../../src/js/utils/fixed-queue' + +const { module, test } = Helpers + +module('Unit: Utils: FixedQueue') + +test('basic implementation', assert => { + let queue = new FixedQueue(3) + for (let i = 0; i < 3; i++) { + queue.push(i) + } + + assert.equal(queue.length, 3) + + let popped = [] + while (queue.length) { + popped.push(queue.pop()) + } + + assert.deepEqual(popped, [2, 1, 0]) +}) + +test('empty queue', assert => { + let queue = new FixedQueue(0) + assert.equal(queue.length, 0) + assert.equal(queue.pop(), undefined) + queue.push(1) + + assert.equal(queue.length, 0) + assert.deepEqual(queue.toArray(), []) +}) + +test('push onto full queue ejects first item', assert => { + let queue = new FixedQueue(1) + queue.push(0) + queue.push(1) + + assert.deepEqual(queue.toArray(), [1]) +}) diff --git a/tests/unit/utils/key-test.js b/tests/unit/utils/key-test.js index 514b082f0..e5e35747e 100644 --- a/tests/unit/utils/key-test.js +++ b/tests/unit/utils/key-test.js @@ -1,93 +1,77 @@ -import Helpers from '../../test-helpers'; -import Key from 'mobiledoc-kit/utils/key'; -import { MODIFIERS } from 'mobiledoc-kit/utils/key'; -import Keys from 'mobiledoc-kit/utils/keys'; -import Keycodes from 'mobiledoc-kit/utils/keycodes'; +import Helpers from '../../test-helpers' +import Key from 'mobiledoc-kit/utils/key' +import { MODIFIERS } from 'mobiledoc-kit/utils/key' +import Keys from 'mobiledoc-kit/utils/keys' +import Keycodes from 'mobiledoc-kit/utils/keycodes' -const {module, test} = Helpers; +const { module, test } = Helpers -module('Unit: Utils: Key'); +module('Unit: Utils: Key') -test('#hasModifier with no modifier', (assert) => { - const event = Helpers.dom.createMockEvent('keydown', null, { keyCode: 42 }); - const key = Key.fromEvent(event); +test('#hasModifier with no modifier', assert => { + const event = Helpers.dom.createMockEvent('keydown', null, { keyCode: 42 }) + const key = Key.fromEvent(event) - assert.ok(!key.hasModifier(MODIFIERS.META), "META not pressed"); - assert.ok(!key.hasModifier(MODIFIERS.CTRL), "CTRL not pressed"); - assert.ok(!key.hasModifier(MODIFIERS.SHIFT), "SHIFT not pressed"); -}); + assert.ok(!key.hasModifier(MODIFIERS.META), 'META not pressed') + assert.ok(!key.hasModifier(MODIFIERS.CTRL), 'CTRL not pressed') + assert.ok(!key.hasModifier(MODIFIERS.SHIFT), 'SHIFT not pressed') +}) -test('#hasModifier with META', (assert) => { - const event = Helpers.dom.createMockEvent('keyup', null, { metaKey: true }); - const key = Key.fromEvent(event); +test('#hasModifier with META', assert => { + const event = Helpers.dom.createMockEvent('keyup', null, { metaKey: true }) + const key = Key.fromEvent(event) - assert.ok(key.hasModifier(MODIFIERS.META), "META pressed"); - assert.ok(!key.hasModifier(MODIFIERS.CTRL), "CTRL not pressed"); - assert.ok(!key.hasModifier(MODIFIERS.SHIFT), "SHIFT not pressed"); -}); + assert.ok(key.hasModifier(MODIFIERS.META), 'META pressed') + assert.ok(!key.hasModifier(MODIFIERS.CTRL), 'CTRL not pressed') + assert.ok(!key.hasModifier(MODIFIERS.SHIFT), 'SHIFT not pressed') +}) -test('#hasModifier with CTRL', (assert) => { - const event = Helpers.dom.createMockEvent('keypress', null, { ctrlKey: true }); - const key = Key.fromEvent(event); +test('#hasModifier with CTRL', assert => { + const event = Helpers.dom.createMockEvent('keypress', null, { ctrlKey: true }) + const key = Key.fromEvent(event) - assert.ok(!key.hasModifier(MODIFIERS.META), "META not pressed"); - assert.ok(key.hasModifier(MODIFIERS.CTRL), "CTRL pressed"); - assert.ok(!key.hasModifier(MODIFIERS.SHIFT), "SHIFT not pressed"); -}); + assert.ok(!key.hasModifier(MODIFIERS.META), 'META not pressed') + assert.ok(key.hasModifier(MODIFIERS.CTRL), 'CTRL pressed') + assert.ok(!key.hasModifier(MODIFIERS.SHIFT), 'SHIFT not pressed') +}) -test('#hasModifier with SHIFT', (assert) => { - const event = Helpers.dom.createMockEvent('keydown', null, { shiftKey: true }); - const key = Key.fromEvent(event); +test('#hasModifier with SHIFT', assert => { + const event = Helpers.dom.createMockEvent('keydown', null, { shiftKey: true }) + const key = Key.fromEvent(event) - assert.ok(!key.hasModifier(MODIFIERS.META), "META not pressed"); - assert.ok(!key.hasModifier(MODIFIERS.CTRL), "CTRL not pressed"); - assert.ok(key.hasModifier(MODIFIERS.SHIFT), "SHIFT pressed"); -}); + assert.ok(!key.hasModifier(MODIFIERS.META), 'META not pressed') + assert.ok(!key.hasModifier(MODIFIERS.CTRL), 'CTRL not pressed') + assert.ok(key.hasModifier(MODIFIERS.SHIFT), 'SHIFT pressed') +}) // Firefox will fire keypress events for some keys that should not be printable -test('firefox: non-printable are treated as not printable', (assert) => { - const KEYS = [ - Keys.DOWN, - Keys.HOME, - Keys.END, - Keys.PAGEUP, - Keys.PAGEDOWN, - Keys.INS, - Keys.CLEAR, - Keys.PAUSE, - Keys.ESC - ]; - - KEYS.forEach((key) => { - let element = $('#qunit-fixture')[0]; +test('firefox: non-printable are treated as not printable', assert => { + const KEYS = [Keys.DOWN, Keys.HOME, Keys.END, Keys.PAGEUP, Keys.PAGEDOWN, Keys.INS, Keys.CLEAR, Keys.PAUSE, Keys.ESC] + + KEYS.forEach(key => { + let element = $('#qunit-fixture')[0] let event = Helpers.dom.createMockEvent('keypress', element, { key, - }); - let keyInstance = Key.fromEvent(event); + }) + let keyInstance = Key.fromEvent(event) - assert.ok(!keyInstance.isPrintable(), `key ${key} is not printable`); - }); -}); + assert.ok(!keyInstance.isPrintable(), `key ${key} is not printable`) + }) +}) -test('uses keyCode as a fallback if key is not supported', (assert) => { - let element = $('#qunit-fixture')[0]; +test('uses keyCode as a fallback if key is not supported', assert => { + let element = $('#qunit-fixture')[0] let event = Helpers.dom.createMockEvent('keypress', element, { key: Keys.ESC, - keyCode: Keycodes.SPACE - }); - let keyInstance = Key.fromEvent(event); - assert.ok( - keyInstance.isEscape(), - 'key is preferred over keyCode if supported' - ); + keyCode: Keycodes.SPACE, + }) + let keyInstance = Key.fromEvent(event) + assert.ok(keyInstance.isEscape(), 'key is preferred over keyCode if supported') event = Helpers.dom.createMockEvent('keypress', element, { - keyCode: Keycodes.SPACE - }); - keyInstance = Key.fromEvent(event); - assert.ok( - keyInstance.isSpace(), - 'keyCode is used if key is not supported' - ); -}); + keyCode: Keycodes.SPACE, + }) + keyInstance = Key.fromEvent(event) + assert.ok(keyInstance.isSpace(), 'keyCode is used if key is not supported') +}) diff --git a/tests/unit/utils/linked-list-test.js b/tests/unit/utils/linked-list-test.js index 706883820..e162b379d 100644 --- a/tests/unit/utils/linked-list-test.js +++ b/tests/unit/utils/linked-list-test.js @@ -1,547 +1,554 @@ -import Helpers from '../../test-helpers'; -const {module, test} = Helpers; +import Helpers from '../../test-helpers' +const { module, test } = Helpers -import LinkedList from 'mobiledoc-kit/utils/linked-list'; -import LinkedItem from 'mobiledoc-kit/utils/linked-item'; +import LinkedList from '../../../src/js/utils/linked-list' +import LinkedItem from '../../../src/js/utils/linked-item' -const INSERTION_METHODS = ['append', 'prepend', 'insertBefore', 'insertAfter']; +const INSERTION_METHODS = ['append', 'prepend', 'insertBefore', 'insertAfter'] -module('Unit: Utils: LinkedList'); +module('Unit: Utils: LinkedList') -test('initial state', (assert) => { - let list = new LinkedList(); - assert.equal(list.head, null, 'head is null'); - assert.equal(list.tail, null ,'tail is null'); - assert.equal(list.length, 0, 'length is one'); - assert.equal(list.isEmpty, true, 'isEmpty is true'); -}); +test('initial state', assert => { + let list = new LinkedList() + assert.equal(list.head, null, 'head is null') + assert.equal(list.tail, null, 'tail is null') + assert.equal(list.length, 0, 'length is one') + assert.equal(list.isEmpty, true, 'isEmpty is true') +}) INSERTION_METHODS.forEach(method => { - test(`#${method} initial item`, (assert) => { - let list = new LinkedList(); - let item = new LinkedItem(); - list[method](item); - assert.equal(list.length, 1, 'length is one'); - assert.equal(list.isEmpty, false, 'isEmpty is false'); - assert.equal(list.head, item, 'head is item'); - assert.equal(list.tail, item, 'tail is item'); - assert.equal(item.next, null, 'item next is null'); - assert.equal(item.prev, null, 'item prev is null'); - }); - - test(`#${method} calls adoptItem`, (assert) => { - let adoptedItem; + test(`#${method} initial item`, assert => { + let list = new LinkedList() + let item = new LinkedItem() + list[method](item) + assert.equal(list.length, 1, 'length is one') + assert.equal(list.isEmpty, false, 'isEmpty is false') + assert.equal(list.head, item, 'head is item') + assert.equal(list.tail, item, 'tail is item') + assert.equal(item.next, null, 'item next is null') + assert.equal(item.prev, null, 'item prev is null') + }) + + test(`#${method} calls adoptItem`, assert => { + let adoptedItem let list = new LinkedList({ adoptItem(item) { - adoptedItem = item; - } - }); - let item = new LinkedItem(); - list[method](item); - assert.equal(adoptedItem, item, 'item is adopted'); - }); - - test(`#${method} throws when inserting item that is already in this list`, (assert) => { - let list = new LinkedList(); - let item = new LinkedItem(); - list[method](item); - - assert.throws( - () => list[method](item), - /Cannot insert.*already in a list/ - ); - }); - - test(`#${method} throws if item is in another list`, (assert) => { - let [list, otherList] = [new LinkedList(), new LinkedList()]; - let [item, otherItem] = [new LinkedItem(), new LinkedItem()]; - list[method](item); - otherList[method](otherItem); - - assert.throws( - () => list[method](otherItem), - /Cannot insert.*already in a list/ - ); - }); -}); - -test(`#append second item`, (assert) => { - let list = new LinkedList(); - let itemOne = new LinkedItem(); - let itemTwo = new LinkedItem(); - list.append(itemOne); - list.append(itemTwo); - assert.equal(list.length, 2, 'length is two'); - assert.equal(list.head, itemOne, 'head is itemOne'); - assert.equal(list.tail, itemTwo, 'tail is itemTwo'); - assert.equal(itemOne.prev, null, 'itemOne prev is null'); - assert.equal(itemOne.next, itemTwo, 'itemOne next is itemTwo'); - assert.equal(itemTwo.prev, itemOne, 'itemTwo prev is itemOne'); - assert.equal(itemTwo.next, null, 'itemTwo next is null'); -}); - -test(`#prepend additional item`, (assert) => { - let list = new LinkedList(); - let itemOne = new LinkedItem(); - let itemTwo = new LinkedItem(); - list.prepend(itemTwo); - list.prepend(itemOne); - assert.equal(list.length, 2, 'length is two'); - assert.equal(list.head, itemOne, 'head is itemOne'); - assert.equal(list.tail, itemTwo, 'tail is itemTwo'); - assert.equal(itemOne.prev, null, 'itemOne prev is null'); - assert.equal(itemOne.next, itemTwo, 'itemOne next is itemTwo'); - assert.equal(itemTwo.prev, itemOne, 'itemTwo prev is itemOne'); - assert.equal(itemTwo.next, null, 'itemTwo next is null'); -}); - -test(`#insertBefore a middle item`, (assert) => { - let list = new LinkedList(); - let itemOne = new LinkedItem(); - let itemTwo = new LinkedItem(); - let itemThree = new LinkedItem(); - list.prepend(itemOne); - list.append(itemThree); - list.insertBefore(itemTwo, itemThree); - assert.equal(list.length, 3, 'length is three'); - assert.equal(list.head, itemOne, 'head is itemOne'); - assert.equal(list.tail, itemThree, 'tail is itemThree'); - assert.equal(itemOne.prev, null, 'itemOne prev is null'); - assert.equal(itemOne.next, itemTwo, 'itemOne next is itemTwo'); - assert.equal(itemTwo.prev, itemOne, 'itemTwo prev is itemOne'); - assert.equal(itemTwo.next, itemThree, 'itemTwo next is itemThree'); - assert.equal(itemThree.prev, itemTwo, 'itemThree prev is itemTwo'); - assert.equal(itemThree.next, null, 'itemThree next is null'); -}); - -test('#insertBefore null reference item appends the item', (assert) => { - let list = new LinkedList(); - let item1 = new LinkedItem(); - let item2 = new LinkedItem(); - list.append(item1); - list.insertBefore(item2, null); - - assert.equal(list.length, 2); - assert.equal(list.tail, item2, 'item2 is appended'); - assert.equal(list.head, item1, 'item1 is at head'); - assert.equal(item2.prev, item1, 'item2.prev'); - assert.equal(item1.next, item2, 'item1.next'); - assert.equal(item2.next, null); - assert.equal(item1.prev, null); -}); - -test(`#insertAfter a middle item`, (assert) => { - let list = new LinkedList(); - let itemOne = new LinkedItem(); - let itemTwo = new LinkedItem(); - let itemThree = new LinkedItem(); - list.prepend(itemOne); - list.append(itemThree); - list.insertAfter(itemTwo, itemOne); - - assert.equal(list.length, 3); - assert.equal(list.head, itemOne, 'head is itemOne'); - assert.equal(list.tail, itemThree, 'tail is itemThree'); - assert.equal(itemOne.prev, null, 'itemOne prev is null'); - assert.equal(itemOne.next, itemTwo, 'itemOne next is itemTwo'); - assert.equal(itemTwo.prev, itemOne, 'itemTwo prev is itemOne'); - assert.equal(itemTwo.next, itemThree, 'itemTwo next is itemThree'); - assert.equal(itemThree.prev, itemTwo, 'itemThree prev is itemTwo'); - assert.equal(itemThree.next, null, 'itemThree next is null'); -}); - -test('#insertAfter null reference item prepends the item', (assert) => { - let list = new LinkedList(); - let item1 = new LinkedItem(); - let item2 = new LinkedItem(); - list.append(item2); - list.insertAfter(item1, null); - - assert.equal(list.length, 2); - assert.equal(list.head, item1, 'item2 is appended'); - assert.equal(list.tail, item2, 'item1 is at tail'); - assert.equal(item1.next, item2, 'item1.next = item2'); - assert.equal(item1.prev, null, 'item1.prev = null'); - assert.equal(item2.prev, item1, 'item2.prev = item1'); - assert.equal(item2.next, null, 'item2.next = null'); -}); - -test(`#remove an only item`, (assert) => { - let list = new LinkedList(); - let item = new LinkedItem(); - list.append(item); - list.remove(item); - assert.equal(list.length, 0, 'length is zero'); - assert.equal(list.isEmpty, true, 'isEmpty is true'); - assert.equal(list.head, null, 'head is null'); - assert.equal(list.tail, null, 'tail is null'); - assert.equal(item.prev, null, 'item prev is null'); - assert.equal(item.next, null, 'item next is null'); -}); - -test(`#remove calls freeItem`, (assert) => { - let freed = []; + adoptedItem = item + }, + }) + let item = new LinkedItem() + list[method](item) + assert.equal(adoptedItem, item, 'item is adopted') + }) + + test(`#${method} throws when inserting item that is already in this list`, assert => { + let list = new LinkedList() + let item = new LinkedItem() + list[method](item) + + assert.throws(() => list[method](item), /Cannot insert.*already in a list/) + }) + + test(`#${method} throws if item is in another list`, assert => { + let [list, otherList] = [new LinkedList(), new LinkedList()] + let [item, otherItem] = [new LinkedItem(), new LinkedItem()] + list[method](item) + otherList[method](otherItem) + + assert.throws(() => list[method](otherItem), /Cannot insert.*already in a list/) + }) +}) + +test(`#append second item`, assert => { + let list = new LinkedList() + let itemOne = new LinkedItem() + let itemTwo = new LinkedItem() + list.append(itemOne) + list.append(itemTwo) + assert.equal(list.length, 2, 'length is two') + assert.equal(list.head, itemOne, 'head is itemOne') + assert.equal(list.tail, itemTwo, 'tail is itemTwo') + assert.equal(itemOne.prev, null, 'itemOne prev is null') + assert.equal(itemOne.next, itemTwo, 'itemOne next is itemTwo') + assert.equal(itemTwo.prev, itemOne, 'itemTwo prev is itemOne') + assert.equal(itemTwo.next, null, 'itemTwo next is null') +}) + +test(`#prepend additional item`, assert => { + let list = new LinkedList() + let itemOne = new LinkedItem() + let itemTwo = new LinkedItem() + list.prepend(itemTwo) + list.prepend(itemOne) + assert.equal(list.length, 2, 'length is two') + assert.equal(list.head, itemOne, 'head is itemOne') + assert.equal(list.tail, itemTwo, 'tail is itemTwo') + assert.equal(itemOne.prev, null, 'itemOne prev is null') + assert.equal(itemOne.next, itemTwo, 'itemOne next is itemTwo') + assert.equal(itemTwo.prev, itemOne, 'itemTwo prev is itemOne') + assert.equal(itemTwo.next, null, 'itemTwo next is null') +}) + +test(`#insertBefore a middle item`, assert => { + let list = new LinkedList() + let itemOne = new LinkedItem() + let itemTwo = new LinkedItem() + let itemThree = new LinkedItem() + list.prepend(itemOne) + list.append(itemThree) + list.insertBefore(itemTwo, itemThree) + assert.equal(list.length, 3, 'length is three') + assert.equal(list.head, itemOne, 'head is itemOne') + assert.equal(list.tail, itemThree, 'tail is itemThree') + assert.equal(itemOne.prev, null, 'itemOne prev is null') + assert.equal(itemOne.next, itemTwo, 'itemOne next is itemTwo') + assert.equal(itemTwo.prev, itemOne, 'itemTwo prev is itemOne') + assert.equal(itemTwo.next, itemThree, 'itemTwo next is itemThree') + assert.equal(itemThree.prev, itemTwo, 'itemThree prev is itemTwo') + assert.equal(itemThree.next, null, 'itemThree next is null') +}) + +test('#insertBefore null reference item appends the item', assert => { + let list = new LinkedList() + let item1 = new LinkedItem() + let item2 = new LinkedItem() + list.append(item1) + list.insertBefore(item2, null) + + assert.equal(list.length, 2) + assert.equal(list.tail, item2, 'item2 is appended') + assert.equal(list.head, item1, 'item1 is at head') + assert.equal(item2.prev, item1, 'item2.prev') + assert.equal(item1.next, item2, 'item1.next') + assert.equal(item2.next, null) + assert.equal(item1.prev, null) +}) + +test(`#insertAfter a middle item`, assert => { + let list = new LinkedList() + let itemOne = new LinkedItem() + let itemTwo = new LinkedItem() + let itemThree = new LinkedItem() + list.prepend(itemOne) + list.append(itemThree) + list.insertAfter(itemTwo, itemOne) + + assert.equal(list.length, 3) + assert.equal(list.head, itemOne, 'head is itemOne') + assert.equal(list.tail, itemThree, 'tail is itemThree') + assert.equal(itemOne.prev, null, 'itemOne prev is null') + assert.equal(itemOne.next, itemTwo, 'itemOne next is itemTwo') + assert.equal(itemTwo.prev, itemOne, 'itemTwo prev is itemOne') + assert.equal(itemTwo.next, itemThree, 'itemTwo next is itemThree') + assert.equal(itemThree.prev, itemTwo, 'itemThree prev is itemTwo') + assert.equal(itemThree.next, null, 'itemThree next is null') +}) + +test('#insertAfter null reference item prepends the item', assert => { + let list = new LinkedList() + let item1 = new LinkedItem() + let item2 = new LinkedItem() + list.append(item2) + list.insertAfter(item1, null) + + assert.equal(list.length, 2) + assert.equal(list.head, item1, 'item2 is appended') + assert.equal(list.tail, item2, 'item1 is at tail') + assert.equal(item1.next, item2, 'item1.next = item2') + assert.equal(item1.prev, null, 'item1.prev = null') + assert.equal(item2.prev, item1, 'item2.prev = item1') + assert.equal(item2.next, null, 'item2.next = null') +}) + +test(`#remove an only item`, assert => { + let list = new LinkedList() + let item = new LinkedItem() + list.append(item) + list.remove(item) + assert.equal(list.length, 0, 'length is zero') + assert.equal(list.isEmpty, true, 'isEmpty is true') + assert.equal(list.head, null, 'head is null') + assert.equal(list.tail, null, 'tail is null') + assert.equal(item.prev, null, 'item prev is null') + assert.equal(item.next, null, 'item next is null') +}) + +test(`#remove calls freeItem`, assert => { + let freed = [] let list = new LinkedList({ freeItem(item) { - freed.push(item); - } - }); - let item = new LinkedItem(); - list.append(item); - list.remove(item); - assert.deepEqual(freed, [item]); -}); - -test(`#remove a first item`, (assert) => { - let list = new LinkedList(); - let itemOne = new LinkedItem(); - let itemTwo = new LinkedItem(); - list.append(itemOne); - list.append(itemTwo); - list.remove(itemOne); - - assert.equal(list.length, 1); - assert.equal(list.head, itemTwo, 'head is itemTwo'); - assert.equal(list.tail, itemTwo, 'tail is itemTwo'); - assert.equal(itemOne.prev, null, 'itemOne prev is null'); - assert.equal(itemOne.next, null, 'itemOne next is null'); - assert.equal(itemTwo.prev, null, 'itemTwo prev is null'); - assert.equal(itemTwo.next, null, 'itemTwo next is null'); -}); - -test(`#remove a last item`, (assert) => { - let list = new LinkedList(); - let itemOne = new LinkedItem(); - let itemTwo = new LinkedItem(); - list.append(itemOne); - list.append(itemTwo); - list.remove(itemTwo); - assert.equal(list.length, 1); - assert.equal(list.head, itemOne, 'head is itemOne'); - assert.equal(list.tail, itemOne, 'tail is itemOne'); - assert.equal(itemOne.prev, null, 'itemOne prev is null'); - assert.equal(itemOne.next, null, 'itemOne next is null'); - assert.equal(itemTwo.prev, null, 'itemTwo prev is null'); - assert.equal(itemTwo.next, null, 'itemTwo next is null'); -}); - -test(`#remove a middle item`, (assert) => { - let list = new LinkedList(); - let itemOne = new LinkedItem(); - let itemTwo = new LinkedItem(); - let itemThree = new LinkedItem(); - list.append(itemOne); - list.append(itemTwo); - list.append(itemThree); - list.remove(itemTwo); - - assert.equal(list.length, 2); - assert.equal(list.head, itemOne, 'head is itemOne'); - assert.equal(list.tail, itemThree, 'tail is itemThree'); - assert.equal(itemOne.prev, null, 'itemOne prev is null'); - assert.equal(itemOne.next, itemThree, 'itemOne next is itemThree'); - assert.equal(itemTwo.prev, null, 'itemTwo prev is null'); - assert.equal(itemTwo.next, null, 'itemTwo next is null'); - assert.equal(itemThree.prev, itemOne, 'itemThree prev is itemOne'); - assert.equal(itemThree.next, null, 'itemThree next is null'); -}); - -test(`#remove item that is not in the list is no-op`, (assert) => { - let list = new LinkedList(); - let otherItem = new LinkedItem(); - - list.remove(otherItem); - assert.equal(list.length, 0); -}); - -test(`#remove throws if item is in another list`, (assert) => { - let list = new LinkedList(); - let otherList = new LinkedList(); - let otherItem = new LinkedItem(); - - otherList.append(otherItem); - - assert.throws( - () => list.remove(otherItem), - /Cannot remove.*other list/ - ); -}); - -test(`#forEach iterates many`, (assert) => { - let list = new LinkedList(); - let itemOne = new LinkedItem(); - let itemTwo = new LinkedItem(); - let itemThree = new LinkedItem(); - list.append(itemOne); - list.append(itemTwo); - list.append(itemThree); - let items = []; - let indexes = []; + freed.push(item) + }, + }) + let item = new LinkedItem() + list.append(item) + list.remove(item) + assert.deepEqual(freed, [item]) +}) + +test(`#remove a first item`, assert => { + let list = new LinkedList() + let itemOne = new LinkedItem() + let itemTwo = new LinkedItem() + list.append(itemOne) + list.append(itemTwo) + list.remove(itemOne) + + assert.equal(list.length, 1) + assert.equal(list.head, itemTwo, 'head is itemTwo') + assert.equal(list.tail, itemTwo, 'tail is itemTwo') + assert.equal(itemOne.prev, null, 'itemOne prev is null') + assert.equal(itemOne.next, null, 'itemOne next is null') + assert.equal(itemTwo.prev, null, 'itemTwo prev is null') + assert.equal(itemTwo.next, null, 'itemTwo next is null') +}) + +test(`#remove a last item`, assert => { + let list = new LinkedList() + let itemOne = new LinkedItem() + let itemTwo = new LinkedItem() + list.append(itemOne) + list.append(itemTwo) + list.remove(itemTwo) + assert.equal(list.length, 1) + assert.equal(list.head, itemOne, 'head is itemOne') + assert.equal(list.tail, itemOne, 'tail is itemOne') + assert.equal(itemOne.prev, null, 'itemOne prev is null') + assert.equal(itemOne.next, null, 'itemOne next is null') + assert.equal(itemTwo.prev, null, 'itemTwo prev is null') + assert.equal(itemTwo.next, null, 'itemTwo next is null') +}) + +test(`#remove a middle item`, assert => { + let list = new LinkedList() + let itemOne = new LinkedItem() + let itemTwo = new LinkedItem() + let itemThree = new LinkedItem() + list.append(itemOne) + list.append(itemTwo) + list.append(itemThree) + list.remove(itemTwo) + + assert.equal(list.length, 2) + assert.equal(list.head, itemOne, 'head is itemOne') + assert.equal(list.tail, itemThree, 'tail is itemThree') + assert.equal(itemOne.prev, null, 'itemOne prev is null') + assert.equal(itemOne.next, itemThree, 'itemOne next is itemThree') + assert.equal(itemTwo.prev, null, 'itemTwo prev is null') + assert.equal(itemTwo.next, null, 'itemTwo next is null') + assert.equal(itemThree.prev, itemOne, 'itemThree prev is itemOne') + assert.equal(itemThree.next, null, 'itemThree next is null') +}) + +test(`#remove item that is not in the list is no-op`, assert => { + let list = new LinkedList() + let otherItem = new LinkedItem() + + list.remove(otherItem) + assert.equal(list.length, 0) +}) + +test(`#remove throws if item is in another list`, assert => { + let list = new LinkedList() + let otherList = new LinkedList() + let otherItem = new LinkedItem() + + otherList.append(otherItem) + + assert.throws(() => list.remove(otherItem), /Cannot remove.*other list/) +}) + +test(`#forEach iterates many`, assert => { + let list = new LinkedList() + let itemOne = new LinkedItem() + let itemTwo = new LinkedItem() + let itemThree = new LinkedItem() + list.append(itemOne) + list.append(itemTwo) + list.append(itemThree) + let items = [] + let indexes = [] list.forEach((item, index) => { - items.push(item); - indexes.push(index); - }); - assert.deepEqual(items, [itemOne, itemTwo, itemThree], 'items correct'); - assert.deepEqual(indexes, [0, 1, 2], 'indexes correct'); -}); - -test(`#forEach iterates one`, (assert) => { - let list = new LinkedList(); - let itemOne = new LinkedItem(); - list.append(itemOne); - let items = []; - let indexes = []; + items.push(item) + indexes.push(index) + }) + assert.deepEqual(items, [itemOne, itemTwo, itemThree], 'items correct') + assert.deepEqual(indexes, [0, 1, 2], 'indexes correct') +}) + +test(`#forEach iterates one`, assert => { + let list = new LinkedList() + let itemOne = new LinkedItem() + list.append(itemOne) + let items = [] + let indexes = [] list.forEach((item, index) => { - items.push(item); - indexes.push(index); - }); - assert.deepEqual(items, [itemOne], 'items correct'); - assert.deepEqual(indexes, [0], 'indexes correct'); -}); - -test('#forEach exits early if item is removed by callback', (assert) => { - let list = new LinkedList(); - [0,1,2].forEach(val => { - let i = new LinkedItem(); - i.value = val; - list.append(i); - }); - - let iterated = []; + items.push(item) + indexes.push(index) + }) + assert.deepEqual(items, [itemOne], 'items correct') + assert.deepEqual(indexes, [0], 'indexes correct') +}) + +test('#forEach exits early if item is removed by callback', assert => { + let list = new LinkedList() + ;[0, 1, 2].forEach(val => { + let i = new LinkedItem() + i.value = val + list.append(i) + }) + + let iterated = [] list.forEach((item, index) => { - iterated.push(item.value); + iterated.push(item.value) if (index === 1) { - list.remove(item); // iteration stops, skipping value 2 + list.remove(item) // iteration stops, skipping value 2 } - }); - - assert.deepEqual(iterated, [0,1], 'iteration stops when item.next is null'); -}); - -test(`#readRange walks from start to end`, (assert) => { - let list = new LinkedList(); - let itemOne = new LinkedItem(); - let itemTwo = new LinkedItem(); - let itemThree = new LinkedItem(); - list.append(itemOne); - list.append(itemTwo); - list.append(itemThree); - let items = []; - let indexes = []; + }) + + assert.deepEqual(iterated, [0, 1], 'iteration stops when item.next is null') +}) + +test(`#readRange walks from start to end`, assert => { + let list = new LinkedList() + let itemOne = new LinkedItem() + let itemTwo = new LinkedItem() + let itemThree = new LinkedItem() + list.append(itemOne) + list.append(itemTwo) + list.append(itemThree) + let items = [] + let indexes = [] list.forEach((item, index) => { - items.push(item); - indexes.push(index); - }); - assert.deepEqual(list.readRange(itemOne, itemOne), [itemOne], 'items correct'); - assert.deepEqual(list.readRange(itemTwo, itemThree), [itemTwo, itemThree], 'items correct'); - assert.deepEqual(list.readRange(itemOne, itemTwo), [itemOne, itemTwo], 'items correct'); - assert.deepEqual(list.readRange(itemOne, null), [itemOne, itemTwo, itemThree], 'items correct'); -}); - -test(`#toArray builds array`, (assert) => { - let list = new LinkedList(); - let itemOne = new LinkedItem(); - list.append(itemOne); - assert.deepEqual(list.toArray(), [itemOne], 'items correct'); -}); - -test(`#toArray builds many array`, (assert) => { - let list = new LinkedList(); - let itemOne = new LinkedItem(); - let itemTwo = new LinkedItem(); - let itemThree = new LinkedItem(); - list.append(itemOne); - list.append(itemTwo); - list.append(itemThree); - assert.deepEqual(list.toArray(), [itemOne, itemTwo, itemThree], 'items correct'); -}); - -test(`#detect finds`, (assert) => { - let list = new LinkedList(); - let itemOne = new LinkedItem(); - let itemTwo = new LinkedItem(); - let itemThree = new LinkedItem(); - list.append(itemOne); - list.append(itemTwo); - list.append(itemThree); - assert.equal(list.detect(item => item === itemOne), itemOne, 'itemOne detected'); - assert.equal(list.detect(item => item === itemTwo), itemTwo, 'itemTwo detected'); - assert.equal(list.detect(item => item === itemThree), itemThree, 'itemThree detected'); - assert.equal(list.detect(() => false), undefined, 'no item detected'); -}); - -test(`#detect finds w/ start`, (assert) => { - let list = new LinkedList(); - let itemOne = new LinkedItem(); - let itemTwo = new LinkedItem(); - let itemThree = new LinkedItem(); - list.append(itemOne); - list.append(itemTwo); - list.append(itemThree); - assert.equal(list.detect(item => item === itemOne, itemOne), itemOne, 'itemOne detected'); - assert.equal(list.detect(item => item === itemTwo, itemThree), null, 'no item detected'); -}); - -test(`#detect finds w/ reverse`, (assert) => { - let list = new LinkedList(); - let itemOne = new LinkedItem(); - let itemTwo = new LinkedItem(); - let itemThree = new LinkedItem(); - list.append(itemOne); - list.append(itemTwo); - list.append(itemThree); - assert.equal(list.detect(item => item === itemOne, itemOne, true), itemOne, 'itemTwo detected'); - assert.equal(list.detect(item => item === itemThree, itemThree, true), itemThree, 'itemThree'); -}); - -test(`#objectAt looks up by index`, (assert) => { - let list = new LinkedList(); - let itemOne = new LinkedItem(); - list.append(itemOne); - assert.equal(list.objectAt(0), itemOne, 'itemOne looked up'); - - let itemTwo = new LinkedItem(); - let itemThree = new LinkedItem(); - list.append(itemTwo); - list.append(itemThree); - assert.equal(list.objectAt(0), itemOne, 'itemOne looked up'); - assert.equal(list.objectAt(1), itemTwo, 'itemTwo looked up'); - assert.equal(list.objectAt(2), itemThree, 'itemThree looked up'); -}); - -test(`#splice removes a target and inserts an array of items`, (assert) => { - let list = new LinkedList(); - let itemOne = new LinkedItem(); - let itemTwo = new LinkedItem(); - let itemThree = new LinkedItem(); - list.append(itemOne); - list.append(itemThree); - - list.splice(itemOne, 1, [itemTwo]); - - assert.equal(list.head, itemTwo, 'itemOne is head'); - assert.equal(list.objectAt(1), itemThree, 'itemThree is present'); -}); - -test(`#splice remove nothing and inserts an array of nothing`, (assert) => { - let list = new LinkedList(); - let itemOne = new LinkedItem(); - let itemTwo = new LinkedItem(); - list.append(itemOne); - list.append(itemTwo); - - list.splice(itemTwo, 0, []); - - assert.equal(list.head, itemOne, 'itemOne is head'); - assert.equal(list.objectAt(1), itemTwo, 'itemTwo is present'); -}); - -test(`#splice can reorganize items`, (assert) => { - let list = new LinkedList(); - let itemOne = new LinkedItem(); - let itemTwo = new LinkedItem(); - let itemThree = new LinkedItem(); - list.append(itemOne); - list.append(itemTwo); - list.append(itemThree); - - list.splice(itemOne, 3, [itemThree, itemOne, itemTwo]); - - assert.equal(list.head, itemThree, 'itemThree is head'); - assert.equal(list.objectAt(1), itemOne, 'itemOne is present'); - assert.equal(list.objectAt(2), itemTwo, 'itemTwo is present'); -}); - -test(`#removeBy mutates list when item is in middle`, (assert) => { - let list = new LinkedList(); - let items = [ - new LinkedItem(), - new LinkedItem(), - new LinkedItem(), - new LinkedItem() - ]; - items[1].shouldRemove = true; - items.forEach(i => list.append(i)); - - assert.equal(list.length, 4); - list.removeBy(i => i.shouldRemove); - assert.equal(list.length, 3); - assert.equal(list.head, items[0]); - assert.equal(list.objectAt(1), items[2]); - assert.equal(list.objectAt(2), items[3]); - assert.equal(list.tail, items[3]); -}); - -test(`#removeBy mutates list when item is first`, (assert) => { - let list = new LinkedList(); - let items = [ - new LinkedItem(), - new LinkedItem(), - new LinkedItem(), - new LinkedItem() - ]; - items[0].shouldRemove = true; - items.forEach(i => list.append(i)); - - assert.equal(list.length, 4); - list.removeBy(i => i.shouldRemove); - assert.equal(list.length, 3); - assert.equal(list.head, items[1]); - assert.equal(list.objectAt(1), items[2]); - assert.equal(list.tail, items[3]); -}); - -test(`#removeBy mutates list when item is last`, (assert) => { - let list = new LinkedList(); - let items = [ - new LinkedItem(), - new LinkedItem(), - new LinkedItem(), - new LinkedItem() - ]; - items[3].shouldRemove = true; - items.forEach(i => list.append(i)); - - assert.equal(list.length, 4); - list.removeBy(i => i.shouldRemove); - assert.equal(list.length, 3); - assert.equal(list.head, items[0]); - assert.equal(list.objectAt(1), items[1]); - assert.equal(list.tail, items[2]); -}); - -test('#removeBy calls `freeItem` for each item removed', (assert) => { - let freed = []; + items.push(item) + indexes.push(index) + }) + assert.deepEqual(list.readRange(itemOne, itemOne), [itemOne], 'items correct') + assert.deepEqual(list.readRange(itemTwo, itemThree), [itemTwo, itemThree], 'items correct') + assert.deepEqual(list.readRange(itemOne, itemTwo), [itemOne, itemTwo], 'items correct') + assert.deepEqual(list.readRange(itemOne, null), [itemOne, itemTwo, itemThree], 'items correct') +}) + +test(`#toArray builds array`, assert => { + let list = new LinkedList() + let itemOne = new LinkedItem() + list.append(itemOne) + assert.deepEqual(list.toArray(), [itemOne], 'items correct') +}) + +test(`#toArray builds many array`, assert => { + let list = new LinkedList() + let itemOne = new LinkedItem() + let itemTwo = new LinkedItem() + let itemThree = new LinkedItem() + list.append(itemOne) + list.append(itemTwo) + list.append(itemThree) + assert.deepEqual(list.toArray(), [itemOne, itemTwo, itemThree], 'items correct') +}) + +test(`#detect finds`, assert => { + let list = new LinkedList() + let itemOne = new LinkedItem() + let itemTwo = new LinkedItem() + let itemThree = new LinkedItem() + list.append(itemOne) + list.append(itemTwo) + list.append(itemThree) + assert.equal( + list.detect(item => item === itemOne), + itemOne, + 'itemOne detected' + ) + assert.equal( + list.detect(item => item === itemTwo), + itemTwo, + 'itemTwo detected' + ) + assert.equal( + list.detect(item => item === itemThree), + itemThree, + 'itemThree detected' + ) + assert.equal( + list.detect(() => false), + undefined, + 'no item detected' + ) +}) + +test(`#detect finds w/ start`, assert => { + let list = new LinkedList() + let itemOne = new LinkedItem() + let itemTwo = new LinkedItem() + let itemThree = new LinkedItem() + list.append(itemOne) + list.append(itemTwo) + list.append(itemThree) + assert.equal( + list.detect(item => item === itemOne, itemOne), + itemOne, + 'itemOne detected' + ) + assert.equal( + list.detect(item => item === itemTwo, itemThree), + null, + 'no item detected' + ) +}) + +test(`#detect finds w/ reverse`, assert => { + let list = new LinkedList() + let itemOne = new LinkedItem() + let itemTwo = new LinkedItem() + let itemThree = new LinkedItem() + list.append(itemOne) + list.append(itemTwo) + list.append(itemThree) + assert.equal( + list.detect(item => item === itemOne, itemOne, true), + itemOne, + 'itemTwo detected' + ) + assert.equal( + list.detect(item => item === itemThree, itemThree, true), + itemThree, + 'itemThree' + ) +}) + +test(`#objectAt looks up by index`, assert => { + let list = new LinkedList() + let itemOne = new LinkedItem() + list.append(itemOne) + assert.equal(list.objectAt(0), itemOne, 'itemOne looked up') + + let itemTwo = new LinkedItem() + let itemThree = new LinkedItem() + list.append(itemTwo) + list.append(itemThree) + assert.equal(list.objectAt(0), itemOne, 'itemOne looked up') + assert.equal(list.objectAt(1), itemTwo, 'itemTwo looked up') + assert.equal(list.objectAt(2), itemThree, 'itemThree looked up') +}) + +test(`#splice removes a target and inserts an array of items`, assert => { + let list = new LinkedList() + let itemOne = new LinkedItem() + let itemTwo = new LinkedItem() + let itemThree = new LinkedItem() + list.append(itemOne) + list.append(itemThree) + + list.splice(itemOne, 1, [itemTwo]) + + assert.equal(list.head, itemTwo, 'itemOne is head') + assert.equal(list.objectAt(1), itemThree, 'itemThree is present') +}) + +test(`#splice remove nothing and inserts an array of nothing`, assert => { + let list = new LinkedList() + let itemOne = new LinkedItem() + let itemTwo = new LinkedItem() + list.append(itemOne) + list.append(itemTwo) + + list.splice(itemTwo, 0, []) + + assert.equal(list.head, itemOne, 'itemOne is head') + assert.equal(list.objectAt(1), itemTwo, 'itemTwo is present') +}) + +test(`#splice can reorganize items`, assert => { + let list = new LinkedList() + let itemOne = new LinkedItem() + let itemTwo = new LinkedItem() + let itemThree = new LinkedItem() + list.append(itemOne) + list.append(itemTwo) + list.append(itemThree) + + list.splice(itemOne, 3, [itemThree, itemOne, itemTwo]) + + assert.equal(list.head, itemThree, 'itemThree is head') + assert.equal(list.objectAt(1), itemOne, 'itemOne is present') + assert.equal(list.objectAt(2), itemTwo, 'itemTwo is present') +}) + +test(`#removeBy mutates list when item is in middle`, assert => { + let list = new LinkedList() + let items = [new LinkedItem(), new LinkedItem(), new LinkedItem(), new LinkedItem()] + items[1].shouldRemove = true + items.forEach(i => list.append(i)) + + assert.equal(list.length, 4) + list.removeBy(i => i.shouldRemove) + assert.equal(list.length, 3) + assert.equal(list.head, items[0]) + assert.equal(list.objectAt(1), items[2]) + assert.equal(list.objectAt(2), items[3]) + assert.equal(list.tail, items[3]) +}) + +test(`#removeBy mutates list when item is first`, assert => { + let list = new LinkedList() + let items = [new LinkedItem(), new LinkedItem(), new LinkedItem(), new LinkedItem()] + items[0].shouldRemove = true + items.forEach(i => list.append(i)) + + assert.equal(list.length, 4) + list.removeBy(i => i.shouldRemove) + assert.equal(list.length, 3) + assert.equal(list.head, items[1]) + assert.equal(list.objectAt(1), items[2]) + assert.equal(list.tail, items[3]) +}) + +test(`#removeBy mutates list when item is last`, assert => { + let list = new LinkedList() + let items = [new LinkedItem(), new LinkedItem(), new LinkedItem(), new LinkedItem()] + items[3].shouldRemove = true + items.forEach(i => list.append(i)) + + assert.equal(list.length, 4) + list.removeBy(i => i.shouldRemove) + assert.equal(list.length, 3) + assert.equal(list.head, items[0]) + assert.equal(list.objectAt(1), items[1]) + assert.equal(list.tail, items[2]) +}) + +test('#removeBy calls `freeItem` for each item removed', assert => { + let freed = [] let list = new LinkedList({ freeItem(item) { - freed.push(item); - } - }); + freed.push(item) + }, + }) - let items = [ - new LinkedItem(), - new LinkedItem(), - new LinkedItem() - ]; - items[0].name = '0'; - items[1].name = '1'; - items[2].name = '2'; + let items = [new LinkedItem(), new LinkedItem(), new LinkedItem()] + items[0].name = '0' + items[1].name = '1' + items[2].name = '2' - items[0].shouldRemove = true; - items[1].shouldRemove = true; + items[0].shouldRemove = true + items[1].shouldRemove = true - items.forEach(i => list.append(i)); + items.forEach(i => list.append(i)) - list.removeBy(i => i.shouldRemove); + list.removeBy(i => i.shouldRemove) - assert.deepEqual(freed, [items[0], items[1]]); -}); + assert.deepEqual(freed, [items[0], items[1]]) +}) -test('#every', (assert) => { - let list = new LinkedList(); - [2,3,4].forEach(n => list.append({val: n})); +test('#every', assert => { + let list = new LinkedList() + ;[2, 3, 4].forEach(n => list.append({ val: n })) - assert.ok(list.every(i => i.val > 0), '> 0'); - assert.ok(!list.every(i => i.val % 2 === 0), 'even'); -}); + assert.ok( + list.every(i => i.val > 0), + '> 0' + ) + assert.ok(!list.every(i => i.val % 2 === 0), 'even') +}) diff --git a/tests/unit/utils/object-utils-test.js b/tests/unit/utils/object-utils-test.js deleted file mode 100644 index ab11cf7ae..000000000 --- a/tests/unit/utils/object-utils-test.js +++ /dev/null @@ -1,13 +0,0 @@ -import Helpers from '../../test-helpers'; -import { entries } from 'mobiledoc-kit/utils/object-utils'; - -const { module, test } = Helpers; - -module('Unit: Utils: Object Utils'); - -test('#entries works', assert => { - assert.deepEqual(entries({ hello: 'world', goodbye: 'moon' }), [ - ['hello', 'world'], - ['goodbye', 'moon'] - ]); -}); diff --git a/tests/unit/utils/object-utils-test.ts b/tests/unit/utils/object-utils-test.ts new file mode 100644 index 000000000..ce027a3d6 --- /dev/null +++ b/tests/unit/utils/object-utils-test.ts @@ -0,0 +1,13 @@ +import Helpers from '../../test-helpers' +import { entries } from '../../../src/js/utils/object-utils' + +const { module, test } = Helpers + +module('Unit: Utils: Object Utils') + +test('#entries works', assert => { + assert.deepEqual(entries({ hello: 'world', goodbye: 'moon' }), [ + ['hello', 'world'], + ['goodbye', 'moon'], + ]) +}) diff --git a/tests/unit/utils/parse-utils-test.js b/tests/unit/utils/parse-utils-test.js index 8b066bab6..ce8d6963a 100644 --- a/tests/unit/utils/parse-utils-test.js +++ b/tests/unit/utils/parse-utils-test.js @@ -1,115 +1,115 @@ -import Helpers from '../../test-helpers'; +import Helpers from '../../test-helpers' import { MIME_TEXT_PLAIN, MIME_TEXT_HTML, NONSTANDARD_IE_TEXT_TYPE, getContentFromPasteEvent, - setClipboardData -} from 'mobiledoc-kit/utils/parse-utils'; + setClipboardData, +} from '../../../src/js/utils/parse-utils' -const {module, test} = Helpers; +const { module, test } = Helpers -module('Unit: Utils: Parse Utils'); +module('Unit: Utils: Parse Utils') -test('#getContentFromPasteEvent reads from clipboardData', (assert) => { - let element = null; +test('#getContentFromPasteEvent reads from clipboardData', assert => { + let element = null let expected = { [MIME_TEXT_PLAIN]: 'text', - [MIME_TEXT_HTML]: '

html

' - }; + [MIME_TEXT_HTML]: '

html

', + } let event = Helpers.dom.createMockEvent('paste', element, { clipboardData: { getData(type) { - return expected[type]; - } - } - }); + return expected[type] + }, + }, + }) let mockWindow = { clipboardData: { getData() { - assert.ok(false, 'should not get clipboard data from window'); - } - } - }; + assert.ok(false, 'should not get clipboard data from window') + }, + }, + } - let { html, text } = getContentFromPasteEvent(event, mockWindow); + let { html, text } = getContentFromPasteEvent(event, mockWindow) - assert.equal(html, expected[MIME_TEXT_HTML], 'correct html'); - assert.equal(text, expected[MIME_TEXT_PLAIN], 'correct text'); -}); + assert.equal(html, expected[MIME_TEXT_HTML], 'correct html') + assert.equal(text, expected[MIME_TEXT_PLAIN], 'correct text') +}) -test('#getContentFromPasteEvent reads data from window.clipboardData when event.clipboardData is not present (IE compat)', (assert) => { - assert.expect(3); - let element = null; - let event = Helpers.dom.createMockEvent('paste', element, {clipboardData:null}); - let requestedType; - let expectedHTML = 'hello'; - let expectedText = ''; +test('#getContentFromPasteEvent reads data from window.clipboardData when event.clipboardData is not present (IE compat)', assert => { + assert.expect(3) + let element = null + let event = Helpers.dom.createMockEvent('paste', element, { clipboardData: null }) + let requestedType + let expectedHTML = 'hello' + let expectedText = '' let mockWindow = { clipboardData: { getData(type) { - requestedType = type; - return expectedHTML; - } - } - }; + requestedType = type + return expectedHTML + }, + }, + } - let { html, text } = getContentFromPasteEvent(event, mockWindow); + let { html, text } = getContentFromPasteEvent(event, mockWindow) - assert.equal(requestedType, NONSTANDARD_IE_TEXT_TYPE, 'requests IE nonstandard mime type'); - assert.equal(html, expectedHTML, 'correct html'); - assert.equal(text, expectedText, 'correct text'); -}); + assert.equal(requestedType, NONSTANDARD_IE_TEXT_TYPE, 'requests IE nonstandard mime type') + assert.equal(html, expectedHTML, 'correct html') + assert.equal(text, expectedText, 'correct text') +}) -test('#setClipboardData uses event.clipboardData.setData when available', (assert) => { - let element = null; - let setData = {}; +test('#setClipboardData uses event.clipboardData.setData when available', assert => { + let element = null + let setData = {} let data = { html: '

html

', - text: 'text' - }; + text: 'text', + } let event = Helpers.dom.createMockEvent('copy', element, { clipboardData: { setData(type, value) { - setData[type] = value; - } - } - }); + setData[type] = value + }, + }, + }) let mockWindow = { clipboardData: { setData() { - assert.ok(false, 'should not set clipboard data on window'); - } - } - }; + assert.ok(false, 'should not set clipboard data on window') + }, + }, + } - setClipboardData(event, data, mockWindow); + setClipboardData(event, data, mockWindow) - assert.equal(setData[MIME_TEXT_HTML], data.html); - assert.equal(setData[MIME_TEXT_PLAIN], data.text); -}); + assert.equal(setData[MIME_TEXT_HTML], data.html) + assert.equal(setData[MIME_TEXT_PLAIN], data.text) +}) -test('#setClipboardData uses window.clipboardData.setData when event.clipboardData not present (IE compat)', (assert) => { - let element = null; - let setData = {}; +test('#setClipboardData uses window.clipboardData.setData when event.clipboardData not present (IE compat)', assert => { + let element = null + let setData = {} let data = { html: '

html

', - text: 'text' - }; + text: 'text', + } let event = Helpers.dom.createMockEvent('paste', element, { - clipboardData: null - }); + clipboardData: null, + }) let mockWindow = { clipboardData: { setData(type, value) { - setData[type] = value; - } - } - }; + setData[type] = value + }, + }, + } - setClipboardData(event, data, mockWindow); + setClipboardData(event, data, mockWindow) - assert.equal(setData[NONSTANDARD_IE_TEXT_TYPE], data.html, 'sets NONSTANDARD_IE_TEXT_TYPE type'); - assert.ok(!setData[MIME_TEXT_HTML], 'does not set MIME_TEXT_HTML'); - assert.ok(!setData[MIME_TEXT_PLAIN], 'does not set MIME_TEXT_PLAIN'); -}); + assert.equal(setData[NONSTANDARD_IE_TEXT_TYPE], data.html, 'sets NONSTANDARD_IE_TEXT_TYPE type') + assert.ok(!setData[MIME_TEXT_HTML], 'does not set MIME_TEXT_HTML') + assert.ok(!setData[MIME_TEXT_PLAIN], 'does not set MIME_TEXT_PLAIN') +}) diff --git a/tests/unit/utils/selection-utils-test.js b/tests/unit/utils/selection-utils-test.js deleted file mode 100644 index e484f6d30..000000000 --- a/tests/unit/utils/selection-utils-test.js +++ /dev/null @@ -1,85 +0,0 @@ -import Helpers from '../../test-helpers'; -const {module, test} = Helpers; - -import { comparePosition } from 'mobiledoc-kit/utils/selection-utils'; -import { DIRECTION } from 'mobiledoc-kit/utils/key'; - -module('Unit: Utils: Selection Utils'); - -test('#comparePosition returns the forward direction of selection', (assert) => { - let div = document.createElement('div'); - div.innerHTML = 'Howdy'; - let selection = { - anchorNode: div, - anchorOffset: 0, - focusNode: div, - focusOffset: 1 - }; - let result = comparePosition(selection); - assert.equal(DIRECTION.FORWARD, result.direction); -}); - -test('#comparePosition returns the backward direction of selection', (assert) => { - let div = document.createElement('div'); - div.innerHTML = 'Howdy'; - let selection = { - anchorNode: div, - anchorOffset: 1, - focusNode: div, - focusOffset: 0 - }; - let result = comparePosition(selection); - assert.equal(DIRECTION.BACKWARD, result.direction); -}); - -test('#comparePosition returns the direction of selection across nodes', (assert) => { - let div = document.createElement('div'); - div.innerHTML = 'Howdy Friend'; - let selection = { - anchorNode: div.childNodes[0], - anchorOffset: 1, - focusNode: div.childNodes[2], - focusOffset: 0 - }; - let result = comparePosition(selection); - assert.equal(DIRECTION.FORWARD, result.direction); -}); - -test('#comparePosition returns the backward direction of selection across nodes', (assert) => { - let div = document.createElement('div'); - div.innerHTML = 'Howdy Friend'; - let selection = { - anchorNode: div.childNodes[2], - anchorOffset: 1, - focusNode: div.childNodes[1], - focusOffset: 0 - }; - let result = comparePosition(selection); - assert.equal(DIRECTION.BACKWARD, result.direction); -}); - -test('#comparePosition returns the direction of selection with nested nodes', (assert) => { - let div = document.createElement('div'); - div.innerHTML = 'Howdy Friend'; - let selection = { - anchorNode: div, - anchorOffset: 1, - focusNode: div.childNodes[1], - focusOffset: 1 - }; - let result = comparePosition(selection); - assert.equal(DIRECTION.FORWARD, result.direction); -}); - -test('#comparePosition returns the backward direction of selection with nested nodes', (assert) => { - let div = document.createElement('div'); - div.innerHTML = 'Howdy Friend'; - let selection = { - anchorNode: div.childNodes[2], - anchorOffset: 1, - focusNode: div, - focusOffset: 2 - }; - let result = comparePosition(selection); - assert.equal(DIRECTION.BACKWARD, result.direction); -}); diff --git a/tests/unit/utils/selection-utils-test.ts b/tests/unit/utils/selection-utils-test.ts new file mode 100644 index 000000000..caa2e2ba5 --- /dev/null +++ b/tests/unit/utils/selection-utils-test.ts @@ -0,0 +1,85 @@ +import Helpers from '../../test-helpers' +import { comparePosition } from '../../../src/js/utils/selection-utils' +import { DIRECTION } from '../../../src/js/utils/key' + +const { module, test } = Helpers + +module('Unit: Utils: Selection Utils') + +test('#comparePosition returns the forward direction of selection', assert => { + let div = document.createElement('div') + div.innerHTML = 'Howdy' + let selection = { + anchorNode: div, + anchorOffset: 0, + focusNode: div, + focusOffset: 1, + } + let result = comparePosition(selection) + assert.equal(DIRECTION.FORWARD, result.direction) +}) + +test('#comparePosition returns the backward direction of selection', assert => { + let div = document.createElement('div') + div.innerHTML = 'Howdy' + let selection = { + anchorNode: div, + anchorOffset: 1, + focusNode: div, + focusOffset: 0, + } + let result = comparePosition(selection) + assert.equal(DIRECTION.BACKWARD, result.direction) +}) + +test('#comparePosition returns the direction of selection across nodes', assert => { + let div = document.createElement('div') + div.innerHTML = 'Howdy Friend' + let selection = { + anchorNode: div.childNodes[0], + anchorOffset: 1, + focusNode: div.childNodes[2], + focusOffset: 0, + } + let result = comparePosition(selection) + assert.equal(DIRECTION.FORWARD, result.direction) +}) + +test('#comparePosition returns the backward direction of selection across nodes', assert => { + let div = document.createElement('div') + div.innerHTML = 'Howdy Friend' + let selection = { + anchorNode: div.childNodes[2], + anchorOffset: 1, + focusNode: div.childNodes[1], + focusOffset: 0, + } + let result = comparePosition(selection) + assert.equal(DIRECTION.BACKWARD, result.direction) +}) + +test('#comparePosition returns the direction of selection with nested nodes', assert => { + let div = document.createElement('div') + div.innerHTML = 'Howdy Friend' + let selection = { + anchorNode: div, + anchorOffset: 1, + focusNode: div.childNodes[1], + focusOffset: 1, + } + let result = comparePosition(selection) + assert.equal(DIRECTION.FORWARD, result.direction) +}) + +test('#comparePosition returns the backward direction of selection with nested nodes', assert => { + let div = document.createElement('div') + div.innerHTML = 'Howdy Friend' + let selection = { + anchorNode: div.childNodes[2], + anchorOffset: 1, + focusNode: div, + focusOffset: 2, + } + let result = comparePosition(selection) + assert.equal(DIRECTION.BACKWARD, result.direction) +}) diff --git a/tsconfig.json b/tsconfig.json index 6761e940f..90578b282 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["src/**/*"], + "include": ["src/**/*", "tests/**/*"], "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ @@ -43,11 +43,14 @@ /* Module Resolution Options */ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ - // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + "paths": { + "mobiledoc-kit": ["src/js"], + "mobiledoc-kit/*": ["src/js/*"] + }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "typeRoots": [], /* List of folders to include type definitions from. */ - "types": [], /* Type declaration files to be included in compilation. */ + "types": ["qunit", "jquery"], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ diff --git a/yarn.lock b/yarn.lock index 2200a86fb..a2c3b2df3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -164,6 +164,13 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/jquery@^3.5.2": + version "3.5.2" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.2.tgz#e17c1756ecf7bbb431766c6761674a5d1de16579" + integrity sha512-+MFOdKF5Zr41t3y2wfzJvK1PrUK0KtPLAFwYownp/0nCoMIANDDu5aFSpWfb8S0ZajCSNeaBnMrBGxksXK5yeg== + dependencies: + "@types/sizzle" "*" + "@types/json-schema@^7.0.3": version "7.0.4" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" @@ -189,6 +196,11 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== +"@types/qunit@^2.9.5": + version "2.9.5" + resolved "https://registry.yarnpkg.com/@types/qunit/-/qunit-2.9.5.tgz#2f938870dac68e9464463ee36cdfb29538f4a2da" + integrity sha512-CdIcVLscYdlw1eLdHMF1Mw5uf/RbZ13JfM2nA0LNoJvonE91XFNu7njh1GcC1IFDKV1VTnyStQ1H8yCwk63hbw== + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -196,6 +208,11 @@ dependencies: "@types/node" "*" +"@types/sizzle@*": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" + integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== + "@typescript-eslint/eslint-plugin@^3.6.1": version "3.10.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz#7e061338a1383f59edc204c605899f93dc2e2c8f"