substance
Version:
Substance is a JavaScript library for web-based content editing. It provides building blocks for realizing custom text editors and web-based publishing system. It is developed to power our online editing platform [Substance](http://substance.io).
531 lines (465 loc) • 22.8 kB
JavaScript
import { test as _test } from 'substance-test'
import { DefaultDOMElement, platform, find, documentHelpers, Clipboard } from 'substance'
import setupEditor from './shared/setupEditor'
import { ClipboardEventData } from './shared/testHelpers'
import simple from './clipboard/simple'
import BrowserLinuxPLainTextFixture from './clipboard/browser-linux-plain-text'
import BrowserLinuxAnnotatedTextFixture from './clipboard/browser-linux-annotated-text'
import BrowserLinuxTwoParagraphsFixture from './clipboard/browser-linux-two-paragraphs'
import BrowserWindowsPlainTextFixture from './clipboard/browser-windows-plain-text'
import BrowserWindowsAnnotatedTextFixture from './clipboard/browser-windows-annotated-text'
import BrowserWindowsTwoParagraphsFixture from './clipboard/browser-windows-two-paragraphs'
import BrowserLinuxFirefoxPlainTextFixture from './clipboard/browser-linux-firefox-plain-text'
import BrowserLinuxFirefoxAnnotatedTextFixture from './clipboard/browser-linux-firefox-annotated-text'
import BrowserLinuxFirefoxTwoParagraphsFixture from './clipboard/browser-linux-firefox-two-paragraphs'
import BrowserLinuxFirefoxWholePageFixture from './clipboard/browser-linux-firefox-whole-page'
import BrowserOSXFirefoxPlainTextFixture from './clipboard/browser-osx-firefox-plain-text'
import BrowserOSXFirefoxAnnotatedTextFixture from './clipboard/browser-osx-firefox-annotated-text'
import BrowserOSXFirefoxTwoParagraphsFixture from './clipboard/browser-osx-firefox-two-paragraphs'
import BrowserWindowsFirefoxPlainTextFixture from './clipboard/browser-windows-firefox-plain-text'
import BrowserWindowsFirefoxAnnotatedTextFixture from './clipboard/browser-windows-firefox-annotated-text'
import BrowserWindowsFirefoxTwoParagraphsFixture from './clipboard/browser-windows-firefox-two-paragraphs'
import BrowserWindowsEdgePlainTextFixture from './clipboard/browser-windows-edge-plain-text'
import BrowserWindowsEdgeAnnotatedTextFixture from './clipboard/browser-windows-edge-annotated-text'
import BrowserWindowsEdgeTwoParagraphsFixture from './clipboard/browser-windows-edge-two-paragraphs'
import GDocsOSXLinuxChromePlainTextFixture from './clipboard/google-docs-osx-linux-chrome-plain-text'
import GDocsOSXLinuxChromeAnnotatedTextFixture from './clipboard/google-docs-osx-linux-chrome-annotated-text'
import GDocsOSXLinuxChromeTwoParagraphsFixture from './clipboard/google-docs-osx-linux-chrome-two-paragraphs'
import GDocsOSXLinuxChromeExtendedFixture from './clipboard/google-docs-osx-linux-chrome-extended'
import GDocsLinuxFirefoxPlainTextFixture from './clipboard/google-docs-linux-firefox-plain-text'
import GDocsLinuxFirefoxAnnotatedTextFixture from './clipboard/google-docs-linux-firefox-annotated-text'
import GDocsOSXFirefoxPlainTextFixture from './clipboard/google-docs-osx-firefox-plain-text'
import LibreOfficeOSXPlainTextFixture from './clipboard/libre-office-osx-linux-plain-text'
import LibreOfficeOSXAnnotatedTextFixture from './clipboard/libre-office-osx-linux-annotated-text'
import LibreOfficeOSXTwoParagraphsFixture from './clipboard/libre-office-osx-linux-two-paragraphs'
import LibreOfficeOSXExtendedFixture from './clipboard/libre-office-osx-linux-extended'
import MSW11OSXPlainTextFixture from './clipboard/word-11-osx-plain-text'
import MSW11OSXAnnotatedTextFixture from './clipboard/word-11-osx-annotated-text'
import MSW11OSXTwoParagraphsFixture from './clipboard/word-11-osx-two-paragraphs'
import MSW11OSXExtendedFixture from './clipboard/word-11-osx-extended'
const PARAGRAPH_TYPE = 'paragraph'
const HEADING_TYPE = 'heading'
const LINK_TYPE = 'link'
const EMPHASIS_TYPE = 'emphasis'
const STRONG_TYPE = 'strong'
const SUPERSCRIPT_TYPE = 'superscript'
const SUBSCRIPT_TYPE = 'subscript'
const CODEBLOCK_TYPE = 'preformat'
const BODY_CONTENT_PATH = ['body', 'nodes']
ClipboardTests()
if (platform.inBrowser) {
ClipboardTests('memory')
}
function ClipboardTests (memory) {
function test (title, fn) {
_test('Clipboard' + (memory ? ' [memory]' : '') + ': ' + title, fn, {
before () {
if (memory) platform.values.inBrowser = false
},
after () {
platform._reset()
}
})
}
test('Copying HTML, and plain text', t => {
const { editorSession, clipboard, context } = _setup(t, simple)
editorSession.setSelection({ type: 'property', path: ['p1', 'content'], startOffset: 0, endOffset: 5 })
const clipboardData = _createClipboardData()
clipboard.copy(clipboardData, context)
t.notNil(clipboardData.data['text/plain'], 'Clipboard should contain plain text data.')
t.notNil(clipboardData.data['text/html'], 'Clipboard should contain HTML data.')
const htmlDoc = DefaultDOMElement.parseHTML(clipboardData.data['text/html'])
const body = htmlDoc.find('body')
t.notNil(body, 'The copied HTML should always be a full HTML document string, containing a body element.')
t.end()
})
test('Copying a property selection', t => {
const { editorSession, clipboard, context } = _setup(t, simple)
editorSession.setSelection({ type: 'property', path: ['p1', 'content'], startOffset: 0, endOffset: 5 })
const TEXT = '01234'
const clipboardData = _createClipboardData()
clipboard.copy(clipboardData, context)
t.equal(clipboardData.data['text/plain'], TEXT, 'Plain text should be correct.')
const htmlDoc = DefaultDOMElement.parseHTML(clipboardData.data['text/html'])
const body = htmlDoc.find('body')
t.equal(body.text(), TEXT, 'HTML text should be correct.')
t.end()
})
test('Copying a container selection', t => {
const { editorSession, clipboard, context } = _setup(t, simple)
editorSession.setSelection({
type: 'container',
containerPath: BODY_CONTENT_PATH,
startPath: ['p1', 'content'],
startOffset: 1,
endPath: ['p3', 'content'],
endOffset: 5
})
const TEXT = [
'123456789',
'0123456789',
'01234'
]
const LINE_SEP = '\n\n'
const clipboardData = _createClipboardData()
clipboard.copy(clipboardData, context)
t.equal(clipboardData.data['text/plain'], TEXT.join(LINE_SEP), 'Plain text should be correct.')
const htmlDoc = DefaultDOMElement.parseHTML(clipboardData.data['text/html'])
const elements = htmlDoc.find('body').getChildren()
t.equal(elements.length, 3, 'HTML should consist of three elements.')
const p1 = elements[0]
t.equal(p1.attr('data-id'), 'p1', 'First element should have correct data-id.')
t.equal(p1.text(), TEXT[0], 'First element should have correct text content.')
const p2 = elements[1]
t.equal(p2.attr('data-id'), 'p2', 'Second element should have correct data-id.')
t.equal(p2.text(), TEXT[1], 'Second element should have correct text content.')
const p3 = elements[2]
t.equal(p3.attr('data-id'), 'p3', 'Third element should have correct data-id.')
t.equal(p3.text(), TEXT[2], 'Third element should have correct text content.')
t.end()
})
test("Pasting text into ContainerEditor using 'text/plain'.", t => {
const { editorSession, clipboard, doc, context } = _setup(t, simple)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 1,
containerPath: BODY_CONTENT_PATH
})
const clipboardData = _createClipboardData()
clipboardData.setData('text/plain', 'XXX')
clipboard.paste(clipboardData, context)
t.equal(doc.get(['p1', 'content']), '0XXX123456789', 'Plain text should be correct.')
t.end()
})
test('Pasting without any data given.', t => {
const { editorSession, clipboard, doc, context } = _setup(t, simple)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 1,
containerPath: BODY_CONTENT_PATH
})
const clipboardData = _createClipboardData()
clipboard.paste(clipboardData, context)
t.equal(doc.get(['p1', 'content']), '0123456789', 'Text should be still the same.')
t.end()
})
test("Pasting text into ContainerEditor using 'text/html'.", t => {
const { editorSession, clipboard, doc, context } = _setup(t, simple)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 1,
containerPath: BODY_CONTENT_PATH
})
const TEXT = 'XXX'
const clipboardData = _createClipboardData()
clipboardData.setData('text/plain', TEXT)
clipboardData.setData('text/html', TEXT)
clipboard.paste(clipboardData, context)
t.equal(doc.get(['p1', 'content']), '0XXX123456789', 'Plain text should be correct.')
t.end()
})
// this test revealed #700: the problem was that in source code there where
// `"` and `'` characters which did not survive the way through HTML correctly
test('Copy and Pasting source code.', t => {
const { editorSession, clipboard, doc, context } = _setup(t, simple)
const cb = doc.create({
type: CODEBLOCK_TYPE,
id: 'cb1',
content: [
'function hello_world() {',
" alert('Hello World!');",
'}'
].join('\n')
})
documentHelpers.insertAt(doc, BODY_CONTENT_PATH, doc.get('p1').getPosition() + 1, cb.id)
editorSession.setSelection(doc.createSelection({
type: 'container',
startPath: ['p1', 'content'],
startOffset: 1,
endPath: ['p2', 'content'],
endOffset: 1,
containerPath: BODY_CONTENT_PATH
}))
const clipboardData = _createClipboardData()
clipboard.cut(clipboardData, context)
let cb1 = doc.get('cb1')
t.isNil(cb1, 'Codeblock should have been cutted.')
clipboard.paste(clipboardData, context)
cb1 = doc.get('cb1')
t.notNil(cb1, 'Codeblock should have been pasted.')
t.deepEqual(cb1.toJSON(), cb.toJSON(), 'Codeblock should have been pasted correctly.')
t.end()
})
test('Browser - Chrome (OSX/Linux) - Plain Text', t => {
_plainTextTest(t, BrowserLinuxPLainTextFixture)
})
test('Browser - Chrome (OSX/Linux) - Annotated Text', t => {
_annotatedTextTest(t, BrowserLinuxAnnotatedTextFixture)
})
test('Browser - Chrome (OSX/Linux) - Two Paragraphs', t => {
_twoParagraphsTest(t, BrowserLinuxTwoParagraphsFixture)
})
test('Browser - Chrome (Windows) - Plain Text', t => {
_plainTextTest(t, BrowserWindowsPlainTextFixture, 'forceWindows')
})
test('Browser - Chrome (Windows) - Annotated Text', t => {
_annotatedTextTest(t, BrowserWindowsAnnotatedTextFixture, 'forceWindows')
})
test('Browser - Chrome (Windows) - Two Paragraphs', t => {
_twoParagraphsTest(t, BrowserWindowsTwoParagraphsFixture, 'forceWindows')
})
test('Browser - Firefox (Linux) - Plain Text', t => {
_plainTextTest(t, BrowserLinuxFirefoxPlainTextFixture)
})
test('Browser - Firefox (Linux) - Annotated Text', t => {
_annotatedTextTest(t, BrowserLinuxFirefoxAnnotatedTextFixture)
})
test('Browser - Firefox (Linux) - Two Paragraphs', t => {
_twoParagraphsTest(t, BrowserLinuxFirefoxTwoParagraphsFixture)
})
// TODO: bring back an fall-back converter for unsupported content
test('Browser - Firefox (Linux) - Whole Page', t => {
const html = BrowserLinuxFirefoxWholePageFixture
_fixtureTest(t, html, (doc, clipboard, context) => {
const clipboardData = _createClipboardData()
clipboardData.setData('text/plain', 'XXX')
clipboardData.setData('text/html', html)
clipboard.paste(clipboardData, context)
// make sure HTML paste succeeded, by checking against the result of plain text insertion
t.notOk(doc.get('p1').getText() === '0XXX123456789', 'HTML conversion and paste should have been successful (not fall back to plain-text).')
t.ok(doc.get('body').getLength() > 10, 'There should be a lot of paragraphs')
t.end()
})
})
test('Browser - Firefox (OSX) - Plain Text', t => {
_plainTextTest(t, BrowserOSXFirefoxPlainTextFixture)
})
test('Browser - Firefox (OSX) - Annotated Text', t => {
_annotatedTextTest(t, BrowserOSXFirefoxAnnotatedTextFixture)
})
test('Browser - Firefox (OSX) - Two Paragraphs', t => {
_twoParagraphsTest(t, BrowserOSXFirefoxTwoParagraphsFixture)
})
test('Browser - Firefox (Windows) - Plain Text', t => {
_plainTextTest(t, BrowserWindowsFirefoxPlainTextFixture, 'forceWindows')
})
test('Browser - Firefox (Windows) - Annotated Text', t => {
_annotatedTextTest(t, BrowserWindowsFirefoxAnnotatedTextFixture, 'forceWindows')
})
test('Browser - Firefox (Windows) - Two Paragraphs', t => {
_twoParagraphsTest(t, BrowserWindowsFirefoxTwoParagraphsFixture, 'forceWindows')
})
test('Browser - Edge (Windows) - Plain Text', t => {
_plainTextTest(t, BrowserWindowsEdgePlainTextFixture, 'forceWindows')
})
test('Browser - Edge (Windows) - Annotated Text', t => {
_annotatedTextTest(t, BrowserWindowsEdgeAnnotatedTextFixture, 'forceWindows')
})
test('Browser - Edge (Windows) - Two Paragraphs', t => {
_twoParagraphsTest(t, BrowserWindowsEdgeTwoParagraphsFixture, 'forceWindows')
})
test('GoogleDocs - Chrome (OSX/Linux) - Plain Text', t => {
_plainTextTest(t, GDocsOSXLinuxChromePlainTextFixture)
})
test('GoogleDocs - Chrome (OSX/Linux) - Annotated Text', t => {
_annotatedTextTest(t, GDocsOSXLinuxChromeAnnotatedTextFixture)
})
test('GoogleDocs - Chrome (OSX/Linux) - Two Paragraphs', t => {
_twoParagraphsTest(t, GDocsOSXLinuxChromeTwoParagraphsFixture)
})
test('GoogleDocs - Chrome (OSX/Linux) - Extended', t => {
_extendedTest(t, GDocsOSXLinuxChromeExtendedFixture)
})
test('GoogleDocs - Firefox (Linux) - Plain Text', t => {
_plainTextTest(t, GDocsLinuxFirefoxPlainTextFixture)
})
test('GoogleDocs - Firefox (Linux) - Annotated Text', t => {
_annotatedTextTest(t, GDocsLinuxFirefoxAnnotatedTextFixture)
})
test('GoogleDocs - Firefox (OSX) - Plain Text', t => {
_plainTextTest(t, GDocsOSXFirefoxPlainTextFixture)
})
test('LibreOffice (OSX/Linux) - Plain Text', t => {
_plainTextTest(t, LibreOfficeOSXPlainTextFixture)
})
test('LibreOffice (OSX/Linux) - Annotated Text', t => {
_annotatedTextTest(t, LibreOfficeOSXAnnotatedTextFixture)
})
test('LibreOffice (OSX/Linux) - Two Paragraphs', t => {
_twoParagraphsTest(t, LibreOfficeOSXTwoParagraphsFixture)
})
test('LibreOffice (OSX/Linux) - Extended', t => {
_extendedTest(t, LibreOfficeOSXExtendedFixture)
})
test('Microsoft Word 11 (OSX) - Plain Text', t => {
_plainTextTest(t, MSW11OSXPlainTextFixture)
})
test('Microsoft Word 11 (OSX) - Annotated Text', t => {
_annotatedTextTest(t, MSW11OSXAnnotatedTextFixture)
})
test('Microsoft Word 11 (OSX) - Two Paragraphs', t => {
_twoParagraphsTest(t, MSW11OSXTwoParagraphsFixture)
})
test('Microsoft Word 11 (OSX) - Extended', t => {
_extendedTest(t, MSW11OSXExtendedFixture)
})
}
function _fixtureTest (t, html, impl, forceWindows) {
const { editorSession, clipboard, doc, context } = _setup(t, simple)
platform.values.isWindows = Boolean(forceWindows)
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 1,
containerPath: BODY_CONTENT_PATH
})
try {
impl(doc, clipboard, context)
} finally {
platform._reset()
}
}
function _emptyFixtureTest (t, html, impl, forceWindows) {
const { context, editorSession, clipboard, doc } = _setup(t, _emptyParagraphSeed)
if (forceWindows) {
// NOTE: faking 'Windows' mode in importer so that
// the correct implementation will be used
clipboard.htmlImporter._isWindows = true
}
editorSession.setSelection({
type: 'property',
path: ['p1', 'content'],
startOffset: 0,
containerPath: BODY_CONTENT_PATH
})
impl(doc, clipboard, context)
}
function _plainTextTest (t, html, forceWindows) {
_fixtureTest(t, html, (doc, clipboard, context) => {
const clipboardData = _createClipboardData()
clipboardData.setData('text/plain', '')
clipboardData.setData('text/html', html)
clipboard.paste(clipboardData, context)
t.equal(doc.get(['p1', 'content']), '0XXX123456789', 'Content should have been pasted correctly.')
t.end()
}, forceWindows)
}
function _annotatedTextTest (t, html, forceWindows) {
_fixtureTest(t, html, (doc, clipboard, context) => {
const clipboardData = _createClipboardData()
clipboardData.setData('text/plain', '')
clipboardData.setData('text/html', html)
clipboard.paste(clipboardData, context)
t.equal(doc.get(['p1', 'content']), '0XXX123456789', 'Content should have been pasted correctly.')
const annotations = doc.getIndex('annotations').get(['p1', 'content'])
t.equal(annotations.length, 1, 'There should be one annotation on the property now.')
const anno = annotations[0]
t.equal(anno.type, LINK_TYPE, 'The annotation should be a link.')
t.end()
}, forceWindows)
}
function _twoParagraphsTest (t, html, forceWindows) {
_fixtureTest(t, html, (doc, clipboard, context) => {
const clipboardData = _createClipboardData()
clipboardData.setData('text/plain', '')
clipboardData.setData('text/html', html)
clipboard.paste(clipboardData, context)
const body = doc.get('body')
const [p1, p2, p3] = body.getNodes()
t.equal(p1.content, '0AAA', 'First paragraph should be truncated.')
t.equal(p2.content, 'BBB', "Second paragraph should contain 'BBB'.")
t.equal(p3.content, '123456789', 'Remainder of original p1 should go into forth paragraph.')
t.end()
}, forceWindows)
}
function _extendedTest (t, html, forceWindows) {
_emptyFixtureTest(t, html, (doc, clipboard, context) => {
const clipboardData = _createClipboardData()
clipboardData.setData('text/plain', '')
clipboardData.setData('text/html', html)
clipboard.paste(clipboardData, context)
// First node is a paragraph with strong, emphasis, superscript and subscript annos
const body = doc.get('body')
const [node1, node2, node3] = body.getNodes()
t.equal(node1.type, PARAGRAPH_TYPE, 'First node should be a paragraph.')
t.equal(node1.content.length, 121, 'First paragraph should contain 121 symbols.')
const annotationsNode1 = doc.getIndex('annotations').get([node1.id, 'content']).sort((a, b) => {
return a.start.offset - b.start.offset
})
t.equal(annotationsNode1.length, 4, 'There should be four annotations inside a first paragraph.')
const annoFirstNode1 = annotationsNode1[0] || {}
t.equal(annoFirstNode1.type, EMPHASIS_TYPE, 'The annotation should be an emphasis.')
t.equal(annoFirstNode1.start.offset, 4, 'Emphasis annotation should start from 5th symbol.')
t.equal(annoFirstNode1.end.offset, 11, 'Emphasis annotation should end at 12th symbol.')
const annoSecondNode1 = annotationsNode1[1] || {}
t.equal(annoSecondNode1.type, STRONG_TYPE, 'The annotation should be a strong.')
t.equal(annoSecondNode1.start.offset, 18, 'Strong annotation should start from 19th symbol.')
t.equal(annoSecondNode1.end.offset, 30, 'Strong annotation should end at 31th symbol.')
const annoThirdNode1 = annotationsNode1[2] || {}
t.equal(annoThirdNode1.type, SUPERSCRIPT_TYPE, 'The annotation should be a superscript.')
t.equal(annoThirdNode1.start.offset, 41, 'Superscript annotation should start from 42th symbol.')
t.equal(annoThirdNode1.end.offset, 49, 'Superscript annotation should end at 50th symbol.')
const annoFourthNode1 = annotationsNode1[3] || {}
t.equal(annoFourthNode1.type, SUBSCRIPT_TYPE, 'The annotation should be a subscript.')
t.equal(annoFourthNode1.start.offset, 50, 'Subscript annotation should start from 51th symbol.')
t.equal(annoFourthNode1.end.offset, 56, 'Subscript annotation should end at 57th symbol.')
// Second node is a first level heading without annos
t.equal(node2.type, HEADING_TYPE, 'Second node should be a heading.')
t.equal(node2.level, 1, 'Second node should be a first level heading.')
t.equal(node2.content.length, 12, 'Heading should contain 12 symbols.')
const annotationsNode2 = doc.getIndex('annotations').get([node2.id, 'content'])
t.equal(annotationsNode2.length, 0, 'There should be no annotations inside a heading.')
// Third node is a paragraph with overlapping annos
t.equal(node3.type, PARAGRAPH_TYPE, 'Third node should be a paragraph.')
t.equal(node3.content.length, 178, 'Second paragraph should contain 178 symbols.')
// let annotationsNode3 = doc.getIndex('annotations').get([node3.id, 'content']).sort((a, b) => {
// return a.start.offset - b.start.offset
// })
// While we are not supporting combined formatting annotations
// we will run selective tests for selections to ensure that annotations are exist
// Get annotations for range without annotations
const path = [node3.id, 'content']
const compare = function (a, b) {
if (a.id < b.id) return -1
if (a.id > b.id) return 1
return 0
}
let annos = doc.getIndex('annotations').get(path, 3, 5)
t.equal(annos.length, 0, 'There should be no annotations within given range.')
// Get annotations for range with string, emphasis and superscript annotations
annos = doc.getIndex('annotations').get(path, 17, 18).sort(compare)
t.equal(annos.length, 3, 'There should be three annotations within given range.')
t.notNil(find(annos, { type: EMPHASIS_TYPE }), 'Should contain emphasis annotation.')
t.notNil(find(annos, { type: STRONG_TYPE }), 'Should contain strong annotation.')
t.notNil(find(annos, { type: SUPERSCRIPT_TYPE }), 'Should contain superscript annotation.')
// Get annotations for range with string, emphasis and subscript annotations
annos = doc.getIndex('annotations').get(path, 22, 23).sort(compare)
t.notNil(find(annos, { type: EMPHASIS_TYPE }), 'Should contain emphasis annotation.')
t.notNil(find(annos, { type: STRONG_TYPE }), 'Should contain strong annotation.')
t.notNil(find(annos, { type: SUBSCRIPT_TYPE }), 'Should contain subscript annotation.')
// Get annotations for range with string and emphasis
annos = doc.getIndex('annotations').get(path, 27, 29).sort(compare)
t.notNil(find(annos, { type: EMPHASIS_TYPE }), 'Should contain emphasis annotation.')
t.notNil(find(annos, { type: STRONG_TYPE }), 'Should contain strong annotation.')
t.end()
}, forceWindows)
}
function _createClipboardData () {
return new ClipboardEventData()
}
function _setup (t, seed) {
const { context, editorSession, doc } = setupEditor(t, seed)
const clipboard = new Clipboard()
return { context, editorSession, doc, clipboard }
}
function _emptyParagraphSeed (tx) {
tx.create({
type: PARAGRAPH_TYPE,
id: 'p1',
content: ''
})
documentHelpers.append(tx, BODY_CONTENT_PATH, 'p1')
}