summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntoine Guenet <age@odoo.com>2018-12-28 17:59:35 +0000
committerChristophe Matthieu <chm@odoo.com>2019-01-17 08:40:21 +0000
commiteb0ea8efbe932c140302e9f8da33a839bcd73146 (patch)
treee097625171b2b67ed5f5288ced67446608b9f633
parentf296992317e96562c66bd7ad59a5080d6c551ed5 (diff)
[ADD] web_editor,*: Tests for new structure of web editor
The refactoring also had to be done in order to create a set of unit tests. Each behavior can be tested, including the behaviors performed as a consequence of keyboard interactions (for instance: Enter, Tab...). This commit also contains some changes to test_utils that were necessary given the new structure of the wysiwyg editor. Co-authored-by: Gorash <chm@odoo.com>
-rw-r--r--addons/mass_mailing/static/src/js/mass_mailing_widget.js4
-rw-r--r--addons/mass_mailing/static/tests/mass_mailing_html_tests.js138
-rw-r--r--addons/mass_mailing/views/editor_field_html.xml6
-rw-r--r--addons/web/static/tests/helpers/test_utils_mock.js2
-rw-r--r--addons/web_editor/static/tests/field_html_tests.js954
-rw-r--r--addons/web_editor/static/tests/test_utils.js674
-rw-r--r--addons/web_editor/static/tests/wysiwyg_keyboard_tests.js4179
-rw-r--r--addons/web_editor/static/tests/wysiwyg_snippets_tests.js403
-rw-r--r--addons/web_editor/static/tests/wysiwyg_tests.js3859
-rw-r--r--addons/web_editor/tests/__init__.py1
-rw-r--r--addons/web_editor/tests/test_ui.py13
-rw-r--r--addons/web_tour/static/src/js/running_tour_action_helper.js4
-rw-r--r--addons/website/static/src/js/tours/rte.js40
-rw-r--r--addons/website_blog/static/src/js/website.tour.blog.js4
-rw-r--r--addons/website_sale/static/src/js/website_sale_tour_shop.js19
-rw-r--r--addons/website_sale/static/src/js/website_sale_tour_shop_backend.js8
-rw-r--r--addons/website_sale/static/src/js/website_sale_tour_shop_frontend.js12
17 files changed, 10083 insertions, 237 deletions
diff --git a/addons/mass_mailing/static/src/js/mass_mailing_widget.js b/addons/mass_mailing/static/src/js/mass_mailing_widget.js
index 1e2bf350fec..e633ad2b4a1 100644
--- a/addons/mass_mailing/static/src/js/mass_mailing_widget.js
+++ b/addons/mass_mailing/static/src/js/mass_mailing_widget.js
@@ -56,8 +56,8 @@ var MassMailingFieldHtml = FieldHtml.extend({
convertInline.classToStyle($editable);
self.trigger_up('field_changed', {
- dataPointID: this.dataPointID,
- changes: _.object([fieldName], [this._unWrap($editable.html())])
+ dataPointID: self.dataPointID,
+ changes: _.object([fieldName], [self._unWrap($editable.html())])
});
if (self._isDirty && self.mode === 'edit') {
diff --git a/addons/mass_mailing/static/tests/mass_mailing_html_tests.js b/addons/mass_mailing/static/tests/mass_mailing_html_tests.js
new file mode 100644
index 00000000000..36bea2db624
--- /dev/null
+++ b/addons/mass_mailing/static/tests/mass_mailing_html_tests.js
@@ -0,0 +1,138 @@
+odoo.define('mass_mailing.field_html_tests', function (require) {
+"use strict";
+
+var ajax = require('web.ajax');
+var FormView = require('web.FormView');
+var testUtils = require('web.test_utils');
+var weTestUtils = require('web_editor.test_utils');
+var Wysiwyg = require('web_editor.wysiwyg');
+
+
+QUnit.module('mass_mailing', {}, function () {
+QUnit.module('field html', {
+ beforeEach: function () {
+ this.data = weTestUtils.wysiwygData({
+ 'mail.mass_mailing': {
+ fields: {
+ display_name: {
+ string: "Displayed name",
+ type: "char"
+ },
+ body_html: {
+ string: "Message Body inline (to send)",
+ type: "html"
+ },
+ body_arch: {
+ string: "Message Body for edition",
+ type: "html"
+ },
+ },
+ records: [{
+ id: 1,
+ display_name: "first record",
+ body_html: "<div class='field_body' style='background-color: red;'><p>code to edit</p></div>",
+ body_arch: "<div class='field_body'><p>code to edit</p></div>",
+ }],
+ },
+ });
+
+ testUtils.mock.patch(ajax, {
+ loadAsset: function (xmlId) {
+ if (xmlId === 'template.assets') {
+ return $.when({
+ cssLibs: [],
+ cssContents: ['.field_body {background-color: red;}']
+ });
+ }
+ if (xmlId === 'template.assets_all_style') {
+ return $.when({
+ cssLibs: $('head link[href]:not([type="image/x-icon"])').map(function () {
+ return $(this).attr('href');
+ }).get(),
+ cssContents: ['.field_body {background-color: red;}']
+ });
+ }
+ throw 'Wrong template';
+ },
+ });
+ },
+ afterEach: function () {
+ testUtils.mock.unpatch(ajax);
+ },
+}, function () {
+
+QUnit.test('save arch and html', function (assert) {
+ var done = assert.async();
+ assert.expect(6);
+
+ testUtils.createAsyncView({
+ View: FormView,
+ model: 'mail.mass_mailing',
+ data: this.data,
+ arch: '<form>' +
+ ' <field name="body_html" class="oe_read_only" widget="html"'+
+ ' options="{'+
+ ' \'cssReadonly\': \'template.assets\','+
+ ' }"'+
+ ' />'+
+ ' <field name="body_arch" class="oe_edit_only" widget="mass_mailing_html"'+
+ ' options="{'+
+ ' \'snippets\': \'template.assets\','+
+ ' \'cssEdit\': \'template.assets\','+
+ ' \'inline-field\': \'body_html\''+
+ ' }"'+
+ ' />'+
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === "write") {
+ var values = args.args[1];
+ assert.strictEqual(values.body_html,
+ '<div class="field_body" style="background-color:red;"><p>c<b>od</b>e to edit</p></div>',
+ "should save the content");
+ assert.strictEqual(values.body_arch,
+ '<div class="field_body"><p>c<b>od</b>e to edit</p></div>',
+ "should save the content");
+
+ }
+ return this._super.apply(this, arguments);
+ },
+ }).then(function (form) {
+ var $fieldReadonly = form.$('.oe_form_field[name="body_html"]');
+ var $fieldEdit = form.$('.oe_form_field[name="body_arch"]');
+
+ assert.strictEqual($fieldReadonly.css('display'), 'block', "should display the readonly mode");
+ assert.strictEqual($fieldEdit.css('display'), 'none', "should hide the edit mode");
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ $fieldReadonly = form.$('.oe_form_field[name="body_html"]');
+ $fieldEdit = form.$('.oe_form_field[name="body_arch"]');
+
+ assert.strictEqual($fieldReadonly.css('display'), 'none', "should hide the readonly mode");
+ assert.strictEqual($fieldEdit.css('display'), 'block', "should display the edit mode");
+
+ var $iframe = $fieldEdit.find('iframe');
+
+ $iframe.data('load-def').then(function () {
+ var doc = $iframe.contents()[0];
+ var $content = $('#iframe_target', doc);
+
+ // select the text
+ var pText = $content.find('.note-editable p').first().contents()[0];
+ Wysiwyg.setRange(pText, 1, pText, 3);
+ // text is selected
+
+ $content.find('.note-toolbar .note-font .note-btn-bold').mousedown().click();
+
+ form.$buttons.find('.o_form_button_save').click();
+
+ form.destroy();
+ done();
+ });
+ });
+});
+
+});
+});
+});
diff --git a/addons/mass_mailing/views/editor_field_html.xml b/addons/mass_mailing/views/editor_field_html.xml
index ab665061fdd..25f2be6fe23 100644
--- a/addons/mass_mailing/views/editor_field_html.xml
+++ b/addons/mass_mailing/views/editor_field_html.xml
@@ -23,4 +23,10 @@
<template id="iframe_css_assets_readonly">
<link rel="stylesheet" type="text/scss" href="/mass_mailing/static/src/css/basic_theme_readonly.css"/>
</template>
+
+<template id="qunit_suite" inherit_id="web.qunit_suite">
+ <xpath expr="." position="inside">
+ <script type="text/javascript" src="/mass_mailing/static/tests/mass_mailing_html_tests.js"/>
+ </xpath>
+</template>
</odoo>
diff --git a/addons/web/static/tests/helpers/test_utils_mock.js b/addons/web/static/tests/helpers/test_utils_mock.js
index 3eaaae0a46f..27dbaeeaa44 100644
--- a/addons/web/static/tests/helpers/test_utils_mock.js
+++ b/addons/web/static/tests/helpers/test_utils_mock.js
@@ -69,6 +69,7 @@ function removeSrcAttribute(el, widget) {
if (widget) {
widget._rpc({ route: src });
}
+ $(node).trigger('load');
}
}
}
@@ -228,6 +229,7 @@ function addMockEnvironment(widget, params) {
}
$('body').off('DOMNodeInserted.removeSRC');
+ $('body').removeClass('debug');
$('.blockUI').remove();
widgetDestroy.call(this);
diff --git a/addons/web_editor/static/tests/field_html_tests.js b/addons/web_editor/static/tests/field_html_tests.js
index 1995add777d..b01ab48586b 100644
--- a/addons/web_editor/static/tests/field_html_tests.js
+++ b/addons/web_editor/static/tests/field_html_tests.js
@@ -1,327 +1,895 @@
-odoo.define('web_editor.web_editor_tests', function (require) {
+odoo.define('web_editor.field_html_tests', function (require) {
"use strict";
+var ajax = require('web.ajax');
var FormView = require('web.FormView');
var testUtils = require('web.test_utils');
+var weTestUtils = require('web_editor.test_utils');
var core = require('web.core');
-var web_editor = require('web_editor.editor');
+var Wysiwyg = require('web_editor.wysiwyg');
+var MediaDialog = require('wysiwyg.widgets.MediaDialog');
var _t = core._t;
-QUnit.module('web_editor', {
- beforeEach: function() {
- this.data = {
+QUnit.module('web_editor', {}, function () {
+
+QUnit.module('field html', {
+ beforeEach: function () {
+ this.data = weTestUtils.wysiwygData({
+ 'note.note': {
+ fields: {
+ display_name: {
+ string: "Displayed name",
+ type: "char"
+ },
+ body: {
+ string: "Message",
+ type: "html"
+ },
+ },
+ records: [{
+ id: 1,
+ display_name: "first record",
+ body: "<p>toto toto toto</p><p>tata</p>",
+ }],
+ },
'mass.mailing': {
fields: {
- display_name: { string: "Displayed name", type: "char" },
- body: {string: "Message Body", type: "html"},
+ display_name: {
+ string: "Displayed name",
+ type: "char"
+ },
+ body_html: {
+ string: "Message Body inline (to send)",
+ type: "html"
+ },
+ body_arch: {
+ string: "Message Body for edition",
+ type: "html"
+ },
},
records: [{
id: 1,
display_name: "first record",
- body: "<div class='field_body'>yep</div>",
+ body_html: "<div class='field_body' style='background-color: red;'>yep</div>",
+ body_arch: "<div class='field_body'>yep</div>",
}],
- onchanges: {},
},
- };
- }
-});
+ });
+
+ testUtils.mock.patch(ajax, {
+ loadAsset: function (xmlId) {
+ if (xmlId === 'template.assets') {
+ return $.when({
+ cssLibs: [],
+ cssContents: ['body {background-color: red;}']
+ });
+ }
+ if (xmlId === 'template.assets_all_style') {
+ return $.when({
+ cssLibs: $('head link[href]:not([type="image/x-icon"])').map(function () {
+ return $(this).attr('href');
+ }).get(),
+ cssContents: ['body {background-color: red;}']
+ });
+ }
+ throw 'Wrong template';
+ },
+ });
+ },
+ afterEach: function () {
+ testUtils.mock.unpatch(ajax);
+ },
+}, function () {
+
+QUnit.module('basic');
-QUnit.test('field html widget', function (assert) {
+QUnit.test('simple rendering', function (assert) {
var done = assert.async();
assert.expect(3);
- var form = testUtils.createView({
+ testUtils.createAsyncView({
View: FormView,
- model: 'mass.mailing',
+ model: 'note.note',
data: this.data,
- arch: '<form string="Partners">' +
- '<field name="body" widget="html" style="height: 100px"/>' +
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px"/>' +
'</form>',
res_id: 1,
- });
-
- assert.strictEqual(form.$('.field_body').text(), 'yep',
- "should have rendered a div with correct content in readonly");
- assert.hasAttrValue(form.$('div[name=body]'), 'style', 'height: 100px',
- "should have applied the style correctly");
-
- testUtils.form.clickEdit(form);
-
- assert.strictEqual(form.$('.note-editable').html(), '<div class="field_body">yep</div>',
+ }).then(function (form) {
+ var $field = form.$('.oe_form_field[name="body"]');
+ assert.strictEqual($field.children('.o_readonly').html(),
+ '<p>toto toto toto</p><p>tata</p>',
+ "should have rendered a div with correct content in readonly");
+ assert.strictEqual($field.attr('style'), 'height: 100px',
+ "should have applied the style correctly");
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ $field = form.$('.oe_form_field[name="body"]');
+ assert.strictEqual($field.find('.note-editable').html(),
+ '<p>toto toto toto</p><p>tata</p>',
"should have rendered the field correctly in edit");
- // summernote invokes handlers after a setTimeout, so we must wait as well
- // before destroying the widget (otherwise we'll have a crash later on)
- setTimeout(function () {
form.destroy();
done();
- }, 0);
+ });
});
-QUnit.test('field html widget (with options inline-style)', function (assert) {
+QUnit.test('colorpicker', function (assert) {
var done = assert.async();
- assert.expect(3);
+ assert.expect(6);
- var form = testUtils.createView({
+ testUtils.createAsyncView({
View: FormView,
- model: 'mass.mailing',
+ model: 'note.note',
data: this.data,
- arch: '<form string="Partners">' +
- '<field name="body" widget="html" style="height: 100px" options="{\'style-inline\': true}"/>' +
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px"/>' +
'</form>',
res_id: 1,
- });
+ }).then(function (form) {
+ form.$buttons.find('.o_form_button_edit').click();
+ var $field = form.$('.oe_form_field[name="body"]');
+
+ // select the text
+ var pText = $field.find('.note-editable p').first().contents()[0];
+ Wysiwyg.setRange(pText, 1, pText, 10);
+ // text is selected
+
+ var range = Wysiwyg.getRange($field[0]);
+ assert.strictEqual(range.sc, pText,
+ "should select the text");
- assert.containsOnce(form, 'iframe',
- "should have rendered an iframe without crashing in readonly");
- assert.hasAttrValue(form.$('div[name=body]'), 'style', 'height: 100px',
- "should have applied the style correctly");
+ $field.find('.note-toolbar .note-bg-color button:first').mousedown().click();
- testUtils.form.clickEdit(form);
+ assert.ok($field.find('.note-bg-color').hasClass('show') && $field.find('.note-bg-color .dropdown-menu').hasClass('show'),
+ "should display the color picker");
- assert.strictEqual(form.$('.note-editable').html(), '<div class="field_body">yep</div>',
+ $field.find('.note-toolbar .note-bg-color button[data-value="#00FFFF"]').mousedown().click();
+
+ assert.ok(!$field.find('.note-bg-color').hasClass('show') && !$field.find('.note-bg-color .dropdown-menu').hasClass('show'),
+ "should close the color picker");
+
+ assert.strictEqual($field.find('.note-editable').html(),
+ '<p>t<font style="background-color: rgb(0, 255, 255);">oto toto&nbsp;</font>toto</p><p>tata</p>',
+ "should have rendered the field correctly in edit");
+
+ var fontContent = $field.find('.note-editable font').contents()[0];
+ var rangeControl = {
+ sc: fontContent,
+ so: 0,
+ ec: fontContent,
+ eo: fontContent.length,
+ };
+ range = Wysiwyg.getRange($field[0]);
+ assert.deepEqual(_.pick(range, 'sc', 'so', 'ec', 'eo'), rangeControl,
+ "should select the text after color change");
+
+ // select the text
+ pText = $field.find('.note-editable p').first().contents()[2];
+ Wysiwyg.setRange(fontContent, 5, pText, 2);
+ // text is selected
+
+ $field.find('.note-toolbar .note-bg-color button:first').mousedown().click();
+ $field.find('.note-toolbar .note-bg-color button[data-value="bg-gamma"]').mousedown().click();
+
+ assert.strictEqual($field.find('.note-editable').html(),
+ '<p>t<font style="background-color: rgb(0, 255, 255);">oto t</font><font class="bg-gamma">oto&nbsp;to</font>to</p><p>tata</p>',
"should have rendered the field correctly in edit");
- // summernote invokes handlers after a setTimeout, so we must wait as well
- // before destroying the widget (otherwise we'll have a crash later on)
- setTimeout(function () {
form.destroy();
done();
- }, 0);
+ });
});
-QUnit.test('field html translatable', function (assert) {
- assert.expect(3);
-
- var multiLang = _t.database.multi_lang;
- _t.database.multi_lang = true;
-
- this.data['mass.mailing'].fields.body.translate = true;
+QUnit.test('media dialog: image', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
- var form = testUtils.createView({
+ testUtils.createAsyncView({
View: FormView,
- model: 'mass.mailing',
+ model: 'note.note',
data: this.data,
- arch: '<form string="Partners">' +
- '<field name="body" widget="html"/>' +
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
- if (route === '/web/dataset/call_button' && args.method === 'translate_fields') {
- assert.deepEqual(args.args, ['mass.mailing',1,'body',{}], "should call 'call_button' route");
- return $.when();
+ if (args.model === 'ir.attachment') {
+ if (args.method === "generate_access_token") {
+ return $.when();
+ }
+ if (args.kwargs.domain[7][2].join(',') === "image/gif,image/jpe,image/jpeg,image/jpg,image/gif,image/png") {
+ return $.when([{
+ "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
+ }]);
+ }
}
- return this._super.apply(this, arguments);
+ return this._super(route, args);
},
- });
+ }).then(function (form) {
+ form.$buttons.find('.o_form_button_edit').click();
+ var $field = form.$('.oe_form_field[name="body"]');
+
+ // the dialog load some xml assets
+ var defMediaDialog = $.Deferred();
+ testUtils.mock.patch(MediaDialog, {
+ init: function () {
+ this._super.apply(this, arguments);
+ this.opened(defMediaDialog.resolve.bind(defMediaDialog));
+ }
+ });
+
+ var pText = $field.find('.note-editable p').first().contents()[0];
+ Wysiwyg.setRange(pText, 1);
+
+ $field.find('.note-toolbar .note-insert button:has(.fa-file-image-o)').mousedown().click();
+
+ // load static xml file (dialog, media dialog, unsplash image widget)
+ defMediaDialog.then(function () {
+ $('.modal #editor-media-image .o_image:first').click();
+ $('.modal .modal-footer button.btn-primary').click();
- assert.containsNone(form, '.oe_form_field_html_text .o_field_translate',
- "should not have a translate button in readonly mode");
+ var $editable = form.$('.oe_form_field[name="body"] .note-editable');
- testUtils.form.clickEdit(form);
- var $button = form.$('.oe_form_field_html_text .o_field_translate');
- assert.strictEqual($button.length, 1, "should have a translate button");
- testUtils.dom.click($button);
+ assert.strictEqual($editable.data('wysiwyg').getValue(),
+ '<p>t<img class="img-fluid o_we_custom_image" data-src="/web_editor/static/src/img/transparent.png">oto toto toto</p><p>tata</p>',
+ "should have the image in the dom");
- form.destroy();
- _t.database.multi_lang = multiLang;
+ testUtils.mock.unpatch(MediaDialog);
+
+ form.destroy();
+ done();
+ });
+ });
});
-QUnit.test('field html_frame widget', function (assert) {
- assert.expect(6);
+QUnit.test('media dialog: icon', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
- var form = testUtils.createView({
+ testUtils.createAsyncView({
View: FormView,
- model: 'mass.mailing',
+ model: 'note.note',
data: this.data,
- arch: '<form string="Partners">' +
- '<field name="body" widget="html_frame" options="{\'editor_url\': \'/logo\'}"/>' +
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px"/>' +
'</form>',
res_id: 1,
- session: {user_context: {lang: "en_us"}},
- mockRPC: function (route) {
- if (_.str.startsWith(route, '/logo')) {
- // those tests will be executed twice, once in readonly and once in edit
- assert.ok(route.search('model=mass.mailing') > 0,
- "the route should specify the correct model");
- assert.ok(route.search('res_id=1') > 0,
- "the route should specify the correct id");
- return $.when();
+ mockRPC: function (route, args) {
+ if (args.model === 'ir.attachment') {
+ return $.when([]);
}
- return this._super.apply(this, arguments);
+ return this._super(route, args);
},
- });
+ }).then(function (form) {
+ form.$buttons.find('.o_form_button_edit').click();
+ var $field = form.$('.oe_form_field[name="body"]');
+
+ // the dialog load some xml assets
+ var defMediaDialog = $.Deferred();
+ testUtils.mock.patch(MediaDialog, {
+ init: function () {
+ this._super.apply(this, arguments);
+ this.opened(defMediaDialog.resolve.bind(defMediaDialog));
+ }
+ });
+
+ var pText = $field.find('.note-editable p').first().contents()[0];
+ Wysiwyg.setRange(pText, 1);
+
+ $field.find('.note-toolbar .note-insert button:has(.fa-file-image-o)').mousedown().click();
+
+ // load static xml file (dialog, media dialog, unsplash image widget)
+ defMediaDialog.then(function () {
+ $('.modal .tab-content .tab-pane').removeClass('fade'); // to be sync in test
+ $('.modal a[aria-controls="editor-media-icon"]').click();
+ $('.modal #editor-media-icon .font-icons-icon.fa-glass').click();
+ $('.modal .modal-footer button.btn-primary').click();
- assert.containsOnce(form, 'iframe', "should have rendered an iframe without crashing");
+ var $editable = form.$('.oe_form_field[name="body"] .note-editable');
- testUtils.form.clickEdit(form);
+ assert.strictEqual($editable.data('wysiwyg').getValue(),
+ '<p>t<span class="fa fa-glass"></span>oto toto toto</p><p>tata</p>',
+ "should have the image in the dom");
- assert.containsOnce(form, 'iframe', "should have rendered an iframe without crashing");
+ testUtils.mock.unpatch(MediaDialog);
- form.destroy();
+ form.destroy();
+ done();
+ }, 200);
+ });
});
-QUnit.test('field htmlsimple does not crash when commitChanges is called in mode=readonly', function (assert) {
+QUnit.test('save', function (assert) {
+ var done = assert.async();
assert.expect(1);
- var form = testUtils.createView({
+ testUtils.createAsyncView({
View: FormView,
- model: 'mass.mailing',
+ model: 'note.note',
data: this.data,
- arch: '<form string="Partners">' +
- '<header>' +
- '<button name="some_method" class="s" string="Do it" type="object"/>' +
- '</header>' +
- '<sheet>' +
- '<field name="body"/>' +
- '</sheet>' +
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px"/>' +
'</form>',
res_id: 1,
- intercepts: {
- execute_action: function () {
- assert.step('execute_action');
+ mockRPC: function (route, args) {
+ if (args.method === "write") {
+ assert.strictEqual(args.args[1].body,
+ '<p>t<font class="bg-gamma">oto toto&nbsp;</font>toto</p><p>tata</p>',
+ "should save the content");
+
}
+ return this._super.apply(this, arguments);
},
- });
+ }).then(function (form) {
+ form.$buttons.find('.o_form_button_edit').click();
+ var $field = form.$('.oe_form_field[name="body"]');
+
+ // select the text
+ var pText = $field.find('.note-editable p').first().contents()[0];
+ Wysiwyg.setRange(pText, 1, pText, 10);
+ // text is selected
+
+ $field.find('.note-toolbar .note-bg-color button:first').mousedown().click();
+ $field.find('.note-toolbar .note-bg-color button[data-value="bg-gamma"]').mousedown().click();
+
+ form.$buttons.find('.o_form_button_save').click();
- testUtils.dom.click(form.$('button:contains(Do it)'));
- form.destroy();
+ form.destroy();
+ done();
+ });
});
-QUnit.test('html_frame does not crash when saving in readonly', function (assert) {
- // The 'Save' action may be triggered even in readonly (e.g. when clicking
- // on a button in the form view)
- assert.expect(2);
+QUnit.module('inline-style');
+
+QUnit.test('convert style to class on edit', function (assert) {
+ var done = assert.async();
+ assert.expect(5);
+
+ this.data['note.note'].records[0].body = '<p class="pull-right" style="float: right;">toto ' +
+ '<img data-class="fa fa-star" data-style="color: red;" src="/web_editor/font_to_img/61445/rgb(255,0,0)/13" style="border-style:none;vertical-align:middle;height: auto; width: auto;">' +
+ 'toto toto</p><p>tata</p>';
- var form = testUtils.createView({
+ testUtils.createAsyncView({
View: FormView,
- model: 'mass.mailing',
+ model: 'note.note',
data: this.data,
- arch: '<form string="Partners">' +
- '<sheet>' +
- '<field name="body" widget="html_frame" options="{\'editor_url\': \'/logo\'}"/>' +
- '</sheet>' +
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px" options="{\'style-inline\': true}"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
- if (args.method) {
- assert.step(args.method);
+ if (args.method === "write") {
+ assert.strictEqual(args.args[1].body,
+ '<p class="pull-right" style="margin:0px;font-size:13px;font-family:&quot;Lucida Grande&quot;, Helvetica, Verdana, Arial, sans-serif;float:right;">' +
+ 'toto ' +
+ '<img ' +
+ 'data-class="fa fa-star" ' +
+ 'data-style="color: red;" ' +
+ 'style="border-style:none;vertical-align:middle;color: red; height: auto; width: auto;" ' +
+ 'data-src="/web_editor/font_to_img/61445/rgb(255,0,0)/13"' +
+ '>' +
+ 'toto toto' +
+ '</p>' +
+ '<p style="margin:0px;font-size:13px;font-family:&quot;Lucida Grande&quot;, Helvetica, Verdana, Arial, sans-serif;">' +
+ 'tata' +
+ '</p>',
+ "should save the content");
}
- if (_.str.startsWith(route, '/logo')) {
- // manually call the callback to simulate that the iframe has
- // been loaded (note: just the content, not the editor)
- window.odoo[$.deparam(route).callback + '_content'].call();
- return $.when();
+ if (route.indexOf("/web_editor/font_to_img/61445/rgb(") !== -1) {
+ assert.strictEqual(route,
+ '/web_editor/font_to_img/61445/rgb(255,0,0)/13',
+ "should use the image in function of the font and the color");
+ return $.when('#');
}
return this._super.apply(this, arguments);
},
+ }).then(function (form) {
+ var $field = form.$('.oe_form_field[name="body"]');
+
+ assert.strictEqual($field.children('.o_readonly').html(),
+ '<p class="pull-right" style="float: right;">' +
+ 'toto ' +
+ '<img data-class="fa fa-star" data-style="color: red;" ' +
+ 'style="border-style:none;vertical-align:middle;height: auto; width: auto;" ' +
+ 'data-src="/web_editor/font_to_img/61445/rgb(255,0,0)/13">' +
+ 'toto toto' +
+ '</p>' +
+ '<p>tata</p>',
+ "should have rendered a div with correct content in readonly");
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ $field = form.$('.oe_form_field[name="body"]');
+ assert.strictEqual($field.find('.note-editable').html(),
+ '<p class="pull-right">' +
+ 'toto ' +
+ '<span class="fa fa-star o_fake_not_editable" style="color: red;" contenteditable="false"></span>' +
+ 'toto toto' +
+ '</p>' +
+ '<p>tata</p>',
+ "should have rendered the field correctly in edit (remove inline style that used class)");
+
+ form.$buttons.find('.o_form_button_save').click();
+
+ form.destroy();
+ done();
});
+});
- form.saveRecord(); // before the fix done in this commit, it crashed here
+QUnit.module('cssReadonly');
- assert.verifySteps(['read']);
+QUnit.test('rendering with iframe for readonly mode', function (assert) {
+ var done = assert.async();
+ assert.expect(3);
- form.destroy();
+ testUtils.createAsyncView({
+ View: FormView,
+ model: 'note.note',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px" options="{\'cssReadonly\': \'template.assets\'}"/>' +
+ '</form>',
+ res_id: 1,
+ }).then(function (form) {
+ var $field = form.$('.oe_form_field[name="body"]');
+ var $iframe = $field.find('iframe.o_readonly');
+ $iframe.data('load-def').then(function () {
+ var doc = $iframe.contents()[0];
+ assert.strictEqual($(doc).find('#iframe_target').html(),
+ '<p>toto toto toto</p><p>tata</p>',
+ "should have rendered a div with correct content in readonly");
+
+ assert.strictEqual(doc.defaultView.getComputedStyle(doc.body).backgroundColor,
+ 'rgb(255, 0, 0)',
+ "should load the asset css");
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ $field = form.$('.oe_form_field[name="body"]');
+ assert.strictEqual($field.find('.note-editable').html(),
+ '<p>toto toto toto</p><p>tata</p>',
+ "should have rendered the field correctly in edit");
+
+ form.destroy();
+ done();
+ });
+ });
});
-QUnit.test('html_frame does not crash when saving in edit mode (editor not loaded)', function (assert) {
- // The 'Save' action may be triggered when saving in edit mode very fast
- // so that the editor may be not loaded, even though the content is!
- assert.expect(2);
+QUnit.module('cssEdit');
- var form = testUtils.createView({
+QUnit.test('rendering with iframe for edit mode', function (assert) {
+ var done = assert.async();
+ assert.expect(4);
+
+ testUtils.createAsyncView({
View: FormView,
- model: 'mass.mailing',
+ model: 'note.note',
data: this.data,
- arch: '<form string="Partners">' +
- '<sheet>' +
- '<field name="display_name"/>' +
- '<field name="body" widget="html_frame" options="{\'editor_url\': \'/logo\'}"/>' +
- '</sheet>' +
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px" options="{\'cssEdit\': \'template.assets\'}"/>' +
+ '</form>',
+ res_id: 1,
+ }).then(function (form) {
+ var $field = form.$('.oe_form_field[name="body"]');
+ assert.strictEqual($field.children('.o_readonly').html(),
+ '<p>toto toto toto</p><p>tata</p>',
+ "should have rendered a div with correct content in readonly");
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ $field = form.$('.oe_form_field[name="body"]');
+ var $iframe = $field.find('iframe');
+
+ $iframe.data('load-def').then(function () {
+ var doc = $iframe.contents()[0];
+ var $content = $('#iframe_target', doc);
+
+ assert.strictEqual($content.find('.note-editable').html(),
+ '<p>toto toto toto</p><p>tata</p>',
+ "should have rendered a div with correct content in edit mode");
+
+ assert.strictEqual(doc.defaultView.getComputedStyle(doc.body).backgroundColor,
+ 'rgb(255, 0, 0)',
+ "should load the asset css");
+
+ $content.find('.note-toolbar .note-bg-color button:first').mousedown().click();
+
+ assert.ok($content.find('.note-bg-color').hasClass('show') && $content.find('.note-bg-color .dropdown-menu').hasClass('show'),
+ "should display toolbar dropdown menu in iframe");
+
+ form.destroy();
+ done();
+ });
+ });
+});
+
+QUnit.test('use colorpicker and save', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ testUtils.createAsyncView({
+ View: FormView,
+ model: 'note.note',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px" options="{\'cssEdit\': \'template.assets\'}"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
- if (args.method) {
- assert.step(args.method);
- }
- if (_.str.startsWith(route, '/logo')) {
- // manually call the callback to simulate that the iframe has
- // been partially loaded (just the content, not the editor)
- window.odoo[$.deparam(route).callback + '_content']();
- return $.when();
+ if (args.method === "write") {
+ assert.strictEqual(args.args[1].body,
+ '<p>t<font class="bg-gamma">oto toto&nbsp;</font>toto</p><p>tata</p>',
+ "should save the content");
}
return this._super.apply(this, arguments);
},
+ }).then(function (form) {
+ form.$buttons.find('.o_form_button_edit').click();
+ var $field = form.$('.oe_form_field[name="body"]');
+ var $iframe = $field.find('iframe');
+
+ $iframe.data('load-def').then(function () {
+ var doc = $iframe.contents()[0];
+ var $content = $('#iframe_target', doc);
+ var $editable = $content.find('.note-editable');
+
+ // select the text
+ var pText = $editable.find('p').first().contents()[0];
+ Wysiwyg.setRange(pText, 1, pText, 10);
+ // text is selected
+
+ $content.find('.note-toolbar .note-bg-color button:first').mousedown().click();
+ $content.find('.note-toolbar .note-bg-color button[data-value="bg-gamma"]').mousedown().click();
+
+
+ form.$buttons.find('.o_form_button_save').click();
+
+ form.destroy();
+ done();
+ });
});
+});
- testUtils.form.clickEdit(form);
- testUtils.fields.editInput(form.$('input.o_input.o_field_char'), 'trululu');
- testUtils.form.clickSave(form); // crash without editor fully loaded
+QUnit.module('wrapper');
- assert.verifySteps(['read']);
+// todo add test wrapper
- form.destroy();
+QUnit.module('snippet (without iframe)');
+
+QUnit.test('rendering with snippet panel', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ testUtils.createAsyncView({
+ View: FormView,
+ model: 'note.note',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px" options="{\'snippets\': \'web_editor.snippets\'}"/>' +
+ '</form>',
+ res_id: 1,
+ }).then(function (form) {
+ testUtils.mock.intercept(form, "snippets_loaded", function () {
+ assert.strictEqual(form.$('.oe_form_field #oe_snippets').length, 1,
+ "should display the snippet panel");
+ form.destroy();
+ done();
+ });
+ form.$buttons.find('.o_form_button_edit').click();
+ });
});
-QUnit.test('html_frame saving in edit mode (editor and content fully loaded)', function (assert) {
+QUnit.test('drag&drop snippet', function (assert) {
var done = assert.async();
- assert.expect(4);
+ assert.expect(1);
- var editorBar = new web_editor.Class();
- var loadDeferred = $.Deferred();
- var writeDeferred = $.Deferred();
+ testUtils.createAsyncView({
+ View: FormView,
+ model: 'note.note',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px" options="{\'snippets\': \'web_editor.snippets\'}"/>' +
+ '</form>',
+ res_id: 1,
+ }).then(function (form) {
+ var defSnippets = $.Deferred();
+ testUtils.mock.intercept(form, "snippets_loaded", function () {
+ defSnippets.resolve();
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ var $field = form.$('.oe_form_field[name="body"]');
+ var $editable = $field.find('.note-editable');
+
+ defSnippets.then(function () {
+ var $hr = $field.find('.oe_snippet_thumbnail:first');
+ testUtils.dom.dragAndDrop($hr, $editable.find('p'));
+
+ assert.strictEqual($editable.data('wysiwyg').getValue().replace(/\s+/g, ' '),
+ '<div class=\"s_hr pt32 pb32\"> <hr class=\"s_hr_1px s_hr_solid w-100 mx-auto\"> </div><p>toto toto toto</p><p>tata</p>',
+ "should drop the snippet");
+
+ form.destroy();
+ done();
+ });
+ });
+});
+
+// todo add test snippet + customize snippet
+
+QUnit.module('snippet & cssEdit');
- var form = testUtils.createView({
+QUnit.test('rendering with snippet panel', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ testUtils.createAsyncView({
View: FormView,
- model: 'mass.mailing',
+ model: 'note.note',
data: this.data,
- arch: '<form string="Partners">' +
- '<sheet>' +
- '<field name="display_name"/>' +
- '<field name="body" widget="html_frame" options="{\'editor_url\': \'/logo\'}"/>' +
- '</sheet>' +
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px" options="{\'snippets\': \'web_editor.snippets\', \'cssEdit\': \'template.assets\'}"/>' +
'</form>',
res_id: 1,
- mockRPC: function (route, args) {
- if (args.method) {
- assert.step(args.method);
- if (args.method === 'write') {
- writeDeferred.resolve();
- }
+ }).then(function (form) {
+ testUtils.mock.intercept(form, "snippets_loaded", function () {
+ var doc = form.$('iframe').contents()[0];
+ var $content = $('#iframe_target', doc);
+ assert.strictEqual($content.find('#oe_snippets').length, 1,
+ "should display the snippet panel");
+ form.destroy();
+ done();
+ });
+ form.$buttons.find('.o_form_button_edit').click();
+ });
+});
+
+QUnit.test('drag&drop snippet', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ testUtils.createAsyncView({
+ View: FormView,
+ model: 'note.note',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px" options="{\'snippets\': \'web_editor.snippets\', \'cssEdit\': \'template.assets_all_style\'}"/>' +
+ '</form>',
+ res_id: 1,
+ }).then(function (form) {
+ var defSnippets = $.Deferred();
+ testUtils.mock.intercept(form, "snippets_loaded", function () {
+ defSnippets.resolve();
+ });
+ form.$buttons.find('.o_form_button_edit').click();
+
+ defSnippets.then(function () {
+ var doc = form.$('iframe').contents()[0];
+ var $content = $('#iframe_target', doc);
+ var $editable = $content.find('.note-editable');
+
+ var $hr = $content.find('.oe_snippet_thumbnail:first');
+ var from = $hr.offset();
+ var to = $editable.find('p').offset();
+
+ $hr.trigger($.Event("mousedown", {
+ which: 1,
+ pageX: from.left + 1,
+ pageY: from.top + 1
+ }));
+ $hr.trigger($.Event("mousemove", {
+ which: 1,
+ pageX: to.left,
+ pageY: to.top
+ }));
+ $hr.trigger($.Event("mouseup", {
+ which: 1,
+ pageX: to.left,
+ pageY: to.top
+ }));
+
+ assert.strictEqual($editable.data('wysiwyg').getValue().replace(/\s+/g, ' '),
+ '<div class=\"s_hr pt32 pb32\"> <hr class=\"s_hr_1px s_hr_solid w-100 mx-auto\"> </div><p>toto toto toto</p><p>tata</p>',
+ "should drop the snippet");
+
+ form.destroy();
+ done();
+ });
+ });
+});
+
+QUnit.test('drag&drop snippet options', function (assert) {
+ var done = assert.async();
+ assert.expect(6);
+
+ testUtils.createAsyncView({
+ View: FormView,
+ model: 'note.note',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px" options="{\'snippets\': \'web_editor.snippets\', \'cssEdit\': \'template.assets_all_style\'}"/>' +
+ '</form>',
+ res_id: 1,
+ }).then(function (form) {
+ var defSnippets = $.Deferred();
+ testUtils.mock.intercept(form, "snippets_loaded", function () {
+ defSnippets.resolve();
+ });
+
+ var defActivateSnippet;
+ testUtils.mock.intercept(form, "snippet_focused", function (e) {
+ defActivateSnippet.resolve(e.target);
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ defSnippets.then(function () {
+ var doc = form.$('iframe').contents()[0];
+ var $content = $('#iframe_target', doc);
+ var $editable = $content.find('.note-editable');
+
+ function dropSnippet() {
+ defActivateSnippet = $.Deferred();
+
+ var $hr = $content.find('.oe_snippet_thumbnail:first');
+ var from = $hr.offset();
+ var to = $editable.find('p').offset();
+
+ $hr.trigger($.Event("mousedown", {
+ which: 1,
+ pageX: from.left + 1,
+ pageY: from.top + 1
+ }));
+ $hr.trigger($.Event("mousemove", {
+ which: 1,
+ pageX: to.left,
+ pageY: to.top
+ }));
+ $hr.trigger($.Event("mouseup", {
+ which: 1,
+ pageX: to.left,
+ pageY: to.top
+ }));
}
- if (_.str.startsWith(route, '/logo')) {
- // manually call the callback to simulate that the iframe has
- // been fully loaded (content + editor)
- var callback = $.deparam(route).callback;
- return loadDeferred.then(function () {
- var contentCallback = window.odoo[callback + '_content'];
- var editorCallback = window.odoo[callback + '_editor'];
- if (editorCallback && contentCallback) {
- contentCallback();
- editorCallback(editorBar);
- }
+
+ dropSnippet();
+ defActivateSnippet.then(function (snippet) {
+ assert.strictEqual($editable.data('wysiwyg').getValue().replace(/\s+/g, ' '),
+ '<div class=\"s_hr pt32 pb32 built focus\"> <hr class=\"s_hr_1px s_hr_solid w-100 mx-auto\"> </div><p>toto toto toto</p><p>tata</p>',
+ "should drop the snippet");
+
+ assert.strictEqual(snippet.$target.index(), 0,
+ 'should show the snippet editor for the first dropped block');
+
+ dropSnippet();
+ defActivateSnippet.then(function (snippet) {
+ assert.strictEqual($editable.data('wysiwyg').getValue().replace(/\s+/g, ' '),
+ '<div class=\"s_hr pt32 pb32 built\"> <hr class=\"s_hr_1px s_hr_solid w-100 mx-auto\"> ' +
+ '</div><div class=\"s_hr pt32 pb32 built focus\"> <hr class=\"s_hr_1px s_hr_solid w-100 mx-auto\"> ' +
+ '</div><p>toto toto toto</p><p>tata</p>',
+ "should drop the snippet");
+
+ assert.strictEqual(snippet.$target.index(), 1,
+ 'should show the snippet editor for the second dropped block');
+
+ assert.strictEqual($('.oe_active .oe_overlay_options .btn:visible', doc).length, 4,
+ 'should show the snippet editor buttons');
+
+ defActivateSnippet = $.Deferred();
+ $editable.find('hr:first').trigger('click');
+ defActivateSnippet.then(function (snippet) {
+
+ assert.strictEqual(snippet.$target.index(), 0,
+ 'should show the snippet editor for the first dropped block');
+
+ form.destroy();
+ done();
+ });
});
+ });
+ });
+ });
+});
+
+QUnit.module('translation');
+
+QUnit.test('field html translatable', function (assert) {
+ assert.expect(3);
+
+ var multiLang = _t.database.multi_lang;
+ _t.database.multi_lang = true;
+
+ this.data['note.note'].fields.body.translate = true;
+
+ testUtils.createAsyncView({
+ View: FormView,
+ model: 'note.note',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="body" widget="html"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_button' && args.method === 'translate_fields') {
+ assert.deepEqual(args.args, ['note.note', 1, 'body', {}], "should call 'call_button' route");
+ return $.when();
}
return this._super.apply(this, arguments);
},
+ }).then(function (form) {
+ assert.strictEqual(form.$('.oe_form_field_html .o_field_translate').length, 0,
+ "should not have a translate button in readonly mode");
+
+ form.$buttons.find('.o_form_button_edit').click();
+ var $button = form.$('.oe_form_field_html .note-toolbar .o_field_translate');
+ assert.strictEqual($button.length, 1, "should have a translate button");
+ $button.click();
+
+ form.destroy();
+ _t.database.multi_lang = multiLang;
+
});
+});
- testUtils.form.clickEdit(form);
- testUtils.fields.editInput(form.$('input.o_input.o_field_char'), 'trululu');
- testUtils.form.clickSave(form);
+QUnit.test('field html translatable in iframe', function (assert) {
+ var done = assert.async();
+ assert.expect(2);
- loadDeferred.resolve(); // simulate late loading of html frame
+ var multiLang = _t.database.multi_lang;
+ _t.database.multi_lang = true;
- assert.strictEqual(form.$('.o_field_char').val(), 'trululu',
- "should have saved the char field text");
+ this.data['note.note'].fields.body.translate = true;
- writeDeferred.then( function () { // html_frame is async with write
- assert.verifySteps(['read', 'write']);
- form.destroy();
- done();
+ testUtils.createAsyncView({
+ View: FormView,
+ model: 'note.note',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px" options="{\'cssEdit\': \'template.assets\'}"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_button' && args.method === 'translate_fields') {
+ assert.deepEqual(args.args, ['note.note', 1, 'body', {}], "should call 'call_button' route");
+ return $.when();
+ }
+ return this._super.apply(this, arguments);
+ },
+ }).then(function (form) {
+ var $field = form.$('.oe_form_field[name="body"]');
+ form.$buttons.find('.o_form_button_edit').click();
+ $field = form.$('.oe_form_field[name="body"]');
+ var $iframe = $field.find('iframe');
+
+ $iframe.data('load-def').then(function () {
+ var doc = $iframe.contents()[0];
+ var $content = $('#iframe_target', doc);
+
+ var $button = $content.find('.note-toolbar .o_field_translate');
+ assert.strictEqual($button.length, 1, "should have a translate button");
+ $button.click();
+
+ form.destroy();
+ _t.database.multi_lang = multiLang;
+ done();
+ });
});
-
});
});
+});
+});
diff --git a/addons/web_editor/static/tests/test_utils.js b/addons/web_editor/static/tests/test_utils.js
new file mode 100644
index 00000000000..ff740e52f36
--- /dev/null
+++ b/addons/web_editor/static/tests/test_utils.js
@@ -0,0 +1,674 @@
+odoo.define('web_editor.test_utils', function (require) {
+"use strict";
+
+var ajax = require('web.ajax');
+var testUtils = require('web.test_utils');
+var Widget = require('web.Widget');
+var Wysiwyg = require('web_editor.wysiwyg');
+var options = require('web_editor.snippets.options');
+
+
+/**
+ * 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 $.when({
+ cssLibs: [],
+ cssContents: ['body {background-color: red;}']
+ });
+ }
+ if (xmlId === 'template.assets_all_style') {
+ return $.when({
+ cssLibs: $('head 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 = $.Deferred();
+ 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 = $.when();
+
+ function pollTest(test) {
+ var def = $.when();
+ $editable.data('wysiwyg').setValue(test.content);
+
+ function poll(step) {
+ var def = $.Deferred();
+ 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, '&#8203;');
+ var result = test.test.content.replace(allInvisible, '&#8203;');
+ 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,
+};
+
+
+});
diff --git a/addons/web_editor/static/tests/wysiwyg_keyboard_tests.js b/addons/web_editor/static/tests/wysiwyg_keyboard_tests.js
new file mode 100644
index 00000000000..dfbddb2dc55
--- /dev/null
+++ b/addons/web_editor/static/tests/wysiwyg_keyboard_tests.js
@@ -0,0 +1,4179 @@
+odoo.define('web_editor.wysiwyg.keyboard_tests', function (require) {
+"use strict";
+
+var weTestUtils = require('web_editor.test_utils');
+
+//--------------------------------------------------------------------------
+// tests
+//--------------------------------------------------------------------------
+
+QUnit.module('web_editor', {}, function () {
+QUnit.module('wysiwyg', {}, function () {
+
+//--------------------------------------------------------------------------
+// Unbreakable tests
+//--------------------------------------------------------------------------
+QUnit.module('Unbreakable');
+
+var unbreakableTestDom = [
+ ' <noteditable id="a">',
+ ' content_a_0',
+ ' <noteditable id="b">content_b_0</noteditable>',
+ ' <editable id="c">',
+ ' content_c_0',
+ ' <noteditable id="d">content_d_0</noteditable>',
+ ' <noteditable id="e">',
+ ' content_e_0',
+ ' <editable id="f">content_f_0</editable>',
+ ' content_e_1',
+ ' </noteditable>',
+ ' content_c_4',
+ ' </editable>',
+ ' <noteditable id="g">content_g_0</noteditable>',
+ ' <editable id="h">',
+ ' content_h_0',
+ ' <noteditable id="i">content_i_0</noteditable>',
+ ' content_h_2',
+ ' </editable>',
+ ' <editable id="j">',
+ ' content_j_0',
+ ' <editable id="k">content_k_0</editable>',
+ ' <editable id="l">content_l_0</editable>',
+ ' content_j_4',
+ ' </editable>',
+ ' </noteditable>',
+ ' <editable id="m">content_m_0</editable>',
+ ' <editable id="n">content_n_0</editable>',
+ ''
+].join('\n');
+var UnbreakableTests = [{
+ name: "nothing to do",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#c:contents()[0]->13",
+ end: "#c:contents()[0]->15",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#c:contents()[0]->13",
+ end: "#c:contents()[0]->15",
+ },
+ },
+ {
+ name: "nothing to do 2",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#j:contents()[0]->13",
+ end: "#l:contents()[0]->5",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#j:contents()[0]->13",
+ end: "#l:contents()[0]->5",
+ },
+ },
+ {
+ name: "nothing to do 3",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#k:contents()[0]->3",
+ end: "#l:contents()[0]->5",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#k:contents()[0]->3",
+ end: "#l:contents()[0]->5",
+ },
+ },
+ {
+ name: "find the first allowed node",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#a:contents()[0]->13",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#c:contents()[0]->0",
+ end: "#c:contents()[0]->0",
+ },
+ },
+ {
+ name: "find the first allowed node and collapse the selection",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#a:contents()[0]->10",
+ end: "#b:contents()[0]->3",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#c:contents()[0]->0",
+ end: "#c:contents()[0]->0",
+ },
+ },
+ {
+ name: "resize the range to the allowed end",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#a:contents()[0]->10",
+ end: "#c:contents()[0]->14",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#c:contents()[0]->0",
+ end: "#c:contents()[0]->14",
+ },
+ },
+ {
+ name: "resize the range to the allowed start",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#c:contents()[0]->15",
+ end: "#d:contents()[0]->4",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#c:contents()[0]->15",
+ end: "#c:contents()[0]->37",
+ },
+ },
+ {
+ name: "resize the range to the allowed node that contains unbreakable node",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#g:contents()[0]->5",
+ end: "#h:contents()[2]->15",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#h:contents()[0]->0",
+ end: "#h:contents()[2]->15",
+ },
+ },
+ {
+ name: "resize the range to the allowed node between the start and the end",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#e:contents()[0]->15",
+ end: "#c:contents()[4]->15",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#f:contents()[0]->0",
+ end: "#f:contents()[0]->11",
+ },
+ },
+ {
+ name: "resize the range to the allowed start with the entirety of the unbreakable node",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#c:contents()[0]->15",
+ end: "#e:contents()[0]->15",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#c:contents()[0]->15",
+ end: "#d->1",
+ },
+ },
+ {
+ name: "resize the range to the allowed start and delete content",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#c:contents()[0]->15",
+ end: "#e:contents()[0]->15",
+ key: "DELETE",
+ }],
+ test: {
+ content: unbreakableTestDom
+ // when we remove, we clean the invisible space before and after the deleted area
+ .replace(/>[\s]+content_c_0[\s\S]+content_d_0<\/noteditable>\s+/, '>co')
+ .replace(/^\s+/, ''), // corner effect of the clean and normalize
+ start: "#c:contents()[0]->2",
+ end: "#c:contents()[0]->2",
+ },
+ },
+ {
+ name: "delete unbreakable nodes in a breakable",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#c:contents()[0]->15",
+ end: "#c:contents()[4]->19",
+ key: "DELETE",
+ }],
+ test: {
+ content: unbreakableTestDom
+ // when we remove, we clean the invisible space before and after the deleted area
+ .replace(/>[\s]+content_c_0[\s\S]*content_c_4\s*/, '>cot_c_4')
+ .replace(/^\s+/, ''), // corner effect of the clean and normalize
+ start: "#c:contents()[0]->2",
+ end: "#c:contents()[0]->2",
+ },
+ },
+ {
+ name: "select range with 2 nodes on the root",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#m:contents()[0]->5",
+ end: "#n:contents()[0]->5",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#m:contents()[0]->5",
+ end: "#n:contents()[0]->5",
+ },
+ },
+ {
+ name: "ENTER in an unbreakable p",
+ content: "<p class='unbreakable'>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ end: "p:contents()[0]->1",
+ key: 'ENTER',
+ }],
+ test: {
+ content: '<p class="unbreakable">d<br>om to edit</p>',
+ start: "p:contents()[2]->0",
+ },
+ },
+];
+
+QUnit.test('Unbreakable selection and edition', function (assert) {
+ var done = assert.async();
+ weTestUtils.createWysiwyg({
+ data: this.data,
+ useOnlyTestUnbreakable: true,
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+ weTestUtils.testKeyboard($editable, assert, UnbreakableTests).then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+//--------------------------------------------------------------------------
+// Keyboard integration
+//--------------------------------------------------------------------------
+
+QUnit.module('Keyboard');
+
+var keyboardTestsChar = [{
+ name: "visible char in a p tag",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->3",
+ keyCode: 66,
+ }],
+ test: {
+ content: "<p>domB to edit</p>",
+ start: "p:contents()[0]->4",
+ end: null,
+ check: function ($editable, assert) {
+ assert.ok(true);
+ },
+ },
+ },
+ {
+ name: "visible char in a link tag entirely selected",
+ content: '<div><a href="#">dom to edit</a></div>',
+ steps: [{
+ start: "a:contents()[0]->0",
+ end: "a:contents()[0]->11",
+ key: 'a',
+ }],
+ test: {
+ content: '<div><a href="#">&#8203;a&#8203;</a></div>',
+ start: "a:contents()[0]->2",
+ },
+ },
+ {
+ name: "'a' on a selection of all the contents of a complex dom",
+ content: "<p><b>dom</b></p><p><b>to<br>completely</b>remov<i>e</i></p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "i:contents()[0]->1",
+ key: 'a',
+ }],
+ test: {
+ content: "<p><b>a</b></p>",
+ start: "b:contents()[0]->1",
+ },
+ },
+ {
+ name: "'a' on a selection of all the contents of a complex dom (2)",
+ content: "<h1 class=\"a\"><font style=\"font-size: 62px;\"><b>dom to</b>edit</font></h1>",
+ steps: [{
+ start: "font:contents()[0]->0",
+ end: "font:contents()[1]->4",
+ key: 'a',
+ }],
+ test: {
+ content: "<h1 class=\"a\"><font style=\"font-size: 62px;\"><b>a</b></font></h1>",
+ start: "b:contents()[0]->1",
+ },
+ },
+ {
+ name: "'a' before an image",
+ content: '<p>xxx <img src="/web_editor/static/src/img/transparent.png"> yyy</p>',
+ steps: [{
+ start: "p:contents()[0]->4",
+ key: 'a',
+ }],
+ test: {
+ content: '<p>xxx a<img data-src="/web_editor/static/src/img/transparent.png"> yyy</p>',
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "'a' before an image (2)",
+ content: '<p>xxx <img src="/web_editor/static/src/img/transparent.png"> yyy</p>',
+ steps: [{
+ start: "img->0",
+ key: 'a',
+ }],
+ test: {
+ content: '<p>xxx a<img data-src="/web_editor/static/src/img/transparent.png"> yyy</p>',
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "'a' before an image in table",
+ content: '<table><tbody><tr><td><p>xxx</p></td><td><p><img src="/web_editor/static/src/img/transparent.png"></p></td><td><p>yyy</p></td></tr></tbody></table>',
+ steps: [{
+ start: "img->0",
+ key: 'a',
+ }],
+ test: {
+ content: '<table><tbody><tr><td><p>xxx</p></td><td><p>a<img data-src="/web_editor/static/src/img/transparent.png"></p></td><td><p>yyy</p></td></tr></tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "'a' on invisible text before an image in table",
+ content: '<table><tbody><tr><td><p>xxx</p></td><td><p><img src="/web_editor/static/src/img/transparent.png"></p></td><td><p>yyy</p></td></tr></tbody></table>',
+ steps: [{
+ start: "p:eq(1)->0",
+ key: 'a',
+ }],
+ test: {
+ content: '<table><tbody><tr><td><p>xxx</p></td><td><p>a<img data-src="/web_editor/static/src/img/transparent.png"></p></td><td><p>yyy</p></td></tr></tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "'a' before an image in table without spaces",
+ content: '<table><tbody><tr><td><p>xxx</p></td><td><p><img src="/web_editor/static/src/img/transparent.png"></p></td><td><p>yyy</p></td></tr></tbody></table>',
+ steps: [{
+ start: "img->0",
+ key: 'a',
+ }],
+ test: {
+ content: '<table><tbody><tr><td><p>xxx</p></td><td><p>a<img data-src="/web_editor/static/src/img/transparent.png"></p></td><td><p>yyy</p></td></tr></tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "'a' before an image in table without spaces (2)",
+ content: '<table><tbody><tr><td><p>xxx</p></td><td><p><img src="/web_editor/static/src/img/transparent.png"></p></td><td><p>yyy</p></td></tr></tbody></table>',
+ steps: [{
+ start: "td:eq(1)->0",
+ key: 'a',
+ }],
+ test: {
+ content: '<table><tbody><tr><td><p>xxx</p></td><td><p>a<img data-src="/web_editor/static/src/img/transparent.png"></p></td><td><p>yyy</p></td></tr></tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "'a' before an image in table with spaces",
+ content:
+ '<table><tbody>\n' +
+ ' <tr>\n' +
+ ' <td>\n' +
+ ' <p>xxx</p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p><img src="/web_editor/static/src/img/transparent.png"></p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p>yyy</p>\n' +
+ ' </td>\n' +
+ ' </tr>\n' +
+ '</tbody></table>',
+ steps: [{
+ start: "img->0",
+ key: 'a',
+ }],
+ test: {
+ content:
+ '<table><tbody>\n' +
+ ' <tr>\n' +
+ ' <td>\n' +
+ ' <p>xxx</p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p>a<img data-src="/web_editor/static/src/img/transparent.png"></p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p>yyy</p>\n' +
+ ' </td>\n' +
+ ' </tr>\n' +
+ '</tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "'a' before an image in table with spaces (2)",
+ content:
+ '<table><tbody>\n' +
+ ' <tr>\n' +
+ ' <td>\n' +
+ ' <p>xxx</p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p><img src="/web_editor/static/src/img/transparent.png"></p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p>yyy</p>\n' +
+ ' </td>\n' +
+ ' </tr>\n' +
+ '</tbody></table>',
+ steps: [{
+ start: "td:eq(1)->1",
+ key: 'a',
+ }],
+ test: {
+ content:
+ '<table><tbody>\n' +
+ ' <tr>\n' +
+ ' <td>\n' +
+ ' <p>xxx</p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p>a<img data-src="/web_editor/static/src/img/transparent.png"></p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p>yyy</p>\n' +
+ ' </td>\n' +
+ ' </tr>\n' +
+ '</tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "'a' before an image in table with spaces (3)",
+ content:
+ '<table><tbody>\n' +
+ ' <tr>\n' +
+ ' <td>\n' +
+ ' <p>xxx</p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p><img src="/web_editor/static/src/img/transparent.png"></p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p>yyy</p>\n' +
+ ' </td>\n' +
+ ' </tr>\n' +
+ '</tbody></table>',
+ steps: [{
+ start: "td:eq(1):contents()[0]->12",
+ key: 'a',
+ }],
+ test: {
+ content:
+ '<table><tbody>\n' +
+ ' <tr>\n' +
+ ' <td>\n' +
+ ' <p>xxx</p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p>a<img data-src="/web_editor/static/src/img/transparent.png"></p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p>yyy</p>\n' +
+ ' </td>\n' +
+ ' </tr>\n' +
+ '</tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "'a' on begin of a span with fake_editable",
+ content:
+ '<div class="o_fake_not_editable" contentEditable="false">\n' +
+ ' <div>\n' +
+ ' <label>\n' +
+ ' <input type="checkbox"/>\n' +
+ ' <span class="o_fake_editable" contentEditable="true">\n' +
+ ' dom to edit\n' +
+ ' </span>\n' +
+ ' </label>\n' +
+ ' </div>\n' +
+ '</div>',
+ steps: [{
+ start: "span:contents(0)->10",
+ key: 'a',
+ }],
+ test: {
+ content:
+ '<div>\n' +
+ ' <div>\n' +
+ ' <label>\n' +
+ ' <input type="checkbox">\n' +
+ ' <span>\n' +
+ ' adom to edit\n' +
+ ' </span>\n' +
+ ' </label>\n' +
+ ' </div>\n' +
+ '</div>',
+ start: "span:contents(0)->11",
+ },
+ },
+ {
+ name: "'a' on all contents of p starting with an icon",
+ content: '<p><span class="fa fa-star"></span>bbb</p>',
+ steps: [{
+ start: "span->0",
+ end: 'p:contents(1)->3',
+ key: 'a',
+ }],
+ test: {
+ content: '<p>a</p>',
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "' ' at start of p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: ' ',
+ }],
+ test: {
+ content: '<p>&nbsp;dom to edit</p>',
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "' ' at end of p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->11",
+ key: ' ',
+ }],
+ test: {
+ content: '<p>dom to edit&nbsp;</p>',
+ start: "p:contents()[0]->12",
+ },
+ },
+ {
+ name: "' ' within p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->2",
+ key: ' ',
+ }],
+ test: {
+ content: '<p>do m to edit</p>',
+ start: "p:contents()[0]->3",
+ },
+ },
+ {
+ name: "' ' before space within p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->3",
+ key: ' ',
+ }],
+ test: {
+ content: '<p>dom&nbsp;&nbsp;to edit</p>',
+ start: "p:contents()[0]->4",
+ },
+ },
+ {
+ name: "' ' after space within p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->4",
+ key: ' ',
+ }],
+ test: {
+ content: '<p>dom&nbsp;&nbsp;to edit</p>',
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "3x ' ' at start of p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: ' ',
+ }, {
+ key: ' ',
+ }, {
+ key: ' ',
+ }],
+ test: {
+ content: '<p>&nbsp;&nbsp;&nbsp;dom to edit</p>',
+ start: "p:contents()[0]->3",
+ },
+ },
+ {
+ name: "3x ' ' at end of p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->11",
+ key: ' ',
+ }, {
+ key: ' ',
+ }, {
+ key: ' ',
+ }],
+ test: {
+ content: '<p>dom to edit&nbsp;&nbsp;&nbsp;</p>',
+ start: "p:contents()[0]->14",
+ },
+ },
+ {
+ name: "3x ' ' within p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->2",
+ key: ' ',
+ }, {
+ key: ' ',
+ }, {
+ key: ' ',
+ }],
+ test: {
+ content: '<p>do&nbsp;&nbsp;&nbsp;m to edit</p>',
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "3x ' ' before space in p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->3",
+ key: ' ',
+ }, {
+ key: ' ',
+ }, {
+ key: ' ',
+ }],
+ test: {
+ content: '<p>dom&nbsp;&nbsp;&nbsp;&nbsp;to edit</p>',
+ start: "p:contents()[0]->6",
+ },
+ },
+ {
+ name: "3x ' ' after space in p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->4",
+ key: ' ',
+ }, {
+ key: ' ',
+ }, {
+ key: ' ',
+ }],
+ test: {
+ content: '<p>dom&nbsp;&nbsp;&nbsp;&nbsp;to edit</p>',
+ start: "p:contents()[0]->7",
+ },
+ },
+ {
+ name: "'a' in unbreakable with font",
+ content: '<div class="unbreakable">dom <span class="fa fa-heart"></span>to edit</div>',
+ steps: [{
+ start: "div:contents(2)->2",
+ key: 'a',
+ }],
+ test: {
+ content: '<div class="unbreakable">dom <span class="fa fa-heart"></span>toa edit</div>',
+ start: "div:contents(2)->3",
+ },
+ },
+ {
+ name: "'a' on begin of unbreakable inline node",
+ content: 'dom <strong class="unbreakable">to</strong> edit',
+ steps: [{
+ start: "strong:contents(0)->0",
+ key: 'a',
+ }],
+ test: {
+ content: 'dom <strong class="unbreakable">ato</strong> edit',
+ start: "strong:contents(0)->1",
+ },
+ },
+ {
+ name: "'a' on end of unbreakable inline node",
+ content: '<div>dom <strong class="unbreakable">to</strong> edit</div>',
+ steps: [{
+ start: "strong:contents(0)->2",
+ key: 'a',
+ }],
+ test: {
+ content: '<div>dom <strong class="unbreakable">toa</strong> edit</div>',
+ start: "strong:contents(0)->3",
+ },
+ },
+ {
+ name: "'1' on begin of value of a field currency",
+ content:
+ '<noteditable>\n' +
+ '<b data-oe-type="monetary" class="oe_price editable">$&nbsp;<span class="oe_currency_value">750.00</span></b>\n' +
+ '</noteditable>',
+ steps: [{
+ start: "span:contents(0)->0",
+ key: '1',
+ }],
+ test: {
+ content:
+ '<noteditable>\n' +
+ '<b data-oe-type="monetary" class="oe_price editable">$&nbsp;<span class="oe_currency_value">1750.00</span></b>\n' +
+ '</noteditable>',
+ start: "span:contents(0)->1",
+ },
+ },
+ {
+ name: "'1' on end of value of a field currency",
+ content:
+ '<noteditable>\n' +
+ '<b data-oe-type="monetary" class="oe_price editable">$&nbsp;<span class="oe_currency_value">750.00</span></b>\n' +
+ '</noteditable>',
+ steps: [{
+ start: "span:contents(0)->6",
+ key: '1',
+ }],
+ test: {
+ content:
+ '<noteditable>\n' +
+ '<b data-oe-type="monetary" class="oe_price editable">$&nbsp;<span class="oe_currency_value">750.001</span></b>\n' +
+ '</noteditable>',
+ start: "span:contents(0)->7",
+ },
+ },
+ {
+ name: "'1' on begin of editable in noteditable",
+ content:
+ '<noteditable contenteditable="false">\n' +
+ '<b data-oe-type="monetary" class="oe_price editable" contenteditable="true">$&nbsp;<span class="oe_currency_value">750.00</span></b>\n' +
+ '</noteditable>',
+ steps: [{
+ start: "span:contents(0)->0",
+ key: '1',
+ }],
+ test: {
+ content:
+ '<noteditable>\n' +
+ '<b data-oe-type="monetary" class="oe_price editable">$&nbsp;<span class="oe_currency_value">1750.00</span></b>\n' +
+ '</noteditable>',
+ start: "span:contents(0)->1",
+ },
+ },
+ {
+ name: "'1' on end of editable in noteditable",
+ content:
+ '<noteditable contenteditable="false">\n' +
+ '<b data-oe-type="monetary" class="oe_price editable" contenteditable="true">$&nbsp;<span class="oe_currency_value">750.00</span></b>\n' +
+ '</noteditable>',
+ steps: [{
+ start: "span:contents(0)->6",
+ key: '1',
+ }],
+ test: {
+ content:
+ '<noteditable>\n' +
+ '<b data-oe-type="monetary" class="oe_price editable">$&nbsp;<span class="oe_currency_value">750.001</span></b>\n' +
+ '</noteditable>',
+ start: "span:contents(0)->7",
+ },
+ },
+ {
+ name: "'a' on editable with before&after in noteditable",
+ content:
+ '<style>#test-before-after:before { content: "placeholder";} #test-before-after:after { content: "\\00a0";}</style>\n' +
+ '<noteditable contenteditable="false">\n' +
+ '<b id="test-before-after" class="editable" contenteditable="true"></b>\n' +
+ '</noteditable>',
+ steps: [{
+ start: "b->0",
+ key: 'a',
+ }],
+ test: {
+ content:
+ '<style>#test-before-after:before { content: "placeholder";} #test-before-after:after { content: "\\00a0";}</style>\n' +
+ '<noteditable>\n' +
+ '<b id="test-before-after" class="editable">a</b>\n' +
+ '</noteditable>',
+ start: "b:contents(0)->1",
+ },
+ },
+];
+
+QUnit.test('Char', function (assert) {
+ var done = assert.async();
+ weTestUtils.createWysiwyg({
+ data: this.data,
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+ weTestUtils.testKeyboard($editable, assert, keyboardTestsChar).then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+var keyboardTestsEnter = [{
+ name: "in p: ENTER",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p>d</p><p>om to edit</p>",
+ start: "p:contents()[1]->0",
+ },
+ },
+ {
+ name: "in p: 2x ENTER",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ key: 'ENTER',
+ }, {
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p>d</p><p><br></p><p>om to edit</p>",
+ start: "p:contents()[2]->0",
+ },
+ },
+ {
+ name: "in p: SHIFT+ENTER",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p>d<br>om to edit</p>",
+ start: "p:contents()[2]->0",
+ },
+ },
+ {
+ name: "in empty-p: SHIFT+ENTER",
+ content: "<p><br></p>",
+ steps: [{
+ start: "p->1",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p><br><br></p>",
+ start: "br:eq(1)->0",
+ },
+ },
+ {
+ name: "in p: 2x SHIFT+ENTER",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p>d<br><br>om to edit</p>",
+ start: "p:contents()[3]->0",
+ },
+ },
+ {
+ name: "in empty-p: 2x SHIFT+ENTER",
+ content: "<p><br></p>",
+ steps: [{
+ start: "p->1",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p><br><br><br></p>",
+ start: "br:eq(2)->0",
+ },
+ },
+ {
+ name: "in p: ENTER -> SHIFT+ENTER",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ key: 'ENTER',
+ }, {
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p>d</p><p><br>om to edit</p>",
+ start: "p:eq(1):contents()[1]->0",
+ },
+ },
+ {
+ name: "in p: ENTER on selection",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ end: "p:contents()[0]->7",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p>d</p><p>edit</p>",
+ start: "p:contents()[1]->0",
+ end: "p:contents()[1]->0",
+ },
+ },
+ {
+ name: "in p: SHIFT+ENTER on selection",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ end: "p:contents()[0]->7",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p>d<br>edit</p>",
+ start: "p:contents()[2]->0",
+ },
+ },
+
+ // list UL / OL
+
+ {
+ name: "in li: SHIFT+ENTER at start",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<ul><li><br>dom to edit</li></ul>",
+ start: "li:contents()[1]->0",
+ },
+ },
+ {
+ name: "in li: SHIFT+ENTER within contents",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->5",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<ul><li>dom t<br>o edit</li></ul>",
+ start: "li:contents()[2]->0",
+ },
+ },
+ {
+ name: "in empty-li: SHIFT+ENTER",
+ content: "<ul><li><br></li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<ul><li><br><br></li></ul>",
+ start: "br:eq(1)->0",
+ },
+ },
+ {
+ name: "in li: SHIFT+ENTER on selection",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->1",
+ end: "li:contents()[0]->7",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<ul><li>d<br>edit</li></ul>",
+ start: "li:contents()[2]->0",
+ },
+ },
+ {
+ name: "in li: ENTER at start",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<ul><li><br></li><li>dom to edit</li></ul>",
+ start: "li:contents()[1]->0",
+ },
+ },
+ {
+ name: "in li: ENTER within contents",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->5",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<ul><li>dom t</li><li>o edit</li></ul>",
+ start: "li:eq(1):contents()[0]->0",
+ },
+ },
+ {
+ name: "in li: ENTER on selection",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->1",
+ end: "li:contents()[0]->7",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<ul><li>d</li><li>edit</li></ul>",
+ start: "li:eq(1):contents()[0]->0",
+ },
+ },
+ {
+ name: "in li: ENTER on selection of all contents",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ end: "li:contents()[0]->11",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p><br></p>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in li: ENTER -> 'a' on selection of all contents",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ end: "li:contents()[0]->11",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>a</p>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "across 2 li: ENTER on partial selection",
+ content: "<ul><li>dom to edit</li><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->1",
+ end: "li:eq(1):contents()[0]->4",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<ul><li>d</li><li>to edit</li></ul>",
+ start: "li:eq(1):contents()[0]->0", // we are after the <br>, the carret is on the li with an offset equal to the node length
+ },
+ },
+ {
+ name: "in li: ENTER at end",
+ content: "<ul><li><p>dom to edit</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->11",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<ul><li><p>dom to edit</p></li><li><p><br></p></li></ul>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in li: 2x ENTER at end",
+ content: "<ul><li><p>dom to edit</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->11",
+ key: 'ENTER',
+ }, {
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<ul><li><p>dom to edit</p></li></ul><p><br></p>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in ul.list-group > li: ENTER at end",
+ content: '<ul class="list-group"><li><p>dom to edit</p></li></ul>',
+ steps: [{
+ start: "p:contents()[0]->11",
+ key: 'ENTER',
+ }, {
+ key: 'ENTER',
+ }],
+ test: {
+ content: '<ul class="list-group"><li><p>dom to edit</p></li><li><p><br></p></li><li><p><br></p></li></ul>',
+ start: "p:eq(2) br->0",
+ },
+ },
+ {
+ name: "in indented-li: 2x ENTER at end",
+ content: "<ul><li><p>aaa</p></li><ul><li><p>dom to edit</p></li></ul><li><p>bbb</p></li></ul>",
+ steps: [{
+ start: "ul ul p:contents()[0]->11",
+ key: 'ENTER',
+ }, {
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<ul><li><p>aaa</p></li><ul><li><p>dom to edit</p></li></ul><li><p><br></p></li><li><p>bbb</p></li></ul>",
+ start: "p:eq(2) br->0",
+ },
+ },
+ {
+ name: "in indented-li with font: 2x ENTER at end",
+ content: "<ul><li><p>aaa</p></li><ul><li><p><font style=\"color\">dom to edit</font></p></li></ul><li><p>bbb</p></li></ul>",
+ steps: [{
+ start: "font:contents()[0]->11",
+ key: 'ENTER',
+ }, {
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<ul><li><p>aaa</p></li><ul><li><p><font style=\"color\">dom to edit</font></p></li></ul><li><p><font style=\"color\"><br></font></p></li><li><p>bbb</p></li></ul>",
+ start: "p:eq(2) br->0",
+ },
+ },
+ {
+ name: "in li > empty-p: ENTER",
+ content: "<ul><li><p><br></p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p><br></p>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in li > p > empty-b: ENTER",
+ content: "<ul><li><p><b><br></b></p></li></ul>",
+ steps: [{
+ start: "b:contents()[0]->0",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p><b><br></b></p>",
+ start: "br->0",
+ },
+ },
+
+ // end list UL / OL
+
+ {
+ name: "after p > b: SHIFT+ENTER",
+ content: "<p><b>dom</b> to edit</p>",
+ steps: [{
+ start: "p:contents()[1]->0",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p><b>dom</b><br>&nbsp;to edit</p>",
+ start: "p:contents()[2]->0",
+ },
+ },
+ {
+ name: "in p > b: ENTER -> 'a'",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->2",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><b>do</b></p><p><b>am to edit</b></p>",
+ start: "b:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "after p > b: ENTER -> 'a'",
+ content: "<p><b>dom</b> to edit</p>",
+ steps: [{
+ start: "p:contents()[1]->0",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><b>dom</b></p><p>a to edit</p>",
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "after p > b: SHIFT+ENTER -> 'a'",
+ content: "<p><b>dom</b>&nbsp;to edit</p>",
+ steps: [{
+ start: "p:contents()[1]->0",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><b>dom</b><br>a to edit</p>",
+ start: "p:contents()[2]->1",
+ },
+ },
+ {
+ name: "in p (other-p > span.a before - p > span.b after): ENTER at beginning",
+ content: "<p><span class=\"a\">dom to</span></p><p><span class=\"b\">edit</span></p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p><span class=\"a\">dom to</span></p><p><span class=\"b\"><br></span></p><p><span class=\"b\">edit</span></p>",
+ start: "span:eq(2):contents()[0]->0",
+ },
+ },
+ {
+ name: "in p (other-p > span.a before - p > span.b after): ENTER -> 'a' at beginning",
+ content: "<p><span class=\"a\">dom to</span></p><p><span class=\"b\">edit</span></p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><span class=\"a\">dom to</span></p><p><span class=\"b\"><br></span></p><p><span class=\"b\">aedit</span></p>",
+ start: "span:eq(2):contents()[0]->1",
+ },
+ },
+ {
+ name: "in p (other-p > span.a before - p > span.b after): SHIFT+ENTER at beginning",
+ content: "<p><span class=\"a\">dom to</span></p><p><span class=\"b\">edit</span></p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p><span class=\"a\">dom to</span></p><p><span class=\"b\"><br>edit</span></p>",
+ start: "span:eq(1):contents()[1]->0",
+ },
+ },
+ {
+ name: "in p (other-p > span.a before - p > span.b after): SHIFT+ENTER -> 'a' at beginning",
+ content: "<p><span class=\"a\">dom to</span></p><p><span class=\"b\">edit</span></p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><span class=\"a\">dom to</span></p><p><span class=\"b\"><br>aedit</span></p>",
+ start: "span:eq(1):contents()[1]->1",
+ },
+ },
+ {
+ name: "in p: ENTER on selection of all contents",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "p:contents()[0]->11",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p><br></p><p><br></p>",
+ start: "p:eq(1) br->0",
+ },
+ },
+ {
+ name: "in p: ENTER -> 'a' on selection of all contents",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "p:contents()[0]->11",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><br></p><p>a</p>",
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in p: SHIFT+ENTER on selection of all contents",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "p:contents()[0]->11",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p><br><br></p>",
+ start: "br:eq(1)->0",
+ },
+ },
+ {
+ name: "in p: SHIFT+ENTER -> 'a' on selection of all contents",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "p:contents()[0]->11",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><br>a</p>",
+ start: "p:contents()[1]->1",
+ },
+ },
+ {
+ name: "in p: 2x ENTER -> 'a' on selection of all contents",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->3",
+ key: 'ENTER',
+ }, {
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>dom</p><p><br></p><p>a to edit</p>",
+ start: "p:eq(2):contents()[0]->1",
+ },
+ },
+ {
+ name: "in p > b: ENTER at start",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->0",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p><b><br></b></p><p><b>dom to edit</b></p>",
+ start: "b:eq(1):contents()[0]->0",
+ },
+ },
+ {
+ name: "in p > b: ENTER",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p><b>dom</b></p><p><b>&nbsp;to edit</b></p>",
+ start: "b:eq(1):contents()[0]->0",
+ },
+ },
+ {
+ name: "in p > b: SHIFT+ENTER -> ENTER",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p><b>dom<br>&#8203;</b></p><p><b>&nbsp;to edit</b></p>",
+ start: "b:eq(1):contents()[0]->0",
+ },
+ },
+ {
+ name: "in p > b: ENTER -> a'",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><b>dom</b></p><p><b>a to edit</b></p>",
+ start: "b:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in p > b: SHIFT+ENTER",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p><b>dom<br>&nbsp;to edit</b></p>",
+ start: "b:contents()[2]->0",
+ },
+ },
+ {
+ name: "in p > b: SHIFT+ENTER -> 'a'",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><b>dom<br>a to edit</b></p>",
+ start: "b:contents()[2]->1",
+ },
+ },
+ {
+ name: "in p > b: SHIFT+ENTER -> ENTER -> 'a'",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><b>dom<br>&#8203;</b></p><p><b>a to edit</b></p>",
+ start: "b:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in span > b: ENTER",
+ content: "<span><b>dom to edit</b></span>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<span><b>dom</b></span><br><span><b>&#8203;&nbsp;to edit</b></span>",
+ start: "b:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in span > b: SHIFT+ENTER -> ENTER",
+ content: "<span><b>dom to edit</b></span>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<span><b>dom<br></b></span><br><span><b>&#8203;&nbsp;to edit</b></span>",
+ start: "b:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in span > b: ENTER -> a'",
+ content: "<span><b>dom to edit</b></span>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<span><b>dom</b></span><br><span><b>a to edit</b></span>",
+ start: "b:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in span > b: SHIFT+ENTER",
+ content: "<span><b>dom to edit</b></span>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<span><b>dom<br>&nbsp;to edit</b></span>",
+ start: "b:contents()[2]->0",
+ },
+ },
+ {
+ name: "in span > b: SHIFT+ENTER -> 'a'",
+ content: "<span><b>dom to edit</b></span>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<span><b>dom<br>a to edit</b></span>",
+ start: "b:contents()[2]->1",
+ },
+ },
+ {
+ name: "in span > b: SHIFT+ENTER -> ENTER -> 'a'",
+ content: "<span><b>dom to edit</b></span>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<span><b>dom<br></b></span><br><span><b>a to edit</b></span>",
+ start: "b:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in p: 2x SHIFT+ENTER -> 'a'",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>dom<br><br>a to edit</p>",
+ start: "p:contents()[3]->1",
+ },
+ },
+ {
+ name: "in p: ENTER -> SHIFT+ENTER -> 'a'",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->3",
+ key: 'ENTER',
+ }, {
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>dom</p><p><br>a to edit</p>",
+ start: "p:eq(1):contents()[1]->1",
+ },
+ },
+ {
+ name: "in empty-p (p before and after): ENTER -> 'a'",
+ content: "<p>dom </p><p><br></p><p>to edit</p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>dom </p><p><br></p><p>a</p><p>to edit</p>",
+ start: "p:eq(2):contents()[0]->1",
+ },
+ },
+ {
+ name: "in p: SHIFT+ENTER at end",
+ content: "<p>dom </p><p>to edit</p>",
+ steps: [{
+ start: "p:first:contents()[0]->4",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p>dom <br>&#8203;</p><p>to edit</p>",
+ start: "p:first:contents()[2]->0",
+ },
+ },
+ {
+ name: "in p: SHIFT+ENTER at end -> '寺'",
+ content: "<p>dom </p><p>to edit</p>",
+ steps: [{
+ start: "p:first:contents()[0]->4",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ keyCode: 23546, /*temple in chinese*/
+ }],
+ test: {
+ content: "<p>dom <br>寺</p><p>to edit</p>",
+ start: "p:first:contents()[2]->1",
+ },
+ },
+ {
+ name: "in empty-p (div > a after): 3x SHIFT+ENTER -> 'a'",
+ content: "<p><br></p><div><a href='#'>dom to edit</a></div>",
+ steps: [{
+ start: "p->1",
+ key: 'ENTER',
+ shiftKey: true,
+ },
+ {
+ key: 'ENTER',
+ shiftKey: true,
+ },
+ {
+ key: 'ENTER',
+ shiftKey: true,
+ },
+ {
+ key: 'a',
+ }
+ ],
+ test: {
+ content: "<p><br><br><br>a</p><div><a href=\"#\">dom to edit</a></div>",
+ start: "p:contents()[3]->1",
+ },
+ },
+
+ // Buttons
+
+ {
+ name: "in div > a.btn: ENTER -> 'a' at start (before invisible space)",
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;dom to edit&#8203;</a></div>",
+ steps: [{
+ start: "a:contents()[0]->0",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;a&#8203;</a><a class=\"btn\" href=\"#\">dom to edit</a></div>",
+ // split button has no text so the placeholder text is selected then replaced by 'a'
+ start: "a:contents()[0]->2",
+ },
+ },
+ {
+ name: "in div > a.btn: ENTER -> 'a' at start (after invisible space)",
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;dom to edit&#8203;</a></div>",
+ steps: [{
+ start: "a:contents()[0]->1",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;a&#8203;</a><a class=\"btn\" href=\"#\">dom to edit</a></div>",
+ // split button has no text so the placeholder text is selected then replaced by 'a'
+ start: "a:contents()[0]->2",
+ },
+ },
+ {
+ name: "in div > a.btn: ENTER -> 'a' within contents",
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;dom to edit&#8203;</a></div>",
+ steps: [{
+ start: "a:contents()[0]->6",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">dom t</a><a class=\"btn\" href=\"#\">&#8203;ao edit&#8203;</a></div>",
+ start: "a:eq(1):contents()[0]->2",
+ },
+ },
+ {
+ name: "in div > a.btn: ENTER -> 'a' at end (before invisible space)",
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;dom to edit&#8203;</a></div>",
+ steps: [{
+ start: "a:contents()[0]->12",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">dom to edit</a><a class=\"btn\" href=\"#\">&#8203;a&#8203;</a></div>",
+ start: "a:eq(1):contents()[0]->2",
+ },
+ },
+ {
+ name: "in div > a.btn: ENTER -> 'a' at end (after invisible space)",
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;dom to edit&#8203;</a></div>",
+ steps: [{
+ start: "a:contents()[0]->13",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">dom to edit</a><a class=\"btn\" href=\"#\">&#8203;a&#8203;</a></div>",
+ start: "a:eq(1):contents()[0]->2",
+ },
+ },
+ {
+ name: "in div > button.btn: ENTER -> 'a' at end (after invisible space)",
+ content: "<div class=\"unbreakable\"><button class=\"btn\" href=\"#\">&#8203;dom to edit&#8203;</button></div>",
+ steps: [{
+ start: "button:contents()[0]->13",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<div class=\"unbreakable\"><button class=\"btn\" href=\"#\">dom to edit</button><button class=\"btn\" href=\"#\">&#8203;a&#8203;</button></div>",
+ start: "button:eq(1):contents()[0]->2",
+ },
+ },
+ {
+ name: "in div > a.btn: ENTER -> 'a' on partial selection",
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;dom to edit&#8203;</a></div>",
+ steps: [{
+ start: "a:contents()[0]->4",
+ end: "a:contents()[0]->8",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">dom</a><a class=\"btn\" href=\"#\">&#8203;aedit&#8203;</a></div>",
+ start: "a:eq(1):contents()[0]->2",
+ },
+ },
+ {
+ name: "in div > a.btn: ENTER -> 'a' on selection of all visible text",
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">dom to edit</a></div>",
+ steps: [{
+ start: "a:contents()[0]->0",
+ end: "a:contents()[0]->11",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;a&#8203;</a><a class=\"btn\" href=\"#\">Label</a></div>",
+ // Removing all text in a link replaces that text with "Label"
+ start: "a:contents()[0]->2",
+ },
+ },
+ {
+ name: "across 2 a.btn: ENTER on selection across two a.btn",
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">dom not to edit</a><a class=\"btn\" href=\"#\">other dom not to edit</a></div>",
+ steps: [{
+ start: "a:contents()[0]->0",
+ end: "a:eq(1):contents()[0]->11",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;Label&#8203;</a><a class=\"btn\" href=\"#\">ot to edit</a></div>",
+ start: "a:contents()[0]->1",
+ end: "a:contents()[0]->6",
+ },
+ },
+];
+
+QUnit.test('Enter', function (assert) {
+ var done = assert.async();
+ weTestUtils.createWysiwyg({
+ data: this.data,
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+ weTestUtils.testKeyboard($editable, assert, keyboardTestsEnter).then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+var keyboardTestsComplex = [{
+ name: "in span > b: SHIFT+ENTER -> BACKSPACE -> 'a'",
+ content: "<span><b>dom to edit</b></span>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<span><b>doma to edit</b></span>",
+ start: "b:contents()[0]->4",
+ },
+ },
+ {
+ name: "in span > b: SHIFT+ENTER -> ENTER -> BACKSPACE",
+ content: "<span><b>dom to edit</b></span>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<span><b>dom<br>&nbsp;to edit</b></span>",
+ start: "b:contents(2)->0",
+ },
+ },
+ {
+ name: "in span > b: SHIFT+ENTER -> ENTER -> BACKSPACE -> 'a'",
+ content: "<span><b>dom to edit</b></span>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<span><b>dom<br>a to edit</b></span>",
+ start: "b:contents()[2]->1",
+ },
+ },
+ {
+ name: "in p > b: 2x SHIFT+ENTER -> BACKSPACE",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><b>dom<br>&nbsp;to edit</b></p>",
+ start: "b:contents()[2]->0",
+ },
+ },
+ {
+ name: "in p > b: 2x ENTER -> 2x BACKSPACE",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: false
+ }, {
+ key: 'ENTER',
+ shiftKey: false
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><b>dom&nbsp;to edit</b></p>",
+ start: "b:contents()[0]->3",
+ },
+ },
+ {
+ name: "in empty-p: 2x ENTER -> 2x BACKSPACE",
+ content: "<p><br></p>",
+ steps: [{
+ start: "p->1",
+ key: 'ENTER',
+ shiftKey: false
+ }, {
+ key: 'ENTER',
+ shiftKey: false
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><br></p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in empty-p (p before): ENTER -> 2x BACKSPACE",
+ content: "<p>dom not to edit</p><p><br></p>",
+ steps: [{
+ start: "p:eq(1)->1",
+ key: 'ENTER',
+ shiftKey: false
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom not to edit</p>",
+ start: "p:first:contents()[0]->15",
+ },
+ },
+ {
+ name: "in p > b: 2x SHIFT+ENTER -> BACKSPACE -> 'a'",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><b>dom<br>a to edit</b></p>",
+ start: "b:contents()[2]->1",
+ },
+ },
+ {
+ name: "in li -> ENTER before br -> 'a'",
+ content: "<ul><li><p>dom<br/>&nbsp;to edit</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->3",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<ul><li><p>dom</p></li><li><p>a<br>&nbsp;to edit</p></li></ul>",
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in li -> ENTER after br -> 'a'",
+ content: "<ul><li><p>dom<br/>&nbsp;to edit</p></li></ul>",
+ steps: [{
+ start: "p:contents()[2]->0",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<ul><li><p>dom<br>&#8203;</p></li><li><p>a to edit</p></li></ul>",
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "within p (p before and after): 3x SHIFT+ENTER -> 3x BACKSPACE",
+ content: "<p>dom not to edit</p><p>dom to edit</p><p>dom not to edit</p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->5",
+ key: 'ENTER',
+ shiftKey: true,
+ },
+ {
+ key: 'ENTER',
+ shiftKey: true,
+ },
+ {
+ key: 'ENTER',
+ shiftKey: true,
+ },
+ {
+ key: 'BACKSPACE',
+ },
+ {
+ key: 'BACKSPACE',
+ },
+ {
+ key: 'BACKSPACE',
+ }
+ ],
+ test: {
+ content: "<p>dom not to edit</p><p>dom to edit</p><p>dom not to edit</p>",
+ start: "p:eq(1):contents()[0]->5",
+ },
+ },
+ {
+ name: "in h1.a > font: 'a' on selection of all contents",
+ content: "<h1 class=\"a\"><font style=\"font-size: 62px;\">dom to edit</font></h1>",
+ steps: [{
+ start: "h1->0",
+ end: "h1->1",
+ key: 'a',
+ }],
+ test: {
+ content: "<h1 class=\"a\"><font style=\"font-size: 62px;\">a</font></h1>",
+ start: "font:contents()[0]->1",
+ },
+ },
+ {
+ name: "in complex-dom: BACKSPACE on partial selection (requires merging non-similar blocks)",
+ content: "<p class=\"a\">pif</p><p><span><b>paf</b></span></p><ul><li><p>p<i>ouf</i></p></li></ul>",
+ steps: [{
+ start: 'p:contents()[0]->2',
+ end: 'i:contents()[0]->2',
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p class=\"a\">pi<i>f</i></p>',
+ start: "p:contents()[0]->2",
+ },
+ },
+ {
+ name: "in complex-dom: DELETE on partial selection (requires merging non-similar blocks)",
+ content: "<p class=\"a\">pif</p><p><span><b>paf</b></span></p><ul><li><p>p<i>ouf</i></p></li></ul>",
+ steps: [{
+ start: 'p:contents()[0]->2',
+ end: 'i:contents()[0]->2',
+ key: 'DELETE',
+ }],
+ test: {
+ content: '<p class=\"a\">pi<i>f</i></p>',
+ start: "p:contents()[0]->2",
+ },
+ },
+
+ // paragraph outdent
+
+ {
+ name: "in p (h1 before): TAB -> BACKSPACE at start (must indent then outdent)",
+ content: "<h1>dom not to edit</h1><p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'TAB',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<h1>dom not to edit</h1><p>dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in p (h1 before): TAB -> 2x BACKSPACE at start (must indent then outdent, then merge blocks)",
+ content: "<h1>dom not to edit</h1><p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'TAB',
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<h1>dom not to editdom to edit</h1>",
+ start: "h1:contents()[0]->15",
+ },
+ },
+];
+
+QUnit.test('Complex', function (assert) {
+ var done = assert.async();
+ weTestUtils.createWysiwyg({
+ data: this.data,
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+ weTestUtils.testKeyboard($editable, assert, keyboardTestsComplex).then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+var keyboardTestsTab = [{
+ name: "in li: TAB at start",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'TAB',
+ }],
+ test: {
+ content: "<ul><li class=\"o_indent\"><ul><li>dom to edit</li></ul></li></ul>",
+ start: "ul ul li:contents()[0]->0",
+ },
+ },
+ {
+ name: "in indented-li: SHIFT+TAB at start",
+ content: "<ul><ul><li>dom to edit</li></ul></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'TAB',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<ul><li>dom to edit</li></ul>",
+ start: "li:contents()[0]->0",
+ },
+ },
+ {
+ name: "in li: TAB within contents",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->5",
+ key: 'TAB',
+ }],
+ test: {
+ content: "<ul><li>dom t&nbsp;&nbsp;&nbsp;&nbsp;o edit</li></ul>",
+ start: "li:contents()[0]->9",
+ },
+ },
+ {
+ name: "in li: SHIFT+TAB within contents (should do nothing)",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->5",
+ key: 'TAB',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<ul><li>dom to edit</li></ul>",
+ start: "li:contents()[0]->5",
+ },
+ },
+ {
+ name: "in td > p: TAB within contents",
+ content: "<table><tbody><tr><td><p>dom to edit</p></td><td><p>node</p></td></tr></tbody></table>",
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'TAB',
+ }],
+ test: {
+ content: "<table><tbody><tr><td><p>dom to edit</p></td><td><p>node</p></td></tr></tbody></table>",
+ start: "td:eq(1) p:contents(0)->0",
+ },
+ },
+ {
+ name: "in p: TAB at start",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'TAB',
+ }],
+ test: {
+ content: "<p style=\"margin-left: 1.5em;\">dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in indented-p: SHIFT+TAB at start",
+ content: "<p style=\"margin-left: 1.5em;\">dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'TAB',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p>dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in p: TAB -> BACKSPACE at start",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'TAB',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in p > b: TAB at start",
+ content: "<p>dom <b>to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->0",
+ key: 'TAB',
+ }],
+ test: {
+ content: "<p>dom <b>&nbsp;&nbsp;&nbsp;&nbsp;to edit</b></p>",
+ start: "b:contents()[0]->4",
+ },
+ },
+ {
+ name: "in p > b: TAB -> BACKSPACE at start",
+ content: "<p>dom <b>to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->0",
+ key: 'TAB',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom <b>&nbsp;&nbsp;&nbsp;to edit</b></p>",
+ start: "b:contents()[0]->3",
+ },
+ },
+];
+
+QUnit.test('Tab', function (assert) {
+ var done = assert.async();
+ weTestUtils.createWysiwyg({
+ data: this.data,
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+ weTestUtils.testKeyboard($editable, assert, keyboardTestsTab).then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+var keyboardTestsBackspace = [{
+ name: "in p: BACKSPACE after span.fa",
+ content: '<p>aaa<span class="fa fa-star"></span>bbb</p>',
+ steps: [{
+ start: "p:contents()[2]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>aaabbb</p>',
+ start: "p:contents()[0]->3",
+ },
+ },
+ {
+ name: "in empty-p (no br): BACKSPACE (must insert br)",
+ content: "<p></p>",
+ steps: [{
+ start: "p->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><br></p>", // The br is there to ensure the carret can enter the p tag
+ start: "br->0",
+ },
+ },
+ {
+ name: "in empty-p: BACKSPACE (must leave it unchanged)",
+ content: "<p><br></p>",
+ steps: [{
+ start: "p->1",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><br></p>", // The br is there to ensure the carret can enter the p tag
+ start: "br->0",
+ },
+ },
+ {
+ name: "in p (empty-p before): BACKSPACE",
+ content: "<p><br></p><p>dom to edit</p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in p (empty-p.a before): BACKSPACE",
+ content: "<p class=\"a\"><br></p><p>dom to edit</p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in p: BACKSPACE within text",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom o edit</p>", // The br is there to ensure the carret can enter the p tag
+ start: "p:contents()[0]->4",
+ },
+ },
+ {
+ name: "in p: 2x BACKSPACE within text",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>domo edit</p>", // The br is there to ensure the carret can enter the p tag
+ start: "p:contents()[0]->3",
+ },
+ },
+ {
+ name: "in p (p > span.a before - span.b after): BACKSPACE at beginning (must attach them)",
+ content: "<p><span class=\"a\">dom to</span></p><p><span class=\"b\">edit</span></p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><span class=\"a\">dom to</span><span class=\"b\">edit</span></p>",
+ start: "span:eq(0):contents()[0]->6",
+ },
+ },
+ {
+ name: "in p (p > span.a before - span.a after): BACKSPACE (must merge them)",
+ content: "<p><span class=\"a\">dom to&nbsp;</span></p><p><span class=\"a\">edit</span></p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><span class=\"a\">dom to&nbsp;edit</span></p>",
+ start: "span:contents()[0]->7",
+ },
+ },
+ {
+ name: "in p (b before): BACKSPACE",
+ content: "<p><b>dom</b> to edit</p>",
+ steps: [{
+ start: "p:contents()[1]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><b>do</b>&nbsp;to edit</p>",
+ start: "b:contents()[0]->2",
+ },
+ },
+ {
+ name: "in p (div > span.a before - span.a after): BACKSPACE at beginning (must do nothing)",
+ content: "<div><span class=\"a\">dom to&nbsp;</span></div><p><span class=\"a\">edit</span></p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<div><span class=\"a\">dom to&nbsp;</span></div><p><span class=\"a\">edit</span></p>",
+ start: "span:eq(1):contents()[0]->0",
+ },
+ },
+ {
+ name: "in p: BACKSPACE on partial selection",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ end: "p:contents()[0]->7",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dedit</p>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "across 2 p's: BACKSPACE on partial selection",
+ content: "<p>dom</p><p>to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ end: "p:eq(1):contents()[0]->3",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dedit</p>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "in p: BACKSPACE within text, with space at beginning",
+ content: "<p> dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->10",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom o edit</p>",
+ start: "p:contents()[0]->4",
+ },
+ },
+ {
+ name: "in p: BACKSPACE within text, with one space at beginning",
+ content: "<p> dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->6",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>&nbsp;dom o edit</p>",
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "in p: BACKSPACE within text, with space at end",
+ content: "<p>dom to edit </p>",
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom o edit</p>",
+ start: "p:contents()[0]->4",
+ },
+ },
+ {
+ name: "in p: BACKSPACE within text, with one space at end",
+ content: "<p>dom to edit </p>",
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom o edit&nbsp;</p>",
+ start: "p:contents()[0]->4",
+ },
+ },
+ {
+ name: "in p: BACKSPACE within text, with space at beginning and end",
+ content: "<p> dom to edit </p>",
+ steps: [{
+ start: "p:contents()[0]->10",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom o edit</p>",
+ start: "p:contents()[0]->4",
+ },
+ },
+ {
+ name: "in p: BACKSPACE within text, with one space at beginning and one at end",
+ content: "<p> dom to edit </p>",
+ steps: [{
+ start: "p:contents()[0]->6",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>&nbsp;dom o edit&nbsp;</p>",
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "in p: BACKSPACE after \\w<br>\\w",
+ content: "<p>dom to edi<br>t</p>",
+ steps: [{
+ start: "p:contents()[2]->1",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom to edi<br>&#8203;</p>",
+ start: "p:contents()[2]->1",
+ },
+ },
+ {
+ name: "in p: BACKSPACE -> 'a' within text, after \\s\\w",
+ content: "<p>dom t</p>",
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>dom a</p>",
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "in pre: BACKSPACE within text, with space at beginning",
+ content: "<pre> dom to edit</pre>",
+ steps: [{
+ start: "pre:contents()[0]->10",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<pre> dom o edit</pre>",
+ start: "pre:contents()[0]->9",
+ },
+ },
+ {
+ name: "in pre: BACKSPACE within text, with one space at beginning",
+ content: "<pre> dom to edit</pre>",
+ steps: [{
+ start: "pre:contents()[0]->6",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<pre> dom o edit</pre>",
+ start: "pre:contents()[0]->5",
+ },
+ },
+ {
+ name: "in pre: BACKSPACE within text, with space at end",
+ content: "<pre>dom to edit </pre>",
+ steps: [{
+ start: "pre:contents()[0]->5",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<pre>dom o edit </pre>",
+ start: "pre:contents()[0]->4",
+ },
+ },
+ {
+ name: "in pre: BACKSPACE within text, with one space at end",
+ content: "<pre>dom to edit </pre>",
+ steps: [{
+ start: "pre:contents()[0]->5",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<pre>dom o edit </pre>",
+ start: "pre:contents()[0]->4",
+ },
+ },
+ {
+ name: "in pre: BACKSPACE within text, with space at beginning and end",
+ content: "<pre> dom to edit </pre>",
+ steps: [{
+ start: "pre:contents()[0]->10",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<pre> dom o edit </pre>",
+ start: "pre:contents()[0]->9",
+ },
+ },
+ {
+ name: "in pre: BACKSPACE within text, with one space at beginning and one at end",
+ content: "<pre> dom to edit </pre>",
+ steps: [{
+ start: "pre:contents()[0]->6",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<pre> dom o edit </pre>",
+ start: "pre:contents()[0]->5",
+ },
+ },
+
+ // list UL / OL
+
+ {
+ name: "from p to ul > li: BACKSPACE on whole list",
+ content: "<p>dom not to edit</p><ul><li><p>dom to edit</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->15",
+ end: "p:eq(1):contents()[0)->11",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom not to edit</p>",
+ start: "p:contents()[0]->15",
+ },
+ },
+ {
+ name: "in ul > second-li > p: BACKSPACE within text",
+ content: "<ul><li><p>dom to</p></li><li><p>edit</p></li></ul>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->4",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li><p>dom to</p></li><li><p>edi</p></li></ul>",
+ start: "p:eq(1):contents()[0]->3",
+ },
+ },
+ {
+ name: "in ul > second-li > empty-p: BACKSPACE at beginning",
+ content: "<ul><li><p><br></p></li><li><p><br></p></li></ul>",
+ steps: [{
+ start: "p:eq(1)->1",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li><p><br></p></li></ul>",
+ start: "p:first:contents()[0]->0",
+ },
+ },
+ {
+ name: "in ul > indented-li (no other li - p before): BACKSPACE at beginning",
+ content: "<p>dom to</p><ul><ul><li>edit</li></ul></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom to</p><ul><li>edit</li></ul>",
+ start: "li:contents()[0]->0",
+ },
+ },
+ {
+ name: "in ul > indented-li (no other li - p before): BACKSPACE -> 'a' at beginning",
+ content: "<p>dom to</p><ul><ul><li>edit</li></ul></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>dom to</p><ul><li>aedit</li></ul>",
+ start: "li:contents()[0]->1",
+ },
+ },
+ {
+ name: "in ul > indented-li (no other li - none before): BACKSPACE at beginning",
+ content: "<ul><ul><li>dom to edit</li></ul></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li>dom to edit</li></ul>",
+ start: "li:contents()[0]->0",
+ },
+ },
+ {
+ name: "in li: BACKSPACE on partial selection",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->1",
+ end: "li:contents()[0]->7",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li>dedit</li></ul>",
+ start: "li:contents()[0]->1",
+ },
+ },
+ {
+ name: "across 2 li: BACKSPACE on partial selection",
+ content: "<ul><li>dom to edit</li><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->1",
+ end: "li:eq(1):contents()[0]->7",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li>dedit</li></ul>",
+ start: "li:contents()[0]->1",
+ },
+ },
+ {
+ name: "in li (no other li): BACKSPACE on selection of all contents",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ end: "li:contents()[0]->11",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li><br></li></ul>",
+ start: "li->0",
+ },
+ },
+ {
+ name: "in li (no other li): BACKSPACE -> 'a' on selection of all contents",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ end: "li:contents()[0]->11",
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<ul><li><p>a</p></li></ul>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "in empty-li: BACKSPACE (must remove list)",
+ content: "<ul><li><br></li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><br></p>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in empty-li (no other li - empty-p before): BACKSPACE -> 'a'",
+ content: "<p><br></p><ul><li><br></li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><br></p><p>a</p>",
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in empty-li (no other li - p before): BACKSPACE",
+ content: "<p>toto</p><ul><li><br></li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>toto</p><p><br></p>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in li (no other li - p before): BACKSPACE at start",
+ content: "<p>toto</p><ul><li>&nbsp;<img src='/web_editor/static/src/img/transparent.png'></li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>toto</p><p>&nbsp;<img data-src=\"/web_editor/static/src/img/transparent.png\"></p>",
+ start: "p:eq(1):contents()[0]->0",
+ },
+ },
+ {
+ name: "in empty-indented-li (other li - no other indented-li): BACKSPACE",
+ content: "<ul><li><p>toto</p></li><ul><li><br></li></ul><li><p>tutu</p></li></ul>",
+ steps: [{
+ start: "ul ul li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li><p>toto</p></li><li><br></li><li><p>tutu</p></li></ul>",
+ start: "li:eq(1) br->0",
+ },
+ },
+ {
+ name: "in empty-indented-li (other li - other indented-li): BACKSPACE",
+ content: "<ul><li><p>toto</p></li><ul><li><br></li><li><br></li></ul><li><p>tutu</p></li></ul>",
+ steps: [{
+ start: "ul ul li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li><p>toto</p></li><li><br></li><ul><li><br></li></ul><li><p>tutu</p></li></ul>",
+ start: "li:eq(1) br->0",
+ },
+ },
+ {
+ name: "in empty-indented-li (no other li, no other indented-li): BACKSPACE",
+ content: "<ul><ul><li><br></li></ul></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li><br></li></ul>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in indented-li (other li, other indented-li): BACKSPACE at start",
+ content: "<ul><li><p>toto</p></li><ul><li><p>xxx</p></li><li><p>yyy</p></li></ul><li><p>tutu</p></li></ul>",
+ steps: [{
+ start: "ul ul li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li><p>toto</p></li><li><p>xxx</p></li><ul><li><p>yyy</p></li></ul><li><p>tutu</p></li></ul>",
+ start: "p:eq(1):contents()[0]->0",
+ },
+ },
+ {
+ name: "in li > second-p: BACKSPACE at start",
+ content: "<ul><li><p>toto</p></li><li><p>xxx</p><p>yyy</p></li><li><p>tutu</p></li></ul>",
+ steps: [{
+ start: "li:eq(1) p:eq(1):contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li><p>toto</p></li><li><p>xxxyyy</p></li><li><p>tutu</p></li></ul>",
+ start: "li:eq(1) p:contents()[0]->3",
+ },
+ },
+ {
+ name: "in li (li after): BACKSPACE at start, with spaces",
+ content: "<p>dom to edit&nbsp; </p><ul><li><p> &nbsp; dom to edit</p></li><li><p>dom not to edit</p></li></ul>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->6",
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom to edit&nbsp;dom to edit</p><ul><li><p>dom not to edit</p></li></ul>",
+ start: "p:contents()[0]->12",
+ },
+ },
+ {
+ name: "in li > p: BACKSPACE after single character",
+ content: "<ul><li><p>a</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li><p><br></p></li></ul>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in li > p: BACKSPACE -> 'a' after single character",
+ content: "<ul><li><p>a</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<ul><li><p>a</p></li></ul>",
+ start: "p:contents()[0]->1",
+ },
+ },
+
+ // end list UL / OL
+
+ {
+ name: "in p: BACKSPACE on selection of all contents",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "p:contents()[0]->11",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><br></p>",
+ start: "p->0",
+ },
+ },
+ {
+ name: "in p: BACKSPACE -> 'a' on selection of all contents",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "p:contents()[0]->11",
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>a</p>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "in complex-dom: BACKSPACE on selection of most contents",
+ content: "<p><b>dom</b></p><p><b>to<br>partially</b>re<i>m</i>ove</p>",
+ steps: [{
+ start: "b:contents()[0]->2",
+ end: "i:contents()[0]->1",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><b>do</b>ove</p>",
+ start: "b:contents()[0]->2",
+ },
+ },
+ {
+ name: "in complex-dom: BACKSPACE on selection of all contents",
+ content: "<p><b>dom</b></p><p><b>to<br>completely</b>remov<i>e</i></p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "i:contents()[0]->1",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><br></p>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in p: BACKSPACE after br",
+ content: "<p>dom <br>to edit</p>",
+ steps: [{
+ start: "p:contents()[2]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom to edit</p>",
+ start: "p:contents()[0]->4",
+ },
+ },
+ {
+ name: "in complex-dom (span > b -> ENTER in contents)",
+ content: "<span><b>dom<br></b></span><br><span><b>&nbsp;to edit</b></span>",
+ steps: [{
+ start: "b:eq(1):contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<span><b>dom<br>&nbsp;to edit</b></span>",
+ start: "b:contents(2)->0",
+ },
+ },
+ {
+ name: "in complex-dom (span > b -> ENTER in contents): 2 x BACKSPACE",
+ content: "<span><b>dom<br></b></span><br><span><b>a to edit</b></span>",
+ steps: [{
+ start: "b:eq(1):contents()[0]->1",
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<span><b>dom<br>&nbsp;to edit</b></span>",
+ start: "b:contents(2)->0",
+ },
+ },
+ {
+ name: "in p (hr before): BACKSPACE",
+ content: '<p>aaa</p><hr><p>bbb</p>',
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>aaabbb</p>',
+ start: "p:contents()[0]->3",
+ },
+ },
+ {
+ name: "in p with multi br[id] (p before and after) (1)",
+ content: '<p>dom not to edit</p><p><br id="br-1"><br id="br-2"><br id="br-3"><br id="br-4"></p><p>dom not to edit</p>',
+ steps: [{
+ start: "p:eq(1)->2",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>dom not to edit</p><p><br id="br-1"><br id="br-3"><br id="br-4"></p><p>dom not to edit</p>',
+ start: "p:eq(1)->1",
+ },
+ },
+ {
+ name: "in p with multi br[id] (p before and after) (2)",
+ content: '<p>dom not to edit</p><p><br id="br-1"><br id="br-2"><br id="br-3"><br id="br-4"></p><p>dom not to edit</p>',
+ steps: [{
+ start: "br:eq(2)->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>dom not to edit</p><p><br id="br-1"><br id="br-3"><br id="br-4"></p><p>dom not to edit</p>',
+ start: "p:eq(1)->1",
+ },
+ },
+ {
+ name: "in p with multi br[id] (p before and after): 2x BACKSPACE",
+ content: '<p>dom not to edit</p><p><br id="br-1"><br id="br-2"><br id="br-3"><br id="br-4"></p><p>dom not to edit</p>',
+ steps: [{
+ start: "p:eq(1)->2",
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>dom not to edit</p><p><br id="br-3"><br id="br-4"></p><p>dom not to edit</p>',
+ start: "p:eq(1)->0",
+ },
+ },
+ {
+ name: "in p with multi br (p before and after) (1)",
+ content: '<p>dom not to edit</p><p><br><br><br><br></p><p>dom not to edit</p>',
+ steps: [{
+ start: "p:eq(1)->2",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>dom not to edit</p><p><br><br><br></p><p>dom not to edit</p>',
+ start: "p:eq(1)->1",
+ },
+ },
+ {
+ name: "in p with multi br (p before and after) (2)",
+ content: '<p>dom not to edit</p><p><br><br><br><br></p><p>dom not to edit</p>',
+ steps: [{
+ start: "br:eq(2)->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>dom not to edit</p><p><br><br><br></p><p>dom not to edit</p>',
+ start: "p:eq(1)->1",
+ },
+ },
+ {
+ name: "in p with multi br (p before and after): 2x BACKSPACE",
+ content: '<p>dom not to edit</p><p><br><br><br><br></p><p>dom not to edit</p>',
+ steps: [{
+ start: "p:eq(1)->2",
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>dom not to edit</p><p><br><br></p><p>dom not to edit</p>',
+ start: "p:eq(1)->0",
+ },
+ },
+ {
+ name: "in p: BACKSPACE within text after \w+<br>",
+ content: '<p>dom to<br>edit</p>',
+ steps: [{
+ start: "p:contents()[2]->3",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>dom to<br>edt</p>',
+ start: "p:contents()[2]->2",
+ },
+ },
+
+ // table
+
+ {
+ name: "in empty-td (td before): BACKSPACE -> 'a' at start",
+ content: '<table class="table table-bordered"><tbody><tr><td><p><br></p></td><td><p><br></p></td></tr></tbody></table>',
+ steps: [{
+ start: "p:eq(1)->1",
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: '<table class="table table-bordered"><tbody><tr><td><p><br></p></td><td><p>a</p></td></tr></tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in td (td before): 2x BACKSPACE -> 'a' after first character",
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom not to edit</p></td><td><p>dom to edit</p></td></tr></tbody></table>',
+ steps: [{
+ start: "p:eq(1):contents()[0]->1",
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom not to edit</p></td><td><p>aom to edit</p></td></tr></tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in td (no other td): BACKSPACE within text",
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom to edit</p></td></tr></tbody></table>',
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom o edit</p></td></tr></tbody></table>',
+ start: "p:contents()[0]->4",
+ },
+ },
+ {
+ name: "in complex-dom (empty-td (td before) -> 2x SHIFT-ENTER): 3x BACKSPACE",
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom not to edit</p></td><td><p><br><br><br></p></td></tr></tbody></table>',
+ steps: [{
+ start: 'p:eq(1)->3',
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom not to edit</p></td><td><p><br></p></td></tr></tbody></table>',
+ start: "p:eq(1)->0",
+ },
+ },
+ {
+ name: "in complex-dom (empty-td (td before) -> 2x SHIFT-ENTER): 3x BACKSPACE -> 'a'",
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom not to edit</p></td><td><p><br><br><br></p></td></tr></tbody></table>',
+ steps: [{
+ start: 'p:eq(1)->3',
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom not to edit</p></td><td><p>a</p></td></tr></tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+
+ // merging non-similar blocks
+
+ {
+ name: "in p (h1 before): BACKSPACE at start",
+ content: '<h1>node to merge with</h1><p>node to merge</p>',
+ steps: [{
+ start: 'p:contents()[0]->0',
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<h1>node to merge withnode to merge</h1>',
+ start: "h1:contents()[0]->18",
+ },
+ },
+ {
+ name: "in empty-p (h1 before): BACKSPACE",
+ content: "<h1>dom to edit</h1><p><br></p>",
+ steps: [{
+ start: "p->1",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<h1>dom to edit</h1>",
+ start: "h1:contents()[0]->11",
+ },
+ },
+ {
+ name: "in empty-p (h1 before): 2x BACKSPACE",
+ content: "<h1>dom to edit</h1><p><br></p>",
+ steps: [{
+ start: "p->1",
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<h1>dom to edi</h1>",
+ start: "h1:contents()[0]->10",
+ },
+ },
+ {
+ name: "in p (ul before): BACKSPACE at start",
+ content: '<ul><li><p>node to merge with</p></li></ul><p>node to merge</p>',
+ steps: [{
+ start: 'p:eq(1):contents()[0]->0',
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<ul><li><p>node to merge withnode to merge</p></li></ul>',
+ start: "p:contents()[0]->18",
+ },
+ },
+ {
+ name: "in p > b (ul before, i after): BACKSPACE at start",
+ content: '<ul><li><p>node to merge with</p></li></ul><p><b>node</b><i> to merge</i></p>',
+ steps: [{
+ start: 'b:contents()[0]->0',
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<ul><li><p>node to merge with<b>node</b><i> to merge</i></p></li></ul>',
+ start: "p:contents()[0]->18",
+ },
+ },
+ {
+ name: "in p > b (ul > i before, i after): BACKSPACE at start",
+ content: '<ul><li><p><i>node to merge with</i></p></li></ul><p><b>node</b><i> to merge</i></p>',
+ steps: [{
+ start: 'b:contents()[0]->0',
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<ul><li><p><i>node to merge with</i><b>node</b><i> to merge</i></p></li></ul>',
+ start: "i:contents()[0]->18",
+ },
+ },
+ {
+ name: "in p.c (p.a > span.b before - span.b after): BACKSPACE at beginning",
+ content: "<p class=\"a\"><span class=\"b\">dom to&nbsp;</span></p><p class=\"c\"><span class=\"b\">edit</span></p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p class=\"a\"><span class=\"b\">dom to&nbsp;edit</span></p>",
+ start: "span:eq(0):contents()[0]->7",
+ },
+ },
+ {
+ name: "from h1 to p: BACKSPACE",
+ content: '<h1>node to merge with</h1><p>node to merge</p>',
+ steps: [{
+ start: 'h1:contents()[0]->5',
+ end: 'p:contents()[0]->2',
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<h1>node de to merge</h1>',
+ start: "h1:contents()[0]->5",
+ },
+ },
+ {
+ name: "from h1 to p: BACKSPACE at start",
+ content: '<h1><b>node to merge with</b></h1><p>node to merge</p>',
+ steps: [{
+ start: 'b:contents()[0]->0',
+ end: 'p:contents()[0]->2',
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>de to merge</p>',
+ start: "p:contents()[0]->0",
+ },
+ },
+];
+
+QUnit.test('Backspace', function (assert) {
+ var done = assert.async();
+ weTestUtils.createWysiwyg({
+ data: this.data,
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+ weTestUtils.testKeyboard($editable, assert, keyboardTestsBackspace).then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+var keyboardTestsDelete = [{
+ name: "in empty-p (no br): DELETE (must insert br)",
+ content: "<p></p>",
+ steps: [{
+ start: "p->0",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p><br></p>", // The br is there to ensure the carret can enter the p tag
+ start: "br->0",
+ },
+ },
+ {
+ name: "in empty-p: DELETE (must leave it unchanged)",
+ content: "<p><br></p>",
+ steps: [{
+ start: "p->0",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p><br></p>", // The br is there to ensure the carret can enter the p tag
+ start: "p->0",
+ },
+ },
+ {
+ name: "in empty-p: DELETE (must leave it unchanged) (2)",
+ content: "<p>\n <br>\n</p>",
+ steps: [{
+ start: "p->1",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>\n <br>\n</p>", // The br is there to ensure the carret can enter the p tag
+ start: "p:contents()[1]->0",
+ },
+ },
+ {
+ name: "in empty-p (p after): DELETE",
+ content: "<p><br></p><p>dom to edit</p>",
+ steps: [{
+ start: "p->0",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in empty-p (p after): DELETE (2)",
+ content: "<p>\n <br>\n</p><p>\n dom to edit</p>",
+ steps: [{
+ start: "p:contents()[1]->0",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in empty-p.a (p after): DELETE",
+ content: "<p class=\"a\"><br></p><p>dom to edit</p>",
+ steps: [{
+ start: "p->0",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in empty-p.a (p after): DELETE (2)",
+ content: "<p class=\"a\"><br></p><p>dom to edit</p>",
+ steps: [{
+ start: "p->1",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in p: DELETE at start",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>om to edit</p>", // The br is there to ensure the carret can enter the p tag
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in p: 2x DELETE at start",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'DELETE',
+ }, {
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>m to edit</p>", // The br is there to ensure the carret can enter the p tag
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in p: DELETE within text",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom t edit</p>",
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "in p: DELETE within text, with space at beginning",
+ content: "<p> dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->10",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom t edit</p>",
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "in p: DELETE within text, with one space at beginning",
+ content: "<p> dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->6",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>&nbsp;dom t edit</p>",
+ start: "p:contents()[0]->6",
+ },
+ },
+ {
+ name: "in p: DELETE within text, with space at end",
+ content: "<p>dom to edit </p>",
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom t edit</p>",
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "in p: DELETE within text, with one space at end",
+ content: "<p>dom to edit </p>",
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom t edit&nbsp;</p>",
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "in p: DELETE within text, with space at beginning and end",
+ content: "<p> dom to edit </p>",
+ steps: [{
+ start: "p:contents()[0]->10",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom t edit</p>",
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "in p: DELETE within text, with one space at beginning and one at end",
+ content: "<p> dom to edit </p>",
+ steps: [{
+ start: "p:contents()[0]->6",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>&nbsp;dom t edit&nbsp;</p>",
+ start: "p:contents()[0]->6",
+ },
+ },
+ {
+ name: "in p: DELETE -> 'a' within text, before \\w\\s",
+ content: "<p>dom t</p>",
+ steps: [{
+ start: "p:contents()[0]->2",
+ key: 'DELETE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>doa t</p>",
+ start: "p:contents()[0]->3",
+ },
+ },
+ {
+ name: "in pre: DELETE within text, with space at beginning",
+ content: "<pre> dom to edit</pre>",
+ steps: [{
+ start: "pre:contents()[0]->10",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<pre> dom t edit</pre>",
+ start: "pre:contents()[0]->10",
+ },
+ },
+ {
+ name: "in pre: DELETE within text, with one space at beginning",
+ content: "<pre> dom to edit</pre>",
+ steps: [{
+ start: "pre:contents()[0]->6",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<pre> dom t edit</pre>",
+ start: "pre:contents()[0]->6",
+ },
+ },
+ {
+ name: "in pre: DELETE within text, with space at end",
+ content: "<pre>dom to edit </pre>",
+ steps: [{
+ start: "pre:contents()[0]->5",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<pre>dom t edit </pre>",
+ start: "pre:contents()[0]->5",
+ },
+ },
+ {
+ name: "in pre: DELETE within text, with one space at end",
+ content: "<pre>dom to edit </pre>",
+ steps: [{
+ start: "pre:contents()[0]->5",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<pre>dom t edit </pre>",
+ start: "pre:contents()[0]->5",
+ },
+ },
+ {
+ name: "in pre: DELETE within text, with space at beginning and end",
+ content: "<pre> dom to edit </pre>",
+ steps: [{
+ start: "pre:contents()[0]->10",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<pre> dom t edit </pre>",
+ start: "pre:contents()[0]->10",
+ },
+ },
+ {
+ name: "in pre: DELETE within text, with one space at beginning and one at end",
+ content: "<pre> dom to edit </pre>",
+ steps: [{
+ start: "pre:contents()[0]->6",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<pre> dom t edit </pre>",
+ start: "pre:contents()[0]->6",
+ },
+ },
+
+ // list UL / OL
+
+ {
+ name: "in empty-li (no other li): DELETE (must do nothing)",
+ content: "<ul><li><br></li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li><br></li></ul>",
+ start: "li->0",
+ },
+ },
+ {
+ name: "from p to li > p: DELETE on whole list",
+ content: "<p>dom not to edit</p><ul><li><p>dom to edit</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->15",
+ end: "p:eq(1):contents()[0)->11",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom not to edit</p>",
+ start: "p:contents()[0]->15",
+ },
+ },
+ {
+ name: "in empty-li (no other li): DELETE -> 'a' (must write into it)",
+ content: "<ul><li><br></li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'DELETE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<ul><li><p>a</p></li></ul>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "in li (li after): DELETE at end (must move contents of second li to carret)",
+ content: "<ul><li>dom to&nbsp;</li><li>edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->7",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li>dom to&nbsp;edit</li></ul>",
+ start: "li:contents()[0]->7",
+ },
+ },
+ {
+ name: "in li (li after): DELETE -> 'a' at end (must move contents of second li to carret, then write)",
+ content: "<ul><li>dom to&nbsp;</li><li>edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->7",
+ key: 'DELETE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<ul><li>dom to aedit</li></ul>",
+ start: "li:contents()[0]->8",
+ },
+ },
+ {
+ name: "in li > p: DELETE before single character",
+ content: "<ul><li><p>a</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li><p><br></p></li></ul>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in li > p: DELETE -> 'a' before single character",
+ content: "<ul><li><p>a</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'DELETE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<ul><li><p>a</p></li></ul>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "in li > p (p after): DELETE at end",
+ content: "<ul><li><p>toto</p></li><li><p>xxx</p><p>yyy</p></li><li><p>tutu</p></li></ul>",
+ steps: [{
+ start: "li:eq(1) p:eq(0):contents()[0]->3",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li><p>toto</p></li><li><p>xxxyyy</p></li><li><p>tutu</p></li></ul>",
+ start: "li:eq(1) p:eq(0):contents()[0]->3",
+ },
+ },
+ {
+ name: "in li > p (b before - p after): DELETE at end",
+ content: "<ul><li><p>toto</p></li><li><p><b>x</b>xx</p><p><b>y</b>yy</p></li><li><p>tutu</p></li></ul>",
+ steps: [{
+ start: "li:eq(1) p:eq(0):contents()[1]->2",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li><p>toto</p></li><li><p><b>x</b>xx<b>y</b>yy</p></li><li><p>tutu</p></li></ul>",
+ start: "li:eq(1) p:eq(0):contents()[1]->2",
+ },
+ },
+ {
+ name: "in li > p (p.o_default_snippet_text after): DELETE at end",
+ content: '<ul><li><p>toto</p></li><li><p>xxx</p><p class="o_default_snippet_text">yyy</p></li><li><p>tutu</p></li></ul>',
+ steps: [{
+ start: "li:eq(1) p:eq(0):contents()[0]->3",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li><p>toto</p></li><li><p>xxxyyy</p></li><li><p>tutu</p></li></ul>",
+ start: "li:eq(1) p:eq(0):contents()[0]->3",
+ },
+ },
+ {
+ name: "in indented-ul > li: DELETE at end (must do nothing)",
+ content: "<ul><ul><li>dom to edit</li></ul></ul>",
+ steps: [{
+ start: "li:contents()[0]->11",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><ul><li>dom to edit</li></ul></ul>",
+ start: "li:contents()[0]->11",
+ },
+ },
+ {
+ name: "in indented-ul > li: DELETE at end -> 'a' at end (must write)",
+ content: "<ul><ul><li>dom to edit</li></ul></ul>",
+ steps: [{
+ start: "li:contents()[0]->11",
+ key: 'DELETE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<ul><ul><li>dom to edita</li></ul></ul>",
+ start: "li:contents()[0]->12",
+ },
+ },
+ {
+ name: "in indented-ul > li (non-indented-li after): DELETE at end",
+ content: "<ul><ul><li>dom to edit</li></ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->11",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><ul><li>dom to editdom to edit</li></ul></ul>",
+ start: "li:contents()[0]->11",
+ },
+ },
+ {
+ name: "in ul > li (indented-li after): DELETE at end",
+ content: "<ul><li>dom to edit</li><ul><li>dom to edit</li></ul></ul>",
+ steps: [{
+ start: "li:contents()[0]->11",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li>dom to editdom to edit</li></ul>",
+ start: "li:contents()[0]->11",
+ },
+ },
+ {
+ name: "in li: DELETE on partial selection",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->1",
+ end: "li:contents()[0]->7",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li>dedit</li></ul>",
+ start: "li:contents()[0]->1",
+ },
+ },
+ {
+ name: "across 2 li: DELETE on partial selection",
+ content: "<ul><li>dom to edit</li><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->1",
+ end: "li:eq(1):contents()[0]->7",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li>dedit</li></ul>",
+ start: "li:contents()[0]->1",
+ },
+ },
+ {
+ name: "in li: DELETE on selection of all contents",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ end: "li:contents()[0]->11",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li><br></li></ul>",
+ start: "li->0",
+ },
+ },
+ {
+ name: "in li: DELETE -> 'a' on selection of all contents",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ end: "li:contents()[0]->11",
+ key: 'DELETE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<ul><li><p>a</p></li></ul>",
+ start: "p:contents()[0]->1",
+ },
+ },
+
+ // end list UL / OL
+
+ {
+ name: "in p (b before): DELETE",
+ content: "<p><b>dom</b>to edit</p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p><b>dom</b>o edit</p>",
+ start: "p:contents(1)->0",
+ },
+ },
+ {
+ name: "in p > span.a (div > span.a after): DELETE at end (must do nothing)",
+ content: "<p><span class=\"a\">dom to</span></p><div><span class=\"a\">edit</span></div>",
+ steps: [{
+ start: "span:contents()[0]->6",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p><span class=\"a\">dom to</span></p><div><span class=\"a\">edit</span></div>",
+ start: "span:contents()[0]->6",
+ },
+ },
+ {
+ name: "in p: DELETE on partial selection",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ end: "p:contents()[0]->7",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dedit</p>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "across 2 p: DELETE on partial selection",
+ content: "<p>dom</p><p>to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ end: "p:eq(1):contents()[0]->3",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dedit</p>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "in p: DELETE on selection of all contents",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "p:contents()[0]->11",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p><br></p>",
+ start: "p->0",
+ },
+ },
+ {
+ name: "in p: DELETE -> 'a' on selection of all contents",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "p:contents()[0]->11",
+ key: 'DELETE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>a</p>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "in p: DELETE before br",
+ content: "<p>dom <br>to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->4",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom&nbsp;to edit</p>",
+ start: "p:contents()[0]->4",
+ },
+ },
+ {
+ name: "in complex-dom: DELETE on selection of most contents",
+ content: "<p><b>dom</b></p><p><b>to<br>partially</b>re<i>m</i>ove</p>",
+ steps: [{
+ start: "b:contents()[0]->2",
+ end: "i:contents()[0]->1",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p><b>do</b>ove</p>",
+ start: "b:contents()[0]->2",
+ },
+ },
+ {
+ name: "in complex-dom: DELETE on selection of all contents",
+ content: "<p><b>dom</b></p><p><b>to<br>completely</b>remov<i>e</i></p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "i:contents()[0]->1",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p><br></p>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in complex-dom (span > b -> ENTER): DELETE",
+ content: "<span><b>dom</b></span><br><span><b> to edit</b></span>",
+ steps: [{
+ start: "b:eq(0):contents()[0]->3",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<span><b>dom&nbsp;to edit</b></span>",
+ start: "b:contents()[0]->3",
+ },
+ },
+ {
+ name: "in p (span.fa after): DELETE",
+ content: '<p>aaa<span class="fa fa-star"></span>bbb</p>',
+ steps: [{
+ start: "p:contents()[0]->3",
+ key: 'DELETE',
+ }],
+ test: {
+ content: '<p>aaabbb</p>',
+ start: "p:contents()[0]->3",
+ },
+ },
+ {
+ name: "in p (hr after): DELETE",
+ content: '<p>aaa</p><hr><p>bbb</p>',
+ steps: [{
+ start: "p:eq(0):contents()[0]->3",
+ key: 'DELETE',
+ }],
+ test: {
+ content: '<p>aaabbb</p>',
+ start: "p:contents()[0]->3",
+ },
+ },
+
+ // table
+
+ {
+ name: "in empty-td (td after): DELETE -> 'a' at start",
+ content: '<table class="table table-bordered"><tbody><tr><td><p><br></p></td><td><p><br></p></td></tr></tbody></table>',
+ steps: [{
+ start: "p->1",
+ key: 'DELETE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: '<table class="table table-bordered"><tbody><tr><td><p>a</p></td><td><p><br></p></td></tr></tbody></table>',
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "in td (td after): 2x DELETE -> 'a' after first character",
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom to edit</p></td><td><p>dom not to edit</p></td></tr></tbody></table>',
+ steps: [{
+ start: "p:contents()[0]->1",
+ key: 'DELETE',
+ }, {
+ key: 'DELETE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: '<table class="table table-bordered"><tbody><tr><td><p>da to edit</p></td><td><p>dom not to edit</p></td></tr></tbody></table>',
+ start: "p:contents()[0]->2",
+ },
+ },
+ {
+ name: "in td: DELETE within text",
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom to edit</p></td></tr></tbody></table>',
+ steps: [{
+ start: "p:contents()[0]->4",
+ key: 'DELETE',
+ }],
+ test: {
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom o edit</p></td></tr></tbody></table>',
+ start: "p:contents()[0]->4",
+ },
+ },
+
+ // merging non-similar blocks
+
+ {
+ name: "in p (ul after, with spaces): DELETE at end (must bring contents of first li to end of p)",
+ content: "<p>dom to edit&nbsp; </p><ul><li><p> &nbsp; dom to edit</p></li><li><p>dom not to edit</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->11",
+ key: 'DELETE',
+ }, {
+ key: 'DELETE',
+ }, {
+ key: 'DELETE',
+ }, {
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom to editdom to edit</p><ul><li><p>dom not to edit</p></li></ul>",
+ start: "p:contents()[0]->11",
+ },
+ },
+ {
+ name: "in h1 (p after): DELETE at end",
+ content: '<h1>node to merge with</h1><p>node to merge</p>',
+ steps: [{
+ start: 'h1:contents()[0]->18',
+ key: 'DELETE',
+ }],
+ test: {
+ content: '<h1>node to merge withnode to merge</h1>',
+ start: "h1:contents()[0]->18",
+ },
+ },
+ {
+ name: "in h1 (empty-p after): DELETE at end",
+ content: "<h1>dom to edit</h1><p><br></p>",
+ steps: [{
+ start: "h1:contents()[0]->11",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<h1>dom to edit</h1>",
+ start: "h1:contents()[0]->11",
+ },
+ },
+ {
+ name: "in h1 (empty-p after): 2x DELETE at end",
+ content: "<h1>dom to edit</h1><p><br></p>",
+ steps: [{
+ start: "h1:contents()[0]->11",
+ key: 'DELETE',
+ }, {
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<h1>dom to edit</h1>",
+ start: "h1:contents()[0]->11",
+ },
+ },
+ {
+ name: "in li > p (p after): DELETE at end",
+ content: '<ul><li><p>node to merge with</p></li></ul><p>node to merge</p>',
+ steps: [{
+ start: 'p:contents()[0]->18',
+ key: 'DELETE',
+ }],
+ test: {
+ content: '<ul><li><p>node to merge withnode to merge</p></li></ul>',
+ start: "p:contents()[0]->18",
+ },
+ },
+ {
+ name: "in li > p (i before - p > b after): DELETE at end",
+ content: '<ul><li><p><i>node to merge</i> with</p></li></ul><p><b>node</b> to merge</p>',
+ steps: [{
+ start: 'p:contents()[1]->5',
+ key: 'DELETE',
+ }],
+ test: {
+ content: '<ul><li><p><i>node to merge</i>&nbsp;with<b>node</b> to merge</p></li></ul>',
+ start: "p:contents()[1]->5",
+ },
+ },
+ {
+ name: "in li > p > i (b before - p > b after): DELETE at end",
+ content: '<ul><li><p><b>node to </b><i>merge with</i></p></li></ul><p><b>node</b> to merge</p>',
+ steps: [{
+ start: 'i:contents()[0]->10',
+ key: 'DELETE',
+ }],
+ test: {
+ content: '<ul><li><p><b>node to </b><i>merge with</i><b>node</b> to merge</p></li></ul>',
+ start: "i:contents()[0]->10",
+ },
+ },
+ {
+ name: "in p.a > span.b (p.c > span.b after): DELETE at end",
+ content: "<p class=\"a\"><span class=\"b\">dom to&nbsp;</span></p><p class=\"c\"><span class=\"b\">edit</span></p>",
+ steps: [{
+ start: "span:contents()[0]->7",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p class=\"a\"><span class=\"b\">dom to&nbsp;edit</span></p>",
+ start: "span:contents()[0]->7",
+ },
+ },
+];
+
+var keyboardTestsDeleteDOM = [
+ '<div style="margin:0px; padding:0px;">',
+ ' <p style="margin:0px; padding:0px;">',
+ ' Dear Ready Mat',
+ ' <br><br>',
+ ' Here is',
+ ' the quotation <strong>SO003</strong>',
+ ' amounting in <strong>$&nbsp;1,127.50</strong>',
+ ' from YourCompany.',
+ ' <br><br>',
+ ' Do not hesitate to contact us if you have any question.',
+ ' <br><br>',
+ ' </p>',
+ ' <table width="100%">',
+ ' <tbody><tr>',
+ ' <td>',
+ ' <strong>Virtual Interior Design</strong><br>On Site ',
+ ' </td>',
+ ' <td width="100px" align="right">',
+ ' 10 Hour(s)',
+ ' </td>',
+ ' </tr>',
+ ' </tbody></table>',
+ '</div>',
+ ' '
+].join('\n');
+keyboardTestsDelete.push({
+ name: "in complex-dom: DELETE text selection",
+ content: keyboardTestsDeleteDOM,
+ steps: [{
+ start: "p:contents()[0]->20",
+ end: 'p:contents()[0]->23',
+ key: 'DELETE',
+ },
+ {
+ start: "p:contents()[10]->55",
+ end: 'p:contents()[10]->63',
+ key: 'DELETE',
+ },
+ ],
+ test: {
+ content: keyboardTestsDeleteDOM.replace('question', '').replace('Mat', ''),
+ start: "p:contents()[10]->55",
+ },
+}, {
+ name: "in complex-dom: DELETE br",
+ content: keyboardTestsDeleteDOM,
+ steps: [{
+ start: "p->2",
+ key: 'DELETE',
+ }, ],
+ test: {
+ content: keyboardTestsDeleteDOM.replace('<br>', ''),
+ start: "p:contents()[2]->0",
+ },
+}, {
+ name: "in complex-dom: DELETE selection (all strong text content)",
+ content: keyboardTestsDeleteDOM,
+ steps: [{
+ start: "strong:eq(0):contents()[0]->0",
+ end: 'strong:eq(0):contents()[0]->5',
+ key: 'DELETE',
+ }, ],
+ test: {
+ content: keyboardTestsDeleteDOM.replace(/<strong>SO003<\/strong>\s+/, '&nbsp;'),
+ start: "p:contents()[3]->43",
+ },
+}, {
+ name: "in complex-dom: DELETE selection (in strong text content before br)",
+ content: keyboardTestsDeleteDOM,
+ steps: [{
+ start: "strong:eq(2):contents()[0]->17",
+ end: 'strong:eq(2):contents()[0]->23',
+ key: 'DELETE',
+ }, ],
+ test: {
+ content: keyboardTestsDeleteDOM.replace(/Design/, ''),
+ start: "strong:eq(2):contents()[0]->17",
+ },
+});
+
+QUnit.test('Delete', function (assert) {
+ var done = assert.async();
+ weTestUtils.createWysiwyg({
+ data: this.data,
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+ weTestUtils.testKeyboard($editable, assert, keyboardTestsDelete).then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+});
+});
+});
diff --git a/addons/web_editor/static/tests/wysiwyg_snippets_tests.js b/addons/web_editor/static/tests/wysiwyg_snippets_tests.js
new file mode 100644
index 00000000000..2441897b36d
--- /dev/null
+++ b/addons/web_editor/static/tests/wysiwyg_snippets_tests.js
@@ -0,0 +1,403 @@
+odoo.define('web_editor.wysiwyg_snippets_tests', function (require) {
+"use strict";
+
+var testUtils = require('web.test_utils');
+var weTestUtils = require('web_editor.test_utils');
+var MediaDialog = require('wysiwyg.widgets.MediaDialog');
+
+
+QUnit.module('web_editor', {}, function () {
+QUnit.module('wysiwyg', {}, function () {
+QUnit.module('Snippets', {}, function () {
+
+QUnit.test('drag&drop', function (assert) {
+ var done = assert.async();
+ assert.expect(2);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value: '<p>toto toto toto</p><p>tata</p>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ var $hr = wysiwyg.snippets.$('.oe_snippet_thumbnail:first');
+ testUtils.dom.dragAndDrop($hr, $editable.find('p'));
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>toto toto toto</p><div class=\"s_hr pt32 pb32\"> <hr class=\"s_hr_1px s_hr_solid w-100 mx-auto\"> </div><p>tata</p>',
+ "should drop the snippet");
+
+ testUtils.mock.intercept(wysiwyg, "snippet_focused", function () {
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>toto toto toto</p><div class=\"s_hr pt32 pb32 built focus\"> <hr class=\"s_hr_1px s_hr_solid w-100 mx-auto\"> </div><p>tata</p>',
+ "should build and focus the snippet");
+
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+QUnit.test('begin drag&drop must hide popovers', function (assert) {
+ var done = assert.async();
+ assert.expect(2);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value:
+ '<div class="s_hr pt32 pb32"> ' +
+ '<hr class="s_hr_1px s_hr_solid w-100 mx-auto"> ' +
+ '<img class="img-fluid o_we_custom_image" data-src="/web_editor/static/src/img/transparent.png"> ' +
+ '</div>' +
+ '<h1>test</h1>' +
+ '<p><b>test</b></p>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ $editable.find('.s_hr img').mousedown().click();
+ assert.strictEqual($('.note-popover:visible').length, 1, "should display the image popover");
+
+ var $hr = wysiwyg.snippets.$('.oe_snippet_thumbnail:first');
+ testUtils.dom.dragAndDrop($hr, $editable.find('h1'), {
+ disableDrop: true,
+ });
+ assert.strictEqual($('.note-popover:visible').length, 0, "should hide the image popover");
+ testUtils.dom.dragAndDrop($hr, $editable.find('h1'), {
+ continueMove: true,
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('change size option must hide popovers', function (assert) {
+ var done = assert.async();
+ assert.expect(4);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value:
+ '<div class="s_hr pt32 pb32"> ' +
+ '<hr class="s_hr_1px s_hr_solid w-100 mx-auto"> ' +
+ '<img class="img-fluid o_we_custom_image" data-src="/web_editor/static/src/img/transparent.png"> ' +
+ '</div>' +
+ '<h1>test</h1>' +
+ '<p><b>test</b></p>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ $editable.find('.s_hr img').mousedown().click();
+ assert.strictEqual($('.note-popover:visible').length, 1, "should display the image popover");
+
+ var $handle = $('#oe_manipulators .o_handle.s');
+ testUtils.dom.dragAndDrop($handle, $editable.find('b'), {
+ disableDrop: true,
+ });
+ assert.ok($handle.hasClass('o_active'), "should active the handle");
+ assert.notOk($handle.hasClass('pb32'), "should change the padding bottom");
+ assert.strictEqual($('.note-popover:visible').length, 0, "should hide the image popover");
+ testUtils.dom.dragAndDrop($handle, $editable.find('b'), {
+ continueMove: true,
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('clean the dom before save, after drag&drop', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value: '<p>toto toto toto</p><p>tata</p>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ var $hr = wysiwyg.snippets.$('.oe_snippet_thumbnail:first');
+ testUtils.dom.dragAndDrop($hr, $editable.find('p'));
+
+ testUtils.mock.intercept(wysiwyg, "snippet_focused", function () {
+ wysiwyg.save().then(function (isDirty, html) {
+ assert.strictEqual(html.replace(/\s+/g, ' '),
+ '<p>toto toto toto</p><div class=\"s_hr pt32 pb32 built cleanForSave\"> <hr class=\"s_hr_1px s_hr_solid w-100 mx-auto\"> </div><p>tata</p>',
+ "should clean the snippet");
+
+ wysiwyg.destroy();
+ done();
+ });
+ });
+ });
+});
+
+QUnit.test('clean the dom before save', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value: '<div class="s_hr pt32 pb32"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ testUtils.mock.intercept(wysiwyg, "snippet_focused", function () {
+ // add dirty flag to remove warning because the cleaned dom is different of the initial value and no dirty flag
+ $editable.find('.s_hr').keydown();
+
+ // trigger change to avoid warning because the DOM change on clean without trigger onchange by snippet or edition
+ $editable.trigger('content_changed');
+
+ wysiwyg.save().then(function (isDirty, html) {
+ assert.strictEqual(html.replace(/\s+/g, ' '),
+ '<div class=\"s_hr pt32 pb32 cleanForSave\"> <hr class=\"s_hr_1px s_hr_solid w-100 mx-auto\"> </div>',
+ "should clean the snippet");
+
+ wysiwyg.destroy();
+ done();
+ });
+ });
+
+ $editable.find('.s_hr').mousedown().click();
+ });
+});
+
+QUnit.test('remove snippet', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value: '<div class="s_hr pt32 pb32"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ testUtils.mock.intercept(wysiwyg, "snippet_focused", function () {
+ $('#oe_manipulators .oe_overlay_options .oe_snippet_remove').click();
+
+ wysiwyg.save().then(function (isDirty, html) {
+ assert.strictEqual(html.replace(/\s+/g, ' '), '', "should remove the snippet");
+
+ wysiwyg.destroy();
+ done();
+ });
+ });
+
+ $editable.find('.s_hr').mousedown().click();
+ });
+});
+
+QUnit.test('move a snippet', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value: '<div class="s_hr pt32 pb32"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><h1>test</h1><p><b>test</b></p>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ var first = true;
+ testUtils.mock.intercept(wysiwyg, "snippet_focused", function () {
+ if (first) {
+ first = false;
+ var $hr = $('#oe_manipulators .oe_overlay_options .oe_snippet_move');
+ testUtils.dom.dragAndDrop($hr, $editable.find('b'));
+ } else {
+ wysiwyg.save().then(function (isDirty, html) {
+ assert.strictEqual(html.replace(/\s+/g, ' '),
+ '<h1>test</h1><div class="s_hr pt32 pb32 move cleanForSave"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><p><b>test</b></p>',
+ "should move the snippet on the bottom");
+
+ wysiwyg.destroy();
+ done();
+ });
+ }
+ });
+
+ $editable.find('.s_hr').mousedown().click();
+ });
+});
+
+QUnit.test('clone a snippet', function (assert) {
+ var done = assert.async();
+ assert.expect(2);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value: '<div class="s_hr pt32 pb32"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><h1>test</h1><p><b>test</b></p>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ testUtils.mock.intercept(wysiwyg, "snippet_focused", function () {
+ $('#oe_manipulators .oe_overlay_options .oe_snippet_clone').click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<div class="s_hr pt32 pb32 focus"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><div class="s_hr pt32 pb32 clone"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><h1>test</h1><p><b>test</b></p>',
+ "should duplicate the snippet");
+
+ wysiwyg.save().then(function (isDirty, html) {
+ assert.strictEqual(html.replace(/\s+/g, ' '),
+ '<div class="s_hr pt32 pb32 cleanForSave"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><div class="s_hr pt32 pb32 clone cleanForSave"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><h1>test</h1><p><b>test</b></p>',
+ "should duplicate the snippet");
+
+ wysiwyg.destroy();
+ done();
+ });
+ });
+
+ $editable.find('.s_hr').mousedown().click();
+ });
+});
+
+QUnit.test('customize snippet', function (assert) {
+ var done = assert.async();
+ assert.expect(3);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value: '<h1>test</h1><div class="s_hr pt32 pb32"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><p><b>test</b></p>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ testUtils.mock.intercept(wysiwyg, "snippet_focused", function () {
+ $('#oe_manipulators .oe_overlay_options .oe_options a:first').click();
+ var $option = $('#oe_manipulators .oe_overlay_options a[data-select-class="align-items-center"]');
+
+ assert.strictEqual($option.size(), 1, "should display the snippet option");
+
+ $option.click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<h1>test</h1><div class="s_hr pt32 pb32 focus align-items-center"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><p><b>test</b></p>',
+ "should customized the snippet");
+
+ $('#oe_manipulators .oe_overlay_options .oe_options a:first').click();
+ $('#oe_manipulators .oe_overlay_options a[data-select-class="align-items-end"]').click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<h1>test</h1><div class="s_hr pt32 pb32 focus align-items-end"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><p><b>test</b></p>',
+ "should twice customized the snippet");
+
+ wysiwyg.destroy();
+ done();
+ });
+
+ $editable.find('.s_hr').mousedown().click();
+ });
+});
+
+QUnit.test('background-image', function (assert) {
+ var done = assert.async();
+ assert.expect(2);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value:
+ '<h1>Big Title</h1>' +
+ '<section class="test_option_all pt32 pb32">' +
+ ' <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>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ var defMediaDialogInit = $.Deferred();
+ var defMediaDialogSave = $.Deferred();
+ testUtils.mock.patch(MediaDialog, {
+ init: function () {
+ this._super.apply(this, arguments);
+ this.opened(defMediaDialogInit.resolve.bind(defMediaDialogInit));
+ },
+ save: function () {
+ $.when(this._super.apply(this, arguments)).then(function () {
+ defMediaDialogSave.resolve();
+ });
+ },
+ });
+
+ testUtils.mock.intercept(wysiwyg, "snippet_focused", function () {
+ $('#oe_manipulators .oe_overlay_options .oe_options a:first').click();
+ $('#oe_manipulators .oe_overlay_options a.snippet-option-background[data-choose-image]').click();
+
+ defMediaDialogInit.then(function () {
+ $('.modal.note-image-dialog .existing-attachments .o_image').click();
+ $('.modal.note-image-dialog .modal-footer .btn-primary').click();
+ });
+
+ defMediaDialogSave.then(function () {
+ var $snippet = $editable.find('.test_option_all');
+ assert.ok($snippet.hasClass('oe_custom_bg'), "should use custom background");
+ var background = $snippet.css('background-image') || '';
+ assert.strictEqual(background.replace(/[^(\("'\/]+\/\/[^\("'\/]+/, ''),
+ 'url("/web_editor/static/src/img/transparent.png")',
+ "should change the background image");
+
+ testUtils.mock.unpatch(MediaDialog);
+ wysiwyg.destroy();
+ done();
+ });
+ });
+
+ $editable.find('h2').mousedown().click();
+ });
+});
+
+QUnit.test('update content', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value: '<div class="s_hr pt32 pb32"> ' +
+ '<img class="img-fluid o_we_custom_image test_option_all" data-src="/web_editor/static/src/img/transparent.png"> ' +
+ '</div>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ testUtils.mock.intercept(wysiwyg, "snippet_focused", function () {
+ var $snippetOption = $('#oe_manipulators .oe_overlay_options:visible');
+ wysiwyg.$el.siblings('.note-image-popover').find('.note-media .note-icon-trash').mousedown().click();
+ assert.ok($snippetOption.is(':hidden'), "should remove the snippet editor because the target are removed");
+
+ wysiwyg.destroy();
+ done();
+ });
+
+ $editable.find('img').mousedown().click();
+ });
+});
+
+});
+});
+});
+});
diff --git a/addons/web_editor/static/tests/wysiwyg_tests.js b/addons/web_editor/static/tests/wysiwyg_tests.js
new file mode 100644
index 00000000000..c2033848e67
--- /dev/null
+++ b/addons/web_editor/static/tests/wysiwyg_tests.js
@@ -0,0 +1,3859 @@
+odoo.define('web_editor.wysiwyg_tests', function (require) {
+"use strict";
+
+var AltDialog = require('wysiwyg.widgets.AltDialog');
+var ColorpickerDialog = require('wysiwyg.widgets.ColorpickerDialog');
+var CropDialog = require('wysiwyg.widgets.CropImageDialog');
+var LinkDialog = require('wysiwyg.widgets.LinkDialog');
+var MediaDialog = require('wysiwyg.widgets.MediaDialog');
+var testUtils = require('web.test_utils');
+var weTestUtils = require('web_editor.test_utils');
+var Wysiwyg = require('web_editor.wysiwyg');
+
+var testName = "";
+var carretTestSuffix = " (carret position)";
+
+QUnit.module('web_editor', {
+ afterEach: function () {
+ $('body').removeClass('modal-open');
+ $('.note-popover, .modal, .modal-backdrop').remove();
+ },
+}, function () {
+QUnit.module('wysiwyg', {}, function () {
+QUnit.module('toolbar', {}, function () {
+
+QUnit.test('Magic wand', function (assert) {
+ var done = assert.async();
+ assert.expect(8);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var $dropdownStyle = wysiwyg.$('.note-style .dropdown-toggle');
+ var $btnsStyle = wysiwyg.$('.note-style .dropdown-menu .dropdown-item');
+
+ var wandTests = [{
+ name: "Click H1: p -> h1",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ do: function () {
+ $dropdownStyle.mousedown().click();
+ $btnsStyle.find('h1').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><h1>dom to edit</h1>',
+ start: 'h1:contents()[0]->0',
+ end: 'h1:contents()[0]->11',
+ },
+ },
+ {
+ name: "Click CODE: h1 -> pre",
+ content: '<p>dom not to edit</p><h1>dom to edit</h1>',
+ start: 'h1:contents()[0]->1',
+ do: function () {
+ $dropdownStyle.mousedown().click();
+ $btnsStyle.find('pre').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><pre>dom to edit</pre>',
+ start: 'pre:contents()[0]->0',
+ end: 'pre:contents()[0]->11',
+ },
+ },
+ {
+ name: "Click NORMAL: pre -> p",
+ content: '<p>dom not to edit</p><pre>dom to edit</pre>',
+ start: 'pre:contents()[0]->1',
+ do: function () {
+ $dropdownStyle.mousedown().click();
+ $btnsStyle.find('p').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->0',
+ end: 'p:eq(1):contents()[0]->11',
+ },
+ },
+ {
+ name: "Click H1 in empty p: empty p -> empty h1",
+ content: '<p><br></p>',
+ start: 'p->1',
+ do: function () {
+ $dropdownStyle.mousedown().click();
+ $btnsStyle.find('h1').mousedown().click();
+ },
+ test: {
+ content: '<h1><br></h1>',
+ start: 'h1:contents()[0]->0',
+ },
+ },
+ ];
+
+ _.each(wandTests, function (test) {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ test.do();
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), weTestUtils.select(test.test.start, test.test.end, $editable), testName + carretTestSuffix);
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('Font style', function (assert) {
+ var done = assert.async();
+ assert.expect(56);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+ generateOptions: function (options) {
+ options.toolbar[1][1] = ['bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', 'clear'];
+ },
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var $btnBold = wysiwyg.$('.note-font .note-btn-bold');
+ var $btnItalic = wysiwyg.$('.note-font .note-btn-italic');
+ var $btnUnderline = wysiwyg.$('.note-font .note-btn-underline');
+ var $strikethrough = wysiwyg.$('.note-font .note-icon-strikethrough');
+ var $superscript = wysiwyg.$('.note-font .note-icon-superscript');
+ var $subscript = wysiwyg.$('.note-font .note-icon-subscript');
+ var $btnRemoveStyles = wysiwyg.$('.note-font .btn-sm .note-icon-eraser');
+
+ var styleTests = [
+ /* BOLD */
+ {
+ name: "Click BOLD: normal -> bold",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>d<b>om t</b>o edit</p>',
+ start: 'b:contents()[0]->0',
+ end: 'b:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click BOLD then 'a': normal -> bold (empty p)",
+ content: '<p><br></p>',
+ start: 'p->1',
+ do: function () {
+ $btnBold.mousedown().click();
+ weTestUtils.keydown('a', $editable);
+ },
+ test: {
+ content: '<p><b>a</b></p>',
+ start: 'b:contents()[0]->1',
+ },
+ },
+ {
+ name: "Click BOLD: normal -> bold (across paragraphs)",
+ content: '<p>dom to edit</p><p>dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p>d<b>om to edit</b></p><p><b>dom t</b>o edit</p>',
+ start: 'b:contents()[0]->0',
+ end: 'b:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click BOLD then 'a': normal -> bold (no selection)",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->4',
+ do: function () {
+ $btnBold.mousedown().click();
+ weTestUtils.keydown('a', $editable);
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom <b>a</b>to edit</p>',
+ start: 'b:contents()[0]->1',
+ },
+ },
+ {
+ name: "Click BOLD: bold -> normal",
+ content: '<p>dom not to edit</p><p><b>dom to edit</b></p>',
+ start: 'b:contents()[0]->0',
+ end: 'b:contents()[0]->11',
+ do: function () {
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->0',
+ end: 'p:eq(1):contents()[0]->11',
+ },
+ },
+ {
+ name: "Click BOLD: bold -> normal (partial selection)",
+ content: '<p>dom not to edit</p><p><b>dom to edit</b></p>',
+ start: 'b:contents()[0]->4',
+ end: 'b:contents()[0]->6',
+ do: function () {
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><b>dom </b>to<b>&nbsp;edit</b></p>',
+ start: 'p:eq(1):contents()[1]->0',
+ end: 'p:eq(1):contents()[1]->2',
+ },
+ },
+ {
+ name: "Click BOLD: bold -> normal (no selection)",
+ content: '<p>dom not to edit</p><p><b>dom to edit</b></p>',
+ start: 'b:contents()[0]->4',
+ do: function () {
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><b>dom </b>\u200B<b>to edit</b></p>',
+ start: 'p:eq(1):contents()[1]->1',
+ },
+ },
+ {
+ name: "Click BOLD: bold + normal -> normal",
+ content: '<p><b>dom </b>to edit</p>',
+ start: 'b:contents()[0]->1',
+ end: 'p:contents()[1]->4',
+ do: function () {
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p><b>dom to e</b>dit</p>',
+ start: 'b:contents()[0]->1',
+ end: 'b:contents()[0]->8',
+ },
+ },
+ {
+ name: "Click BOLD: normal -> bold (with fontawesome)",
+ content: '<p>aaa<span class="fa fa-heart"></span>bbb</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[2]->2',
+ do: function () {
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p>a<b>aa<span class="fa fa-heart"></span>bb</b>b</p>',
+ start: 'b:contents()[0]->0',
+ end: 'b:contents()[2]->2',
+ },
+ },
+ {
+ name: "Click BOLD + click BOLD: normal -> bold -> normal (with fontawesome)",
+ content: '<p>aaa<span class="fa fa-heart"></span>bbb</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[2]->2',
+ do: function () {
+ $btnBold.mousedown().click();
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p>aaa<span class="fa fa-heart"></span>bbb</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[2]->2',
+ },
+ },
+ {
+ name: "Click BOLD: bold -> normal (with fontawesome)",
+ content: '<p><b>aaa<span class="fa fa-heart"></span>bbb</b></p>',
+ start: 'b:contents()[0]->1',
+ end: 'b:contents()[2]->2',
+ do: function () {
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p><b>a</b>aa<span class="fa fa-heart"></span>bb<b>b</b></p>',
+ start: 'p:contents()[1]->0',
+ end: 'p:contents()[3]->2',
+ },
+ },
+ /* ITALIC */
+ {
+ name: "Click ITALIC: bold -> bold + italic",
+ content: '<p>dom not to edit</p><p><b>dom to edit</b></p>',
+ start: 'b:contents()[0]->1',
+ end: 'b:contents()[0]->5',
+ do: function () {
+ $btnItalic.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><b>d<i>om t</i>o edit</b></p>',
+ start: 'i:contents()[0]->0',
+ end: 'i:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click ITALIC: bold & normal -> italic & bold + italic (across paragraphs)",
+ content: '<p>dom <b>to</b> edit</p><p><b>dom to edit</b></p>',
+ start: 'p:contents()[0]->1',
+ end: 'b:eq(1):contents()[0]->5',
+ do: function () {
+ $btnItalic.mousedown().click();
+ },
+ test: {
+ content: '<p>d<i>om <b>to</b> edit</i></p><p><b><i>dom t</i>o edit</b></p>',
+ start: 'i:contents()[0]->0',
+ end: 'i:eq(1):contents()[0]->5',
+ },
+ },
+ /* UNDERLINE */
+ {
+ name: "Click UNDERLINE: bold -> bold + underlined",
+ content: '<p>dom not to edit</p><p><b>dom to edit</b></p>',
+ start: 'b:contents()[0]->1',
+ end: 'b:contents()[0]->5',
+ do: function () {
+ $btnUnderline.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><b>d<u>om t</u>o edit</b></p>',
+ start: 'u:contents()[0]->0',
+ end: 'u:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click UNDERLINE: bold & normal -> underlined & bold + underlined (across paragraphs)",
+ content: '<p>dom <b>to</b> edit</p><p><b>dom to edit</b></p>',
+ start: 'p:contents()[0]->1',
+ end: 'b:eq(1):contents()[0]->5',
+ do: function () {
+ $btnUnderline.mousedown().click();
+ },
+ test: {
+ content: '<p>d<u>om <b>to</b> edit</u></p><p><b><u>dom t</u>o edit</b></p>',
+ start: 'u:contents()[0]->0',
+ end: 'u:eq(1):contents()[0]->5',
+ },
+ },
+ /* strikethrough */
+ {
+ name: "Click strikethrough: bold -> bold + strikethrough",
+ content: '<p>dom not to edit</p><p><b>dom to edit</b></p>',
+ start: 'b:contents()[0]->1',
+ end: 'b:contents()[0]->5',
+ do: function () {
+ $strikethrough.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><b>d<s>om t</s>o edit</b></p>',
+ start: 's:contents()[0]->0',
+ end: 's:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click strikethrough: bold & normal -> strikethrough & bold + strikethrough (across paragraphs)",
+ content: '<p>dom <b>to</b> edit</p><p><b>dom to edit</b></p>',
+ start: 'p:contents()[0]->1',
+ end: 'b:eq(1):contents()[0]->5',
+ do: function () {
+ $strikethrough.mousedown().click();
+ },
+ test: {
+ content: '<p>d<s>om <b>to</b> edit</s></p><p><b><s>dom t</s>o edit</b></p>',
+ start: 's:contents()[0]->0',
+ end: 's:eq(1):contents()[0]->5',
+ },
+ },
+ /* superscript */
+ {
+ name: "Click superscript: bold -> bold + superscript",
+ content: '<p>dom not to edit</p><p><b>dom to edit</b></p>',
+ start: 'b:contents()[0]->1',
+ end: 'b:contents()[0]->5',
+ do: function () {
+ $superscript.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><b>d<sup>om t</sup>o edit</b></p>',
+ start: 'sup:contents()[0]->0',
+ end: 'sup:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click superscript: bold & normal -> superscript & bold + superscript (across paragraphs)",
+ content: '<p>dom <b>to</b> edit</p><p><b>dom to edit</b></p>',
+ start: 'p:contents()[0]->1',
+ end: 'b:eq(1):contents()[0]->5',
+ do: function () {
+ $superscript.mousedown().click();
+ },
+ test: {
+ content: '<p>d<sup>om <b>to</b> edit</sup></p><p><b><sup>dom t</sup>o edit</b></p>',
+ start: 'sup:contents()[0]->0',
+ end: 'sup:eq(1):contents()[0]->5',
+ },
+ },
+ /* subscript */
+ {
+ name: "Click subscript: bold -> bold + subscript",
+ content: '<p>dom not to edit</p><p><b>dom to edit</b></p>',
+ start: 'b:contents()[0]->1',
+ end: 'b:contents()[0]->5',
+ do: function () {
+ $subscript.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><b>d<sub>om t</sub>o edit</b></p>',
+ start: 'sub:contents()[0]->0',
+ end: 'sub:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click subscript: bold & normal -> subscript & bold + subscript (across paragraphs)",
+ content: '<p>dom <b>to</b> edit</p><p><b>dom to edit</b></p>',
+ start: 'p:contents()[0]->1',
+ end: 'b:eq(1):contents()[0]->5',
+ do: function () {
+ $subscript.mousedown().click();
+ },
+ test: {
+ content: '<p>d<sub>om <b>to</b> edit</sub></p><p><b><sub>dom t</sub>o edit</b></p>',
+ start: 'sub:contents()[0]->0',
+ end: 'sub:eq(1):contents()[0]->5',
+ },
+ },
+ /* REMOVE FONT STYLE */
+ {
+ name: "Click REMOVE FONT STYLE: bold -> normal",
+ content: '<p>dom not to edit</p><p><b>dom to edit</b></p>',
+ start: 'b:contents()[0]->1',
+ end: 'b:contents()[0]->5',
+ do: function () {
+ $btnRemoveStyles.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><b>d</b>om t<b>o edit</b></p>',
+ start: 'p:eq(1):contents()[1]->0',
+ end: 'p:eq(1):contents()[1]->4',
+ },
+ },
+ {
+ name: "Click REMOVE FONT STYLE: bold, italic, underlined & normal -> normal (across paragraphs)",
+ content: '<p>dom <b>t<i>o</i></b> e<u>dit</u></p><p><b><u>dom</u> to edit</b></p>',
+ start: 'p:contents()[0]->1',
+ end: 'u:eq(1):contents()[0]->3',
+ do: function () {
+ $btnRemoveStyles.mousedown().click();
+ },
+ test: {
+ content: '<p>dom to edit</p><p>dom<b>&nbsp;to edit</b></p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->3',
+ },
+ },
+ {
+ name: "Click REMOVE FONT STYLE: complex -> normal",
+ content: '<p>aaa<font style="background-color: rgb(255, 255, 0);">bbb</font></p><p><font style="color: rgb(255, 0, 0);">ccc</font></p>',
+ start: 'p:contents()[0]->1',
+ end: 'font:eq(1):contents()[0]->1',
+ do: function () {
+ $btnRemoveStyles.mousedown().click();
+ },
+ test: {
+ content: '<p>aaabbb</p><p>c<font style="color: rgb(255, 0, 0);">cc</font></p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->1',
+ },
+ },
+ {
+ name: "Click REMOVE FONT STYLE: complex -> normal (with icon)",
+ content: '<p>a<b>a</b>a<span class="bg-alpha text-alpha fa fa-heart" style="font-size: 10px;"></span>b<b><i>b</i>b</b></p>',
+ start: 'p:contents()[0]->0',
+ end: 'i:contents()[0]->1',
+ do: function () {
+ $btnRemoveStyles.mousedown().click();
+ },
+ test: {
+ content: '<p>aaa<span class="fa fa-heart"></span>bb<b>b</b></p>',
+ start: 'p:contents()[0]->0',
+ end: 'p:contents()[2]->2',
+ },
+ },
+ /* COMPLEX */
+ {
+ name: "COMPLEX Click BOLD: italic -> italic bold (partial selection)",
+ content: '<p>dom not to edit</p><p><i>dom to edit</i></p>',
+ start: 'i:contents()[0]->1',
+ end: 'i:contents()[0]->5',
+ do: function () {
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><i>d<b>om t</b>o edit</i></p>',
+ start: 'b:contents()[0]->0',
+ end: 'b:contents()[0]->4',
+ },
+ },
+ {
+ name: "COMPLEX Click BOLD then 'a': italic bold -> italic (across paragraphs)",
+ content: '<p><b><i>dom to edit</i></b></p><p><i><b>dom to edit</b></i></p>',
+ start: 'i:contents()[0]->1',
+ end: 'b:eq(1):contents()[0]->5',
+ do: function () {
+ $btnBold.mousedown().click();
+ weTestUtils.keydown('a', $editable, {
+ firstDeselect: true,
+ });
+ },
+ test: {
+ content: '<p><b><i>d</i></b><i>om to edit</i></p><p><i>dom ta<b>o edit</b></i></p>',
+ },
+ },
+ {
+ name: "COMPLEX Click BOLD then 'a': bold italic -> italic (no selection)",
+ content: '<p><b><i>dom to edit</i></b></p>',
+ start: 'i:contents()[0]->4',
+ do: function () {
+ $btnBold.mousedown().click();
+ weTestUtils.keydown('a', $editable, {
+ firstDeselect: true,
+ });
+ },
+ test: {
+ content: '<p><b><i>dom </i></b><i>a</i><b><i>to edit</i></b></p>',
+ },
+ },
+ {
+ name: "COMPLEX Click BOLD then 'a': underlined italic -> underlined italic bold (across paragraphs)",
+ content: '<p><u><i>dom to edit</i></u></p><p><i><u>dom to edit</u></i></p>',
+ start: 'i:contents()[0]->1',
+ end: 'u:eq(1):contents()[0]->5',
+ do: function () {
+ $btnBold.mousedown().click();
+ weTestUtils.keydown('a', $editable, {
+ firstDeselect: true,
+ });
+ },
+ test: {
+ content: '<p><u><i>d<b>om to edit</b></i></u></p><p><i><u><b>dom ta</b>o edit</u></i></p>',
+ },
+ },
+ {
+ name: "COMPLEX Click BOLD then 'a': underlined italic -> underlined italic bold (no selection)",
+ content: '<p><u><i>dom to edit</i></u></p>',
+ start: 'i:contents()[0]->1',
+ do: function () {
+ $btnBold.mousedown().click();
+ weTestUtils.keydown('a', $editable, {
+ firstDeselect: true,
+ });
+ },
+ test: {
+ content: '<p><u><i>d<b>a</b>om to edit</i></u></p>',
+ },
+ },
+ ];
+
+ _.each(styleTests, function (test) {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ test.end = test.end || test.start;
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ test.do();
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ $editable[0].normalize();
+ if (test.test.start) {
+ test.test.end = test.test.end || test.test.start;
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), weTestUtils.select(test.test.start, test.test.end, $editable), testName + carretTestSuffix);
+ }
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('Font size', function (assert) {
+ var done = assert.async();
+ assert.expect(10);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var $dropdownFontSize = wysiwyg.$('.note-fontsize .dropdown-toggle');
+ var $linksFontSize = wysiwyg.$('.note-fontsize .dropdown-menu .dropdown-item');
+
+ var sizeTests = [{
+ name: "Click 18: default -> 18px",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $dropdownFontSize.mousedown().click();
+ $linksFontSize.filter(':contains("18")').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>d<font style="font-size: 18px;">om t</font>o edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click DEFAULT: 18px -> default",
+ content: '<p>dom not to edit</p><p><font style="font-size: 18px;">dom to edit</font></p>',
+ start: 'font:contents()[0]->1',
+ end: 'font:contents()[0]->5',
+ do: function () {
+ $dropdownFontSize.mousedown().click();
+ $linksFontSize.filter(':contains("Default")').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><font style="font-size: 18px;">d</font>om t<font style="font-size: 18px;">o edit</font></p>',
+ start: 'p:eq(1):contents()[1]->0',
+ end: 'p:eq(1):contents()[1]->4',
+ },
+ },
+ {
+ name: "Click 18: 26px -> 18px",
+ content: '<p><font style="font-size: 26px;">dom to edit</font></p>',
+ start: 'font:contents()[0]->1',
+ end: 'font:contents()[0]->5',
+ do: function () {
+ $dropdownFontSize.mousedown().click();
+ $linksFontSize.filter(':contains("18")').mousedown().click();
+ },
+ test: {
+ content: '<p>' +
+ '<font style="font-size: 26px;">d</font>' +
+ '<font style="font-size: 18px;">om t</font>' +
+ '<font style="font-size: 26px;">o edit</font></p>',
+ start: 'font:eq(1):contents()[0]->0',
+ end: 'font:eq(1):contents()[0]->4',
+ },
+ },
+ {
+ name: "Click 18 then 'a' (no selection): default -> 18px",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ do: function () {
+ $dropdownFontSize.mousedown().click();
+ $linksFontSize.filter(':contains("18")').mousedown().click();
+ weTestUtils.keydown('a', $editable, {
+ firstDeselect: true,
+ });
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>d<font style="font-size: 18px;">a</font>om to edit</p>',
+ start: 'font:contents()[0]->1',
+ },
+ },
+ {
+ name: "Click 18 then 'a' (no selection): 26px -> 18px",
+ content: '<p><font style="font-size: 26px;">dom to edit</font></p>',
+ start: 'font:contents()[0]->1',
+ do: function () {
+ $dropdownFontSize.mousedown().click();
+ $linksFontSize.filter(':contains("18")').mousedown().click();
+ weTestUtils.keydown('a', $editable, {
+ firstDeselect: true,
+ });
+ },
+ test: {
+ content: '<p>' +
+ '<font style="font-size: 26px;">d</font>' +
+ '<font style="font-size: 18px;">a</font>' +
+ '<font style="font-size: 26px;">om to edit</font></p>',
+ start: 'font:eq(1):contents()[0]->1',
+ },
+ },
+ ];
+
+ _.each(sizeTests, function (test) {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ test.do();
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), weTestUtils.select(test.test.start, test.test.end, $editable), testName + carretTestSuffix);
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('Text forecolor', function (assert) {
+ var done = assert.async();
+ assert.expect(40);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var $dropdownForeColor = wysiwyg.$('.note-color .note-fore-color .dropdown-toggle');
+ var $btnsForeColor = wysiwyg.$('.note-color .note-fore-color .dropdown-menu .note-palette .note-color-btn');
+
+ var defColorpickerDialogInit;
+ testUtils.mock.patch(ColorpickerDialog, {
+ init: function () {
+ this._super.apply(this, arguments);
+ defColorpickerDialogInit = $.Deferred();
+ this.opened(defColorpickerDialogInit.resolve.bind(defColorpickerDialogInit));
+ },
+ });
+
+ var forecolorTests = [{
+ name: "Click THEME COLORS - ALPHA: default -> alpha theme color",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $dropdownForeColor.mousedown().click();
+ $btnsForeColor.filter('.bg-alpha').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>d<font class="text-alpha">om t</font>o edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click THEME COLORS - BLACK 25: alpha theme color & default -> black 25",
+ content: '<p>dom not to edit</p><p>do<font class="text-alpha">m to </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'font:contents()[0]->3',
+ do: function () {
+ $dropdownForeColor.mousedown().click();
+ $btnsForeColor.filter('.bg-black-25').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>d<font class="text-black-25">om t</font><font class="text-alpha">o </font>edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click COMMON COLORS - BLUE #0000FF: black 25 & default -> blue #0000FF",
+ content: '<p>dom not to edit</p><p>do<font class="text-black-25">m to </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'font:contents()[0]->3',
+ do: function () {
+ $dropdownForeColor.mousedown().click();
+ $btnsForeColor.filter('[style="background-color:#0000FF"]').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>d<font style="color: rgb(0, 0, 255);">om t</font><font class="text-black-25">o </font>edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click RESET TO DEFAULT: black 25 & default -> default",
+ content: '<p>dom not to edit</p><p>do<font class="text-black-25">m to </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'font:contents()[0]->3',
+ do: function () {
+ $dropdownForeColor.mousedown().click();
+ $btnsForeColor.filter('.note-color-reset').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom t<font class="text-black-25">o </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click CUSTOM COLORS then CUSTOM COLOR: blue #0000FF & default -> #875A7B",
+ async: true,
+ content: '<p>dom not to edit</p><p>do<font style="color: rgb(0, 0, 255);">m to </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'font:contents()[0]->3',
+ do: function () {
+ var self = this;
+ var def = $.Deferred();
+
+ $dropdownForeColor.mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color').mousedown().click();
+
+ defColorpickerDialogInit.then(function () {
+ $('.modal-dialog .o_hex_input').val('#875A7B').change();
+ $('.o_technical_modal .modal-footer .btn-primary:contains("Choose")').mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color-btn:last').mousedown().click();
+
+ assert.deepEqual(wysiwyg.getValue(),
+ '<p>dom not to edit</p><p>d<font style="color: rgb(135, 90, 123);">om t</font><font style="color: rgb(0, 0, 255);">o </font>edit</p>',
+ self.name);
+ var range = weTestUtils.select('font:contents()[0]->0', 'font:contents()[0]->4', $editable);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), range, self.name + carretTestSuffix);
+ def.resolve();
+ });
+ return def;
+ },
+ },
+ {
+ name: "Click CUSTOM COLORS then CUSTOM COLOR: change blue input",
+ content: '<p>dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[0]->6',
+ do: function () {
+ var self = this;
+ var def = $.Deferred();
+
+ $dropdownForeColor.mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color').mousedown().click();
+
+ defColorpickerDialogInit.then(function () {
+ $('.modal-dialog .o_blue_input').val('100').change();
+
+ assert.deepEqual($('.modal-dialog .o_hex_input').val(), '#ff0064', self.name + ' (hex)');
+ assert.deepEqual($('.modal-dialog .o_hue_input').val(), '337', self.name + ' (hue)');
+
+ $('.o_technical_modal .modal-footer .btn-primary:contains("Choose")').mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color-btn:last').mousedown().click();
+ def.resolve();
+ });
+ return def;
+ },
+ test: {
+ content: '<p>d<font style="color: rgb(255, 0, 100);">om to</font> edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->5',
+ },
+ },
+ {
+ name: "CUSTOM COLOR: change hue, saturation and lightness inputs",
+ content: '<p>dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[0]->6',
+ do: function () {
+ var self = this;
+ var def = $.Deferred();
+
+ $dropdownForeColor.mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color').mousedown().click();
+
+ defColorpickerDialogInit.then(function () {
+ $('.modal-dialog .o_hue_input').val('337').change();
+ $('.modal-dialog .o_saturation_input').val('50').change();
+ $('.modal-dialog .o_lightness_input').val('40').change();
+
+ assert.deepEqual($('.modal-dialog .o_hex_input').val(), '#99335a', self.name + ' (hex)');
+ assert.deepEqual($('.modal-dialog .o_green_input').val(), '51', self.name + ' (green)');
+
+ $('.o_technical_modal .modal-footer .btn-primary:contains("Choose")').mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color-btn:last').mousedown().click();
+ def.resolve();
+ });
+ return def;
+ },
+ test: {
+ content: '<p>d<font style="color: rgb(153, 51, 90);">om to</font> edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->5',
+ },
+ },
+ {
+ name: "CUSTOM COLOR: mousedown on area",
+ content: '<p>dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[0]->6',
+ do: function () {
+ var self = this;
+ var def = $.Deferred();
+
+ $dropdownForeColor.mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color').mousedown().click();
+
+ defColorpickerDialogInit.then(function () {
+ var $area = $('.modal-dialog .o_color_pick_area');
+ var pos = $area.offset();
+ $area.trigger($.Event("mousedown", {
+ which: 1,
+ pageX: pos.left + 50,
+ pageY: pos.top + 50
+ }));
+ $area.trigger('mouseup');
+
+ assert.deepEqual($('.modal-dialog .o_hex_input').val(), '#cfafaf', self.name + ' (hex)');
+ assert.deepEqual($('.modal-dialog .o_red_input').val(), '207', self.name + ' (red)');
+ assert.deepEqual($('.modal-dialog .o_green_input').val(), '175', self.name + ' (green)');
+ assert.deepEqual($('.modal-dialog .o_blue_input').val(), '175', self.name + ' (blue)');
+ assert.deepEqual($('.modal-dialog .o_hue_input').val(), '0', self.name + ' (hue)');
+ assert.deepEqual($('.modal-dialog .o_saturation_input').val(), '25', self.name + ' (saturation)');
+ assert.deepEqual($('.modal-dialog .o_lightness_input').val(), '75', self.name + ' (lightness)');
+
+ $('.o_technical_modal .modal-footer .btn-primary:contains("Choose")').mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color-btn:last').mousedown().click();
+ def.resolve();
+ });
+ return def;
+ },
+ test: {
+ content: '<p>d<font style="color: rgb(207, 175, 175);">om to</font> edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->5',
+ },
+ },
+ {
+ name: "CUSTOM COLOR: mousedow on sliders",
+ content: '<p>dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[0]->6',
+ do: function () {
+ var self = this;
+ var def = $.Deferred();
+
+ $dropdownForeColor.mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color').mousedown().click();
+
+ defColorpickerDialogInit.then(function () {
+ var $slider1 = $('.modal-dialog .o_slider_pointer');
+ var pos1 = $slider1.offset();
+ $slider1.trigger($.Event("mousedown", {
+ which: 1,
+ pageX: pos1.left,
+ pageY: pos1.top + 50
+ }));
+ $slider1.trigger('mouseup');
+
+ assert.deepEqual($('.modal-dialog .o_hex_input').val(), '#83ff00', self.name + ' (hex)');
+
+ var $slider2 = $('.modal-dialog .o_opacity_slider');
+ var pos2 = $slider2.offset();
+ $slider2.trigger($.Event("mousedown", {
+ which: 1,
+ pageX: pos2.left,
+ pageY: pos2.top + 80
+ }));
+ $slider2.trigger('mouseup');
+
+ assert.deepEqual($('.modal-dialog .o_hue_input').val(), '89', self.name + ' (hue)');
+ assert.deepEqual($('.modal-dialog .o_opacity_input').val(), '60', self.name + ' (opacity)');
+
+ $('.o_technical_modal .modal-footer .btn-primary:contains("Choose")').mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color-btn:last').mousedown().click();
+ def.resolve();
+ });
+ return def;
+ },
+ test: {
+ content: '<p>d<font style="color: rgba(131, 255, 0, 0.6);">om to</font> edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->5',
+ },
+ },
+ {
+ name: "Apply a color on a fontawesome",
+ content: '<p>dom <i class="fa fa-glass"/>not to edit</p>',
+ start: 'i->0',
+ do: function () {
+ $dropdownForeColor.mousedown().click();
+ $btnsForeColor.filter('[style="background-color:#0000FF"]').mousedown().click();
+ },
+ test: {
+ content: '<p>dom <i class="fa fa-glass" style="color: rgb(0, 0, 255);"></i>not to edit</p>',
+ start: 'p->2',
+ },
+ },
+ {
+ name: "Apply a color on a font with text",
+ content: '<p>dom <i class="fa fa-glass"/>not to edit</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[2]->6',
+ do: function () {
+ $dropdownForeColor.mousedown().click();
+ $btnsForeColor.filter('[style="background-color:#0000FF"]').mousedown().click();
+ },
+ test: {
+ content: '<p>d<font style="color: rgb(0, 0, 255);">om&nbsp;</font><i class="fa fa-glass" style="color: rgb(0, 0, 255);"></i><font style="color: rgb(0, 0, 255);">not to</font> edit</p>',
+ start: 'font:eq(0):contents()[0]->0',
+ end: 'font:eq(1):contents()[0]->6',
+ },
+ },
+ {
+ name: "Apply color, then 'a' (no selection)",
+ content: '<p>dom not to edit</p>',
+ start: 'p:contents()[0]->1',
+ do: function () {
+ $dropdownForeColor.mousedown().click();
+ $btnsForeColor.filter('[style="background-color:#0000FF"]').mousedown().click();
+ weTestUtils.keydown('a', $editable, {
+ firstDeselect: true,
+ });
+ },
+ test: {
+ content: '<p>d<font style="color: rgb(0, 0, 255);">a</font>om not to edit</p>',
+ start: 'font:contents()[0]->1',
+ },
+ },
+ {
+ name: "Apply color on two ranges with the same color",
+ content: '<p>do<br><span class="toto"> </span>m not to edit</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[3]->4',
+ do: function ($editable) {
+ $dropdownForeColor.mousedown().click();
+ $btnsForeColor.filter('[style="background-color:#0000FF"]').mousedown().click();
+
+ var range = weTestUtils.select('p:contents()[5]->3', 'p:contents()[5]->6', $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+
+ $dropdownForeColor.mousedown().click();
+ $btnsForeColor.filter('[style="background-color:#0000FF"]').mousedown().click();
+ },
+ test: {
+ content: '<p>d<font style="color: rgb(0, 0, 255);">o</font><br><span class="toto"> </span><font style="color: rgb(0, 0, 255);">m no</font>t t<font style=\"color: rgb(0, 0, 255);\">o e</font>dit</p>',
+ start: 'font:eq(2):contents()[0]->0',
+ end: 'font:eq(2):contents()[0]->3',
+ },
+ },
+ ];
+
+ var def = $.when();
+ _.each(forecolorTests, function (test) {
+ def = def.then(function () {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ return $.when(test.do($editable)).then(function () {
+ if (!test.async) {
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), weTestUtils.select(test.test.start, test.test.end, $editable), testName + carretTestSuffix);
+ }
+ });
+ });
+ });
+ def.then(function () {
+ testUtils.mock.unpatch(ColorpickerDialog);
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+QUnit.test('Text bgcolor', function (assert) {
+ var done = assert.async();
+ assert.expect(10);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+ var testName = "";
+ var carretTestSuffix = " (carret position)";
+
+ var $dropdownBgColor = wysiwyg.$('.note-color .note-bg-color .dropdown-toggle');
+ var $btnsBgColor = wysiwyg.$('.note-color .note-bg-color .dropdown-menu .note-palette .note-color-btn');
+
+ var defColorpickerDialogInit;
+ testUtils.mock.patch(ColorpickerDialog, {
+ init: function () {
+ this._super.apply(this, arguments);
+ defColorpickerDialogInit = $.Deferred();
+ this.opened(defColorpickerDialogInit.resolve.bind(defColorpickerDialogInit));
+ },
+ });
+
+ var bgcolorTests = [{
+ name: "Click THEME COLORS - ALPHA: default -> alpha theme color",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $dropdownBgColor.mousedown().click();
+ $btnsBgColor.filter('.bg-alpha').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>d<font class="bg-alpha">om t</font>o edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click THEME COLORS - BLACK 25: alpha theme color & default -> black 25",
+ content: '<p>dom not to edit</p><p>do<font class="bg-alpha">m to </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'font:contents()[0]->3',
+ do: function () {
+ $dropdownBgColor.mousedown().click();
+ $btnsBgColor.filter('.bg-black-25').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>d<font class="bg-black-25">om t</font><font class="bg-alpha">o </font>edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click COMMON COLORS - BLUE #0000FF: black 25 & default -> blue #0000FF",
+ content: '<p>dom not to edit</p><p>do<font class="bg-black-25">m to </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'font:contents()[0]->3',
+ do: function () {
+ $dropdownBgColor.mousedown().click();
+ $btnsBgColor.filter('[style="background-color:#0000FF"]').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>d<font style="background-color: rgb(0, 0, 255);">om t</font><font class="bg-black-25">o </font>edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click RESET TO DEFAULT: black 25 & default -> default",
+ content: '<p>dom not to edit</p><p>do<font class="bg-black-25">m to </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'font:contents()[0]->3',
+ do: function () {
+ $dropdownBgColor.mousedown().click();
+ $btnsBgColor.filter('.note-color-reset').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom t<font class="bg-black-25">o </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click CUSTOM COLORS then CUSTOM COLOR: blue #0000FF & default -> #875A7B",
+ async: true,
+ content: '<p>dom not to edit</p><p>do<font style="background-color: rgb(0, 0, 255);">m to </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'font:contents()[0]->3',
+ do: function () {
+ var def = $.Deferred();
+ testName = "Click CUSTOM COLORS then CUSTOM COLOR: blue #0000FF & default -> #875A7B";
+
+ $dropdownBgColor.mousedown().click();
+ wysiwyg.$('.note-color .note-bg-color .note-custom-color').mousedown().click();
+
+ defColorpickerDialogInit.then(function () {
+ $('.modal-dialog .o_hex_input').val('#875A7B').change();
+ $('.o_technical_modal .modal-footer .btn-primary:contains("Choose")').mousedown().click();
+ wysiwyg.$('.note-color .note-bg-color .note-custom-color-btn:last').mousedown().click();
+
+ assert.deepEqual(wysiwyg.getValue(),
+ '<p>dom not to edit</p><p>d<font style="background-color: rgb(135, 90, 123);">om t</font><font style="background-color: rgb(0, 0, 255);">o </font>edit</p>',
+ testName);
+ var range = weTestUtils.select('font:contents()[0]->0',
+ 'font:contents()[0]->4',
+ $editable);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), range, testName + carretTestSuffix);
+ def.resolve();
+ });
+ return def;
+ },
+ },
+ ];
+
+ var def = $.when();
+ _.each(bgcolorTests, function (test) {
+ def = def.then(function () {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ return $.when(test.do()).then(function () {
+ if (!test.async) {
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), weTestUtils.select(test.test.start, test.test.end, $editable), testName + carretTestSuffix);
+ }
+ });
+ });
+ });
+ def.then(function () {
+ testUtils.mock.unpatch(ColorpickerDialog);
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+QUnit.test('Unordered list', function (assert) {
+ var done = assert.async();
+ assert.expect(10);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var $btnUL = wysiwyg.$('.note-para .note-icon-unorderedlist');
+
+ var ulTests = [{
+ name: "Click UL: p -> ul",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $btnUL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><ul><li><p>dom to edit</p></li></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click UL: p -> ul (across paragraphs)",
+ content: '<p>dom not to edit</p><p>dom to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ do: function () {
+ $btnUL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><ul><li><p>dom to edit</p></li><li><p>dom to edit</p></li></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click UL: ul -> p",
+ content: '<p>dom not to edit</p><ul><li><p>dom to edit</p></li></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $btnUL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click UL: p -> ul (across li's)",
+ content: '<p>dom not to edit</p><ul><li><p>dom to edit</p></li><li><p>dom to edit</p></li></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ do: function () {
+ $btnUL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click UL: ul -> p (from second li)",
+ content: '<p>dom not to edit</p><ul><li><p>xxx</p></li><li><p>dom to edit</p></li></ul>',
+ start: 'li:eq(1) p:contents()[0]->1',
+ end: 'li:eq(1) p:contents()[0]->5',
+ do: function () {
+ $btnUL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><ul><li><p>xxx</p></li></ul><p>dom to edit</p>',
+ start: 'p:eq(2):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ },
+ },
+ ];
+
+ _.each(ulTests, function (test) {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ test.do();
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), weTestUtils.select(test.test.start, test.test.end, $editable), testName + carretTestSuffix);
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('Ordered list', function (assert) {
+ var done = assert.async();
+ assert.expect(32);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var $btnOL = wysiwyg.$('.note-para .note-icon-orderedlist');
+
+ var olTests = [{
+ name: "Click OL: p -> ol",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><ol><li><p>dom to edit</p></li></ol>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click OL: p -> ol (across paragraphs)",
+ content: '<p>dom not to edit</p><p>dom to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><ol><li><p>dom to edit</p></li><li><p>dom to edit</p></li></ol>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click OL: ol -> p",
+ content: '<p>dom not to edit</p><ol><li><p>dom to edit</p></li></ol>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click OL: p -> ol (across li's)",
+ content: '<p>dom not to edit</p><ol><li><p>dom to edit</p></li><li><p>dom to edit</p></li></ol>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click OL: ol -> p (from second li) (2)",
+ content: '<p>dom not to edit</p><ol><li><p>dom to edit</p></li><li><p>dom to edit</p></li></ol>',
+ start: 'li:eq(0) p:contents()[0]->1',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom to edit</p><ol><li><p>dom to edit</p></li></ol>',
+ start: 'p:eq(1):contents()[0]->1',
+ },
+ },
+ {
+ name: "Click OL: ol -> p (from second li)",
+ content: '<p>dom not to edit</p><ol><li><p>dom to edit</p></li><li><p>dom to edit</p></li></ol>',
+ start: 'li:eq(0) p:contents()[0]->1',
+ end: 'li:eq(0) p:contents()[0]->5',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom to edit</p><ol><li><p>dom to edit</p></li></ol>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click OL + Click OL: ul ul -> ul ol -> ul",
+ content: '<p>a</p>' +
+ '<ul>' +
+ '<li><p>b</p></li>' +
+ '<ul>' +
+ '<li><p>c</p></li>' +
+ '<li><p>d</p></li>' +
+ '</ul>' +
+ '<li><p>e</p></li>' +
+ '</ul>',
+ start: 'ul ul li:first:contents()[0]->1',
+ do: function () {
+ $btnOL.mousedown().click();
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<p>a</p>' +
+ '<ul>' +
+ '<li><p>b</p></li>' +
+ '<li><p>c</p></li>' +
+ '<ul>' +
+ '<li><p>d</p></li>' +
+ '</ul>' +
+ '<li><p>e</p></li>' +
+ '</ul>',
+ start: 'ul li:eq(1) p:contents()[0]->1',
+ },
+ },
+ {
+ name: "Click OL in empty table cell in div",
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'tr:eq(0) td:eq(1)->1',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><ol><li><p><br></p></li></ol></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'tr:eq(0) td:eq(1) br->0',
+ },
+ },
+ {
+ name: "Click OL in empty table cell in div (2)",
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'tr:eq(0) td:eq(1) br->0',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><ol><li><p><br></p></li></ol></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'tr:eq(0) td:eq(1) br->0',
+ },
+ },
+ {
+ name: "Click OL in empty table cell in div (3)",
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'tr:eq(0) td:eq(1)->0',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><ol><li><p><br></p></li></ol></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'tr:eq(0) td:eq(1) br->0',
+ },
+ },
+ {
+ name: "Click OL in table cell in div",
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td>aaa</td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'tr:eq(0) td:eq(1):contents(0)->1',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><ol><li><p>aaa</p></li></ol></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'td li p:contents(0)->1',
+ },
+ },
+ {
+ name: "Click OL on image in table cell in div",
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><img data-src="/web_editor/static/src/img/transparent.png"></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'img->0',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><ol><li><img data-src="/web_editor/static/src/img/transparent.png"></li></ol></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'td li->1',
+ },
+ },
+ {
+ name: "Click OL with selected LI in OL",
+ content: '<p>x</p>' +
+ '<ol>' +
+ '<li><p>aaa</p></li>' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ol>' +
+ '<p>y</p>',
+ start: 'p:eq(2):contents(0)->1',
+ end: 'p:eq(3):contents(0)->1',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ol>' +
+ '<li><p>aaa</p></li>' +
+ '</ol>' +
+ '<p>bbb</p>' +
+ '<p>ccc</p>' +
+ '<ol>' +
+ '<li><p>ddd</p></li>' +
+ '</ol>' +
+ '<p>y</p>',
+ start: 'p:eq(2):contents(0)->1',
+ end: 'p:eq(3):contents(0)->1',
+ },
+ },
+ {
+ name: "Click OL with selected LI in UL",
+ content: '<p>x</p>' +
+ '<ul>' +
+ '<li><p>aaa</p></li>' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ start: 'p:eq(2):contents(0)->1',
+ end: 'p:eq(3):contents(0)->1',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul>' +
+ '<li><p>aaa</p></li>' +
+ '</ul>' +
+ '<ol>' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '</ol>' +
+ '<ul>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ start: 'p:eq(2):contents(0)->1',
+ end: 'p:eq(3):contents(0)->1',
+ },
+ },
+ {
+ name: "Click OL after ENTER in p > b",
+ content: "<p><b>dom to edit</b></p>",
+ start: 'b:contents(0)->11',
+ do: function () {
+ weTestUtils.keydown(13, $editable); // ENTER
+ $btnOL.mousedown().click();
+ weTestUtils.keydown('a', $editable); // ENTER
+ },
+ test: {
+ content: "<p><b>dom to edit</b></p><ol><li><p><b>a</b></p></li></ol>",
+ start: 'b:eq(1):contents(0)->1',
+ },
+ },
+ {
+ name: "Click OL in font in H1 (with link) in div",
+ content: '<div><h1><font style="font-size: 62px;">table of contents <a href="p23">p23</a> (cfr: 34)</font></h1></div>',
+ start: 'font:contents(0)->11',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<div><ol><li><h1><font style="font-size: 62px;">table of contents <a href="p23">p23</a> (cfr: 34)</font></h1></li></ol></div>',
+ start: 'font:contents(0)->11',
+ },
+ },
+ ];
+
+ _.each(olTests, function (test) {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ $(range.sc).mousedown();
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ test.do();
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ if (wysiwyg.getValue() === test.test.content) {
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), weTestUtils.select(test.test.start, test.test.end, $editable), testName + carretTestSuffix);
+ } else {
+ assert.notOk(true, testName + ' (Wrong DOM)');
+ }
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('Align', function (assert) {
+ var done = assert.async();
+ assert.expect(20);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var $dropdownPara = wysiwyg.$('.note-para .dropdown-toggle');
+ var $btnAlignLeft = wysiwyg.$('.note-align .note-icon-align-left');
+ var $btnAlignCenter = wysiwyg.$('.note-align .note-icon-align-center');
+ var $btnAlignRight = wysiwyg.$('.note-align .note-icon-align-right');
+ var $btnAlignJustify = wysiwyg.$('.note-align .note-icon-align-justify');
+
+ var alignTests = [
+ /* ALIGN LEFT */
+ {
+ name: "Click ALIGN LEFT: p -> p align left (does nothing)",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignLeft.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ },
+ },
+ {
+ name: "Click ALIGN LEFT: p (parent align right) -> p align left (does nothing)",
+ content: '<div style="text-align: right;"><p>dom not to edit</p><p>dom to edit</p></div>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignLeft.mousedown().click();
+ },
+ test: {
+ content: '<div style="text-align: right;"><p>dom not to edit</p><p style="text-align: left;">dom to edit</p></div>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click ALIGN LEFT: p (parent align left) -> p align left (does nothing)",
+ content: '<div style="text-align: left;"><p>dom not to edit</p><p>dom to edit</p></div>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignLeft.mousedown().click();
+ },
+ test: {
+ content: '<div style="text-align: left;"><p>dom not to edit</p><p>dom to edit</p></div>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click ALIGN LEFT: p align justify & default -> p align right (across paragraphs)",
+ content: '<p>dom not to edit</p><p style="text-align: justify;">dom to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignRight.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p style="text-align: right;">dom to edit</p><p style="text-align: right;">dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ },
+ },
+ /* ALIGN CENTER */
+ {
+ name: "Click ALIGN CENTER: p -> p align center",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignCenter.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p style="text-align: center;">dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ },
+ },
+ {
+ name: "Click ALIGN CENTER: p align left & default -> p align center (across paragraphs)",
+ content: '<p>dom not to edit</p><p style="text-align: left;">dom to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignCenter.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p style="text-align: center;">dom to edit</p><p style="text-align: center;">dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ },
+ },
+ /* ALIGN RIGHT */
+ {
+ name: "Click ALIGN RIGHT: p align center -> p align right",
+ content: '<p>dom not to edit</p><p style="text-align: center;">dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignRight.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p style="text-align: right;">dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ },
+ },
+ {
+ name: "Click ALIGN RIGHT: p align center & default -> p align right (across paragraphs)",
+ content: '<p>dom not to edit</p><p style="text-align: center;">dom to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignRight.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p style="text-align: right;">dom to edit</p><p style="text-align: right;">dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ },
+ },
+ /* ALIGN JUSTIFY */
+ {
+ name: "Click ALIGN JUSTIFY: p align right -> p align justify",
+ content: '<p>dom not to edit</p><p style="text-align: right;">dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignJustify.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p style="text-align: justify;">dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ },
+ },
+ {
+ name: "Click ALIGN JUSTIFY: p align right & default -> p align justify (across paragraphs)",
+ content: '<p>dom not to edit</p><p style="text-align: right;">dom to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignJustify.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p style="text-align: justify;">dom to edit</p><p style="text-align: justify;">dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ },
+ },
+ ];
+
+ _.each(alignTests, function (test) {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ test.do();
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), weTestUtils.select(test.test.start, test.test.end, $editable), testName + carretTestSuffix);
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('Indent/outdent', function (assert) {
+ var done = assert.async();
+ assert.expect(18);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var $dropdownPara = wysiwyg.$('.note-para .dropdown-toggle');
+ var $btnIndent = wysiwyg.$('.note-list .note-btn:eq(1)');
+ var $btnOutdent = wysiwyg.$('.note-list .note-btn:first');
+
+ var indentTests = [
+ /* INDENT */
+ {
+ name: "Click INDENT: p -> indented p",
+ content: '<p>dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnIndent.mousedown().click();
+ },
+ test: {
+ content: '<p style="margin-left: 1.5em;">dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ },
+ },
+ {
+ name: "Click INDENT: li -> indented li",
+ content: '<ul><li><p>dom</p></li><li><p>to edit</p></li></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnIndent.mousedown().click();
+ },
+ test: {
+ content: '<ul><li><p>dom</p></li><li class="o_indent"><ul><li><p>to edit</p></li></ul></li></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ },
+ },
+ /* OUTDENT */
+ {
+ name: "Click OUTDENT: indented p -> p",
+ content: '<p style="margin-left: 1.5em;">dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnOutdent.mousedown().click();
+ },
+ test: {
+ content: '<p>dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ },
+ },
+ {
+ name: "Click OUTDENT: indented li -> li",
+ content: '<ul><li><p>dom</p></li><li><ul><li><p>to edit</p></li></ul></li></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnOutdent.mousedown().click();
+ },
+ test: {
+ content: '<ul><li><p>dom</p></li><li><p>to edit</p></li></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ },
+ },
+ {
+ name: "Click OUTDENT: indented li -> li (2)",
+ content: '<ul><li><p>dom</p></li><ul><li><p>to edit</p></li></ul></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnOutdent.mousedown().click();
+ },
+ test: {
+ content: '<ul><li><p>dom</p></li><li><p>to edit</p></li></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ },
+ },
+ {
+ name: "Click OUTDENT on LI in OL in OL",
+ content: '<p>x</p>' +
+ '<ol>' +
+ '<li><p>aaa</p></li>' +
+ '<li><ol>' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ol></li>' +
+ '<li><p>eee</p></li>' +
+ '</ol>' +
+ '<p>y</p>',
+ start: 'ol ol p:eq(1):contents(0)->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnOutdent.mousedown().click();
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ol>' +
+ '<li><p>aaa</p></li>' +
+ '<li><ol>' +
+ '<li><p>bbb</p></li>' +
+ '</ol></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><ol>' +
+ '<li><p>ddd</p></li>' +
+ '</ol></li>' +
+ '<li><p>eee</p></li>' +
+ '</ol>' +
+ '<p>y</p>',
+ start: 'ol:first > li:eq(2) p:contents(0)->1',
+ },
+ },
+ {
+ name: "Click OUTDENT on LI in OL in OL (2)",
+ content: '<p>x</p>' +
+ '<ol>' +
+ '<li><p>aaa</p></li>' +
+ '<ol>' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ol>' +
+ '<li><p>eee</p></li>' +
+ '</ol>' +
+ '<p>y</p>',
+ start: 'ol ol p:eq(1):contents(0)->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnOutdent.mousedown().click();
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ol>' +
+ '<li><p>aaa</p></li>' +
+ '<ol>' +
+ '<li><p>bbb</p></li>' +
+ '</ol>' +
+ '<li><p>ccc</p></li>' +
+ '<ol>' +
+ '<li><p>ddd</p></li>' +
+ '</ol>' +
+ '<li><p>eee</p></li>' +
+ '</ol>' +
+ '<p>y</p>',
+ start: 'li:eq(2) p:contents(0)->1',
+ },
+ },
+ {
+ name: "Click OUTDENT on LI in OL",
+ content:
+ '<ol>' +
+ '<li><p>aaa</p></li>' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '</ol>',
+ start: 'p:eq(1):contents(0)->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnOutdent.mousedown().click();
+ },
+ test: {
+ content:
+ '<ol>' +
+ '<li><p>aaa</p></li>' +
+ '</ol>' +
+ '<p>bbb</p>' +
+ '<ol>' +
+ '<li><p>ccc</p></li>' +
+ '</ol>',
+ start: 'p:eq(1):contents(0)->1',
+ },
+ },
+ {
+ name: "Click OUTDENT on P with indent in a LI (must outdent the p)",
+ content:
+ '<ul>' +
+ '<li>' +
+ '<ul>' +
+ '<li>' +
+ '<p style="margin-left: 1.5em;">dom</p>' +
+ '</li>' +
+ '</ul>' +
+ '</li>' +
+ '</ul>',
+ start: 'p:contents(0)->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnOutdent.mousedown().click();
+ },
+ test: {
+ content:
+ '<ul>' +
+ '<li>' +
+ '<ul>' +
+ '<li>' +
+ '<p>dom</p>' +
+ '</li>' +
+ '</ul>' +
+ '</li>' +
+ '</ul>',
+ start: 'p:contents(0)->1',
+ },
+ },
+ ];
+
+ _.each(indentTests, function (test) {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ test.do();
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), weTestUtils.select(test.test.start, test.test.end, $editable), testName + carretTestSuffix);
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('checklist', function (assert) {
+ var done = assert.async();
+ assert.expect(10);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var checklistTests = [
+ {
+ name: "check checkbox in checklist with children",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('li:first');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ {
+ name: "check checkbox in checklist with children (2)",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('li:first');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ {
+ name: "uncheck checkbox in checklist with children",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('li:first');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ {
+ name: "uncheck checkbox in checklist with children (2)",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('li:first');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ {
+ name: "uncheck checkbox in checklist in checklist",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('ul ul li:eq(1)');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ {
+ name: "uncheck checkbox in checklist in checklist (2)",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('ul ul li:eq(1)');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ {
+ name: "check checkbox in checklist in checklist",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('ul ul li:eq(1)');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ {
+ name: "check checkbox in checklist in checklist (2)",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('ul ul li:eq(1)');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ {
+ name: "check checkbox in checklist in checklist (full)",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('ul ul li:eq(1)');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ {
+ name: "check checkbox in checklist in checklist (full) (2)",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('ul ul li:eq(1)');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ ];
+
+ _.each(checklistTests, function (test) {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ test.do();
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('Link', function (assert) {
+ var done = assert.async();
+ assert.expect(19);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var $btnLink = wysiwyg.$('.note-insert .note-icon-link');
+
+ var defLinkDialogInit;
+ testUtils.mock.patch(LinkDialog, {
+ init: function () {
+ this._super.apply(this, arguments);
+ defLinkDialogInit = $.Deferred();
+ this.opened(defLinkDialogInit.resolve.bind(defLinkDialogInit));
+ },
+ save: function () {
+ defLinkDialogInit = null;
+ return this._super.apply(this, arguments);
+ },
+ });
+ var _clickLink = function (callbackInit, test) {
+ $btnLink.mousedown().click();
+ defLinkDialogInit.then(callbackInit).then(function () {
+ $('.modal-dialog .btn-primary:contains("Save")').mousedown().click();
+ if (test.check) {
+ test.check();
+ }
+ if (test.content) {
+ assert.deepEqual(wysiwyg.getValue(), test.content, testName);
+ }
+ if (test.start) {
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), range, testName + carretTestSuffix);
+ }
+ });
+ return defLinkDialogInit;
+ };
+
+ var linkTests = [{
+ name: "Click LINK: p -> a in p (w/ URL)",
+ async: true,
+ content: '<p>dom to edit</p>',
+ start: "p:contents()[0]->1",
+ end: "p:contents()[0]->5",
+ do: function () {
+ assert.strictEqual($('.modal-dialog #o_link_dialog_label_input').val(), 'om t', testName + ' (label)');
+ $('.modal-dialog #o_link_dialog_url_input').val('#');
+ },
+ test: {
+ content: '<p>d<a href="#">om t</a>o edit</p>',
+ start: 'a:contents()[0]->0',
+ end: 'a:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click LINK: p -> a in p (w/ URL) (no selection)",
+ async: true,
+ content: '<p>do edit</p>',
+ start: 'p:contents()[0]->1',
+ do: function () {
+ $('.modal-dialog #o_link_dialog_label_input').val('om t');
+ $('.modal-dialog #o_link_dialog_url_input').val('#');
+ },
+ test: {
+ content: '<p>d<a href="#">om t</a>o edit</p>',
+ start: 'p->2', // link not selected, the user can continue to write
+ end: 'p->2',
+ },
+ },
+ {
+ name: "Click LINK: a.btn in div -> a.btn.btn-outline-alpha in div (edit) (no selection)",
+ content: '<div><a href="#" class="btn btn-outline-alpha btn-lg">dom to edit</a></div>',
+ start: 'a:contents()[0]->5',
+ do: function () {
+ assert.strictEqual($('.modal-dialog #o_link_dialog_label_input').val(), 'dom to edit', testName + ' (label)');
+ $('.modal-dialog #o_link_dialog_url_input').val('#newlink');
+ },
+ test: {
+ content: '<div><a href="#newlink" class="btn btn-outline-alpha btn-lg">dom to edit</a></div>',
+ start: 'a->0',
+ end: 'a->1',
+ },
+ },
+ {
+ name: "Click LINK: p -> a in p (w/ Email)",
+ async: true,
+ content: '<p>dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[0]->5',
+ do: function () {
+ $('.modal-dialog #o_link_dialog_url_input').val('john.coltrane@example.com');
+ },
+ test: {
+ content: '<p>d<a href="mailto:john.coltrane@example.com">om t</a>o edit</p>',
+ start: 'a:contents()[0]->0',
+ end: 'a:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click LINK: p -> a in p (w/ URL & Size Large)",
+ async: true,
+ content: '<p>dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[0]->5',
+ do: function () {
+ $('.modal-dialog #o_link_dialog_url_input').val('#');
+ $('.modal-dialog [name="link_style_size"]').val("lg");
+ },
+ test: {
+ content: '<p>d<a href="#" class="btn-lg">om t</a>o edit</p>',
+ start: 'a:contents()[0]->0',
+ end: 'a:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click LINK: a in p -> a.btn-outline-alpha in p with alpha color and target=\"_blank\"",
+ async: true,
+ content: '<p><a href="#">dom to edit</a></p>',
+ start: 'a:contents()[0]->1',
+ do: function () {
+ $('.modal-dialog #o_link_dialog_url_input').val('#');
+ $('.modal-dialog [name="link_style_shape"]').val("outline");
+ $('.modal-dialog .o_link_dialog_color .o_link_dialog_color_item.btn-alpha').mousedown().click();
+ $('.modal-dialog .o_switch [name="is_new_window"]').mousedown().click();
+ },
+ test: {
+ content: '<p><a href="#" target="_blank" class="btn btn-outline-alpha">dom to edit</a></p>',
+ start: 'a->0',
+ end: 'a->1',
+ },
+ },
+ /* POPOVER */
+ {
+ name: "Click LINK in popover after adding link in p",
+ async: true,
+ content: '<p>dom to edit</p>',
+ start: "p:contents()[0]->1",
+ end: "p:contents()[0]->5",
+ do: function () {
+ $('.modal-dialog #o_link_dialog_url_input').val('/link');
+ },
+ test: {
+ check: function () {
+ $('.note-link-popover .note-btn .note-icon-link').mousedown().click();
+ defLinkDialogInit.then(function () {
+ assert.strictEqual($('.modal-dialog #o_link_dialog_label_input').val(), 'om t', testName + ' (label)');
+ assert.strictEqual($('.modal-dialog #o_link_dialog_url_input').val(), '/link', testName + ' (url)');
+ $('.modal-dialog #o_link_dialog_url_input').val('/newlink');
+ $('.modal-dialog .modal-footer .btn.btn-primary:contains("Save")').mousedown().click();
+ assert.deepEqual(wysiwyg.getValue(), '<p>d<a href="/newlink">om t</a>o edit</p>', testName);
+ });
+ },
+ },
+ },
+ {
+ name: "Click UNLINK in popover after adding link in p",
+ async: true,
+ content: '<p>dom to edit</p>',
+ start: "p:contents()[0]->1",
+ end: "p:contents()[0]->5",
+ do: function () {
+ $('.modal-dialog #o_link_dialog_url_input').val('/link');
+ },
+ test: {
+ content: '<p>dom to edit</p>',
+ check: function () {
+ $('.note-link-popover .note-btn .note-icon-chain-broken').mousedown().click();
+ var range = weTestUtils.select('p:contents()[0]->1', 'p:contents()[0]->5', $editable);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), range, testName + carretTestSuffix);
+ },
+ },
+ },
+ ];
+
+ var def = $.when();
+ _.each(linkTests, function (test) {
+ def = def.then(function () {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ return _clickLink(test.do, test.test);
+ });
+ });
+ def.then(function () {
+ testUtils.mock.unpatch(LinkDialog);
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+QUnit.test('Table', function (assert) {
+ var done = assert.async();
+ assert.expect(15);
+
+ function createTable(wysiwyg) {
+ wysiwyg.$('.note-table button:first').mousedown().click();
+
+ var $grid = wysiwyg.$('.note-table .note-dimension-picker-mousecatcher');
+ var pos = $grid.offset();
+ $grid.trigger($.Event('mousemove', {
+ pageX: pos.left + 40,
+ pageY: pos.top + 40,
+ }));
+ $grid.mousedown().click();
+ }
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {},
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ // create a table in an empty dom
+
+ wysiwyg.setValue('<p><br></p>');
+ var range = weTestUtils.select('p->1', 'p->1', $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+
+ createTable(wysiwyg);
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p><br></p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '</tbody></table>' +
+ '<p><br></p>',
+ "should create a table in an empty dom (p > br)");
+
+ // create a table at start of p with content
+
+ wysiwyg.setValue('<p>dom to edit</p>');
+ range = weTestUtils.select('p:contents()[0]->0', 'p:contents()[0]->0', $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+
+ createTable(wysiwyg);
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p><br></p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '</tbody></table>' +
+ '<p>dom to edit</p>',
+ "should create a table at start of single p with content");
+
+ // create a table at end of p with content
+
+ wysiwyg.setValue('<p>dom to edit</p>');
+ range = weTestUtils.select('p:contents()[0]->11', 'p:contents()[0]->11', $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+
+ createTable(wysiwyg);
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom to edit</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '</tbody></table>' +
+ '<p><br></p>',
+ "should create a table at end of single p with content");
+
+ // create a table within p
+
+ wysiwyg.setValue('<p>dom to edit</p>');
+ range = weTestUtils.select('p:contents()[0]->5', 'p:contents()[0]->5', $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+
+ createTable(wysiwyg);
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>',
+ "should create a table");
+
+ // remove a table
+
+ range = weTestUtils.select('td:first->0', 'td:first->0', $editable);
+ $(range.sc).mousedown();
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+
+ var $trash = $('.note-table-popover:visible button:has(.note-icon-trash)');
+ assert.strictEqual($trash.size(), 1, "should display the table popover");
+
+ $trash.mousedown().click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom to edit</p>',
+ "should remove the table");
+
+ // re create a table and table in table
+
+ createTable(wysiwyg);
+ createTable(wysiwyg);
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '</tbody></table>' +
+ '<p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>',
+ "should create a table inside the table");
+
+ // remove inner table
+
+ range = weTestUtils.select('td td:eq(1)->0', 'td td:eq(1)->0', $editable);
+ $(range.sc).mousedown();
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+
+ $trash = $('.note-table-popover:visible button:has(.note-icon-trash)');
+ assert.strictEqual($trash.size(), 1, "should display the table popover");
+ $trash.mousedown().click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>',
+ "should remove the inner table");
+
+ // use popover to change row and line
+
+ wysiwyg.setValue('<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p>0-0</p></td><td><p>0-1</p></td><td><p>0-2</p></td></tr>' +
+ '<tr><td><p>1-0</p></td><td><p>1-1</p></td><td><p>1-2</p></td></tr>' +
+ '<tr><td><p>2-0</p></td><td><p>2-1</p></td><td><p>2-2</p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>');
+
+ // remove a row
+
+ range = weTestUtils.select('td:eq(1)->0', 'td:eq(1)->0', $editable);
+ $(range.sc).mousedown();
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ $('.note-table-popover:visible button:has(.note-icon-col-remove)').mousedown().click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p>0-0</p></td><td><p>0-2</p></td></tr>' +
+ '<tr><td><p>1-0</p></td><td><p>1-2</p></td></tr>' +
+ '<tr><td><p>2-0</p></td><td><p>2-2</p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>',
+ "should remove a row");
+
+ // remove a line
+
+ range = weTestUtils.select('tr:eq(1) td:eq(1)->0', 'tr:eq(1) td:eq(1)->0', $editable);
+ $(range.sc).mousedown();
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ $('.note-table-popover:visible button:has(.note-icon-row-remove)').mousedown().click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p>0-0</p></td><td><p>0-2</p></td></tr>' +
+ '<tr><td><p>2-0</p></td><td><p>2-2</p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>',
+ "should remove a line");
+
+ // add a row after
+
+ range = weTestUtils.select('tr:eq(1) td:first->0', 'tr:eq(1) td:first->0', $editable);
+ $(range.sc).mousedown();
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ $('.note-table-popover:visible button:has(.note-icon-col-after)').mousedown().click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p>0-0</p></td><td><p><br></p></td><td><p>0-2</p></td></tr>' +
+ '<tr><td><p>2-0</p></td><td><p><br></p></td><td><p>2-2</p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>',
+ "should add a row after");
+
+ // add a row before
+
+ $(range.sc).mousedown();
+ $('.note-table-popover:visible button:has(.note-icon-col-before)').mousedown().click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p></td><td><p>0-0</p></td><td><p><br></p></td><td><p>0-2</p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p>2-0</p></td><td><p><br></p></td><td><p>2-2</p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>',
+ "should add a row before");
+
+ // add a line after
+
+ $(range.sc).mousedown();
+ $('.note-table-popover:visible button:has(.note-icon-row-below)').mousedown().click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p></td><td><p>0-0</p></td><td><p><br></p></td><td><p>0-2</p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p>2-0</p></td><td><p><br></p></td><td><p>2-2</p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>',
+ "should add a line after");
+
+ // add a line before
+
+ $(range.sc).mousedown();
+ $('.note-table-popover:visible button:has(.note-icon-row-above)').mousedown().click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p></td><td><p>0-0</p></td><td><p><br></p></td><td><p>0-2</p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p>2-0</p></td><td><p><br></p></td><td><p>2-2</p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>',
+ "should add a line before");
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('CodeView', function (assert) {
+ var done = assert.async();
+ assert.expect(8);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+ codeview: true,
+ },
+ }).then(function (wysiwyg) {
+ wysiwyg.setValue('<p>dom to edit <img src="/web_editor/static/src/img/transparent.png"></p>');
+
+ var $buttonCodeView = wysiwyg.$('button:has(.note-icon-code)');
+
+ // hide popover in CodeView mode and no range error when edit the content
+ var $editable = wysiwyg.getEditable();
+ $editable.find('img').mousedown().click();
+ assert.strictEqual($('.note-popover.note-image-popover:visible').length, 1, "should display the image popover");
+ $buttonCodeView.mousedown().click();
+ assert.strictEqual($('textarea.note-codable:visible').length, 1, "should show the CodeView textarea");
+ assert.strictEqual($editable.is(':visible'), false, "should hide the editable area");
+ assert.strictEqual($('.note-popover:visible').length, 0, "should hide all popovers");
+ assert.strictEqual($('.note-toolbar button:not(.disabled)').length, 1, "should disabled all buttons expect the codeview button");
+ wysiwyg.$('textarea.note-codable').val('<p>dom to edit a <img src="/web_editor/static/src/img/transparent.png"></p>');
+ $buttonCodeView.mousedown().click();
+ assert.strictEqual($('textarea.note-codable:visible').length, 0, "should hide the CodeView textarea");
+ assert.strictEqual($editable.is(':visible'), true, "should show the editable area");
+ assert.strictEqual($editable[0].style.height, '', "should reset the height (not auto or sizing)");
+ // end
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+});
+
+var defMediaDialogInit;
+var defMediaDialogSave;
+var defCropDialogInit;
+var defCropDialogSave;
+var defAltDialogInit;
+var defAltDialogSave;
+var imgWidth = 10;
+var imgHeight = 10;
+
+QUnit.module('Media', {
+ beforeEach: function () {
+ testUtils.mock.patch(MediaDialog, {
+ init: function () {
+ this._super.apply(this, arguments);
+ defMediaDialogInit = $.Deferred();
+ defMediaDialogSave = $.Deferred();
+ this.opened(defMediaDialogInit.resolve.bind(defMediaDialogInit));
+ },
+ save: function () {
+ $.when(this._super.apply(this, arguments)).then(function () {
+ var def = defMediaDialogSave;
+ defMediaDialogInit = null;
+ defMediaDialogSave = null;
+ def.resolve();
+ });
+ },
+ });
+ testUtils.mock.patch(CropDialog, {
+ init: function () {
+ $(arguments[2]).attr('src', $(arguments[2]).data('src'));
+ this._super.apply(this, arguments);
+ var self = this;
+ defCropDialogInit = $.Deferred();
+ defCropDialogSave = $.Deferred();
+ this.opened(function () {
+ var cropper = self.$cropperImage.data('cropper');
+ cropper.clone();
+ $.extend(cropper.image, {
+ naturalWidth: imgWidth,
+ naturalHeight: imgHeight,
+ aspectRatio: imgWidth / imgHeight,
+ });
+ cropper.loaded = true;
+ cropper.build();
+ cropper.render();
+ defCropDialogInit.resolve();
+ });
+ },
+ save: function () {
+ $.when(this._super.apply(this, arguments)).then(function () {
+ var def = defCropDialogSave;
+ defCropDialogInit = null;
+ defCropDialogSave = null;
+ def.resolve();
+ });
+ },
+ });
+ testUtils.mock.patch(AltDialog, {
+ init: function () {
+ this._super.apply(this, arguments);
+ defAltDialogInit = $.Deferred();
+ defAltDialogSave = $.Deferred();
+ this.opened(function () {
+ defAltDialogInit.resolve();
+ });
+ },
+ save: function () {
+ $.when(this._super.apply(this, arguments)).then(function () {
+ var def = defAltDialogSave;
+ defAltDialogInit = null;
+ defAltDialogSave = null;
+ def.resolve();
+ });
+ },
+ });
+ $('body').on('submit.WysiwygTests', function (ev) {
+ ev.preventDefault();
+ var $from = $(ev.target);
+ var iframe = $from.find('iframe[name="' + $from.attr('target') + '"]')[0];
+ if (iframe) {
+ iframe.contentWindow.attachments = [{
+ 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
+ }];
+ $(iframe).trigger('load');
+ }
+ });
+
+ this.data = {
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ mockRPC: function (route, args) {
+ if (args.model === 'ir.attachment' || !args.length) {
+ if (!args.length && route.indexOf('data:image/png;base64') === 0 ||
+ args.method === "search_read" &&
+ args.kwargs.domain[7][2].join(',') === "image/gif,image/jpe,image/jpeg,image/jpg,image/gif,image/png") {
+ return $.when(this.data.records);
+ }
+ }
+ if (route.indexOf('youtube') !== -1) {
+ return $.when();
+ }
+ return this._super(route, args);
+ },
+ };
+ },
+ afterEach: function () {
+ testUtils.mock.unpatch(MediaDialog);
+ testUtils.mock.unpatch(CropDialog);
+ testUtils.mock.unpatch(AltDialog);
+ $('body').off('submit.WysiwygTests');
+ },
+}, function () {
+
+
+var _clickMedia = function (wysiwyg, assert, callbackInit, test) {
+ wysiwyg.$('.note-insert .fa-file-image-o').mousedown().click();
+
+ defMediaDialogSave.then(function () {
+ if (test.check) {
+ test.check();
+ }
+ if (test.content) {
+ assert.deepEqual(wysiwyg.getValue(), test.content, testName);
+ }
+ if (test.start) {
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), range, testName + carretTestSuffix);
+ }
+ });
+ return defMediaDialogInit.then(function () {
+ callbackInit();
+ var def = $.Deferred();
+ setTimeout(function () {
+ def.resolve();
+ }, 0);
+ return def;
+ });
+};
+var _uploadAndInsertImg = function (url) {
+ $('.modal-dialog input[name="url"]:first').val(url).trigger('input');
+ $('.modal-dialog .o_upload_media_url_button:first').mousedown().click();
+};
+var _insertVideo = function (wysiwyg, assert, url, checkOptions) {
+ $('.modal-dialog .nav-link:contains("Video")').mousedown().click();
+ $('.modal-dialog #o_video_text').val(url).trigger('change');
+ if (checkOptions) {
+ assert.strictEqual($('.modal-dialog .o_yt_option').parent().css('display'), 'block', testName + ' (options)');
+ }
+ $('.modal-dialog .modal-footer .btn.btn-primary').mousedown().click();
+};
+var _insertPictogram = function (className) {
+ $('.modal-dialog .nav-link:contains("Pictogram")').mousedown().click();
+ $('.modal-dialog .font-icons-icons .font-icons-icon.fa.' + className).mousedown().click();
+ $('.modal-dialog .modal-footer .btn.btn-primary').mousedown().click();
+};
+var _valueToRatio = function (value) {
+ return value < 0 ? 1 / (1 - value) : 1 + value;
+};
+
+
+QUnit.test('Image', function (assert) {
+ var done = assert.async();
+ assert.expect(13);
+
+ return weTestUtils.createWysiwyg(this.data).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var mediaTests = [{
+ name: "Click ADD AN IMAGE URL in empty p: p -> img in p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ content: '<p>\u200B<img class="img-fluid o_we_custom_image" data-src="/web_editor/static/src/img/transparent.png">\u200B</p>',
+ check: function () {
+ assert.strictEqual($('.note-image-popover').css('display'), 'block', testName + ' (popover)');
+ },
+ },
+ },
+ {
+ name: "add an image in a table",
+ async: true,
+ content: '<section><div class="container"><div class="row"><div class="col-lg-6">' +
+ '<table class="table table-bordered">' +
+ ' <tbody>' +
+ ' <tr>' +
+ ' <td>' +
+ ' aaa' +
+ ' </td>' +
+ ' <td>' +
+ ' bbb' +
+ ' </td>' +
+ ' <td>' +
+ ' ccc' +
+ ' </td>' +
+ ' </tr>' +
+ ' </tbody>' +
+ '</table>' +
+ '</div></div></div></section>',
+ start: "td:eq(1):contents()[0]->18",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ content: '<section><div class="container"><div class="row"><div class="col-lg-6">' +
+ '<table class="table table-bordered">' +
+ ' <tbody>' +
+ ' <tr>' +
+ ' <td>' +
+ ' aaa' +
+ ' </td>' +
+ ' <td>' +
+ ' bb<img class="img-fluid o_we_custom_image" data-src="/web_editor/static/src/img/transparent.png">b' +
+ ' </td>' +
+ ' <td>' +
+ ' ccc' +
+ ' </td>' +
+ ' </tr>' +
+ ' </tbody>' +
+ '</table>' +
+ '</div></div></div></section>',
+ check: function () {
+ assert.strictEqual($('.note-image-popover').css('display'), 'block', testName + ' (popover)');
+ },
+ },
+ },
+ {
+ name: "add an image in an empty table",
+ async: true,
+ content: '<section><div class="container"><div class="row"><div class="col-lg-6">' +
+ '<table class="table table-bordered">' +
+ ' <tbody>' +
+ ' <tr>' +
+ ' <td>' +
+ ' <br>' +
+ ' </td>' +
+ ' <td>' +
+ ' <br>' +
+ ' </td>' +
+ ' <td>' +
+ ' <br>' +
+ ' </td>' +
+ ' </tr>' +
+ ' </tbody>' +
+ '</table>' +
+ '</div></div></div></section>',
+ start: "br:eq(1)->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ content: '<section><div class="container"><div class="row"><div class="col-lg-6">' +
+ '<table class="table table-bordered">' +
+ ' <tbody>' +
+ ' <tr>' +
+ ' <td>' +
+ ' <br>' +
+ ' </td>' +
+ ' <td>' +
+ ' <img class="img-fluid o_we_custom_image" data-src="/web_editor/static/src/img/transparent.png"><br>' +
+ ' </td>' +
+ ' <td>' +
+ ' <br>' +
+ ' </td>' +
+ ' </tr>' +
+ ' </tbody>' +
+ '</table>' +
+ '</div></div></div></section>',
+ check: function () {
+ assert.strictEqual($('.note-image-popover').css('display'), 'block', testName + ' (popover)');
+ },
+ },
+ },
+ /* IMAGE POPOVER */
+ {
+ name: "Click PADDING XL in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ content: '<p>\u200B<img class="img-fluid o_we_custom_image padding-xl" data-src="/web_editor/static/src/img/transparent.png">\u200B</p>',
+ check: function () {
+ $('.note-image-popover .note-padding .dropdown-toggle').mousedown().click();
+ $('.note-image-popover .note-padding .dropdown-item:contains("Xl")').mousedown().click();
+ },
+ },
+ },
+ {
+ name: "Click IMAGE SIZE 25% in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ content: '<p>\u200B<img class="img-fluid o_we_custom_image" data-src="/web_editor/static/src/img/transparent.png" style="width: 25%;">\u200B</p>',
+ check: function () {
+ $('.note-image-popover .note-imagesize .note-btn:contains(25%)').mousedown().click();
+ },
+ },
+ },
+ {
+ name: "Click FLOAT RIGHT in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ content: '<p>\u200B<img class="img-fluid o_we_custom_image pull-right" data-src="/web_editor/static/src/img/transparent.png">\u200B</p>',
+ check: function () {
+ $('.note-image-popover .note-float .note-icon-align-right').mousedown().click();
+ },
+ },
+ },
+ {
+ name: "Click FLOAT CENTER then FLOAT LEFT in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ content: '<p>\u200B<img class="img-fluid o_we_custom_image pull-left" data-src="/web_editor/static/src/img/transparent.png">\u200B</p>',
+ check: function () {
+ $('.note-image-popover .note-float .note-icon-align-center').mousedown().click();
+ $('.note-image-popover .note-float .note-icon-align-left').mousedown().click();
+ },
+ },
+ },
+ {
+ name: "Click SHAPE ROUNDED in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ content: '<p>\u200B<img class="img-fluid o_we_custom_image rounded" data-src="/web_editor/static/src/img/transparent.png">\u200B</p>',
+ check: function () {
+ $('.note-image-popover .note-imageShape .note-btn:has(.fa-square)').mousedown().click();
+ },
+ },
+ },
+ // Remove picture
+ {
+ name: "Click REMOVE in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ content: '<p><br></p>',
+ check: function () {
+ $('.note-image-popover .note-btn .note-icon-trash').mousedown().click();
+ },
+ },
+ },
+ // Describe picture
+ {
+ name: "Click DESCRIPTION in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ check: function () {
+ $('.note-image-popover .note-btn:contains("Description")').mousedown().click();
+ defAltDialogSave.then(function () {
+ assert.deepEqual(wysiwyg.getValue(),
+ '<p>\u200B<img class="img-fluid o_we_custom_image" data-src="/web_editor/static/src/img/transparent.png" alt="Description" title="Title">\u200B</p>',
+ testName);
+ });
+ defAltDialogInit.then(function () {
+ $('.modal-dialog input#alt').val('Description');
+ $('.modal-dialog input#title').val('Title');
+ $('.modal-dialog .modal-footer .btn.btn-primary:contains("Save")').mousedown().click();
+ });
+ },
+ },
+ },
+ ];
+
+ var def = $.when();
+ _.each(mediaTests, function (test) {
+ def = def.then(function () {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ return _clickMedia(wysiwyg, assert, test.do, test.test);
+ });
+ });
+ def.then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+QUnit.test('Image crop', function (assert) {
+ var done = assert.async();
+ assert.expect(5);
+
+ return weTestUtils.createWysiwyg(this.data).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var mediaTests = [{
+ name: "Click CROP 16:9 + ZOOM IN in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ check: function () {
+ var zoomRatio;
+ $('.note-image-popover .note-btn:has(.fa-crop)').mousedown().click();
+ defCropDialogSave.then(function () {
+ var $img = $(wysiwyg.getValue()).find('img.o_cropped_img_to_save');
+ assert.strictEqual($img.data('aspect-ratio'), '16/9', testName + " (aspect-ratio)");
+ assert.strictEqual($img.data('width'), imgWidth / zoomRatio, testName + " (zoom)");
+ });
+ defCropDialogInit.then(function () {
+ $('.o_crop_image_dialog .o_crop_options .btn:contains("16:9")').mousedown().click();
+ var $zoomBtn = $('.o_crop_image_dialog .o_crop_options .btn:has(.fa-search-plus)');
+ zoomRatio = _valueToRatio(Number($zoomBtn.data('value')));
+ $zoomBtn.mousedown().click();
+ $('.modal-dialog .modal-footer .btn.btn-primary:contains("Save")').mousedown().click();
+ });
+ },
+ },
+ },
+ {
+ name: "Click CROP ROTATE LEFT + FLIP HORIZONTAL in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ check: function () {
+ $('.note-image-popover .note-btn:has(.fa-crop)').mousedown().click();
+ defCropDialogSave.then(function () {
+ var $img = $(wysiwyg.getValue()).find('img.o_cropped_img_to_save');
+ assert.strictEqual($img.data('rotate'), -45, testName + " (rotate)");
+ assert.strictEqual($img.data('scale-x'), -1, testName + " (flip)");
+ });
+ defCropDialogInit.then(function () {
+ $('.o_crop_image_dialog .o_crop_options .btn:contains("16:9")').mousedown().click();
+ $('.o_crop_image_dialog .o_crop_options .btn:has(.fa-rotate-left)').mousedown().click();
+ $('.o_crop_image_dialog .o_crop_options .btn:has(.fa-arrows-h)').mousedown().click();
+ $('.modal-dialog .modal-footer .btn.btn-primary:contains("Save")').mousedown().click();
+ });
+ },
+ },
+ },
+ {
+ name: "Click CROP FREE in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ check: function () {
+ var cropFactor = 10;
+ $('.note-image-popover .note-btn:has(.fa-crop)').mousedown().click();
+ defCropDialogSave.then(function () {
+ var $img = $(wysiwyg.getValue()).find('img.o_cropped_img_to_save');
+ assert.strictEqual(Math.round($img.data('width')), Math.round(imgWidth - (imgWidth / cropFactor)), testName + " (rotate)");
+ });
+ defCropDialogInit.then(function () {
+ var $cropperPoints = $('.modal-dialog .cropper-crop-box .cropper-point');
+ var $pointW = $cropperPoints.filter('.point-w');
+ var pos1 = $pointW.offset();
+ var cropperWidth = $cropperPoints.filter('.point-e').offset().left - pos1.left;
+ $pointW.trigger($.Event("pointerdown", {
+ pageX: pos1.left,
+ pageY: pos1.top,
+ }));
+ $pointW.trigger($.Event("pointermove", {
+ pageX: pos1.left + (cropperWidth / cropFactor),
+ pageY: pos1.top,
+ }));
+ $pointW.trigger('pointerup');
+ $('.modal-dialog .modal-footer .btn.btn-primary:contains("Save")').mousedown().click();
+ });
+ },
+ },
+ },
+ ];
+
+ var def = $.when();
+ _.each(mediaTests, function (test) {
+ def = def.then(function () {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ return _clickMedia(wysiwyg, assert, test.do, test.test);
+ });
+ });
+ def.then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+QUnit.test('Pictogram (fontawesome)', function (assert) {
+ var done = assert.async();
+ assert.expect(8);
+
+ return weTestUtils.createWysiwyg(this.data).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var mediaTests = [{
+ name: "Add PICTOGRAM in empty p: p -> span.fa in p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _insertPictogram('fa-glass');
+ },
+ test: {
+ content: '<p>\u200B<span class="fa fa-glass"></span>\u200B</p>',
+ check: function () {
+ assert.strictEqual($('.note-icon-popover').css('display'), 'block', testName + ' (popover)');
+ },
+ },
+ },
+ // Remove font
+ {
+ name: "Click REMOVE in popover after adding font in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _insertPictogram('fa-glass');
+ },
+ test: {
+ content: '<p><br></p>',
+ check: function () {
+ $('.note-image-popover .note-btn .note-icon-trash').mousedown().click();
+ },
+ },
+ },
+ // Icon size
+ {
+ name: "Click ICON SIZE then 5X in popover after adding pictogram in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _insertPictogram('fa-glass');
+ },
+ test: {
+ content: '<p>\u200B<span class="fa fa-glass fa-5x"></span>\u200B</p>',
+ check: function () {
+ $('.note-icon-popover .note-faSize .dropdown-toggle').mousedown().click();
+ $('.note-icon-popover .note-faSize .dropdown-item:contains("5x")').mousedown().click();
+ assert.ok($('.note-icon-popover .note-faSize .dropdown-item:contains("5x")').hasClass('active'), testName + ' (popover)');
+ },
+ },
+ },
+ // Spin
+ {
+ name: "Click SPIN in popover after adding pictogram in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _insertPictogram('fa-glass');
+ },
+ test: {
+ content: '<p>\u200B<span class="fa fa-glass fa-spin"></span>\u200B</p>',
+ check: function () {
+ $('.note-icon-popover .note-faSpin .note-btn').mousedown().click();
+ },
+ },
+ },
+ // Replace pictogram
+ {
+ name: "Add PICTOGRAM in empty p then replace it",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _insertPictogram('fa-glass');
+ },
+ test: {
+ check: function () {
+ _clickMedia(wysiwyg, assert, function () {
+ _insertPictogram('fa-heart');
+ }, {
+ content: '<p>\u200B<span class="fa fa-heart"></span>\u200B</p>',
+ });
+ },
+ },
+ },
+ {
+ name: "Replace PICTOGRAM (i.fa => span.fa)",
+ async: true,
+ content: '<div class="row">\n' +
+ ' <div class="col-lg-12">\n' +
+ ' <i class="fa fa-heart rounded-circle"></i>\n' +
+ ' <div>Other content</div>\n' +
+ ' </div>\n' +
+ '</div>',
+ start: "i->0",
+ do: function () {
+ _insertPictogram('fa-glass');
+ },
+ test: {
+ content: '<div class="row">\n' +
+ ' <div class="col-lg-12">\n' +
+ ' <span class="rounded-circle fa fa-glass"></span>\n' +
+ ' <div>Other content</div>\n' +
+ ' </div>\n' +
+ '</div>',
+ },
+ },
+ ];
+
+ var def = $.when();
+ _.each(mediaTests, function (test) {
+ def = def.then(function () {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ $(range.sc).mousedown().click();
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ return _clickMedia(wysiwyg, assert, test.do, test.test);
+ });
+ });
+ def.then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+QUnit.test('Video', function (assert) {
+ var done = assert.async();
+ assert.expect(10);
+
+ return weTestUtils.createWysiwyg(this.data).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var mediaTests = [{
+ name: "Add VIDEO (youtube) in empty p: p -> div.media_iframe_video after p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _insertVideo(wysiwyg, assert, 'https://www.youtube.com/watch?v=xxxxxxxxxxx', true);
+ },
+ test: {
+ content: '<p><br></p><div class="media_iframe_video" data-oe-expression="about:blank"><div class="css_editable_mode_display">&nbsp;</div><div class="media_iframe_video_size">&nbsp;</div><iframe src="about:blank" frameborder="0"></iframe></div><p><br></p>',
+ check: function () {
+ assert.strictEqual($('.note-video-popover').css('display'), 'block', testName + ' (popover)');
+ },
+ },
+ },
+ {
+ name: "Add VIDEO (youtube) in p in breakable in unbreakable in breakable: p -> div.media_iframe_video after p",
+ async: true,
+ content: '<breakable><unbreakable><breakable><p>tata yoyo</p></breakable></unbreakable></breakable>',
+ start: "p:contents()[0]->4",
+ do: function () {
+ _insertVideo(wysiwyg, assert, 'https://www.youtube.com/watch?v=xxxxxxxxxxx');
+ },
+ test: {
+ content: '<breakable><unbreakable><breakable><p>tata</p></breakable>' +
+ '<div class="media_iframe_video" data-oe-expression="about:blank"><div class="css_editable_mode_display">&nbsp;</div><div class="media_iframe_video_size">&nbsp;</div><iframe src="about:blank" frameborder="0"></iframe></div>' +
+ '<breakable><p> yoyo</p></breakable></unbreakable></breakable>',
+ check: function () {
+ assert.strictEqual($('.note-video-popover').css('display'), 'block', testName + ' (popover)');
+ },
+ },
+ },
+ // Remove video
+ {
+ name: "Click REMOVE in popover after adding video in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _insertVideo(wysiwyg, assert, 'https://www.youtube.com/watch?v=xxxxxxxxxxx');
+ },
+ test: {
+ content: '<p><br></p>',
+ check: function () {
+ $('.note-image-popover .note-btn .note-icon-trash').mousedown().click();
+ },
+ },
+ },
+ /* VIDEO POPOVER */
+ // Multiple clicks
+ {
+ name: "Click FLOAT CENTER then FLOAT LEFT in popover after adding youtube video in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _insertVideo(wysiwyg, assert, 'https://www.youtube.com/watch?v=xxxxxxxxxxx');
+ },
+ test: {
+ content: '<p><br></p><div class="media_iframe_video pull-left" data-oe-expression="about:blank"><div class="css_editable_mode_display">&nbsp;</div><div class="media_iframe_video_size">&nbsp;</div><iframe src="about:blank" frameborder="0"></iframe></div><p><br></p>',
+ check: function () {
+ $('.note-image-popover .note-float .note-icon-align-center').mousedown().click();
+ $('.note-image-popover .note-float .note-icon-align-left').mousedown().click();
+ },
+ },
+ },
+ // Replace picture
+ {
+ name: "replace picture with video",
+ async: true,
+ content: '<p><img src="https://www.odoo.com/logo.png"></p>',
+ start: "img->0",
+ do: function () {
+ _insertVideo(wysiwyg, assert, 'https://www.youtube.com/watch?v=xxxxxxxxxxx');
+ },
+ test: {
+ check: function () {
+ assert.deepEqual(wysiwyg.getValue(),
+ '<p><br></p><div class="media_iframe_video" data-oe-expression="about:blank"><div class="css_editable_mode_display">&nbsp;</div><div class="media_iframe_video_size">&nbsp;</div><iframe src="about:blank" frameborder="0"></iframe></div><p><br></p>',
+ testName);
+ },
+ },
+ },
+ // Replace video
+ {
+ name: "replace video by pictogram",
+ async: true,
+ content: '<p><br></p><div class="media_iframe_video" data-oe-expression="about:blank"><div class="css_editable_mode_display">&nbsp;</div><div class="media_iframe_video_size">&nbsp;</div><iframe src="about:blank" frameborder="0"></iframe></div><p><br></p>',
+ start: "div->0",
+ do: function () {
+ _insertPictogram('fa-glass');
+ },
+ test: {
+ check: function () {
+ assert.deepEqual(wysiwyg.getValue(),
+ '<p>\u200B<span class="fa fa-glass"></span>\u200B</p>',
+ testName);
+ },
+ },
+ },
+ {
+ name: "replace video by pictogram (2)",
+ async: true,
+ content: '<p>aaa</p><div class="media_iframe_video" data-oe-expression="about:blank"><div class="css_editable_mode_display">&nbsp;</div><div class="media_iframe_video_size">&nbsp;</div><iframe src="about:blank" frameborder="0"></iframe></div><p>bbb</p>',
+ start: "div->0",
+ do: function () {
+ _insertPictogram('fa-glass');
+ },
+ test: {
+ check: function () {
+ assert.deepEqual(wysiwyg.getValue(),
+ '<p>aaa<span class="fa fa-glass"></span>bbb</p>',
+ testName);
+ },
+ },
+ },
+ ];
+
+ var def = $.when();
+ _.each(mediaTests, function (test) {
+ def = def.then(function () {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ $(range.sc).mousedown().click();
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ return _clickMedia(wysiwyg, assert, test.do, test.test);
+ });
+ });
+ def.then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+
+});
+});
+});
+});
diff --git a/addons/web_editor/tests/__init__.py b/addons/web_editor/tests/__init__.py
index b531e18c918..8b77588a13c 100644
--- a/addons/web_editor/tests/__init__.py
+++ b/addons/web_editor/tests/__init__.py
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from . import test_ui
from . import test_converter
diff --git a/addons/web_editor/tests/test_ui.py b/addons/web_editor/tests/test_ui.py
deleted file mode 100644
index 9e9527e2d14..00000000000
--- a/addons/web_editor/tests/test_ui.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-import odoo.tests
-
-@odoo.tests.tagged('post_install', '-at_install')
-class TestUi(odoo.tests.HttpCase):
-
- def test_01_admin_rte(self):
- self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('rte')", "odoo.__DEBUG__.services['web_tour.tour'].tours.rte.ready", login='admin')
-
- def test_02_admin_rte_inline(self):
- self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('rte_inline')", "odoo.__DEBUG__.services['web_tour.tour'].tours.rte_inline.ready", login='admin')
diff --git a/addons/web_tour/static/src/js/running_tour_action_helper.js b/addons/web_tour/static/src/js/running_tour_action_helper.js
index 1c4df6c27db..479f3a2ea78 100644
--- a/addons/web_tour/static/src/js/running_tour_action_helper.js
+++ b/addons/web_tour/static/src/js/running_tour_action_helper.js
@@ -92,7 +92,11 @@ var RunningTourActionHelper = core.Class.extend({
$selectedOption.prop("selected", true);
this._click(values);
} else {
+ values.$element.focusIn();
+ values.$element.trigger($.Event( "keydown", {key: '_', keyCode: 95}));
values.$element.text(text);
+ values.$element.focusInEnd();
+ values.$element.trigger($.Event( "keyup", {key: '_', keyCode: 95}));
}
values.$element.trigger("change");
},
diff --git a/addons/website/static/src/js/tours/rte.js b/addons/website/static/src/js/tours/rte.js
index 46fbbff3fce..bdc676f4b33 100644
--- a/addons/website/static/src/js/tours/rte.js
+++ b/addons/website/static/src/js/tours/rte.js
@@ -1,12 +1,18 @@
odoo.define('website.tour.rte', function (require) {
'use strict';
+var ajax = require('web.ajax');
+var session = require('web.session');
var tour = require('web_tour.tour');
-var base = require('web_editor.base');
+var Wysiwyg = require('web_editor.wysiwyg');
+
+var domReady = $.Deferred();
+$(domReady.resolve.bind(domReady));
+var ready = $.when(domReady, session.is_bound, ajax.loadXML());
tour.register('rte_translator', {
test: true,
- wait_for: base.ready(),
+ wait_for: ready,
}, [{
content: "click on Add a language",
trigger: '.js_language_selector a:has(i.fa)',
@@ -59,10 +65,10 @@ tour.register('rte_translator', {
content: "save",
trigger: 'button[data-action=save]',
extra_trigger: '#wrap p:first b',
-
}, {
content : "click language dropdown",
trigger : '.js_language_selector .dropdown-toggle',
+ extra_trigger: 'body:not(.o_wait_reload):not(:has(.note-editor))',
}, {
content: "click on french version",
trigger: '.js_language_selector a[data-lang="fr_BE"]',
@@ -81,14 +87,17 @@ tour.register('rte_translator', {
trigger: '#wrap p font:first',
run: function (action_helper) {
action_helper.text('translated french text');
- this.$anchor.trigger('keydown');
+ Wysiwyg.setRange(this.$anchor.contents()[0], 22);
+ this.$anchor.trigger($.Event( "keyup", {key: '_', keyCode: 95}));
},
}, {
content: "translate text with special char",
trigger: '#wrap input + p span:first',
run: function (action_helper) {
action_helper.click();
- this.$anchor.prepend('&lt;{translated}&gt;').trigger('keydown');
+ this.$anchor.prepend('&lt;{translated}&gt;');
+ Wysiwyg.setRange(this.$anchor.contents()[0], 0);
+ this.$anchor.trigger($.Event( "keyup", {key: '_', keyCode: 95}));
},
}, {
content: "click on input",
@@ -125,9 +134,12 @@ tour.register('rte_translator', {
trigger : '.js_language_selector .dropdown-toggle',
}, {
+ content: "open language selector",
+ trigger: '.js_language_selector button:first',
+ extra_trigger: 'html[lang*="fr"]:not(:has(#wrap p span))',
+}, {
content: "return to english version",
trigger: '.js_language_selector a[data-lang="en_US"]',
- extra_trigger: 'html[lang*="fr"]:not(:has(#wrap p span))',
}, {
content: "edit english version",
trigger: 'a[data-action=edit]',
@@ -139,27 +151,25 @@ tour.register('rte_translator', {
run: function (action_helper) {
action_helper.click();
var el = this.$anchor[0];
- var evt = document.createEvent("MouseEvents");
- $.summernote.core.range.create(el.childNodes[2], 6, el.childNodes[2], 13).select();
- evt.initMouseEvent('mouseup', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el);
- el.dispatchEvent(evt);
+ this.$anchor.trigger('mousedown');
+ Wysiwyg.setRange(el.childNodes[2], 6, el.childNodes[2], 13);
+ this.$anchor.trigger('mouseup');
},
}, {
content: "underline",
- trigger: '.note-air-popover button[data-event="underline"]',
+ trigger: 'button.note-btn-underline',
}, {
content: "save new change",
trigger: 'button[data-action=save]',
- extra_trigger: '#wrap p u',
+ extra_trigger: '#wrap.o_dirty p u',
}, {
content : "click language dropdown",
trigger : '.js_language_selector .dropdown-toggle',
-
+ extra_trigger: '#wrap p u',
}, {
content: "return in french",
- trigger: 'html[lang="en-US"] .js_language_selector a[data-lang="fr_BE"]',
- extra_trigger: 'html[lang*="en"]:not(:has(button[data-action=save]))',
+ trigger : 'html[lang="en-US"] .js_language_selector .js_change_lang[data-lang="fr_BE"]',
}, {
content: "check bis: content is translated",
trigger: '#wrap p font:first:contains(translated french text)',
diff --git a/addons/website_blog/static/src/js/website.tour.blog.js b/addons/website_blog/static/src/js/website.tour.blog.js
index d1afa46f217..a004f360e33 100644
--- a/addons/website_blog/static/src/js/website.tour.blog.js
+++ b/addons/website_blog/static/src/js/website.tour.blog.js
@@ -16,7 +16,7 @@ odoo.define("website_blog.tour", function (require) {
position: "bottom",
}, {
trigger: "h1[data-oe-expression=\"blog_post.name\"]",
- extra_trigger: "body.editor_has_snippets",
+ extra_trigger: ".o_snippets_loaded",
content: _t("Write a title, the subtitle is optional."),
position: "top",
run: "text",
@@ -26,7 +26,7 @@ odoo.define("website_blog.tour", function (require) {
content: _t("Set a blog post <b>cover</b>."),
position: "bottom",
}, {
- trigger: "a:containsExact(" + _t("Change Cover")+ "):eq(1)",
+ trigger: "a:containsExact(" + _t("Change Cover")+ "):visible",
content: _t("Click here to change your post cover."),
position: "right",
}, {
diff --git a/addons/website_sale/static/src/js/website_sale_tour_shop.js b/addons/website_sale/static/src/js/website_sale_tour_shop.js
index 6847bdd3bd2..deafdd83369 100644
--- a/addons/website_sale/static/src/js/website_sale_tour_shop.js
+++ b/addons/website_sale/static/src/js/website_sale_tour_shop.js
@@ -2,15 +2,11 @@ odoo.define("website_sale.tour_shop", function (require) {
"use strict";
var core = require("web.core");
- var tour = require("web_tour.tour");
- var base = require("web_editor.base");
-
var _t = core._t;
- tour.register("shop", {
- url: "/shop",
- wait_for: base.ready(),
- }, [{
+ // return the steps, used for backend and frontend (because web_editor.base is only in the website)
+
+ return [{
trigger: "#new-content-menu > a",
content: _t("Let's create your first product."),
extra_trigger: ".js_sale",
@@ -28,13 +24,14 @@ odoo.define("website_sale.tour_shop", function (require) {
content: _t("Click on <em>Continue</em> to create the product."),
position: "right",
}, {
- trigger: ".product_price .o_is_inline_editable .oe_currency_value",
+ trigger: ".product_price .oe_currency_value:visible",
+ extra_trigger: ".note-editable",
content: _t("Edit the price of this product by clicking on the amount."),
position: "bottom",
run: "text 1.99",
}, {
trigger: "#wrap img.product_detail_img",
- extra_trigger: ".product_price .o_is_inline_editable .oe_currency_value:not(:containsExact(1.00))",
+ extra_trigger: ".product_price .o_dirty .oe_currency_value:not(:containsExact(1.00))",
content: _t("Double click here to set an image describing your product."),
position: "top",
run: function (actions) {
@@ -66,10 +63,10 @@ odoo.define("website_sale.tour_shop", function (require) {
trigger: ".o_main_navbar .o_menu_toggle, #oe_applications .dropdown-toggle",
content: _t("Let's now take a look at your administration dashboard to get your eCommerce website ready in no time."),
position: "bottom",
- }, {
+ }, { // backend
trigger: '.o_apps > a[data-menu-xmlid="website.menu_website_configuration"], #oe_main_menu_navbar a[data-menu-xmlid="website.menu_website_configuration"]',
content: _t("Open your website app here."),
extra_trigger: ".o_apps,#oe_applications",
position: "bottom",
- }]);
+ }];
});
diff --git a/addons/website_sale/static/src/js/website_sale_tour_shop_backend.js b/addons/website_sale/static/src/js/website_sale_tour_shop_backend.js
new file mode 100644
index 00000000000..ad31f8d4ab3
--- /dev/null
+++ b/addons/website_sale/static/src/js/website_sale_tour_shop_backend.js
@@ -0,0 +1,8 @@
+odoo.define("website_sale.tour_shop_backend", function (require) {
+"use strict";
+
+var tour = require("web_tour.tour");
+var steps = require("website_sale.tour_shop");
+tour.register("shop", steps);
+
+});
diff --git a/addons/website_sale/static/src/js/website_sale_tour_shop_frontend.js b/addons/website_sale/static/src/js/website_sale_tour_shop_frontend.js
new file mode 100644
index 00000000000..f2aa39af42b
--- /dev/null
+++ b/addons/website_sale/static/src/js/website_sale_tour_shop_frontend.js
@@ -0,0 +1,12 @@
+odoo.define("website_sale.tour_shop_frontend", function (require) {
+"use strict";
+
+var tour = require("web_tour.tour");
+var base = require("web_editor.base");
+var steps = require("website_sale.tour_shop");
+tour.register("shop", {
+ url: "/shop",
+ wait_for: base.ready(),
+}, steps);
+
+});