Part of task 1896658 Co-authored-by: Aaron Bohy <aab@odoo.com> Co-authored-by: Christophe Matthieu <chm@odoo.com> Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com> Co-authored-by: David Monjoie <dmo@odoo.com> Co-authored-by: Martin Geubelle <mge@odoo.com> Co-authored-by: svs-odoo <svs@odoo.com> Co-authored-by: Vincent Schippefilt <vsc@odoo.com>
776 lines
32 KiB
JavaScript
776 lines
32 KiB
JavaScript
odoo.define('web_editor.test_utils', function (require) {
|
|
"use strict";
|
|
|
|
var ajax = require('web.ajax');
|
|
var MockServer = require('web.MockServer');
|
|
var testUtils = require('web.test_utils');
|
|
var Widget = require('web.Widget');
|
|
var Wysiwyg = require('web_editor.wysiwyg');
|
|
var options = require('web_editor.snippets.options');
|
|
|
|
|
|
MockServer.include({
|
|
//--------------------------------------------------------------------------
|
|
// Private
|
|
//--------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @override
|
|
* @private
|
|
* @returns {Promise}
|
|
*/
|
|
_performRpc: function (route, args) {
|
|
if (args.model === "ir.ui.view") {
|
|
if (args.method === 'read_template' && args.args[0] === "web_editor.colorpicker") {
|
|
var template = '<templates><t t-name="web_editor.colorpicker">' +
|
|
'<colorpicker>' +
|
|
' <div class="o_colorpicker_section" data-name="theme" data-display="Theme Colors" data-icon-class="fa fa-flask">' +
|
|
' <button data-color="alpha"></button>' +
|
|
' <button data-color="beta"></button>' +
|
|
' <button data-color="gamma"></button>' +
|
|
' <button data-color="delta"></button>' +
|
|
' <button data-color="epsilon"></button>' +
|
|
' </div>' +
|
|
' <div class="o_colorpicker_section" data-name="transparent_grayscale" data-display="Transparent Colors" data-icon-class="fa fa-eye-slash">' +
|
|
' <button class="o_btn_transparent"></button>' +
|
|
' <button data-color="black-25"></button>' +
|
|
' <button data-color="black-50"></button>' +
|
|
' <button data-color="black-75"></button>' +
|
|
' <button data-color="white-25"></button>' +
|
|
' <button data-color="white-50"></button>' +
|
|
' <button data-color="white-75"></button>' +
|
|
' </div>' +
|
|
' <div class="o_colorpicker_section" data-name="common" data-display="Common Colors" data-icon-class="fa fa-paint-brush"></div>' +
|
|
'</colorpicker>' +
|
|
'</t></templates>';
|
|
return Promise.resolve(template);
|
|
}
|
|
if (args.method === 'render_template' && args.args[0] === "web_editor.snippets") {
|
|
var template = '<h2 id="snippets_menu">Add blocks</h2>' +
|
|
'<div id="o_scroll">' +
|
|
' <div id="snippet_structure" class="o_panel">' +
|
|
' <div class="o_panel_header">' +
|
|
' <i class="fa fa-th-large"></i> First Panel' +
|
|
' </div>' +
|
|
' <div class="o_panel_body">' +
|
|
' <div name="Separator" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_separator.png">' +
|
|
' <div class="s_hr pt32 pb32">' +
|
|
' <hr class="s_hr_1px s_hr_solid w-100 mx-auto"/>' +
|
|
' </div>' +
|
|
' </div>' +
|
|
' <div name="Content" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_text_block.png">' +
|
|
' <section name="Content+Options" class="test_option_all pt32 pb32" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_text_block.png">' +
|
|
' <div class="container">' +
|
|
' <div class="row">' +
|
|
' <div class="col-lg-10 offset-lg-1 pt32 pb32">' +
|
|
' <h2>Title</h2>' +
|
|
' <p class="lead o_default_snippet_text">Content</p>' +
|
|
' </div>' +
|
|
' </div>' +
|
|
' </div>' +
|
|
' </section>' +
|
|
' </div>' +
|
|
' </div>' +
|
|
' </div>' +
|
|
'</div>' +
|
|
'<div id="snippet_options" class="d-none">' +
|
|
' <div data-js="many2one" data-selector="[data-oe-many2one-model]:not([data-oe-readonly])" data-no-check="true"></div>' +
|
|
' <div data-js="content" data-selector=".s_hr, .test_option_all" ' +
|
|
'data-drop-in=".note-editable" data-drop-near="p, h1, h2, h3, blockquote, .s_hr"></div>' +
|
|
' <div data-js="sizing_y" data-selector=".s_hr, .test_option_all"></div>' +
|
|
' <div data-js="colorpicker" data-selector=".test_option_all">' +
|
|
' <div class="dropdown-submenu">' +
|
|
' <a tabindex="-1" href="#" class="dropdown-item"><i class="fa fa-eyedropper"/>Background Color</a>' +
|
|
' <div class="dropdown-menu" role="menu"/>' +
|
|
' </div>' +
|
|
' </div>' +
|
|
' <div data-js="background" data-selector=".test_option_all">' +
|
|
' <a tabindex="-1" href="#" class="dropdown-item" data-choose-image="true" data-no-preview="true"><i class="fa fa-picture-o"/>Background Image</a>' +
|
|
' </div>' +
|
|
' <div data-js="option_test" data-selector=".s_hr">' +
|
|
' <div class="dropdown-submenu">' +
|
|
' <a tabindex="-2" href="#" class="dropdown-item"><i class="fa fa-arrows-v"/>Alignment</a>' +
|
|
' <div class="dropdown-menu" role="menu">' +
|
|
' <a href="#" class="dropdown-item" data-select-class="align-items-start">Top</a>' +
|
|
' <a href="#" class="dropdown-item" data-select-class="align-items-center">Middle</a>' +
|
|
' <a href="#" class="dropdown-item" data-select-class="align-items-end">Bottom</a>' +
|
|
' <div class="dropdown-divider"/>' +
|
|
' <a href="#" class="dropdown-item" data-select-class="align-items-stretch">Equal height</a>' +
|
|
' </div>' +
|
|
' </div>' +
|
|
' </div>' +
|
|
'</div>';
|
|
return Promise.resolve(template);
|
|
}
|
|
}
|
|
return this._super.apply(this, arguments);
|
|
},
|
|
});
|
|
|
|
/**
|
|
* Options with animation and edition for test.
|
|
*/
|
|
options.registry.option_test = options.Class.extend({
|
|
cleanForSave: function () {
|
|
this.$target.addClass('cleanForSave');
|
|
},
|
|
onBuilt: function () {
|
|
this.$target.addClass('built');
|
|
},
|
|
onBlur: function () {
|
|
this.$target.removeClass('focus');
|
|
},
|
|
onClone: function () {
|
|
this.$target.addClass('clone');
|
|
this.$target.removeClass('focus');
|
|
},
|
|
onFocus: function () {
|
|
this.$target.addClass('focus');
|
|
},
|
|
onMove: function () {
|
|
this.$target.addClass('move');
|
|
},
|
|
onRemove: function () {
|
|
this.$target.closest('.note-editable').addClass('snippet_has_removed');
|
|
},
|
|
});
|
|
|
|
|
|
/**
|
|
* Constructor WysiwygTest why editable and unbreakable node used in test.
|
|
*/
|
|
var WysiwygTest = Wysiwyg.extend({
|
|
_parentToDestroyForTest: null,
|
|
/**
|
|
* Override 'destroy' of discuss so that it calls 'destroy' on the parent.
|
|
*
|
|
* @override
|
|
*/
|
|
destroy: function () {
|
|
unpatch();
|
|
this._super();
|
|
this.$target.remove();
|
|
this._parentToDestroyForTest.destroy();
|
|
},
|
|
/**
|
|
* @override
|
|
*/
|
|
isUnbreakableNode: function (node) {
|
|
var Node = (node.tagName ? node : node.parentNode);
|
|
return (!this.options.useOnlyTestUnbreakable && this._super(node)) ||
|
|
!this.isEditableNode(node) ||
|
|
Node.tagName === "UNBREAKABLE" ||
|
|
(Node.className + '').indexOf('unbreakable') !== -1;
|
|
},
|
|
/**
|
|
* @override
|
|
*/
|
|
isEditableNode: function (node) {
|
|
if (!$(node).closest(this.$el).length) {
|
|
return false;
|
|
}
|
|
while (node) {
|
|
if (node.tagName === "EDITABLE") {
|
|
return true;
|
|
}
|
|
if (node.tagName === "NOTEDITABLE") {
|
|
return false;
|
|
}
|
|
if ((node.className + '').indexOf('editable') !== -1) {
|
|
return true;
|
|
}
|
|
if (this.$el[0] === node) {
|
|
return true;
|
|
}
|
|
node = node.parentNode;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
|
|
function patch() {
|
|
testUtils.mock.patch(ajax, {
|
|
loadAsset: function (xmlId) {
|
|
if (xmlId === 'template.assets') {
|
|
return Promise.resolve({
|
|
cssLibs: [],
|
|
cssContents: ['body {background-color: red;}']
|
|
});
|
|
}
|
|
if (xmlId === 'template.assets_all_style') {
|
|
return Promise.resolve({
|
|
cssLibs: $('link[href]:not([type="image/x-icon"])').map(function () {
|
|
return $(this).attr('href');
|
|
}).get(),
|
|
cssContents: ['body {background-color: red;}']
|
|
});
|
|
}
|
|
throw 'Wrong template';
|
|
},
|
|
});
|
|
}
|
|
|
|
function unpatch() {
|
|
testUtils.mock.unpatch(ajax);
|
|
}
|
|
|
|
/**
|
|
* @param {object} data
|
|
* @returns {object}
|
|
*/
|
|
function wysiwygData(data) {
|
|
return _.defaults({}, data, {
|
|
'ir.ui.view': {
|
|
fields: {
|
|
display_name: {
|
|
string: "Displayed name",
|
|
type: "char",
|
|
},
|
|
},
|
|
records: [],
|
|
read_template: function (args) {
|
|
if (args[0] === 'web_editor.colorpicker') {
|
|
return '<templates><t t-name="web_editor.colorpicker">' +
|
|
'<colorpicker>' +
|
|
' <div class="o_colorpicker_section" data-name="theme" data-display="Theme Colors" data-icon-class="fa fa-flask">' +
|
|
' <button data-color="alpha"></button>' +
|
|
' <button data-color="beta"></button>' +
|
|
' <button data-color="gamma"></button>' +
|
|
' <button data-color="delta"></button>' +
|
|
' <button data-color="epsilon"></button>' +
|
|
' </div>' +
|
|
' <div class="o_colorpicker_section" data-name="transparent_grayscale" data-display="Transparent Colors" data-icon-class="fa fa-eye-slash">' +
|
|
' <button class="o_btn_transparent"></button>' +
|
|
' <button data-color="black-25"></button>' +
|
|
' <button data-color="black-50"></button>' +
|
|
' <button data-color="black-75"></button>' +
|
|
' <button data-color="white-25"></button>' +
|
|
' <button data-color="white-50"></button>' +
|
|
' <button data-color="white-75"></button>' +
|
|
' </div>' +
|
|
' <div class="o_colorpicker_section" data-name="common" data-display="Common Colors" data-icon-class="fa fa-paint-brush"></div>' +
|
|
'</colorpicker>' +
|
|
'</t></templates>';
|
|
}
|
|
},
|
|
render_template: function (args) {
|
|
if (args[0] === 'web_editor.snippets') {
|
|
return '<h2 id="snippets_menu">Add blocks</h2>' +
|
|
'<div id="o_scroll">' +
|
|
' <div id="snippet_structure" class="o_panel">' +
|
|
' <div class="o_panel_header">' +
|
|
' <i class="fa fa-th-large"></i> First Panel' +
|
|
' </div>' +
|
|
' <div class="o_panel_body">' +
|
|
' <div name="Separator" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_separator.png">' +
|
|
' <div class="s_hr pt32 pb32">' +
|
|
' <hr class="s_hr_1px s_hr_solid w-100 mx-auto"/>' +
|
|
' </div>' +
|
|
' </div>' +
|
|
' <div name="Content" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_text_block.png">' +
|
|
' <section name="Content+Options" class="test_option_all pt32 pb32" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_text_block.png">' +
|
|
' <div class="container">' +
|
|
' <div class="row">' +
|
|
' <div class="col-lg-10 offset-lg-1 pt32 pb32">' +
|
|
' <h2>Title</h2>' +
|
|
' <p class="lead o_default_snippet_text">Content</p>' +
|
|
' </div>' +
|
|
' </div>' +
|
|
' </div>' +
|
|
' </section>' +
|
|
' </div>' +
|
|
' </div>' +
|
|
' </div>' +
|
|
'</div>' +
|
|
'<div id="snippet_options" class="d-none">' +
|
|
' <div data-js="many2one" data-selector="[data-oe-many2one-model]:not([data-oe-readonly])" data-no-check="true"></div>' +
|
|
' <div data-js="content" data-selector=".s_hr, .test_option_all" ' +
|
|
'data-drop-in=".note-editable" data-drop-near="p, h1, h2, h3, blockquote, .s_hr"></div>' +
|
|
' <div data-js="sizing_y" data-selector=".s_hr, .test_option_all"></div>' +
|
|
' <div data-js="colorpicker" data-selector=".test_option_all">' +
|
|
' <div class="dropdown-submenu">' +
|
|
' <a tabindex="-1" href="#" class="dropdown-item"><i class="fa fa-eyedropper"/>Background Color</a>' +
|
|
' <div class="dropdown-menu" role="menu"/>' +
|
|
' </div>' +
|
|
' </div>' +
|
|
' <div data-js="background" data-selector=".test_option_all">' +
|
|
' <a tabindex="-1" href="#" class="dropdown-item" data-choose-image="true" data-no-preview="true"><i class="fa fa-picture-o"/>Background Image</a>' +
|
|
' </div>' +
|
|
' <div data-js="option_test" data-selector=".s_hr">' +
|
|
' <div class="dropdown-submenu">' +
|
|
' <a tabindex="-2" href="#" class="dropdown-item"><i class="fa fa-arrows-v"/>Alignment</a>' +
|
|
' <div class="dropdown-menu" role="menu">' +
|
|
' <a href="#" class="dropdown-item" data-select-class="align-items-start">Top</a>' +
|
|
' <a href="#" class="dropdown-item" data-select-class="align-items-center">Middle</a>' +
|
|
' <a href="#" class="dropdown-item" data-select-class="align-items-end">Bottom</a>' +
|
|
' <div class="dropdown-divider"/>' +
|
|
' <a href="#" class="dropdown-item" data-select-class="align-items-stretch">Equal height</a>' +
|
|
' </div>' +
|
|
' </div>' +
|
|
' </div>' +
|
|
'</div>';
|
|
}
|
|
},
|
|
},
|
|
'ir.attachment': {
|
|
fields: {
|
|
display_name: {
|
|
string: "display_name",
|
|
type: 'char',
|
|
},
|
|
datas_fname: {
|
|
string: "datas_fname",
|
|
type: 'char',
|
|
},
|
|
mimetype: {
|
|
string: "mimetype",
|
|
type: 'char',
|
|
},
|
|
checksum: {
|
|
string: "checksum",
|
|
type: 'char',
|
|
},
|
|
url: {
|
|
string: "url",
|
|
type: 'char',
|
|
},
|
|
type: {
|
|
string: "type",
|
|
type: 'char',
|
|
},
|
|
res_id: {
|
|
string: "res_id",
|
|
type: 'integer',
|
|
},
|
|
res_model: {
|
|
string: "res_model",
|
|
type: 'char',
|
|
},
|
|
access_token: {
|
|
string: "access_token",
|
|
type: 'char',
|
|
},
|
|
},
|
|
records: [{
|
|
id: 1,
|
|
public: true,
|
|
name: 'image',
|
|
datas_fname: 'image.png',
|
|
mimetype: 'image/png',
|
|
checksum: false,
|
|
url: '/web_editor/static/src/img/transparent.png',
|
|
type: 'url',
|
|
res_id: 0,
|
|
res_model: false,
|
|
access_token: false
|
|
}],
|
|
generate_access_token: function () {
|
|
return;
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create the wysiwyg instance for test (contains patch, usefull ir.ui.view, snippets).
|
|
*
|
|
* @param {object} params
|
|
*/
|
|
function createWysiwyg(params) {
|
|
patch();
|
|
params.data = wysiwygData(params.data);
|
|
|
|
var parent = new Widget();
|
|
testUtils.mock.addMockEnvironment(parent, params);
|
|
|
|
var wysiwygOptions = _.extend({}, params.wysiwygOptions, {
|
|
recordInfo: {
|
|
context: {},
|
|
res_model: 'module.test',
|
|
res_id: 1,
|
|
},
|
|
useOnlyTestUnbreakable: params.useOnlyTestUnbreakable,
|
|
});
|
|
|
|
var wysiwyg = new WysiwygTest(parent, wysiwygOptions);
|
|
wysiwyg._parentToDestroyForTest = parent;
|
|
|
|
var $textarea = $('<textarea/>');
|
|
if (wysiwygOptions.value) {
|
|
$textarea.val(wysiwygOptions.value);
|
|
}
|
|
var selector = params.debug ? 'body' : '#qunit-fixture';
|
|
$textarea.prependTo($(selector));
|
|
if (params.debug) {
|
|
$('body').addClass('debug');
|
|
}
|
|
return wysiwyg.attachTo($textarea).then(function () {
|
|
if (wysiwygOptions.snippets) {
|
|
var defSnippets = testUtils.makeTestPromise();
|
|
testUtils.mock.intercept(wysiwyg, "snippets_loaded", function () {
|
|
defSnippets.resolve(wysiwyg);
|
|
});
|
|
return defSnippets;
|
|
}
|
|
return wysiwyg;
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Char codes.
|
|
*/
|
|
var dom = $.summernote.dom;
|
|
var keyboardMap = {
|
|
"8": "BACKSPACE",
|
|
"9": "TAB",
|
|
"13": "ENTER",
|
|
"16": "SHIFT",
|
|
"17": "CONTROL",
|
|
"18": "ALT",
|
|
"19": "PAUSE",
|
|
"20": "CAPS_LOCK",
|
|
"27": "ESCAPE",
|
|
"32": "SPACE",
|
|
"33": "PAGE_UP",
|
|
"34": "PAGE_DOWN",
|
|
"35": "END",
|
|
"36": "HOME",
|
|
"37": "LEFT",
|
|
"38": "UP",
|
|
"39": "RIGHT",
|
|
"40": "DOWN",
|
|
"45": "INSERT",
|
|
"46": "DELETE",
|
|
"91": "OS_KEY", // 'left command': Windows Key (Windows) or Command Key (Mac)
|
|
"93": "CONTEXT_MENU", // 'right command'
|
|
};
|
|
_.each(_.range(40, 127), function (keyCode) {
|
|
if (!keyboardMap[keyCode]) {
|
|
keyboardMap[keyCode] = String.fromCharCode(keyCode);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Perform a series of tests (`keyboardTests`) for using keyboard inputs.
|
|
*
|
|
* @see wysiwyg_keyboard_tests.js
|
|
* @see wysiwyg_tests.js
|
|
*
|
|
* @param {jQuery} $editable
|
|
* @param {object} assert
|
|
* @param {object[]} keyboardTests
|
|
* @param {string} keyboardTests.name
|
|
* @param {string} keyboardTests.content
|
|
* @param {object[]} keyboardTests.steps
|
|
* @param {string} keyboardTests.steps.start
|
|
* @param {string} [keyboardTests.steps.end] default: steps.start
|
|
* @param {string} keyboardTests.steps.key
|
|
* @param {object} keyboardTests.test
|
|
* @param {string} [keyboardTests.test.content]
|
|
* @param {string} [keyboardTests.test.start]
|
|
* @param {string} [keyboardTests.test.end] default: steps.start
|
|
* @param {function($editable, assert)} [keyboardTests.test.check]
|
|
* @param {Number} addTests
|
|
*/
|
|
var testKeyboard = function ($editable, assert, keyboardTests, addTests) {
|
|
var tests = _.compact(_.pluck(keyboardTests, 'test'));
|
|
var testNumber = _.compact(_.pluck(tests, 'start')).length +
|
|
_.compact(_.pluck(tests, 'content')).length +
|
|
_.compact(_.pluck(tests, 'check')).length +
|
|
(addTests | 0);
|
|
assert.expect(testNumber);
|
|
|
|
function keydown(target, keypress) {
|
|
var $target = $(target.tagName ? target : target.parentNode);
|
|
if (!keypress.keyCode) {
|
|
keypress.keyCode = +_.findKey(keyboardMap, function (key) {
|
|
return key === keypress.key;
|
|
});
|
|
} else {
|
|
keypress.key = keyboardMap[keypress.keyCode] || String.fromCharCode(keypress.keyCode);
|
|
}
|
|
keypress.keyCode = keypress.keyCode;
|
|
var event = $.Event("keydown", keypress);
|
|
$target.trigger(event);
|
|
if (!event.isDefaultPrevented()) {
|
|
if (keypress.key.length === 1) {
|
|
document.execCommand("insertText", 0, keypress.key);
|
|
} else {
|
|
console.warn('Native "' + keypress.key + '" is not supported in test');
|
|
}
|
|
}
|
|
$target.trigger($.Event("keyup", keypress));
|
|
return $target;
|
|
}
|
|
|
|
function _select(selector) {
|
|
// eg: ".class:contents()[0]->1" selects the first contents of the 'class' class, with an offset of 1
|
|
var reDOMSelection = /^(.+?)(:contents(\(\)\[|\()([0-9]+)[\]|\)])?(->([0-9]+))?$/;
|
|
var sel = selector.match(reDOMSelection);
|
|
var $node = $editable.find(sel[1]);
|
|
var point = {
|
|
node: sel[3] ? $node.contents()[+sel[4]] : $node[0],
|
|
offset: sel[5] ? +sel[6] : 0,
|
|
};
|
|
if (!point.node || point.offset > (point.node.tagName ? point.node.childNodes : point.node.textContent).length) {
|
|
assert.notOk("Node not found: '" + selector + "' " + (point.node ? "(container: '" + (point.node.outerHTML || point.node.textContent) + "')" : ""));
|
|
}
|
|
return point;
|
|
}
|
|
|
|
function selectText(start, end) {
|
|
start = _select(start);
|
|
var target = start.node;
|
|
$(target.tagName ? target : target.parentNode).trigger("mousedown");
|
|
if (end) {
|
|
end = _select(end);
|
|
Wysiwyg.setRange(start.node, start.offset, end.node, end.offset);
|
|
} else {
|
|
Wysiwyg.setRange(start.node, start.offset);
|
|
}
|
|
target = end ? end.node : start.node;
|
|
$(target.tagName ? target : target.parentNode).trigger('mouseup');
|
|
}
|
|
|
|
function endOfAreaBetweenTwoNodes(point) {
|
|
// move the position because some browser make the caret on the end of the previous area after normalize
|
|
if (
|
|
!point.node.tagName &&
|
|
point.offset === point.node.textContent.length &&
|
|
!/\S|\u00A0/.test(point.node.textContent)
|
|
) {
|
|
point = dom.nextPoint(dom.nextPoint(point));
|
|
while (point.node.tagName && point.node.textContent.length) {
|
|
point = dom.nextPoint(point);
|
|
}
|
|
}
|
|
return point;
|
|
}
|
|
|
|
var defPollTest = Promise.resolve();
|
|
|
|
function pollTest(test) {
|
|
var def = Promise.resolve();
|
|
$editable.data('wysiwyg').setValue(test.content);
|
|
|
|
function poll(step) {
|
|
var def = testUtils.makeTestPromise();
|
|
if (step.start) {
|
|
selectText(step.start, step.end);
|
|
if (!Wysiwyg.getRange($editable[0])) {
|
|
throw 'Wrong range! \n' +
|
|
'Test: ' + test.name + '\n' +
|
|
'Selection: ' + step.start + '" to "' + step.end + '"\n' +
|
|
'DOM: ' + $editable.html();
|
|
}
|
|
}
|
|
setTimeout(function () {
|
|
if (step.keyCode || step.key) {
|
|
var target = Wysiwyg.getRange($editable[0]).ec;
|
|
if (window.location.search.indexOf('notrycatch') !== -1) {
|
|
keydown(target, {
|
|
key: step.key,
|
|
keyCode: step.keyCode,
|
|
ctrlKey: !!step.ctrlKey,
|
|
shiftKey: !!step.shiftKey,
|
|
altKey: !!step.altKey,
|
|
metaKey: !!step.metaKey,
|
|
});
|
|
} else {
|
|
try {
|
|
keydown(target, {
|
|
key: step.key,
|
|
keyCode: step.keyCode,
|
|
ctrlKey: !!step.ctrlKey,
|
|
shiftKey: !!step.shiftKey,
|
|
altKey: !!step.altKey,
|
|
metaKey: !!step.metaKey,
|
|
});
|
|
} catch (e) {
|
|
assert.notOk(e.name + '\n\n' + e.stack, test.name);
|
|
}
|
|
}
|
|
}
|
|
setTimeout(function () {
|
|
if (step.keyCode || step.key) {
|
|
var $target = $(target.tagName ? target : target.parentNode);
|
|
$target.trigger($.Event('keyup', {
|
|
key: step.key,
|
|
keyCode: step.keyCode,
|
|
ctrlKey: !!step.ctrlKey,
|
|
shiftKey: !!step.shiftKey,
|
|
altKey: !!step.altKey,
|
|
metaKey: !!step.metaKey,
|
|
}));
|
|
}
|
|
setTimeout(def.resolve.bind(def));
|
|
});
|
|
});
|
|
return def;
|
|
}
|
|
while (test.steps.length) {
|
|
def = def.then(poll.bind(null, test.steps.shift()));
|
|
}
|
|
|
|
return def.then(function () {
|
|
if (!test.test) {
|
|
return;
|
|
}
|
|
|
|
if (test.test.check) {
|
|
test.test.check($editable, assert);
|
|
}
|
|
|
|
// test content
|
|
if (test.test.content) {
|
|
var value = $editable.data('wysiwyg').getValue({
|
|
keepPopover: true,
|
|
});
|
|
var allInvisible = /\u200B/g;
|
|
value = value.replace(allInvisible, '​');
|
|
var result = test.test.content.replace(allInvisible, '​');
|
|
assert.strictEqual(value, result, test.name);
|
|
|
|
if (test.test.start && value !== result) {
|
|
assert.notOk("Wrong DOM (see previous assert)", test.name + " (carret position)");
|
|
return;
|
|
}
|
|
}
|
|
|
|
$editable[0].normalize();
|
|
|
|
// test carret position
|
|
if (test.test.start) {
|
|
var start = _select(test.test.start);
|
|
var range = Wysiwyg.getRange($editable[0]);
|
|
if ((range.sc !== range.ec || range.so !== range.eo) && !test.test.end) {
|
|
assert.ok(false, test.name + ": the carret is not colapsed and the 'end' selector in test is missing");
|
|
return;
|
|
}
|
|
var end = test.test.end ? _select(test.test.end) : start;
|
|
if (start.node && end.node) {
|
|
range = Wysiwyg.getRange($editable[0]);
|
|
var startPoint = endOfAreaBetweenTwoNodes({
|
|
node: range.sc,
|
|
offset: range.so,
|
|
});
|
|
var endPoint = endOfAreaBetweenTwoNodes({
|
|
node: range.ec,
|
|
offset: range.eo,
|
|
});
|
|
var sameDOM = (startPoint.node.outerHTML || startPoint.node.textContent) === (start.node.outerHTML || start.node.textContent);
|
|
var stringify = function (obj) {
|
|
if (!sameDOM) {
|
|
delete obj.sameDOMsameNode;
|
|
}
|
|
return JSON.stringify(obj, null, 2)
|
|
.replace(/"([^"\s-]+)":/g, "\$1:")
|
|
.replace(/([^\\])"/g, "\$1'")
|
|
.replace(/\\"/g, '"');
|
|
};
|
|
assert.deepEqual(stringify({
|
|
startNode: startPoint.node.outerHTML || startPoint.node.textContent,
|
|
startOffset: startPoint.offset,
|
|
endPoint: endPoint.node.outerHTML || endPoint.node.textContent,
|
|
endOffset: endPoint.offset,
|
|
sameDOMsameNode: sameDOM && startPoint.node === start.node,
|
|
}),
|
|
stringify({
|
|
startNode: start.node.outerHTML || start.node.textContent,
|
|
startOffset: start.offset,
|
|
endPoint: end.node.outerHTML || end.node.textContent,
|
|
endOffset: end.offset,
|
|
sameDOMsameNode: true,
|
|
}),
|
|
test.name + " (carret position)");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
while (keyboardTests.length) {
|
|
defPollTest = defPollTest.then(pollTest.bind(null, keyboardTests.shift()));
|
|
}
|
|
|
|
return defPollTest;
|
|
};
|
|
|
|
|
|
/**
|
|
* Select a node in the dom with is offset.
|
|
*
|
|
* @param {String} startSelector
|
|
* @param {String} endSelector
|
|
* @param {jQuery} $editable
|
|
* @returns {Object} {sc, so, ec, eo}
|
|
*/
|
|
var select = (function () {
|
|
var __select = function (selector, $editable) {
|
|
var sel = selector.match(/^(.+?)(:contents\(\)\[([0-9]+)\]|:contents\(([0-9]+)\))?(->([0-9]+))?$/);
|
|
var $node = $editable.find(sel[1]);
|
|
return {
|
|
node: sel[2] ? $node.contents()[sel[3] ? +sel[3] : +sel[4]] : $node[0],
|
|
offset: sel[5] ? +sel[6] : 0,
|
|
};
|
|
};
|
|
return function (startSelector, endSelector, $editable) {
|
|
var start = __select(startSelector, $editable);
|
|
var end = endSelector ? __select(endSelector, $editable) : start;
|
|
return {
|
|
sc: start.node,
|
|
so: start.offset,
|
|
ec: end.node,
|
|
eo: end.offset,
|
|
};
|
|
};
|
|
})();
|
|
|
|
/**
|
|
* Trigger a keydown event.
|
|
*
|
|
* @param {String or Number} key (name or code)
|
|
* @param {jQuery} $editable
|
|
* @param {Object} [options]
|
|
* @param {Boolean} [options.firstDeselect] (default: false) true to deselect before pressing
|
|
*/
|
|
var keydown = function (key, $editable, options) {
|
|
var keyPress = {};
|
|
if (typeof key === 'string') {
|
|
keyPress.key = key;
|
|
keyPress.keyCode = +_.findKey(keyboardMap, function (k) {
|
|
return k === key;
|
|
});
|
|
} else {
|
|
keyPress.key = keyboardMap[key] || String.fromCharCode(key);
|
|
keyPress.keyCode = key;
|
|
}
|
|
var range = Wysiwyg.getRange($editable[0]);
|
|
if (!range) {
|
|
console.error("Editor have not any range");
|
|
return;
|
|
}
|
|
if (options && options.firstDeselect) {
|
|
range.sc = range.ec;
|
|
range.so = range.eo;
|
|
Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
|
|
}
|
|
var target = range.ec;
|
|
var $target = $(target.tagName ? target : target.parentNode);
|
|
var event = $.Event("keydown", keyPress);
|
|
$target.trigger(event);
|
|
};
|
|
|
|
return {
|
|
wysiwygData: wysiwygData,
|
|
createWysiwyg: createWysiwyg,
|
|
testKeyboard: testKeyboard,
|
|
select: select,
|
|
keydown: keydown,
|
|
patch: patch,
|
|
unpatch: unpatch,
|
|
};
|
|
|
|
|
|
});
|