test: 完善项目单元测试,提高覆盖率

This commit is contained in:
luochao 2020-12-02 14:55:25 +08:00
parent 1954d27eff
commit 267128e1bc
35 changed files with 3191 additions and 41 deletions

View File

@ -10,7 +10,7 @@ import Editor from '../../../editor/index'
/**
* Tooltip
*/
function createShowHideFn(editor: Editor) {
export function createShowHideFn(editor: Editor) {
let tooltip: Tooltip | null
/**
@ -65,6 +65,7 @@ function createShowHideFn(editor: Editor) {
* @param e
* @param editor
*/
/* istanbul ignore next */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function preEnterListener(e: KeyboardEvent, editor: Editor) {
// 获取当前标签元素

View File

@ -62,7 +62,7 @@ function showDargBox($textContainerElem: DomElement, $drag: DomElement, $img: Do
/**
* /
*/
function createShowHideFn(editor: Editor) {
export function createShowHideFn(editor: Editor) {
const $textContainerElem = editor.$textContainerElem
let $imgTarget: DomElement

View File

@ -10,7 +10,7 @@ import Editor from '../../../editor/index'
/**
* Tooltip
*/
function createShowHideFn(editor: Editor) {
export function createShowHideFn(editor: Editor) {
let tooltip: Tooltip | null
const t = (text: string, prefix: string = ''): string => {
return editor.i18next.t(prefix + text)

View File

@ -141,6 +141,13 @@ class UploadImg {
config.customAlert(`${t('图片验证未通过')}: \n` + errInfos.join('\n'), 'warning')
return
}
// 如果过滤后文件列表为空直接返回
if (resultFiles.length === 0) {
config.customAlert(t('传入的文件不合法'), 'warning')
return
}
if (resultFiles.length > maxLength) {
config.customAlert(t('一次最多上传') + maxLength + t('张图片'), 'warning')
return

View File

@ -44,7 +44,7 @@ function getChildrenJSON($elem: DomElement): NodeListType {
elemResult.tag = curElem.nodeName.toLowerCase()
// attr
const attrData = []
const attrList = curElem.attributes || []
const attrList = curElem.attributes
const attrListLength = attrList.length || 0
for (let i = 0; i < attrListLength; i++) {
const attr = attrList[i]

View File

@ -0,0 +1,2 @@
export const MOCK_FIEFOX_USER_AGENT =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:82.0) Gecko/20100101 Firefox/82.0'

View File

@ -0,0 +1,6 @@
import { MOCK_FIEFOX_USER_AGENT } from './constants'
// 修改userAgent模拟firefox
Object.defineProperty(navigator, 'userAgent', {
value: MOCK_FIEFOX_USER_AGENT,
})

View File

@ -0,0 +1,28 @@
import { DomElement } from '../../src/utils/dom-core'
const EventType = {
Event: Event,
KeyBoardEvent: KeyboardEvent,
MouseEvent: MouseEvent,
// jest dom 没有ClipboardEvent使用Event替代
ClipboardEvent: Event,
}
type EventTypeKey = keyof typeof EventType
export default function mockEventTrigger(
$el: DomElement,
type: string,
eventType: EventTypeKey = 'Event',
option?: any
) {
const EventConstruct = EventType[eventType]
const event = new EventConstruct(type, {
view: window,
bubbles: true,
cancelable: true,
...option,
})
$el.elems[0].dispatchEvent(event)
}

17
test/helpers/mock-file.ts Normal file
View File

@ -0,0 +1,17 @@
export default function mockFile(option: any) {
const name = option.name ?? 'mock.txt'
const size = option.size ?? 1024
const mimeType = option.mimeType || 'plain/txt'
function range(count: number) {
let output = ''
for (var i = 0; i < count; i++) {
output += 'a'
}
return output
}
const blob = new Blob([range(size)], { type: mimeType })
return new File([blob], name)
}

13
test/helpers/mock-xhr.ts Normal file
View File

@ -0,0 +1,13 @@
const xhrMockClass = (config: any) => ({
open: jest.fn(),
send: jest.fn(),
setRequestHeader: jest.fn(),
ontimeout: jest.fn(),
upload: jest.fn(),
onreadystatechange: jest.fn(),
status: config.status,
readyState: 4,
responseText: config.res,
})
export default xhrMockClass

View File

@ -1,3 +1,8 @@
/**
* @description history compile
* @author luochao
*/
import createEditor from '../../../helpers/create-editor'
import Editor from '../../../../src/editor'
import compile, {
@ -7,6 +12,7 @@ import compile, {
compliePosition,
} from '../../../../src/editor/history/data/node/compile'
import { Compile } from '../../../../src/editor/history/data/type'
import { UA } from '../../../../src/utils/util'
let editor: Editor
@ -27,11 +33,19 @@ function generateCompileData(mutationList: MutationRecord[]) {
return mockData
}
const originalValue = UA.isFirefox
describe('Editor history compile', () => {
beforeEach(() => {
editor = createEditor(document, 'div1')
})
afterEach(() => {
Object.defineProperty(UA, 'isFirefox', {
value: originalValue,
})
})
test('可以将MutationRecord生成Compile数据', done => {
expect.assertions(3)
@ -49,4 +63,28 @@ describe('Editor history compile', () => {
editor.txt.html('<span>123</span>')
})
test('如果在firefox中如果有删除节点的mutaion需要一些特殊处理', done => {
expect.assertions(2)
Object.defineProperty(UA, 'isFirefox', {
value: true,
})
const observer = new MutationObserver((mutationList: MutationRecord[]) => {
const compileData = compile(mutationList)
expect(compileData instanceof Array).toBeTruthy()
expect(compileData.length).toBe(4)
done()
})
const $textEl = editor.$textElem.elems[0]
observer.observe($textEl, { attributes: true, childList: true, subtree: true })
// 添加多的dom变化情况可以使得compile statement 执行到达率高
editor.txt.html('<span id="test">123<i>456</i></span>')
editor.txt.html('<h1>标题</h1><span id="test">123<i>456</i></span>')
editor.txt.html('<span>123</span>')
editor.txt.html('<span></span>')
})
})

View File

@ -0,0 +1,84 @@
/**
* @description Editor history data html cache
* @author luochao
*/
import createEditor from '../../../helpers/create-editor'
import Editor from '../../../../src/editor'
import HtmlCache from '../../../../src/editor/history/data/html'
let editor: Editor
let htmlCache: HtmlCache
describe('Editor history html cache', () => {
beforeEach(() => {
editor = createEditor(document, 'div1', '', {
compatibleMode: () => true,
})
htmlCache = new HtmlCache(editor)
htmlCache.observe()
})
test('可以使用 HtmlCache 实现编辑器内容撤回', () => {
const testHtml1 = '<span>123</span>'
const testHtml2 = '<h1>456</h1>'
editor.txt.html(testHtml1)
expect(editor.$textElem.elems[0]).toContainHTML(testHtml1)
htmlCache.save()
editor.txt.html(testHtml2)
expect(editor.$textElem.elems[0]).toContainHTML(testHtml2)
htmlCache.save()
htmlCache.revoke()
expect(editor.$textElem.elems[0]).toContainHTML(testHtml1)
})
test('可以使用 HtmlCache 撤回编辑器内容,撤回后还可以恢复', () => {
const testHtml1 = '<span>123</span>'
const testHtml2 = '<h1>456</h1>'
editor.txt.html(testHtml1)
htmlCache.save()
expect(editor.$textElem.elems[0]).toContainHTML(testHtml1)
editor.txt.html(testHtml2)
htmlCache.save()
const revokeRes = htmlCache.revoke()
expect(revokeRes).toBeTruthy()
expect(editor.$textElem.elems[0]).not.toContainHTML(testHtml2)
const restoreRes = htmlCache.restore()
expect(restoreRes).toBeTruthy()
expect(editor.$textElem.elems[0]).toContainHTML(testHtml2)
})
test('没有内容撤回时调用revoke返回false', () => {
const testHtml1 = '<span>123</span>'
editor.txt.html(testHtml1)
const res = htmlCache.revoke()
expect(res).toBeFalsy()
})
test('没有内容恢复时调用restore返回false', () => {
const testHtml1 = '<span>123</span>'
editor.txt.html(testHtml1)
const res = htmlCache.restore()
expect(res).toBeFalsy()
})
})

View File

@ -1,3 +1,8 @@
/**
* @description history decompile
* @author luochao
*/
import createEditor from '../../../helpers/create-editor'
import Editor from '../../../../src/editor'
import compile from '../../../../src/editor/history/data/node/compile'
@ -10,7 +15,7 @@ describe('Editor history decompile', () => {
editor = createEditor(document, 'div1')
})
test('可以通过revoke方法撤销编辑器设置的内容', done => {
test('可以通过revoke方法撤销编辑器设置的html', done => {
expect.assertions(3)
const observer = new MutationObserver((mutationList: MutationRecord[]) => {
@ -33,6 +38,58 @@ describe('Editor history decompile', () => {
editor.txt.html('<span>123</span>')
})
test('可以通过revoke方法撤销编辑器设置的属性', done => {
expect.assertions(3)
const observer = new MutationObserver((mutationList: MutationRecord[]) => {
const compileData = compile(mutationList)
expect(compileData instanceof Array).toBeTruthy()
expect(compileData.length).toBe(1)
observer.disconnect()
revoke(compileData)
expect(editor.$textElem.html()).toEqual('<span>123</span>')
done()
})
const $textEl = editor.$textElem.elems[0]
editor.txt.html('<span>123</span>')
observer.observe($textEl, { attributes: true, childList: true, subtree: true })
editor.txt.html('<span id="123">123</span>')
})
test('可以通过revoke方法撤销编辑器设置的文本', done => {
expect.assertions(3)
const observer = new MutationObserver((mutationList: MutationRecord[]) => {
const compileData = compile(mutationList)
expect(compileData instanceof Array).toBeTruthy()
expect(compileData.length).toBe(1)
observer.disconnect()
revoke(compileData)
expect(editor.$textElem.html()).toEqual('<span></span>')
done()
})
const $textEl = editor.$textElem.elems[0]
editor.txt.html('<span></span>')
observer.observe($textEl, { attributes: true, childList: true, subtree: true })
editor.txt.html('<span>123</span>')
})
test('可以通过restore方法恢复撤销的内容', done => {
expect.assertions(2)

View File

@ -0,0 +1,86 @@
/**
* @description Editor catalog scroll to head
* @author luochao
*/
import Editor from '../../../../src/editor'
import scrollToHead from '../../../../src/editor/init-fns/scroll-to-head'
import { TCatalog } from '../../../../src/config/events'
import $ from 'jquery'
const catalogHtml = `<h1>标题一</h1>
<p>
balabala
</p>
<h2></h2>
<p>
balabala
</p>
<h3></h3>
<p>
balabala
</p>
<h4></h4>
<p>
balabala
</p>
<h2></h2>
<p>
balabala
</p>
<h3></h3>
<p>
balabala
</p>
<h3></h3>
<p>
balabala
</p>`
let editor: Editor
let testId = ''
describe('Editor catalog', () => {
beforeEach(() => {
const toolbar = document.createElement('div')
toolbar.id = 'toolbar'
toolbar.innerHTML = catalogHtml
document.body.appendChild(toolbar)
const catalogContainer = document.createElement('div')
catalogContainer.id = 'catalogContainer'
document.body.appendChild(catalogContainer)
editor = new Editor('#toolbar')
editor.config.onCatalogChange = function (arr: TCatalog[]) {
const lastItem = arr[arr.length - 1]
const box = document.getElementById('catalogContainer')
if (box == null) return
const a = document.createElement('a')
a.href = 'javascript:void(0)'
a.innerText = lastItem.text
testId = lastItem.id
a.id = lastItem.id
box.appendChild(a)
}
editor.create()
})
test('能滚动到指定的锚点', done => {
expect.assertions(1)
const a = $(`${testId}`)
expect(a).not.toBeNull()
try {
scrollToHead(editor, testId)
} catch (err) {
done()
}
})
})

View File

@ -0,0 +1,225 @@
/**
* @description post方法
* @author luochao
*/
import post from '../../../src/editor/upload/upload-core'
import mockXHR from '../../helpers/mock-xhr'
const API_URL = 'http://localhost:8881/api/upload-img'
const origilaXHR = window.XMLHttpRequest
const deaultResponse = { status: 200, res: JSON.stringify({ data: ['url1'], errno: 0 }) }
const mockXMLHttpRequest = (resonse: any = deaultResponse) => {
const mockObject = jest.fn().mockImplementation(() => mockXHR(resonse))
// @ts-ignore
window.XMLHttpRequest = mockObject
}
const createFormData = () => {
const data = new FormData()
data.append('name', 'name')
data.append('filename', 'filename')
return data
}
describe('Editor upload core post', () => {
afterAll(() => {
window.XMLHttpRequest = origilaXHR
})
test('能够发送简单的post请求处理返回的json字符串', done => {
mockXMLHttpRequest()
expect.assertions(2)
const data = createFormData()
const xhr = post(API_URL, {
formData: data,
onSuccess: (xhr: XMLHttpRequest, res: any) => {
expect(res.data).toEqual(['url1'])
expect(res.errno).toBe(0)
done()
},
})
// @ts-ignore
xhr.onreadystatechange()
})
test('能够发送简单的post请求处理返回的json对象', done => {
mockXMLHttpRequest()
expect.assertions(2)
const data = createFormData()
const xhr = post(API_URL, {
formData: data,
onSuccess: (xhr: XMLHttpRequest, res: any) => {
expect(res.data).toEqual(['url1'])
expect(res.errno).toBe(0)
done()
},
})
// @ts-ignore
xhr.onreadystatechange()
})
test('发送请求失败后会有错误回调', done => {
mockXMLHttpRequest({ status: 500, res: JSON.stringify({ data: 'error', errno: 1 }) })
expect.assertions(2)
const data = createFormData()
const successFn = jest.fn()
const errorFn = jest.fn()
const xhr = post(API_URL, {
formData: data,
onSuccess: successFn,
onError: errorFn,
})
// @ts-ignore
xhr.onreadystatechange()
setTimeout(() => {
expect(successFn).not.toBeCalled()
expect(errorFn).toBeCalled()
done()
}, 1000)
})
test('发送请求能够监听进度变化', done => {
mockXMLHttpRequest()
expect.assertions(2)
const data = createFormData()
const successFn = jest.fn()
const progressFn = jest.fn()
const xhr = post(API_URL, {
formData: data,
onSuccess: successFn,
onProgress: progressFn,
})
// @ts-ignore
xhr.upload.onprogress({ loaded: 50, total: 100 })
expect(progressFn).toBeCalled()
// @ts-ignore
xhr.onreadystatechange()
setTimeout(() => {
expect(successFn).toBeCalled()
done()
}, 1000)
})
test('发送请求能够自定义请求头', done => {
mockXMLHttpRequest()
expect.assertions(1)
const data = createFormData()
const successFn = jest.fn()
const xhr = post(API_URL, {
formData: data,
onSuccess: successFn,
headers: {
'Content-Type': 'application/json',
},
})
// @ts-ignore
xhr.onreadystatechange()
setTimeout(() => {
expect(successFn).toBeCalled()
done()
}, 1000)
})
test('发送请求可以配置超时时间,并配置超时回调', done => {
mockXMLHttpRequest()
expect.assertions(2)
const data = createFormData()
const successFn = jest.fn()
const timeoutFn = jest.fn()
const xhr = post(API_URL, {
formData: data,
onSuccess: successFn,
headers: {
'Content-Type': 'application/json',
},
timeout: 1000,
onTimeout: timeoutFn,
})
setTimeout(() => {
// @ts-ignore
xhr.ontimeout()
expect(timeoutFn).toBeCalled()
expect(successFn).not.toBeCalled()
done()
}, 1000)
})
test('发送请求前可以添加beforeSend检验', () => {
mockXMLHttpRequest()
const data = createFormData()
const successFn = jest.fn()
const msg = post(API_URL, {
formData: data,
onSuccess: successFn,
beforeSend: () => ({ prevent: true, msg: '阻止发送请求' }),
})
expect(msg).toBe('阻止发送请求')
})
test('发送请求成功返回的结果如果不是json字符串会通过fail回调处理', done => {
mockXMLHttpRequest({ status: 200, res: '{test: 123}' })
expect.assertions(2)
const data = createFormData()
const successFn = jest.fn()
const failFn = jest.fn()
const xhr = post(API_URL, {
formData: data,
onSuccess: successFn,
onFail: failFn,
})
// @ts-ignore
xhr.onreadystatechange()
setTimeout(() => {
expect(successFn).not.toBeCalled()
expect(failFn).toBeCalled()
done()
}, 1000)
})
})

View File

@ -0,0 +1,68 @@
/**
* @description Editor upload progress
* @author luochao
*/
import Progress from '../../../src/editor/upload/progress'
import Editor from '../../../src/editor'
import createEditor from '../../helpers/create-editor'
import $ from 'jquery'
let editor: Editor
const progressClassName = '.w-e-progress'
let id = 1
describe('Editor upload progress', () => {
beforeEach(() => {
editor = createEditor(document, `div${id++}`)
})
test('在编辑器中展示 progress bar', () => {
const progress = new Progress(editor)
progress.show(0.5)
const progressBar = $(`#div${id - 1}`).find(progressClassName)
expect(progressBar.length).toBe(1)
expect(progressBar.get(0)).toHaveStyle('width:50%')
expect(editor.$textContainerElem.elems[0]).toContainHTML(progressBar.get(0).innerHTML)
})
test('多次调用不会重复在编辑器中展示 progress bar', () => {
const progress = new Progress(editor)
progress.show(0.5)
progress.show(0.7)
const progressBar = $(`#div${id - 1}`).find(progressClassName)
expect(progressBar.length).toBe(1)
})
test('在编辑器中展示 progress bar500ms后自动消失', done => {
expect.assertions(2)
const progress = new Progress(editor)
progress.show(0.5)
const progressBar = $(`#div${id - 1}`).find(progressClassName)
expect(progressBar.length).toBe(1)
setTimeout(() => {
const progressBar = $(progressClassName)
expect(progressBar.length).toBe(0)
done()
}, 500)
})
test('如果设置的进度超过1进度长度样式将失效', () => {
const progress = new Progress(editor)
progress.show(1.1)
const progressBar = $(progressClassName)
expect(progressBar.length).toBe(1)
expect(progressBar.get(0)).not.toHaveStyle('width:110%')
})
})

View File

@ -0,0 +1,51 @@
/**
* @description code tooltip-bind-event
* @author luochao
*/
import Editor from '../../../src/editor'
import $ from '../../../src/utils/dom-core'
import createEditor from '../../helpers/create-editor'
import { createShowHideFn } from '../../../src/menus/code/bind-event/tooltip-event'
let editor: Editor
let id = 1
describe('code bind event', () => {
beforeEach(() => {
editor = createEditor(document, `div${id++}`)
})
test('调用 createShowHideFn 返回显示和隐藏code tooltip函数', () => {
const fnObj = createShowHideFn(editor)
expect(fnObj.showCodeTooltip).toBeInstanceOf(Function)
expect(fnObj.hideCodeTooltip).toBeInstanceOf(Function)
})
test('调用 showCodeTooltip 方法展示code tooltip', () => {
const fnObj = createShowHideFn(editor)
const codeDom = $('<div></div>')
fnObj.showCodeTooltip(codeDom)
const tooltip = $(`#div${id - 1} .w-e-tooltip`)
expect(tooltip.elems[0]).not.toBeNull()
})
test('调用 hideCodeTooltip 方法隐藏code tooltip', () => {
const fnObj = createShowHideFn(editor)
const codeDom = $('<div></div>')
fnObj.showCodeTooltip(codeDom)
const tooltip1 = $(`#div${id - 1} .w-e-tooltip`)
expect(tooltip1.elems[0]).not.toBeNull()
fnObj.hideCodeTooltip()
const tooltip2 = $(`#div${id - 1} .w-e-tooltip`)
expect(tooltip2.elems[0]).toBeUndefined()
})
})

View File

@ -0,0 +1,83 @@
/**
* @description droplist menu test
* @author luochao
*/
import DropListMenu from '../../../src/menus/menu-constructors/DropListMenu'
import DropList from '../../../src/menus/menu-constructors/DropList'
import createEditor from '../../helpers/create-editor'
import $ from '../../../src/utils/dom-core'
import dispatchEvent from '../../helpers/mock-dispatch-event'
let editor: ReturnType<typeof createEditor>
let droplistMenu: DropListMenu
let id = 1
let menuEl: ReturnType<typeof $>
describe('dropList menu', () => {
beforeEach(() => {
editor = createEditor(document, `div${id++}`, '', {
lang: 'en',
})
const mockClickFn = jest.fn((value: string) => value)
menuEl = $(`<div id="menu${id++}"></div>`)
const conf = {
title: '设置标题',
type: 'list',
width: 100,
clickHandler: mockClickFn,
list: [
{
value: 'test123',
$elem: $('<span><i>test123</i></span>'),
},
],
}
droplistMenu = new DropListMenu(menuEl, editor, conf)
})
test('初始化基本的下拉菜单', () => {
expect(droplistMenu.dropList instanceof DropList).toBeTruthy()
})
test('初始化基本的下拉菜单模拟菜单mouseenter事件会展开下来菜单', done => {
expect.assertions(2)
dispatchEvent(menuEl, 'mouseenter', 'MouseEvent')
setTimeout(() => {
expect(droplistMenu.dropList.isShow).toBeTruthy()
expect(menuEl.elems[0]).toHaveStyle(`z-index:${editor.zIndex.get('menu')}`)
done()
}, 300)
})
test('初始化基本的下拉菜单模拟菜单mouseleave事件会隐藏菜单', done => {
expect.assertions(3)
dispatchEvent(menuEl, 'mouseenter', 'MouseEvent')
setTimeout(() => {
expect(droplistMenu.dropList.isShow).toBeTruthy()
dispatchEvent(menuEl, 'mouseleave', 'MouseEvent')
setTimeout(() => {
expect(droplistMenu.dropList.isShow).toBeFalsy()
expect(menuEl.elems[0]).toHaveStyle(`z-index:auto`)
done()
}, 20)
}, 300)
})
test('初始化基本的下拉菜单模拟菜单mouseenter事件如果编辑器当前的range为空则直接返回', () => {
const mockGetRage = jest.spyOn(editor.selection, 'getRange')
mockGetRage.mockImplementation(() => null)
dispatchEvent(menuEl, 'mouseenter', 'MouseEvent')
expect(droplistMenu.dropList.isShow).toBeFalsy()
})
})

View File

@ -0,0 +1,10 @@
/**
* @description Img menu bind-event drag-size
* @author luochao
*/
describe('Img menu bind-eveent drag-size', () => {
test('绑定drag-size事件', () => {
expect(true).toBeTruthy()
})
})

View File

@ -0,0 +1,72 @@
/**
* @description Img menu bind-event drop-img
* @author luochao
*/
import createEditor from '../../../helpers/create-editor'
import UploadImg from '../../../../src/menus/img/upload-img'
import bindDropImg from '../../../../src/menus/img/bind-event/drop-img'
import mockFile from '../../../helpers/mock-file'
const mockUploadImg = jest.fn()
jest.mock('../../../../src/menus/img/upload-img', () => {
return jest.fn().mockImplementation(() => {
return { uploadImg: mockUploadImg }
})
})
describe('Img menu bind-event drop-img', () => {
beforeEach(() => {
// @ts-ignore
UploadImg.mockClear()
mockUploadImg.mockClear()
})
test('调用 dropImg 方法绑定drop-img事件', () => {
const editor = createEditor(document, 'div1')
bindDropImg(editor)
expect(editor.txt.eventHooks.dropEvents.length).toBeGreaterThanOrEqual(1)
})
test('调用 dropImg 方法绑定drop-img事件后执行dropEvents里面的方法会触发uploadImg调用', () => {
const editor = createEditor(document, 'div2')
bindDropImg(editor)
const dropEvents = editor.txt.eventHooks.dropEvents
expect(dropEvents.length).toBeGreaterThanOrEqual(1)
const files = [mockFile({ name: 'test.png', size: 200, mimeType: 'image/png' })]
const mockDropEvent = { dataTransfer: { files } }
dropEvents.forEach(fn => {
// @ts-ignore
fn(mockDropEvent)
})
expect(UploadImg).toHaveBeenCalled()
expect(mockUploadImg).toBeCalledWith(files)
})
test('调用 dropImg 方法绑定drop-img事件后如果dropEvent触发的事件参数的文件为空则不触发上传', () => {
const editor = createEditor(document, 'div3')
bindDropImg(editor)
const dropEvents = editor.txt.eventHooks.dropEvents
expect(dropEvents.length).toBeGreaterThanOrEqual(1)
const files: any[] = []
const mockDropEvent = { dataTransfer: { files } }
dropEvents.forEach(fn => {
// @ts-ignore
fn(mockDropEvent)
})
expect(UploadImg).not.toBeCalled()
expect(mockUploadImg).not.toBeCalled()
})
})

View File

@ -4,12 +4,12 @@
*/
import $ from 'jquery'
import Editor from '../../../src/editor'
import createEditor from '../../helpers/create-editor'
import mockCmdFn from '../../helpers/command-mock'
import ImgMenu from '../../../src/menus/img/index'
import { getMenuInstance } from '../../helpers/menus'
import Panel from '../../../src/menus/menu-constructors/Panel'
import Editor from '../../../../src/editor'
import createEditor from '../../../helpers/create-editor'
import mockCmdFn from '../../../helpers/command-mock'
import ImgMenu from '../../../../src/menus/img/index'
import { getMenuInstance } from '../../../helpers/menus'
import Panel from '../../../../src/menus/menu-constructors/Panel'
let editor: Editor
let imgMenu: ImgMenu

View File

@ -0,0 +1,137 @@
/**
* @description Img menu paste-img
* @author luochao
*/
import createEditor from '../../../helpers/create-editor'
import bindPasteImgEvent from '../../../../src/menus/img/bind-event/paste-img'
import UploadImg from '../../../../src/menus/img/upload-img'
import mockFile from '../../../helpers/mock-file'
import mockCmdFn from '../../../helpers/command-mock'
import * as pasteEvents from '../../../../src/text/paste/paste-event'
const mockFiles = [mockFile({ name: 'test.png', size: 200, mimeType: 'image/png' })]
const mockUploadImg = jest.fn()
jest.mock('../../../../src/menus/img/upload-img', () => {
return jest.fn().mockImplementation(() => {
return { uploadImg: mockUploadImg }
})
})
describe('Img menu paste-img', () => {
beforeEach(() => {
// @ts-ignore
UploadImg.mockClear()
mockUploadImg.mockClear()
})
test('调用 bindPasteImgEvent 方法给编辑器绑定paste事件', () => {
const editor = createEditor(document, 'div1')
bindPasteImgEvent(editor)
expect(editor.txt.eventHooks.pasteEvents.length).toBeGreaterThanOrEqual(1)
})
test('调用 bindPasteImgEvent 方法给编辑器绑定paste事件后执行pasteEvent里面的函数会触发上传', () => {
mockCmdFn(document)
const mock = jest.spyOn(pasteEvents, 'getPasteImgs')
mock.mockReturnValue(mockFiles)
const editor = createEditor(document, 'div1')
bindPasteImgEvent(editor)
const mockGetData = jest.fn().mockImplementation(() => '')
const eventHooks = editor.txt.eventHooks.pasteEvents
const mockEvent = { clipboardData: { getData: mockGetData, items: mockFiles } }
eventHooks.forEach(fn => {
// @ts-ignore
fn(mockEvent)
})
expect(UploadImg).toBeCalled()
expect(mockUploadImg).toBeCalledWith(mockFiles)
})
test('调用 bindPasteImgEvent 方法给编辑器绑定paste事件后执行pasteEvent里面的函数会如果粘贴板没有图片文件则不会触发上传逻辑', () => {
mockCmdFn(document)
const mock = jest.spyOn(pasteEvents, 'getPasteImgs')
mock.mockReturnValue([])
const editor = createEditor(document, 'div1')
bindPasteImgEvent(editor)
const mockGetData = jest.fn().mockImplementation(() => '')
const eventHooks = editor.txt.eventHooks.pasteEvents
const mockEvent = { clipboardData: { getData: mockGetData, items: mockFiles } }
eventHooks.forEach(fn => {
// @ts-ignore
fn(mockEvent)
})
expect(UploadImg).not.toBeCalled()
})
test('调用 bindPasteImgEvent 方法给编辑器绑定paste事件后执行pasteEvent里面的函数如果粘贴的内容有HTML会直接返回', () => {
mockCmdFn(document)
const mock = jest.spyOn(pasteEvents, 'getPasteImgs')
mock.mockReturnValue(mockFiles)
const editor = createEditor(document, 'div1')
bindPasteImgEvent(editor)
const mockGetData = jest.fn().mockImplementation(() => '<span></span>')
const eventHooks = editor.txt.eventHooks.pasteEvents
const mockEvent = { clipboardData: { getData: mockGetData, items: mockFiles } }
eventHooks.forEach(fn => {
// @ts-ignore
fn(mockEvent)
})
expect(UploadImg).not.toBeCalled()
})
test('调用 bindPasteImgEvent 方法给编辑器绑定paste事件后执行pasteEvent里面的函数如果粘贴的内容有Text会直接返回', () => {
mockCmdFn(document)
const mock = jest.spyOn(pasteEvents, 'getPasteImgs')
mock.mockReturnValue(mockFiles)
const editor = createEditor(document, 'div1')
bindPasteImgEvent(editor)
const mockGetData = jest.fn().mockImplementation((type: string) => {
if (type === 'text' || type === 'text/plain') {
return '123'
}
return ''
})
const eventHooks = editor.txt.eventHooks.pasteEvents
const mockEvent = { clipboardData: { getData: mockGetData, items: mockFiles } }
eventHooks.forEach(fn => {
// @ts-ignore
fn(mockEvent)
})
expect(UploadImg).not.toBeCalled()
})
})

View File

@ -0,0 +1,60 @@
/**
* @description Img menu tooltip-event
* @author luochao
*/
import createEditor from '../../../helpers/create-editor'
import $ from '../../../../src/utils/dom-core'
import bindTooltipEvent, * as tooltipEvent from '../../../../src/menus/img/bind-event/tooltip-event'
describe('Img menu tooltip-event', () => {
test('绑定 tooltip-event 事件', () => {
const editor = createEditor(document, 'div1')
bindTooltipEvent(editor)
expect(editor.txt.eventHooks.imgClickEvents.length).toBeGreaterThanOrEqual(1)
})
test('调用 createShowHideFn 函数返回显示和隐藏tooltip方法', () => {
const editor = createEditor(document, 'div2')
const fns = tooltipEvent.createShowHideFn(editor)
expect(fns.showImgTooltip instanceof Function).toBeTruthy()
expect(fns.hideImgTooltip instanceof Function).toBeTruthy()
})
test('绑定 tooltip-event 事件执行图片点击事件会展示tooltip', () => {
const editor = createEditor(document, 'div4')
bindTooltipEvent(editor)
const imgClickEvents = editor.txt.eventHooks.imgClickEvents
imgClickEvents.forEach(fn => {
fn($(editor.$textElem))
})
expect($('.w-e-tooltip').elems[0]).not.toBeUndefined()
expect($('.w-e-tooltip').elems[0].childNodes.length).toBe(5)
})
test('绑定 tooltip-event 事件执行图片之外的其它点击事件会隐藏tooltip', () => {
const editor = createEditor(document, 'div5')
bindTooltipEvent(editor)
const imgClickEvents = editor.txt.eventHooks.imgClickEvents
const clickEvents = editor.txt.eventHooks.clickEvents
imgClickEvents.forEach(fn => {
fn($('<div></div>'))
})
clickEvents.forEach(fn => {
// @ts-ignore
fn()
})
expect($('#div5 .w-e-tooltip').elems[0]).toBeUndefined()
})
})

View File

@ -0,0 +1,537 @@
/**
* @description upload-img test
* @author luochao
*/
import createEditor from '../../../helpers/create-editor'
import mockCmdFn from '../../../helpers/command-mock'
import mockFile from '../../../helpers/mock-file'
import mockXHR from '../../../helpers/mock-xhr'
import Editor from '../../../../src/editor'
import UploadImg from '../../../../src/menus/img/upload-img'
let editor: Editor
let id = 1
const imgUrl = 'http://www.wangeditor.com/imgs/logo.jpeg'
const errorUrl = 'logo123.jpeg'
const uploadImgServer = 'http://localhost:8881/api/upload-img'
const defaultRes = {
status: 200,
res: JSON.stringify({ data: ['url1'], errno: 0 }),
}
const mockXHRHttpRequest = (res: any = defaultRes) => {
const mockXHRObject = mockXHR(res)
const mockObject = jest.fn().mockImplementation(() => mockXHRObject)
// @ts-ignore
window.XMLHttpRequest = mockObject
return mockXHRObject
}
const createUploadImgInstance = (config: any) => {
const editor = createEditor(document, `div${id++}`, '', config)
const uploadImg = new UploadImg(editor)
return uploadImg
}
const mockSupportCommand = () => {
mockCmdFn(document)
document.queryCommandSupported = jest.fn(() => true)
}
const deaultFiles = [{ name: 'test.png', size: 512, mimeType: 'image/png' }]
const createMockFilse = (fileList: any[] = deaultFiles) => {
const files = fileList.map(file => mockFile(file))
return files.filter(Boolean)
}
describe('upload img', () => {
// mock img onload and onerror event
beforeAll(() => {
// Mocking Image.prototype.src to call the onload or onerror
// callbacks depending on the src passed to it
Object.defineProperty(global.Image.prototype, 'src', {
// Define the property setter
set(src) {
if (src === errorUrl) {
// Call with setTimeout to simulate async loading
setTimeout(() => this.onerror(new Error('mocked error')))
} else if (src === imgUrl) {
setTimeout(() => this.onload())
}
},
})
})
beforeEach(() => {
editor = createEditor(document, `div${id++}`)
})
test('能够初始化基本的UploadImg类', () => {
const uploadImg = new UploadImg(editor)
expect(uploadImg.insertImg instanceof Function).toBeTruthy()
expect(uploadImg.uploadImg instanceof Function).toBeTruthy()
})
test('调用 insertImg 可以网编辑器里插入图片', () => {
const uploadImg = new UploadImg(editor)
mockSupportCommand()
uploadImg.insertImg(imgUrl)
expect(document.execCommand).toBeCalledWith(
'insertHTML',
false,
`<img src="${imgUrl}" style="max-width:100%;"/>`
)
})
test('调用 insertImg 可以网编辑器里插入图片,可以监听插入图片回调', () => {
const callback = jest.fn()
const uploadImg = createUploadImgInstance({
linkImgCallback: callback,
})
mockSupportCommand()
uploadImg.insertImg(imgUrl)
expect(document.execCommand).toBeCalledWith(
'insertHTML',
false,
`<img src="${imgUrl}" style="max-width:100%;"/>`
)
expect(callback).toBeCalledWith(imgUrl)
})
test('调用 insertImg 可以网编辑器里插入图片插入图片加载失败可以通过customAlert配置错误提示', done => {
expect.assertions(1)
const alertFn = jest.fn()
const uploadImg = createUploadImgInstance({ customAlert: alertFn })
mockSupportCommand()
uploadImg.insertImg(errorUrl)
setTimeout(() => {
expect(alertFn).toBeCalledWith(
'插入图片错误',
'error',
`wangEditor: 插入图片错误,图片链接 "${errorUrl}",下载链接失败`
)
done()
}, 1000)
})
test('调用 uploadImg 上传图片', done => {
expect.assertions(1)
const jestFn = jest.fn()
const upload = createUploadImgInstance({
uploadImgServer,
uploadImgHooks: {
success: jestFn,
},
})
const files = createMockFilse()
const mockXHRObject = mockXHRHttpRequest()
upload.uploadImg(files)
mockXHRObject.onreadystatechange()
setTimeout(() => {
expect(jestFn).toBeCalled()
done()
}, 1000)
})
test('调用 uploadImg 上传图片,如果传入的文件为空直接返回', () => {
const upload = new UploadImg(editor)
const res = upload.uploadImg([])
expect(res).toBeUndefined()
})
test('调用 uploadImg 上传图片如果没有配置customUploadImg, 则必须配置 uploadImgServer 或者 uploadImgShowBase64', () => {
const upload = new UploadImg(editor)
const files = createMockFilse()
const res = upload.uploadImg(files)
expect(res).toBeUndefined()
})
test('调用 uploadImg 上传图片如果文件没有名字或者size为则会被过滤掉', () => {
const fn = jest.fn()
const upload = createUploadImgInstance({
uploadImgServer,
customAlert: fn,
})
const files = createMockFilse([{ name: '', size: 0, mimeType: 'image/png' }])
const res = upload.uploadImg(files)
expect(res).toBeUndefined()
expect(fn).toBeCalledWith('传入的文件不合法', 'warning')
})
test('调用 uploadImg 上传图片,如果文件非图片,则返回并提示错误信息', () => {
const fn = jest.fn()
const upload = createUploadImgInstance({
uploadImgServer,
customAlert: fn,
})
const files = createMockFilse([{ name: 'test.txt', size: 200, mimeType: 'text/plain' }])
const res = upload.uploadImg(files)
expect(res).toBeUndefined()
expect(fn).toBeCalledWith('图片验证未通过: \n【test.txt】不是图片', 'warning')
})
test('调用 uploadImg 上传图片,如果文件体积大小超过配置的大小,则返回并提示错误信息', () => {
const fn = jest.fn()
const upload = createUploadImgInstance({
uploadImgServer,
uploadImgMaxSize: 5 * 1024 * 1024,
customAlert: fn,
})
const files = createMockFilse([
{ name: 'test.png', size: 6 * 1024 * 1024, mimeType: 'image/png' },
])
const res = upload.uploadImg(files)
expect(res).toBeUndefined()
expect(fn).toBeCalledWith(`图片验证未通过: \n【test.png】大于 5M`, 'warning')
})
test('调用 uploadImg 上传图片,如果文件个数超过配置的的大小,则返回并提示错误信息', () => {
const fn = jest.fn()
const upload = createUploadImgInstance({
uploadImgServer,
uploadImgMaxLength: 2,
customAlert: fn,
})
const files = createMockFilse([
{ name: 'test1.png', size: 2048, mimeType: 'image/png' },
{ name: 'test2.png', size: 2048, mimeType: 'image/png' },
{ name: 'test3.png', size: 2048, mimeType: 'image/png' },
])
const res = upload.uploadImg(files)
expect(res).toBeUndefined()
expect(fn).toBeCalledWith('一次最多上传2张图片', 'warning')
})
test('调用 uploadImg 上传图片,如果配置了 customUploadImg 选项则调用customUploadImg上传', () => {
const fn = jest.fn()
const upload = createUploadImgInstance({
uploadImgServer,
customUploadImg: fn,
})
const files = createMockFilse()
const res = upload.uploadImg(files)
expect(res).toBeUndefined()
expect(fn).toBeCalled()
})
test('调用 uploadImg 上传图片如果可以配置uploadImgParamsWithUrl添加query参数', done => {
expect.assertions(1)
const fn = jest.fn()
const upload = createUploadImgInstance({
uploadImgServer,
uploadImgParams: {
a: 'a',
b: 'b',
},
uploadImgParamsWithUrl: true,
uploadImgHooks: {
success: fn,
},
})
const files = createMockFilse()
const mockXHRObject = mockXHRHttpRequest()
upload.uploadImg(files)
mockXHRObject.onreadystatechange()
setTimeout(() => {
expect(fn).toBeCalled()
done()
})
})
test('调用 uploadImg 上传图片uploadImgServer支持hash参数拼接', done => {
expect.assertions(1)
const fn = jest.fn()
const upload = createUploadImgInstance({
uploadImgServer,
uploadImgParams: {
a: 'a',
b: 'b',
},
uploadImgParamsWithUrl: true,
uploadImgHooks: {
success: fn,
},
})
const files = createMockFilse([
{ name: 'test1.png', size: 2048, mimeType: 'image/png' },
{ name: 'test2.png', size: 2048, mimeType: 'image/png' },
])
const mockXHRObject = mockXHRHttpRequest()
upload.uploadImg(files)
mockXHRObject.onreadystatechange()
setTimeout(() => {
expect(fn).toBeCalled()
done()
})
})
test('调用 uploadImg 上传图片失败会有错误提示并支持配置onError hook', done => {
expect.assertions(2)
const fn = jest.fn()
const alertFn = jest.fn()
const upload = createUploadImgInstance({
uploadImgServer,
uploadImgHooks: {
error: fn,
},
customAlert: alertFn,
})
const files = createMockFilse()
const mockXHRObject = mockXHRHttpRequest({ status: 500 })
upload.uploadImg(files)
mockXHRObject.onreadystatechange()
setTimeout(() => {
expect(fn).toBeCalled()
expect(alertFn).toBeCalledWith(
'上传图片错误',
'error',
'上传图片错误,服务器返回状态: 500'
)
done()
})
})
test('调用 uploadImg 上传图片成功后数据返回不正常会有错误提示并支持配置onFail hook', done => {
expect.assertions(2)
const fn = jest.fn()
const alertFn = jest.fn()
const upload = createUploadImgInstance({
uploadImgServer,
uploadImgHooks: {
fail: fn,
},
customAlert: alertFn,
})
const files = createMockFilse()
const mockXHRObject = mockXHRHttpRequest({
status: 200,
res: '{test: 123}',
})
upload.uploadImg(files)
mockXHRObject.onreadystatechange()
setTimeout(() => {
expect(fn).toBeCalled()
expect(alertFn).toBeCalledWith(
'上传图片失败',
'error',
'上传图片返回结果错误,返回结果: {test: 123}'
)
done()
})
})
test('调用 uploadImg 上传图片成功后,支持自定义插入图片函数', done => {
expect.assertions(1)
const insertFn = jest.fn()
const upload = createUploadImgInstance({
uploadImgServer,
uploadImgHooks: {
customInsert: insertFn,
},
})
const files = createMockFilse()
const mockXHRObject = mockXHRHttpRequest()
upload.uploadImg(files)
mockXHRObject.onreadystatechange()
setTimeout(() => {
expect(insertFn).toBeCalled()
done()
})
})
test('调用 uploadImg 上传被阻止,会有错误提示', done => {
expect.assertions(2)
const beforFn = jest.fn(() => ({ prevent: true, msg: '阻止发送请求' }))
const alertFn = jest.fn()
const upload = createUploadImgInstance({
uploadImgServer,
uploadImgHooks: {
before: beforFn,
},
customAlert: alertFn,
})
const files = createMockFilse()
const mockXHRObject = mockXHRHttpRequest()
upload.uploadImg(files)
mockXHRObject.onreadystatechange()
setTimeout(() => {
expect(beforFn).toBeCalled()
expect(alertFn).toBeCalledWith('阻止发送请求', 'error')
done()
})
})
test('调用 uploadImg 上传返回的错误码不符合条件会有错误提示并触发fail回调', done => {
expect.assertions(2)
const failFn = jest.fn()
const alertFn = jest.fn()
const upload = createUploadImgInstance({
uploadImgServer,
uploadImgHooks: {
fail: failFn,
},
customAlert: alertFn,
})
const files = createMockFilse()
const mockXHRObject = mockXHRHttpRequest({
status: 200,
res: { test: 123, errno: -1 },
})
upload.uploadImg(files)
mockXHRObject.onreadystatechange()
setTimeout(() => {
expect(failFn).toBeCalled()
expect(alertFn).toBeCalledWith(
'上传图片失败',
'error',
'上传图片返回结果错误,返回结果 errno=-1'
)
done()
})
})
test('调用 uploadImg 上传,如果配置 uploadImgShowBase64 参数则直接插入base64到编辑器', () => {
const callback = jest.fn()
const upload = createUploadImgInstance({
uploadImgShowBase64: true,
linkImgCallback: callback,
})
const files = createMockFilse()
const mockFn = jest.fn()
// @ts-ignore
jest.spyOn(global, 'FileReader').mockImplementation(() => {
return {
readAsDataURL: mockFn,
}
})
upload.uploadImg(files)
expect(mockFn).toBeCalled()
})
test('调用 uploadImg 上传超时会触发超时回调', done => {
expect.assertions(2)
const timeoutFn = jest.fn()
const alertFn = jest.fn()
const upload = createUploadImgInstance({
uploadImgServer,
uploadImgHooks: {
timeout: timeoutFn,
},
customAlert: alertFn,
})
const files = createMockFilse()
const mockXHRObject = mockXHRHttpRequest()
upload.uploadImg(files)
mockXHRObject.ontimeout()
setTimeout(() => {
expect(timeoutFn).toBeCalled()
expect(alertFn).toBeCalledWith('上传图片超时', 'error')
done()
})
})
})

View File

@ -8,24 +8,147 @@ import createEditor from '../../helpers/create-editor'
import mockCmdFn from '../../helpers/command-mock'
import lineHeight from '../../../src/menus/lineHeight/index'
import { getMenuInstance } from '../../helpers/menus'
import { UA } from '../../../src/utils/util'
let editor: Editor
let lineHeightMenu: lineHeight
let id = 1
test('lineHeight 菜单dropList', () => {
editor = createEditor(document, 'div1') // 赋值给全局变量
lineHeightMenu = getMenuInstance(editor, lineHeight) as lineHeight // 赋值给全局变量
expect(lineHeightMenu.dropList).not.toBeNull()
lineHeightMenu.dropList.show()
expect(lineHeightMenu.dropList.isShow).toBe(true)
lineHeightMenu.dropList.hide()
expect(lineHeightMenu.dropList.isShow).toBe(false)
})
describe('LineHeight menu', () => {
beforeEach(() => {
editor = createEditor(document, `div${id++}`)
lineHeightMenu = getMenuInstance(editor, lineHeight) as lineHeight
})
test('lineHeight 菜单:增加行高', () => {
mockCmdFn(document)
const cmdVal = '2'
lineHeightMenu.command(cmdVal)
// 此处触发 editor.cmd.do('insertHTML', xx),可以被 jest 成功执行,具体参考 mockCmdFn 的描述
expect(editor.$textElem.html().indexOf('<p style="line-height:2;"><br></p>')).toBeGreaterThan(0)
test('lineHeight 菜单dropList', () => {
expect(lineHeightMenu.dropList).not.toBeNull()
lineHeightMenu.dropList.show()
expect(lineHeightMenu.dropList.isShow).toBe(true)
lineHeightMenu.dropList.hide()
expect(lineHeightMenu.dropList.isShow).toBe(false)
})
test('lineHeight 菜单:增加行高', () => {
mockCmdFn(document)
const cmdVal = '2'
lineHeightMenu.command(cmdVal)
// 此处触发 editor.cmd.do('insertHTML', xx),可以被 jest 成功执行,具体参考 mockCmdFn 的描述
expect(editor.$textElem.elems[0]).toContainHTML('<p style="line-height:2;"><br></p>')
})
test('lineHeight 菜单:选择多行增加行高', () => {
mockCmdFn(document)
editor.txt.html('<p>123</p><p>234</p>')
const [startNode, endNode] = Array.from(editor.$textElem.elems[0].childNodes)
lineHeightMenu.setRange(startNode, endNode)
const cmdVal = '2'
lineHeightMenu.command(cmdVal)
// 此处触发 editor.cmd.do('insertHTML', xx),可以被 jest 成功执行,具体参考 mockCmdFn 的描述
expect(
editor.$textElem.elems[0].innerHTML.indexOf('<p style="line-height:2;">123</p>')
).toBeGreaterThanOrEqual(0)
})
test('lineHeight 菜单:选择多行增加行高, 如果是IE浏览器直接返回', () => {
mockCmdFn(document)
const mockIE = jest.spyOn(UA, 'isIE')
mockIE.mockReturnValueOnce(true)
editor.txt.html('<p>123</p><p>234</p>')
const [startNode, endNode] = Array.from(editor.$textElem.elems[0].childNodes)
lineHeightMenu.setRange(startNode, endNode)
const cmdVal = '2'
lineHeightMenu.command(cmdVal)
// 此处触发 editor.cmd.do('insertHTML', xx),可以被 jest 成功执行,具体参考 mockCmdFn 的描述
expect(editor.$textElem.elems[0].innerHTML.indexOf('<p>123</p>')).toBeGreaterThanOrEqual(0)
})
test('lineHeight 菜单:选择多行增加行高, 设置非段落的P标签开头全选', () => {
mockCmdFn(document)
editor.txt.html('<p>234<span>123</span></p><div>345</div>')
const [startNode, endNode] = Array.from(editor.$textElem.elems[0].childNodes)
lineHeightMenu.setRange(startNode, endNode)
const cmdVal = '2'
lineHeightMenu.command(cmdVal)
// 此处触发 editor.cmd.do('insertHTML', xx),可以被 jest 成功执行,具体参考 mockCmdFn 的描述
expect(
editor.$textElem.elems[0].innerHTML.indexOf(
'<p style="line-height:2;">234<span>123</span></p>'
)
).toBeGreaterThanOrEqual(0)
})
test('lineHeight 菜单:选择多行增加行高, 设置无P标签的全选设置无效', () => {
mockCmdFn(document)
editor.txt.html('<div>345</div><div>234<span>123</span></div>')
const [startNode, endNode] = Array.from(editor.$textElem.elems[0].childNodes)
lineHeightMenu.setRange(startNode, endNode)
const cmdVal = '2'
lineHeightMenu.command(cmdVal)
// 此处触发 editor.cmd.do('insertHTML', xx),可以被 jest 成功执行,具体参考 mockCmdFn 的描述
expect(
editor.$textElem.elems[0].innerHTML.indexOf(
'<div>345</div><div>234<span>123</span></div>'
)
).toBeGreaterThanOrEqual(0)
})
test('lineHeight 菜单:增加行高, 如果不传value值则设为默认行高并且不设置 line-height 样式', () => {
mockCmdFn(document)
editor.txt.html('<p style="color:red;">123</p>')
const [startNode] = Array.from(editor.$textElem.elems[0].childNodes)
lineHeightMenu.setRange(startNode, startNode)
lineHeightMenu.command('')
// 此处触发 editor.cmd.do('insertHTML', xx),可以被 jest 成功执行,具体参考 mockCmdFn 的描述
expect(
editor.$textElem.elems[0].innerHTML.indexOf('<p style="color:red;">123</p>')
).toBeGreaterThanOrEqual(0)
})
test('lineHeight 菜单:增加行高, 如果选区的元素有style样式则会在样式上叠加 line-height 样式', () => {
mockCmdFn(document)
editor.txt.html('<p style="color:red;">123</p>')
const [startNode] = Array.from(editor.$textElem.elems[0].childNodes)
lineHeightMenu.setRange(startNode, startNode)
lineHeightMenu.command('2')
// 此处触发 editor.cmd.do('insertHTML', xx),可以被 jest 成功执行,具体参考 mockCmdFn 的描述
expect(
editor.$textElem.elems[0].innerHTML.indexOf(
'<p style="color:red;line-height:2;">123</p>'
)
).toBeGreaterThanOrEqual(0)
})
test('lineHeight 菜单:增加行高, 如果选区的元素为blockquote元素则不会叠加 line-height 样式', () => {
mockCmdFn(document)
editor.txt.html('<blockquote>123</blockquote>')
const [startNode] = Array.from(editor.$textElem.elems[0].childNodes)
lineHeightMenu.setRange(startNode, startNode)
lineHeightMenu.command('2')
// 此处触发 editor.cmd.do('insertHTML', xx),可以被 jest 成功执行,具体参考 mockCmdFn 的描述
expect(
editor.$textElem.elems[0].innerHTML.indexOf('<blockquote>123</blockquote>')
).toBeGreaterThanOrEqual(0)
})
})

View File

@ -5,25 +5,75 @@
import createEditor from '../../helpers/create-editor'
import Editor from '../../../src/editor'
import createQuote from '../../../src/menus/quote/create-quote-node'
import QuoteMenu from '../../../src/menus/quote'
import { getMenuInstance } from '../../helpers/menus'
import $, { DomElement } from '../../../src/utils/dom-core'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let editor: Editor
let id = 1
let quoteMenu: QuoteMenu
describe('Editor quote menu', () => {
beforeEach(() => {
editor = createEditor(document, `div${id++}`)
quoteMenu = getMenuInstance(editor, QuoteMenu)
})
test('创建编辑器会初始化 quote 菜单', () => {
expect(editor.txt.eventHooks.enterDownEvents.length).toBeGreaterThanOrEqual(1)
expect(quoteMenu).not.toBeNull()
})
test('单行引用', () => {
editor = createEditor(document, 'div1') // 赋值给全局变量
const $childElem: DomElement[] = [$(`<p>123</p>`)]
const $quote = createQuote($childElem)
const p = $(`<p></p>`)
p.append($quote)
expect(p.html()).toEqual(`<blockquote><p>123</p></blockquote>`)
})
test('给编辑器添加单行引用', () => {
editor.selection.createEmptyRange()
quoteMenu.clickHandler()
test('多行引用', () => {
editor = createEditor(document, 'div1') // 赋值给全局变量
const $childElem: DomElement[] = [$(`<p>123</p>`), $(`<p>456</p>`)]
const $quote = createQuote($childElem)
const p = $(`<p></p>`)
p.append($quote)
expect(p.html()).toEqual(`<blockquote><p>123</p><p>456</p></blockquote>`)
expect((editor.txt.html() as string).indexOf('blockquote')).toBeGreaterThanOrEqual(0)
})
test('取消编辑器添加单行引用', () => {
editor.selection.createEmptyRange()
quoteMenu.clickHandler()
expect((editor.txt.html() as string).indexOf('blockquote')).toBeGreaterThanOrEqual(0)
quoteMenu.clickHandler()
expect((editor.txt.html() as string).indexOf('blockquote')).toEqual(-1)
})
test('执行 enterDownEvents 里面的函数会触发 quoteEnter 函数执行', () => {
const $childElem: DomElement[] = [$(`<p>123</p>`)]
const $quote = createQuote($childElem)
editor.$textElem.append($quote)
// @ts-ignore
editor.selection.createRangeByElem($quote)
const mockSelectionGetSelectionContainerElem = jest.spyOn(
editor.selection,
'getSelectionContainerElem'
)
const mockGetSelectionRangeTopNodes = jest.spyOn(
editor.selection,
'getSelectionRangeTopNodes'
)
mockSelectionGetSelectionContainerElem.mockImplementation(() => {
return $quote
})
mockGetSelectionRangeTopNodes.mockImplementation(() => {
return [$quote]
})
const event = new KeyboardEvent('keydown')
editor.txt.eventHooks.enterDownEvents.forEach(fn => {
fn(event)
})
})
test('可以使用 createQuote 创建多行引用', () => {
const $childElem: DomElement[] = [$(`<p>123</p>`), $(`<p>456</p>`)]
const $quote = createQuote($childElem)
const p = $(`<p></p>`)
p.append($quote)
expect(p.html()).toEqual(`<blockquote><p>123</p><p>456</p></blockquote>`)
})
})

View File

@ -0,0 +1,69 @@
/**
* @description split-line menu
* @author luochao
*/
import createEditor from '../../helpers/create-editor'
import mockCommand from '../../helpers/command-mock'
import SplitLine from '../../../src/menus/split-line'
import { getMenuInstance } from '../../helpers/menus'
import $ from '../../../src/utils/dom-core'
let editor: ReturnType<typeof createEditor>
let splitLineMenu: SplitLine
let id = 1
describe('split-line menu', () => {
beforeEach(() => {
editor = createEditor(document, `div${id++}`)
splitLineMenu = getMenuInstance(editor, SplitLine)
})
test('初始化编辑器默认会创建 split-line 菜单', () => {
expect(splitLineMenu).not.toBeNull()
})
test('点击分割线菜单会创建分割线', () => {
mockCommand(document)
splitLineMenu.clickHandler()
expect((editor.txt.html() as string).indexOf('<hr>')).toBeGreaterThanOrEqual(0)
})
test('执行 splitLineEvents 里面的钩子函数会展示 tooltip 菜单点击其它地方会隐藏tooptip', () => {
mockCommand(document)
editor.txt.eventHooks.splitLineEvents.forEach(fn => {
fn(splitLineMenu.$elem)
})
const tooltip1 = $('.w-e-tooltip')
expect(tooltip1.elems[0]).not.toBeUndefined()
editor.$textElem.elems[0].click()
const tooltip2 = $('.w-e-tooltip')
expect(tooltip2.elems[0]).toBeUndefined()
})
test('执行 splitLineEvents 里面的钩子函数会展示 tooltip 菜单点击tooltip删除按钮会移除分隔线', () => {
mockCommand(document)
splitLineMenu.clickHandler()
expect((editor.txt.html() as string).indexOf('<hr>')).toBeGreaterThanOrEqual(0)
editor.txt.eventHooks.splitLineEvents.forEach(fn => {
fn(splitLineMenu.$elem)
})
const tooltip = $('.w-e-tooltip')
expect(tooltip.elems[0]).not.toBeUndefined()
const deleteSpan = tooltip.find('.w-e-tooltip-item-wrapper span')
deleteSpan.elems[0].click()
expect(document.execCommand).toBeCalledWith('delete', false, undefined)
})
})

View File

@ -0,0 +1,72 @@
/**
* @description editor.text event-hooks del-to-keep-p test
* @author luochao
*/
import delToKeepP from '../../../src/text/event-hooks/del-to-keep-p'
import createEditor from '../../helpers/create-editor'
describe('editor.text event-hooks tab-to-space test', () => {
test('能绑定分别绑定 一个处理 up 和 down 的函数', () => {
const upFns: Function[] = []
const downFns: Function[] = []
const editor = createEditor(document, 'div1')
delToKeepP(editor, upFns, downFns)
expect(upFns.length).toBe(1)
expect(downFns.length).toBe(1)
})
test('当编辑器内容为空时,执行 up 函数,则会插入 <p><br></p> 内容', () => {
const upFns: Function[] = []
const downFns: Function[] = []
const editor = createEditor(document, 'div2')
delToKeepP(editor, upFns, downFns)
editor.txt.html(' ')
upFns.forEach(fn => {
fn()
})
expect(editor.$textElem.elems[0].innerHTML).toEqual('<p><br></p>')
})
test('当编辑器内容只有 <br> 时,执行 up 函数,则会插入 <p><br></p> 内容', () => {
const upFns: Function[] = []
const downFns: Function[] = []
const editor = createEditor(document, 'div3')
delToKeepP(editor, upFns, downFns)
editor.txt.html('<br>')
upFns.forEach(fn => {
fn()
})
expect(editor.$textElem.elems[0].innerHTML).toEqual(' <p><br></p>')
})
test('当编辑器内容清空到只剩下 <p><br></p> 内容时,则不允许再删除', () => {
const upFns: Function[] = []
const downFns: Function[] = []
const editor = createEditor(document, 'div4')
delToKeepP(editor, upFns, downFns)
editor.txt.html('<p><br></p>')
const e = new KeyboardEvent('mousedown')
const mockPreventDefault = jest.fn()
jest.spyOn(e, 'preventDefault').mockImplementation(mockPreventDefault)
downFns.forEach(fn => {
fn(e)
})
expect(editor.$textElem.elems[0].innerHTML).toEqual('<p><br></p>')
expect(mockPreventDefault).toBeCalled()
})
})

View File

@ -0,0 +1,178 @@
/**
* @description editor.text event-hooks del-to-keep-p test
* @author luochao
*/
import enterToCreateP from '../../../src/text/event-hooks/enter-to-create-p'
import $ from '../../../src/utils/dom-core'
import createEditor from '../../helpers/create-editor'
import commandMock from '../../helpers/command-mock'
type Editor = ReturnType<typeof createEditor>
let editor: Editor
let id = 1
const mockGetSelectionContainerElem = (editor: Editor, tagString: string, isChild = true) => {
const container = $(tagString)
jest.spyOn(editor.selection, 'getSelectionContainerElem').mockImplementation(() =>
isChild ? container.children()! : container
)
}
describe('editor.text event-hooks tab-to-space test', () => {
beforeEach(() => {
editor = createEditor(document, `div${id++}`)
})
afterEach(() => {
jest.clearAllMocks()
})
test('能绑定分别绑定 一个处理 up 和 down 的函数', () => {
const upFns: Function[] = []
const downFns: Function[] = []
enterToCreateP(editor, upFns, downFns)
expect(upFns.length).toBe(1)
expect(downFns.length).toBe(1)
})
test('当编辑器选区内容父元素为 <code><br></code> ,则移除内容, 插入 <p><br></p>', () => {
const upFns: Function[] = []
const downFns: Function[] = []
enterToCreateP(editor, upFns, downFns)
editor.txt.html(' ')
mockGetSelectionContainerElem(editor, '<code><br></code>', false)
upFns.forEach(fn => {
fn()
})
expect(editor.$textElem.elems[0].innerHTML).toEqual('<p><br></p>')
})
test('当编辑器选区内容的父元素不是 $textElm则不处理', () => {
const upFns: Function[] = []
const downFns: Function[] = []
enterToCreateP(editor, upFns, downFns)
editor.txt.html('<p>0</p>')
const container = $('<p>123</p>')
jest.spyOn(editor.selection, 'getSelectionContainerElem').mockImplementation(
() => container
)
upFns.forEach(fn => {
fn()
})
expect(editor.$textElem.elems[0].innerHTML).toEqual('<p>0</p>')
})
test('当编辑器选区内容是P标签则不处理', () => {
const upFns: Function[] = []
const downFns: Function[] = []
enterToCreateP(editor, upFns, downFns)
editor.txt.html('<p>0</p>')
const container = $('<p>123</p>')
editor.$textElem.append(container)
jest.spyOn(editor.selection, 'getSelectionContainerElem').mockImplementation(
() => container
)
upFns.forEach(fn => {
fn()
})
expect(editor.$textElem.elems[0].innerHTML).toEqual('<p>0</p><p>123</p>')
})
test('当编辑器选区内容是非 P 标签并且含有 text 内容,则不处理', () => {
const upFns: Function[] = []
const downFns: Function[] = []
enterToCreateP(editor, upFns, downFns)
editor.txt.html('<div>123</div>')
const container = $('<div>123</div>')
editor.$textElem.append(container)
jest.spyOn(editor.selection, 'getSelectionContainerElem').mockImplementation(
() => container
)
upFns.forEach(fn => {
fn()
})
expect(editor.$textElem.elems[0].innerHTML).toEqual('<div>123</div><div>123</div>')
})
test('当编辑器选区内容为非P标签且没有文本内容插入 <p><br></p>', () => {
const upFns: Function[] = []
const downFns: Function[] = []
enterToCreateP(editor, upFns, downFns)
editor.txt.html('<div></div>')
const container = $('<div></div>')
editor.$textElem.append(container)
jest.spyOn(editor.selection, 'getSelectionContainerElem').mockImplementation(
() => container
)
upFns.forEach(fn => {
fn()
})
expect(editor.$textElem.elems[0].innerHTML.indexOf('<p><br></p>')).toBeGreaterThanOrEqual(0)
})
test('当编辑器选区内容 $textElm执行enter down插入 <p><br></p>', () => {
commandMock(document)
const upFns: Function[] = []
const downFns: Function[] = []
enterToCreateP(editor, upFns, downFns)
editor.txt.html('')
const container = $('<div></div>')
editor.$textElem.append(container)
jest.spyOn(editor.selection, 'getSelectionContainerElem').mockImplementation(
() => editor.$textElem
)
const mockPreventDefault = jest.fn()
const event = new KeyboardEvent('mousedown')
jest.spyOn(event, 'preventDefault').mockImplementation(mockPreventDefault)
jest.spyOn(document, 'queryCommandSupported').mockImplementation(() => true)
downFns.forEach(fn => {
fn(event)
})
expect(mockPreventDefault).toBeCalled()
expect(document.execCommand).toBeCalledWith('insertHTML', false, '<p><br></p>')
})
})

View File

@ -0,0 +1,52 @@
/**
* @description editor.text getHtmlByNodeList test
* @author luochao
*/
import getHtmlByNodeList from '../../../src/text/getHtmlByNodeList'
import getChildrenJSON, { NodeListType } from '../../../src/text/getChildrenJSON'
import $ from '../../../src/utils/dom-core'
describe('txt utils geChildrenJSON', () => {
test('能将元素的所有子元素包括属性还原成json数据', () => {
const nodeList: NodeListType = [
'123',
'',
{
tag: 'div',
attrs: [],
children: [
{
tag: 'span',
attrs: [
{
name: 'id',
value: 'child',
},
],
children: [],
},
],
},
{
tag: 'p',
attrs: [
{
name: 'id',
value: 'node2',
},
],
children: [],
},
]
const parent = document.createElement('div')
const html = getHtmlByNodeList(nodeList, parent)
const json = getChildrenJSON(html)
expect(json).toEqual(nodeList.filter(Boolean))
})
test('如果元素不存在或者没有子元素,则返回空数组', () => {
const json1 = getChildrenJSON($('.div1'))
const json2 = getChildrenJSON($('<div></div>'))
expect(json1.length).toEqual(0)
expect(json2.length).toEqual(0)
})
})

View File

@ -0,0 +1,48 @@
/**
* @description editor.text getHtmlByNodeList test
* @author luochao
*/
import getHtmlByNodeList from '../../../src/text/getHtmlByNodeList'
import { NodeListType } from '../../../src/text/getChildrenJSON'
describe('txt utils getHtmlByNodeList', () => {
test('能将 nodeList 全部聚合成在一个 container 元素中, 并支持子元素嵌套', () => {
const nodeList: NodeListType = [
'123',
{
tag: 'div',
attrs: [
{
name: 'id',
value: 'node1',
},
],
children: [
{
tag: 'span',
attrs: [
{
name: 'id',
value: 'child',
},
],
children: [],
},
],
},
{
tag: 'p',
attrs: [
{
name: 'id',
value: 'node2',
},
],
children: [],
},
]
const html = getHtmlByNodeList(nodeList)
expect(html.elems[0].innerHTML).toBe(
'123<div id="node1"><span id="child"></span></div><p id="node2"></p>'
)
})
})

View File

@ -0,0 +1,58 @@
/**
* @description text utils getPasteImgs test
* @author luochao
*/
import { getPasteImgs } from '../../../src/text/paste/paste-event'
import mockFile from '../../helpers/mock-file'
window.ClipboardEvent = jest.fn().mockImplementation(() => {
return {
clipboardData: {
getData: jest.fn(),
items: [
{
type: 'image/png',
getAsFile: jest.fn(() =>
mockFile({ name: '1.png', size: 1024, mimeType: 'image/png' })
),
},
{
type: 'image/png',
getAsFile: jest.fn(() =>
mockFile({ name: '2.png', size: 1024, mimeType: 'image/png' })
),
},
],
},
}
})
// @ts-ignore
window.clipboardData = {
getData: jest.fn(),
}
describe('text utils getPasteImgs test', () => {
test('能从 clipboradEvent 获取到图片文件', () => {
const clipboradEvent = new ClipboardEvent('')
const mockGetData = jest.fn(() => '')
// @ts-ignore
jest.spyOn(clipboradEvent.clipboardData, 'getData').mockImplementation(mockGetData)
const results = getPasteImgs(clipboradEvent)
expect(results.length).toBe(2)
})
test('能从 clipboradEvent 有text内容直接返回空数组', () => {
const clipboradEvent = new ClipboardEvent('')
const mockGetData = jest.fn(() => 'test123')
// @ts-ignore
jest.spyOn(clipboradEvent.clipboardData, 'getData').mockImplementation(mockGetData)
const result = getPasteImgs(clipboradEvent)
expect(result.length).toBe(0)
expect(mockGetData).toBeCalledWith('text/plain')
})
})

View File

@ -0,0 +1,555 @@
/**
* @description
* @author luochao
*/
import createEditor from '../../helpers/create-editor'
import $ from '../../../src/utils/dom-core'
import dispatchEvent from '../../helpers/mock-dispatch-event'
import { UA } from '../../../src/utils/util'
let editor: ReturnType<typeof createEditor>
let id = 1
const nodeList = [
{
tag: 'div',
attrs: [],
children: [
{
tag: 'span',
attrs: [
{
name: 'id',
value: 'child',
},
],
children: [],
},
],
},
{
tag: 'p',
attrs: [
{
name: 'id',
value: 'node2',
},
],
children: [],
},
]
const nodeListHtml = '<div><span id="child"></span></div><p id="node2"></p>'
describe('Editor Text test', () => {
beforeEach(() => {
editor = createEditor(document, `div${id++}`)
})
test('编辑器初始化,也会初始化 Text', () => {
expect(editor.txt).not.toBeUndefined()
expect(editor.txt.eventHooks).not.toBeUndefined()
})
test('编辑器初始化,会绑定一系列事件', () => {
const eventHooks = editor.txt.eventHooks
Object.keys(eventHooks).forEach(key => {
// @ts-ignore
expect(eventHooks[key].length).toBeGreaterThanOrEqual(0)
})
})
test('编辑器初始化后,调用 txt togglePlaceholder 如果 editor txt 没有 html 内容则会展示 placeholder', () => {
editor.txt.togglePlaceholder()
expect(editor.$textContainerElem.find('.placeholder').elems[0]).not.toBeUndefined()
expect(editor.$textContainerElem.find('.placeholder').elems[0]).toHaveStyle('display:block')
})
test('编辑器初始化后,调用 txt togglePlaceholder 如果 editor txt 有 html 内容则不展示 placeholder', () => {
editor.txt.html('<p>123</p>')
editor.txt.togglePlaceholder()
expect(editor.$textContainerElem.find('.placeholder').elems[0]).toHaveStyle('display:none')
})
test('编辑器初始化后,调用 txt clear 方法,清空编辑内容,只留下 p><br></p>', () => {
editor.txt.html('<p>123</p>')
editor.txt.clear()
expect(editor.txt.html()).toBe('')
expect(editor.$textElem.elems[0].innerHTML).toBe('<p><br></p>')
})
test('编辑器初始化后,调用 txt setJSON 方法将 JSON 内容设置成 html', () => {
editor.txt.setJSON(nodeList)
expect(editor.txt.html()).toBe(nodeListHtml)
})
test('编辑器初始化后,调用 txt getJSON 方法将 html 内容还原成JSON', () => {
editor.txt.html(nodeListHtml)
const res = editor.txt.getJSON()
expect(res).toEqual(nodeList)
})
test('编辑器初始化后,调用 txt text 方法 能获取 html text', () => {
editor.txt.html('<p>12345</p>')
expect(editor.txt.text()).toEqual('12345')
})
test('编辑器初始化后,调用 txt text 方法 能设置 text', () => {
editor.txt.text('12345')
expect(editor.txt.html()).toEqual('<p>12345</p>')
})
test('编辑器初始化后,调用 txt append 方法 能追加 html', () => {
editor.txt.append('12345<span>1234</span>')
expect(editor.txt.html()).toEqual('<p><br></p><p>12345<span>1234</span></p>')
})
test('编辑器初始化后,编辑器区域会绑定 keyup 事件触发保存range和激活菜单函数', () => {
const saveRangeFn = jest.fn()
const changeActiveFn = jest.fn()
jest.spyOn(editor.selection, 'saveRange').mockImplementation(saveRangeFn)
jest.spyOn(editor.menus, 'changeActive').mockImplementation(changeActiveFn)
dispatchEvent(editor.$textElem, 'keyup', 'KeyBoardEvent')
expect(saveRangeFn).toBeCalled()
expect(changeActiveFn).toBeCalled()
})
test('编辑器初始化后,编辑器区域会绑定 mouseup mousedown 事件对range进行处理如果range不存在不处理', () => {
const saveRangeFn = jest.fn()
const getRangeFn = jest.fn(() => null)
jest.spyOn(editor.selection, 'saveRange').mockImplementation(saveRangeFn)
jest.spyOn(editor.selection, 'getRange').mockImplementation(getRangeFn)
dispatchEvent(editor.$textElem, 'mousedown', 'MouseEvent')
dispatchEvent(editor.$textElem, 'mouseup', 'MouseEvent')
expect(saveRangeFn).not.toBeCalled()
})
test('编辑器初始化后,编辑器区域会绑定 mouseup mousedown 事件对存在的range进行处理', () => {
const saveRangeFn = jest.fn()
const getRangeFn = jest.fn(() => ({
startOffest: 10,
endOffset: 14,
endContainer: $('<p>12345</p>').elems[0],
setStart: jest.fn(),
}))
jest.spyOn(editor.selection, 'saveRange').mockImplementation(saveRangeFn)
// @ts-ignore
jest.spyOn(editor.selection, 'getRange').mockImplementation(getRangeFn)
dispatchEvent(editor.$textElem, 'mousedown', 'MouseEvent')
dispatchEvent(editor.$textElem, 'mouseup', 'MouseEvent')
expect(saveRangeFn).toBeCalled()
})
test('编辑器初始化后,编辑器区域会绑定 click 事件触发执行eventsHook clickEvent的函数执行', () => {
const mockClickFn = jest.fn()
Object.defineProperty(editor.txt.eventHooks, 'clickEvents', {
value: [mockClickFn, mockClickFn],
})
dispatchEvent(editor.$textElem, 'click')
expect(mockClickFn.mock.calls.length).toEqual(2)
})
test('编辑器初始化后,编辑器区域会绑定 enter键 keyup 事件触发执行eventsHook enterUpEvents的函数执行', () => {
const mockClickFn = jest.fn()
Object.defineProperty(editor.txt.eventHooks, 'enterUpEvents', {
value: [mockClickFn, mockClickFn],
})
dispatchEvent(editor.$textElem, 'keyup', 'KeyBoardEvent', {
keyCode: 13,
})
// 模拟不是enter键的情况
dispatchEvent(editor.$textElem, 'keyup', 'KeyBoardEvent', {
keyCode: 0,
})
expect(mockClickFn.mock.calls.length).toEqual(2)
})
test('编辑器初始化后,编辑器区域会绑定 keyup 事件触发执行eventsHook keyupEvents的函数执行', () => {
const mockClickFn = jest.fn()
Object.defineProperty(editor.txt.eventHooks, 'keyupEvents', {
value: [mockClickFn, mockClickFn],
})
dispatchEvent(editor.$textElem, 'keyup', 'KeyBoardEvent')
expect(mockClickFn.mock.calls.length).toEqual(2)
})
test('编辑器初始化后,编辑器区域会绑定 delete键 keyup 事件触发执行eventsHook deleteUpEvents的函数执行', () => {
const mockClickFn = jest.fn()
Object.defineProperty(editor.txt.eventHooks, 'deleteUpEvents', {
value: [mockClickFn, mockClickFn],
})
dispatchEvent(editor.$textElem, 'keyup', 'KeyBoardEvent', {
keyCode: 8,
})
// 模拟不是delete键的情况
dispatchEvent(editor.$textElem, 'keyup', 'KeyBoardEvent', {
keyCode: 0,
})
expect(mockClickFn.mock.calls.length).toEqual(2)
})
test('编辑器初始化后,编辑器区域会绑定 delete键 keydown 事件触发执行eventsHook deleteDownEvents的函数执行', () => {
const mockClickFn = jest.fn()
Object.defineProperty(editor.txt.eventHooks, 'deleteDownEvents', {
value: [mockClickFn, mockClickFn],
})
dispatchEvent(editor.$textElem, 'keydown', 'KeyBoardEvent', {
keyCode: 8,
})
// 模拟不是delete键的情况
dispatchEvent(editor.$textElem, 'keydown', 'KeyBoardEvent', {
keyCode: 0,
})
expect(mockClickFn.mock.calls.length).toEqual(2)
})
test('编辑器初始化后,编辑器区域会绑定 paste 事件触发执行eventsHook pasteEvents的函数执行', () => {
const mockClickFn = jest.fn()
Object.defineProperty(editor.txt.eventHooks, 'pasteEvents', {
value: [mockClickFn, mockClickFn],
})
// 模拟IE
jest.spyOn(UA, 'isIE')
.mockImplementationOnce(() => true)
.mockImplementationOnce(() => false)
dispatchEvent(editor.$textElem, 'paste', 'ClipboardEvent')
expect(mockClickFn.mock.calls.length).toEqual(0)
dispatchEvent(editor.$textElem, 'paste', 'ClipboardEvent')
expect(mockClickFn.mock.calls.length).toEqual(2)
})
test('编辑器初始化后,编辑器区域会绑定 撤销和取消 快捷键,触发执行历史撤销和重做的函数执行', () => {
const restoreFn = jest.fn()
const revokeFn = jest.fn()
jest.spyOn(editor.history, 'restore').mockImplementation(restoreFn)
jest.spyOn(editor.history, 'revoke').mockImplementation(revokeFn)
Object.defineProperty(editor, 'isFocus', {
value: true,
})
// 重做事件
dispatchEvent(editor.$textElem, 'keydown', 'KeyBoardEvent', {
keyCode: 90,
shiftKey: true,
ctrlKey: true,
})
expect(restoreFn).toBeCalled()
// 撤回事件
dispatchEvent(editor.$textElem, 'keydown', 'KeyBoardEvent', {
keyCode: 90,
shiftKey: false,
ctrlKey: true,
})
expect(revokeFn).toBeCalled()
})
test('编辑器初始化后,编辑器区域会绑定 tab键 keyup 事件触发执行eventsHook tabUpEvents的函数执行', () => {
const mockClickFn = jest.fn()
Object.defineProperty(editor.txt.eventHooks, 'tabUpEvents', {
value: [mockClickFn, mockClickFn],
})
// 模拟不是tab键的情况
dispatchEvent(editor.$textElem, 'keyup', 'KeyBoardEvent', {
keyCode: 0,
})
expect(mockClickFn.mock.calls.length).toEqual(0)
dispatchEvent(editor.$textElem, 'keyup', 'KeyBoardEvent', {
keyCode: 9,
})
expect(mockClickFn.mock.calls.length).toEqual(2)
})
test('编辑器初始化后,编辑器区域会绑定 tab键 keydown 事件触发执行eventsHook tabDownEvents的函数执行', () => {
const mockClickFn = jest.fn()
Object.defineProperty(editor.txt.eventHooks, 'tabDownEvents', {
value: [mockClickFn, mockClickFn],
})
// 模拟不是tab键的情况
dispatchEvent(editor.$textElem, 'keydown', 'KeyBoardEvent', {
keyCode: 0,
})
expect(mockClickFn.mock.calls.length).toEqual(0)
dispatchEvent(editor.$textElem, 'keydown', 'KeyBoardEvent', {
keyCode: 9,
})
expect(mockClickFn.mock.calls.length).toEqual(2)
})
// todo 没法模拟
test('编辑器初始化后,编辑器区域会绑定 scroll 事件触发执行eventsHook textScrollEvents的函数执行', () => {
const mockClickFn = jest.fn()
Object.defineProperty(editor.txt.eventHooks, 'textScrollEvents', {
value: [mockClickFn, mockClickFn],
})
dispatchEvent(editor.$textElem, 'scroll', 'Event')
expect(mockClickFn.mock.calls.length).toEqual(0)
})
// todo 没法模拟
test('编辑器初始化后,编辑器区域会禁用 dcument dragleave、drop、dragenter、dragover 事件', () => {
const preventDefaultFn = jest.fn()
dispatchEvent(editor.$textElem, 'dragleave', 'MouseEvent', {
preventDefault: preventDefaultFn,
})
dispatchEvent(editor.$textElem, 'drop', 'MouseEvent', {
preventDefault: preventDefaultFn,
})
dispatchEvent(editor.$textElem, 'dragenter', 'MouseEvent', {
preventDefault: preventDefaultFn,
})
dispatchEvent(editor.$textElem, 'dragover', 'MouseEvent', {
preventDefault: preventDefaultFn,
})
expect(preventDefaultFn.mock.calls.length).toEqual(0)
})
test('编辑器初始化后,编辑器区域 监听 链接点击事件, 触发执行eventsHook linkClickEvents的函数执行', () => {
const mockClickFn = jest.fn()
Object.defineProperty(editor.txt.eventHooks, 'linkClickEvents', {
value: [mockClickFn, mockClickFn],
})
const a = $('<a href="http://www.wangeditor.com">wangeditor</a>')
editor.$textElem.append(a)
dispatchEvent(a, 'click', 'Event', {
target: a.elems[0],
})
expect(mockClickFn.mock.calls.length).toEqual(2)
// 模拟事件代理的情况
const target = $('<li></li>')
const link = $('<a href="http://www.wangeditor.com">wangeditor</a>').append(target)
editor.$textElem.append(link)
dispatchEvent(target, 'click', 'Event', {
target,
})
expect(mockClickFn.mock.calls.length).toEqual(4)
})
test('编辑器初始化后,编辑器区域 监听 img点击事件 触发执行eventsHook imgClickEvents的函数执行', () => {
const mockClickFn = jest.fn()
Object.defineProperty(editor.txt.eventHooks, 'imgClickEvents', {
value: [mockClickFn, mockClickFn],
})
const img = $('<img src="http://www.wangeditor.com/imgs/ali-pay.jpeg" />')
editor.$textElem.append(img)
dispatchEvent(img, 'click', 'Event', {
target: img.elems[0],
})
expect(mockClickFn.mock.calls.length).toEqual(2)
// 模拟表情点击的情况,不执行图片钩子函数
const emotiomImg = $(
'<img class="eleImg" src="http://www.wangeditor.com/imgs/ali-pay.jpeg" />'
)
editor.$textElem.append(emotiomImg)
dispatchEvent(emotiomImg, 'click', 'Event', {
target: emotiomImg.elems[0],
})
expect(mockClickFn.mock.calls.length).toEqual(2)
})
test('编辑器初始化后,编辑器区域 监听 code区域点击事件 触发执行eventsHook codeClickEvents的函数执行', () => {
const mockClickFn = jest.fn()
Object.defineProperty(editor.txt.eventHooks, 'codeClickEvents', {
value: [mockClickFn, mockClickFn],
})
const code = $('<pre>123</pre>')
editor.$textElem.append(code)
dispatchEvent(code, 'click', 'Event', {
target: code.elems[0],
})
expect(mockClickFn.mock.calls.length).toEqual(2)
// 模拟点击pre里面的元素
const codeWrapper = $('<pre>123</pre>')
const target = $('<span>123</span>')
codeWrapper.append(target)
editor.$textElem.append(codeWrapper)
dispatchEvent(target, 'click', 'Event', {
target: target.elems[0],
})
editor.txt.html('')
// 模拟不是点击pre区域情况
dispatchEvent(editor.$textElem, 'click', 'Event', {
target: editor.$textElem.elems[0],
})
expect(mockClickFn.mock.calls.length).toEqual(4)
})
test('编辑器初始化后,编辑器区域 监听 hr标签点击事件 触发执行eventsHook splitLineEvents的函数执行', () => {
const mockClickFn = jest.fn()
Object.defineProperty(editor.txt.eventHooks, 'splitLineEvents', {
value: [mockClickFn, mockClickFn],
})
const hr = $('<hr />')
editor.$textElem.append(hr)
dispatchEvent(hr, 'click', 'Event', {
target: hr.elems[0],
})
expect(mockClickFn.mock.calls.length).toEqual(2)
// 模拟点击不是hr情况
const target = $('<span>123</span>')
editor.$textElem.append(target)
dispatchEvent(target, 'click', 'Event', {
target: target.elems[0],
})
expect(mockClickFn.mock.calls.length).toEqual(2)
})
test('编辑器初始化后,编辑区域容器添加监听点击事件, 点击的元素是图片拖拽调整大小的 bar, 触发执行eventsHook imgDragBarMouseDownEvents的函数执行', () => {
const mockClickFn = jest.fn()
Object.defineProperty(editor.txt.eventHooks, 'imgDragBarMouseDownEvents', {
value: [mockClickFn, mockClickFn],
})
const target = $('<div class="w-e-img-drag-rb"></div>')
editor.$textContainerElem.append(target)
dispatchEvent(target, 'mousedown', 'KeyBoardEvent')
expect(mockClickFn.mock.calls.length).toEqual(2)
})
test('编辑器初始化后,编辑器区域监听表格区域点击事件, 触发执行eventsHook tableClickEvents的函数执行', () => {
const mockClickFn = jest.fn()
Object.defineProperty(editor.txt.eventHooks, 'tableClickEvents', {
value: [mockClickFn, mockClickFn],
})
const table = $('<table><tr><td>123</td></tr></table>')
editor.$textElem.append(table)
dispatchEvent($(table.childNodes()), 'click')
expect(mockClickFn.mock.calls.length).toEqual(2)
// 模拟点击非表格区域
const target = $('<span>123</span>')
editor.$textElem.append(target)
dispatchEvent(target, 'click', 'Event')
expect(mockClickFn.mock.calls.length).toEqual(2)
})
test('编辑器初始化后,编辑器区域监听 ednter keydown 事件, 触发执行eventsHook enterDownEvents的函数执行', () => {
const mockClickFn = jest.fn()
Object.defineProperty(editor.txt.eventHooks, 'enterDownEvents', {
value: [mockClickFn, mockClickFn],
})
dispatchEvent(editor.$textElem, 'keydown', 'KeyBoardEvent', {
keyCode: 13,
})
// 模拟非enter键按下
dispatchEvent(editor.$textElem, 'keydown', 'KeyBoardEvent', {
keyCode: 0,
})
expect(mockClickFn.mock.calls.length).toEqual(2)
})
})

View File

@ -0,0 +1,204 @@
/**
* @description text utils paste-text-html test
* @author luochao
*/
import pasteTextHtml from '../../../src/text/event-hooks/paste-text-html'
import createEditor from '../../helpers/create-editor'
import * as pasteEvents from '../../../src/text/paste/paste-event'
import $ from '../../../src/utils/dom-core'
import mockCommand from '../../helpers/command-mock'
describe('text utils getPasteImgs test', () => {
test('执行函数会绑定一个 pasteEvents handler', () => {
const editor = createEditor(document, 'div1')
const pasteEvents: Function[] = []
pasteTextHtml(editor, pasteEvents)
expect(pasteEvents.length).toBeGreaterThanOrEqual(1)
})
test('如果当前选区所在元素不存在,执行 pasteEvents 的函数直接返回', () => {
const editor = createEditor(document, 'div2')
const pasteEventList: Function[] = []
pasteTextHtml(editor, pasteEventList)
jest.spyOn(pasteEvents, 'getPasteText').mockImplementation(() => '123')
jest.spyOn(pasteEvents, 'getPasteHtml').mockImplementation(() => '<p>123</p>')
jest.spyOn(editor.selection, 'getSelectionContainerElem').mockImplementation(
() => undefined
)
pasteEventList.forEach(fn => {
const res = fn(new Event(''))
expect(res).toBeUndefined()
})
})
test('如果当前选区所在元素为CODE 则执行用户配置的 pasteTextHandle 函数', () => {
mockCommand(document)
jest.spyOn(document, 'queryCommandSupported').mockImplementation(() => true)
const mockPasteTextHandle = jest.fn(() => 'mock123<br>')
const editor = createEditor(document, 'div3', '', {
pasteTextHandle: mockPasteTextHandle,
})
const pasteEventList: Function[] = []
pasteTextHtml(editor, pasteEventList)
jest.spyOn(pasteEvents, 'getPasteText').mockImplementation(() => '1234255\n')
jest.spyOn(pasteEvents, 'getPasteHtml').mockImplementation(() => '<p>1234</p>')
jest.spyOn(editor.selection, 'getSelectionContainerElem').mockImplementation(() =>
$('<code></code>')
)
pasteEventList.forEach(fn => {
fn(new Event(''))
})
expect(mockPasteTextHandle).toBeCalledWith('1234255<br>')
expect(document.execCommand).toBeCalledWith('insertHTML', false, 'mock123\n')
})
test('如果复制的文本内容是 url则插入链接', () => {
mockCommand(document)
jest.spyOn(document, 'queryCommandSupported').mockImplementation(() => true)
const editor = createEditor(document, 'div4')
const pasteEventList: Function[] = []
pasteTextHtml(editor, pasteEventList)
const pasteText = 'http://www.wangeditor.com'
jest.spyOn(pasteEvents, 'getPasteText').mockImplementation(() => pasteText)
jest.spyOn(pasteEvents, 'getPasteHtml').mockImplementation(() => '<p>1234</p>')
jest.spyOn(editor.selection, 'getSelectionContainerElem').mockImplementation(() =>
$('<p></p>')
)
pasteEventList.forEach(fn => {
fn(new Event(''))
})
expect(document.execCommand).toBeCalledWith(
'insertHTML',
false,
`<a href="${pasteText}" target="_blank">${pasteText}</a>`
)
})
test('如果复制的内容没有 html 内容,直接返回', () => {
mockCommand(document)
jest.spyOn(document, 'queryCommandSupported').mockImplementation(() => true)
const editor = createEditor(document, 'div4')
const pasteEventList: Function[] = []
pasteTextHtml(editor, pasteEventList)
jest.spyOn(pasteEvents, 'getPasteText').mockImplementation(() => '123')
jest.spyOn(pasteEvents, 'getPasteHtml').mockImplementation(() => '')
jest.spyOn(editor.selection, 'getSelectionContainerElem').mockImplementation(() =>
$('<p></p>')
)
pasteEventList.forEach(fn => {
const res = fn(new Event(''))
expect(res).toBeUndefined()
})
})
test('如果复制的内容没有 html 内容,直接返回', () => {
mockCommand(document)
jest.spyOn(document, 'queryCommandSupported').mockImplementation(() => true)
const editor = createEditor(document, 'div4')
const pasteEventList: Function[] = []
pasteTextHtml(editor, pasteEventList)
jest.spyOn(pasteEvents, 'getPasteText').mockImplementation(() => '123')
jest.spyOn(pasteEvents, 'getPasteHtml').mockImplementation(() => '')
jest.spyOn(editor.selection, 'getSelectionContainerElem').mockImplementation(() =>
$('<p></p>')
)
pasteEventList.forEach(fn => {
const res = fn(new Event(''))
expect(res).toBeUndefined()
})
})
test('如果复制内容有 html 则执行用户配置的 pasteTextHandle 函数,并且会将非 p 标签的元素替换为 p 标签', () => {
mockCommand(document)
jest.spyOn(document, 'queryCommandSupported').mockImplementation(() => true)
const mockPasteTextHandle = jest.fn(() => '<div>123</div><p></p>')
const editor = createEditor(document, 'div3', '', {
pasteTextHandle: mockPasteTextHandle,
})
const pasteEventList: Function[] = []
pasteTextHtml(editor, pasteEventList)
jest.spyOn(pasteEvents, 'getPasteText').mockImplementation(() => '1234255\n')
jest.spyOn(pasteEvents, 'getPasteHtml').mockImplementation(() => '<div>1234</div>')
jest.spyOn(editor.selection, 'getSelectionContainerElem').mockImplementation(() =>
$('<p></p>')
)
pasteEventList.forEach(fn => {
fn(new Event(''))
})
expect(mockPasteTextHandle).toBeCalledWith('<div>1234</div>')
expect(document.execCommand).toBeCalledWith('insertHTML', false, '<p>123</p><p><br></p>')
})
test('如果复制内容有 html 第一次插入 html 报错会使用 pasteText 再执行一次', () => {
mockCommand(document)
jest.spyOn(document, 'queryCommandSupported').mockImplementation(() => true)
jest.spyOn(document, 'execCommand')
.mockImplementationOnce(() => {
throw new Error('error')
})
.mockImplementationOnce(jest.fn())
const mockPasteTextHandle = jest.fn(() => '<div>123</div><p></p>')
const editor = createEditor(document, 'div3', '', {
pasteTextHandle: mockPasteTextHandle,
})
const pasteEventList: Function[] = []
pasteTextHtml(editor, pasteEventList)
jest.spyOn(pasteEvents, 'getPasteText').mockImplementation(() => '<div>12345</div>')
jest.spyOn(pasteEvents, 'getPasteHtml').mockImplementation(() => '<div>1234</div>')
jest.spyOn(editor.selection, 'getSelectionContainerElem').mockImplementation(() =>
$('<p></p>')
)
pasteEventList.forEach(fn => {
fn(new Event(''))
})
expect(mockPasteTextHandle).toBeCalledWith('<div>1234</div>')
expect(mockPasteTextHandle).toBeCalledWith('<div>12345</div>')
expect(document.execCommand).toBeCalledWith('insertHTML', false, '<p>123</p><p><br></p>')
})
})

View File

@ -0,0 +1,159 @@
/**
* @description editor.text event-hooks tab-to-space test
* @author luochao
*/
import tabHandler from '../../../src/text/event-hooks/tab-to-space'
import $ from '../../../src/utils/dom-core'
import createEditor from '../../helpers/create-editor'
import mockCommand from '../../helpers/command-mock'
describe('editor.text event-hooks tab-to-space test', () => {
test('能绑定一个处理 tab 的函数', () => {
const fn: Function[] = []
const editor = createEditor(document, 'div1')
tabHandler(editor, fn)
expect(fn.length).toBe(1)
})
test('能绑定一个处理 tab 的函数,如果不支持 insertHTML 指令,则不执行后续的插入操作', () => {
mockCommand(document)
const fn: Function[] = []
const editor = createEditor(document, 'div1')
jest.spyOn(editor.cmd, 'queryCommandSupported').mockImplementation(() => false)
tabHandler(editor, fn)
fn.forEach(fn => {
fn()
})
expect(document.execCommand).not.toBeCalled()
})
test('能绑定一个处理 tab 的函数,如果没有选区内容,则不执行后续的插入操作', () => {
mockCommand(document)
const fn: Function[] = []
const editor = createEditor(document, 'div1')
jest.spyOn(editor.cmd, 'queryCommandSupported').mockImplementation(() => true)
jest.spyOn(editor.selection, 'getSelectionContainerElem').mockImplementation(
() => undefined
)
tabHandler(editor, fn)
fn.forEach(fn => {
fn()
})
expect(document.execCommand).not.toBeCalled()
})
test('能绑定一个处理 tab 的函数如果有选区内容并且是正常的HTML元素则插入空格', () => {
mockCommand(document)
const fn: Function[] = []
const editor = createEditor(document, 'div1')
jest.spyOn(editor.cmd, 'queryCommandSupported').mockImplementation(() => true)
const container = $('<p><br></p>')
container.append($('<p>123</p>'))
jest.spyOn(editor.selection, 'getSelectionContainerElem').mockImplementation(
() => container
)
tabHandler(editor, fn)
fn.forEach(fn => {
fn()
})
expect(document.execCommand).toBeCalledWith('insertHTML', false, '&nbsp;&nbsp;&nbsp;&nbsp;')
})
describe('当选区内容父元素为codeprehljs或者选区元素为code的情况则插入特殊的空格', () => {
mockCommand(document)
let editor: ReturnType<typeof createEditor>
let id = 1
let fn: Function[] = []
beforeEach(() => {
editor = createEditor(document, `div${id++}`)
tabHandler(editor, fn)
jest.spyOn(editor.cmd, 'queryCommandSupported').mockImplementation(() => true)
})
// mock getSelectionContainerElem return value
const mockGetSelectionContainerElem = (tagString: string, isChild = true) => {
const container = $(tagString)
container.append($('<p>123</p>'))
jest.spyOn(editor.selection, 'getSelectionContainerElem').mockImplementation(() =>
isChild ? container.children()! : container
)
}
test('选区元素是 CODE', () => {
mockGetSelectionContainerElem('<code></code>', false)
fn.forEach(fn => {
fn()
})
expect(document.execCommand).toBeCalledWith(
'insertHTML',
false,
editor.config.languageTab
)
})
test('选区元素父元素是 CODE', () => {
mockGetSelectionContainerElem('<code></code>')
fn.forEach(fn => {
fn()
})
expect(document.execCommand).toBeCalledWith(
'insertHTML',
false,
editor.config.languageTab
)
})
test('选区元素父元素是 PRE', () => {
mockGetSelectionContainerElem('<pre></pre>')
fn.forEach(fn => {
fn()
})
expect(document.execCommand).toBeCalledWith(
'insertHTML',
false,
editor.config.languageTab
)
})
test('选区元素父元素是 hljs', () => {
mockGetSelectionContainerElem('<hljs></hljs>')
fn.forEach(fn => {
fn()
})
expect(document.execCommand).toBeCalledWith(
'insertHTML',
false,
editor.config.languageTab
)
})
})
})