diff options
| author | Antoine Guenet <age@odoo.com> | 2018-12-28 17:59:35 +0000 |
|---|---|---|
| committer | Christophe Matthieu <chm@odoo.com> | 2019-01-17 08:40:21 +0000 |
| commit | eb0ea8efbe932c140302e9f8da33a839bcd73146 (patch) | |
| tree | e097625171b2b67ed5f5288ced67446608b9f633 | |
| parent | f296992317e96562c66bd7ad59a5080d6c551ed5 (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>
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 </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 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 </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:"Lucida Grande", 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:"Lucida Grande", 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 </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, '​'); + var result = test.test.content.replace(allInvisible, '​'); + assert.strictEqual(value, result, test.name); + + if (test.test.start && value !== result) { + assert.notOk("Wrong DOM (see previous assert)", test.name + " (carret position)"); + return; + } + } + + $editable[0].normalize(); + + // test carret position + if (test.test.start) { + var start = _select(test.test.start); + var range = Wysiwyg.getRange($editable[0]); + if ((range.sc !== range.ec || range.so !== range.eo) && !test.test.end) { + assert.ok(false, test.name + ": the carret is not colapsed and the 'end' selector in test is missing"); + return; + } + var end = test.test.end ? _select(test.test.end) : start; + if (start.node && end.node) { + range = Wysiwyg.getRange($editable[0]); + var startPoint = endOfAreaBetweenTwoNodes({ + node: range.sc, + offset: range.so, + }); + var endPoint = endOfAreaBetweenTwoNodes({ + node: range.ec, + offset: range.eo, + }); + var sameDOM = (startPoint.node.outerHTML || startPoint.node.textContent) === (start.node.outerHTML || start.node.textContent); + var stringify = function (obj) { + if (!sameDOM) { + delete obj.sameDOMsameNode; + } + return JSON.stringify(obj, null, 2) + .replace(/"([^"\s-]+)":/g, "\$1:") + .replace(/([^\\])"/g, "\$1'") + .replace(/\\"/g, '"'); + }; + assert.deepEqual(stringify({ + startNode: startPoint.node.outerHTML || startPoint.node.textContent, + startOffset: startPoint.offset, + endPoint: endPoint.node.outerHTML || endPoint.node.textContent, + endOffset: endPoint.offset, + sameDOMsameNode: sameDOM && startPoint.node === start.node, + }), + stringify({ + startNode: start.node.outerHTML || start.node.textContent, + startOffset: start.offset, + endPoint: end.node.outerHTML || end.node.textContent, + endOffset: end.offset, + sameDOMsameNode: true, + }), + test.name + " (carret position)"); + } + } + }); + } + while (keyboardTests.length) { + defPollTest = defPollTest.then(pollTest.bind(null, keyboardTests.shift())); + } + + return defPollTest; +}; + + +/** + * Select a node in the dom with is offset. + * + * @param {String} startSelector + * @param {String} endSelector + * @param {jQuery} $editable + * @returns {Object} {sc, so, ec, eo} + */ +var select = (function () { + var __select = function (selector, $editable) { + var sel = selector.match(/^(.+?)(:contents\(\)\[([0-9]+)\]|:contents\(([0-9]+)\))?(->([0-9]+))?$/); + var $node = $editable.find(sel[1]); + return { + node: sel[2] ? $node.contents()[sel[3] ? +sel[3] : +sel[4]] : $node[0], + offset: sel[5] ? +sel[6] : 0, + }; + }; + return function (startSelector, endSelector, $editable) { + var start = __select(startSelector, $editable); + var end = endSelector ? __select(endSelector, $editable) : start; + return { + sc: start.node, + so: start.offset, + ec: end.node, + eo: end.offset, + }; + }; +})(); + +/** + * Trigger a keydown event. + * + * @param {String or Number} key (name or code) + * @param {jQuery} $editable + * @param {Object} [options] + * @param {Boolean} [options.firstDeselect] (default: false) true to deselect before pressing + */ +var keydown = function (key, $editable, options) { + var keyPress = {}; + if (typeof key === 'string') { + keyPress.key = key; + keyPress.keyCode = +_.findKey(keyboardMap, function (k) { + return k === key; + }); + } else { + keyPress.key = keyboardMap[key] || String.fromCharCode(key); + keyPress.keyCode = key; + } + var range = Wysiwyg.getRange($editable[0]); + if (!range) { + console.error("Editor have not any range"); + return; + } + if (options && options.firstDeselect) { + range.sc = range.ec; + range.so = range.eo; + Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo); + } + var target = range.ec; + var $target = $(target.tagName ? target : target.parentNode); + var event = $.Event("keydown", keyPress); + $target.trigger(event); +}; + +return { + wysiwygData: wysiwygData, + createWysiwyg: createWysiwyg, + testKeyboard: testKeyboard, + select: select, + keydown: keydown, +}; + + +}); 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="#">​a​</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> 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 </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 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 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> 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 </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 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 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 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">$ <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">$ <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">$ <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">$ <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">$ <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">$ <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">$ <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">$ <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> 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> 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> 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>​</b></p><p><b> 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> 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>​</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>​ 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>​ 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> 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>​</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=\"#\">​dom to edit​</a></div>", + steps: [{ + start: "a:contents()[0]->0", + key: 'ENTER', + }, { + key: 'a', + }], + test: { + content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">​a​</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=\"#\">​dom to edit​</a></div>", + steps: [{ + start: "a:contents()[0]->1", + key: 'ENTER', + }, { + key: 'a', + }], + test: { + content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">​a​</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=\"#\">​dom to edit​</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=\"#\">​ao edit​</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=\"#\">​dom to edit​</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=\"#\">​a​</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=\"#\">​dom to edit​</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=\"#\">​a​</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=\"#\">​dom to edit​</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=\"#\">​a​</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=\"#\">​dom to edit​</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=\"#\">​aedit​</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=\"#\">​a​</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=\"#\">​Label​</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> 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> 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 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/> 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> 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/> to edit</p></li></ul>", + steps: [{ + start: "p:contents()[2]->0", + key: 'ENTER', + }, { + key: 'a', + }], + test: { + content: "<ul><li><p>dom<br>​</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 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> 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> 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 </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 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> 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 </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 </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> 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 </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> dom o edit </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>​</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> <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> <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 </p><ul><li><p> 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 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> to edit</b></span>", + steps: [{ + start: "b:eq(1):contents()[0]->0", + key: 'BACKSPACE', + }], + test: { + content: "<span><b>dom<br> 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> 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 </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 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> 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 </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> dom t edit </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 </li><li>edit</li></ul>", + steps: [{ + start: "li:contents()[0]->7", + key: 'DELETE', + }], + test: { + content: "<ul><li>dom to 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 </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 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 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 </p><ul><li><p> 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> 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 </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 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>$ 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+/, ' '), + 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> 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> 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 </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"> </div><div class="media_iframe_video_size"> </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"> </div><div class="media_iframe_video_size"> </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"> </div><div class="media_iframe_video_size"> </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"> </div><div class="media_iframe_video_size"> </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"> </div><div class="media_iframe_video_size"> </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"> </div><div class="media_iframe_video_size"> </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('<{translated}>').trigger('keydown'); + this.$anchor.prepend('<{translated}>'); + 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); + +}); |
