summaryrefslogtreecommitdiff
path: root/addons/web_editor/static/tests
diff options
context:
space:
mode:
authorRobot Odoo <xmo+roboto@odoo.com>2019-01-17 19:17:22 +0100
committerGitHub <noreply@github.com>2019-01-17 19:17:22 +0100
commit22efdfbecc1e8c688000be98903a5a355d1ece8a (patch)
treee097625171b2b67ed5f5288ced67446608b9f633 /addons/web_editor/static/tests
parentf50fbc6d168f0d17fa1e932b4ecc968a92b02b65 (diff)
parenteb0ea8efbe932c140302e9f8da33a839bcd73146 (diff)
[IMP] web_editor,*: Refactoring the wysiwyg editor and 'html' field
_Task #1878502_ - The refactoring allows for the update of the _Summernote_ library. - A new structure was created by transforming all the plugins in the library using the odoo inheritance system. Plugins are easier to implement with the `AbstractPlugin` to add Odoo behaviors. - The cleanup allowed us to remove some of the code that was in `web_editor` but only used in `website` or in `mass_mailing`. Some parts are still in `web_editor` but will be moved at a later time. - 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...). - From now on, the methods of the library (in this case _Summernote_) can no longer be called by other modules or files. Only the `wysiwyg` widgets can access it, to simplify the updating process. The `wysiwyg` object serves as an interface. A lot of tests are done, ensuring the consistency of outside behavior. - Depending on the options the snippets will be loaded or not, the editor will be in an iframe or not... all of this is transparent from the outside. - Regarding iframes, all controllers related to editing have been removed: the new API no longer needs them. This speeds up loading, eases testing and removes complexity for the same features. **PUBLIC FEATURES** There are several public methods on the `Wysiwyg` class: - `Wysiwyg.prepare (WidgetParent)` which returns a deferred resolved when the library (xml, lazy, assets...) is loaded. - `Wysiwyg.getRange (DOM)` returns the range (selection in the dom) - `Wysiwyg.setRange (startNode, startOffset, endNode, endOffset)` which creates a range (selection in the dom) - `Wysiwyg.setRangeFromNode (DOM, options)` that creates a range from an element (option available to select all, start or end) A jQuery selector was added: `:o_editable`, which indicates whether the current element is editable. That is, if it is contained in a tag with the attribute `contentEditable = "true"` or in a tag with the class `o_editable`. Several methods are also present: - `focusIn`: makes a focus and places the cursor at the beginning of the element - `focusInEnd`: makes a focus and places the cursor at the end of the element - `selectContent`: makes a focus and selects the content **HTML FIELD** The HTML field can receive different options: - `style-inline:` {boolean} transforms a class into an inline style when saving and vice versa when reading. - `no-attachment`: {boolean} prevents the use of attachments (in media dialog) - `cssEdit`: {xml_id} to use a template containing the css to loaded in an iframe when editing - `cssReadonly`: {xml_id} to use a template containing the css to load into an iframe when viewing in readonly - `snippets`: {xml_id} snippets template (can be used with or without `cssEdit`) - `wrapper`: {template} qweb static template (containing a tag: `id = "wrapper"`) that will include the content during editing (removed on save) **MASS MAILING** A widget was created for `mass mailing`. There are now two fields: `body_html` and `body_arch`. `body_arch` contains the code with the class without conversion into inline style, useful when editing and one with the inline style that is visible in readonly mode and sent by email. Advantage: no spreading errors, able to update css/theme, able to do more changes when converting to inline style so that a maximum of mail clients have an impeccable rendering. **REMAINING KNOW ISSUES** These will be fixed in a subsequent commit. - A few committed changes made during the development of this refactoring were lost in the process and should be reapplied. - Some minor (mostly range) issues remain with Firefox. -- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr closes odoo/odoo#29775
Diffstat (limited to 'addons/web_editor/static/tests')
-rw-r--r--addons/web_editor/static/tests/field_html_tests.js895
-rw-r--r--addons/web_editor/static/tests/test_utils.js674
-rw-r--r--addons/web_editor/static/tests/web_editor_tests.js327
-rw-r--r--addons/web_editor/static/tests/wysiwyg_keyboard_tests.js4179
-rw-r--r--addons/web_editor/static/tests/wysiwyg_snippets_tests.js403
-rw-r--r--addons/web_editor/static/tests/wysiwyg_tests.js3859
6 files changed, 10010 insertions, 327 deletions
diff --git a/addons/web_editor/static/tests/field_html_tests.js b/addons/web_editor/static/tests/field_html_tests.js
new file mode 100644
index 00000000000..b01ab48586b
--- /dev/null
+++ b/addons/web_editor/static/tests/field_html_tests.js
@@ -0,0 +1,895 @@
+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 Wysiwyg = require('web_editor.wysiwyg');
+var MediaDialog = require('wysiwyg.widgets.MediaDialog');
+
+var _t = core._t;
+
+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_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;'>yep</div>",
+ body_arch: "<div class='field_body'>yep</div>",
+ }],
+ },
+ });
+
+ 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('simple rendering', function (assert) {
+ var done = assert.async();
+ assert.expect(3);
+
+ testUtils.createAsyncView({
+ View: FormView,
+ model: 'note.note',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px"/>' +
+ '</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");
+ 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");
+
+ form.destroy();
+ done();
+ });
+});
+
+QUnit.test('colorpicker', 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"/>' +
+ '</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");
+
+ $field.find('.note-toolbar .note-bg-color button:first').mousedown().click();
+
+ assert.ok($field.find('.note-bg-color').hasClass('show') && $field.find('.note-bg-color .dropdown-menu').hasClass('show'),
+ "should display the color picker");
+
+ $field.find('.note-toolbar .note-bg-color button[data-value="#00FFFF"]').mousedown().click();
+
+ assert.ok(!$field.find('.note-bg-color').hasClass('show') && !$field.find('.note-bg-color .dropdown-menu').hasClass('show'),
+ "should close the color picker");
+
+ assert.strictEqual($field.find('.note-editable').html(),
+ '<p>t<font style="background-color: rgb(0, 255, 255);">oto toto&nbsp;</font>toto</p><p>tata</p>',
+ "should have rendered the field correctly in edit");
+
+ var fontContent = $field.find('.note-editable font').contents()[0];
+ var rangeControl = {
+ sc: fontContent,
+ so: 0,
+ ec: fontContent,
+ eo: fontContent.length,
+ };
+ range = Wysiwyg.getRange($field[0]);
+ assert.deepEqual(_.pick(range, 'sc', 'so', 'ec', 'eo'), rangeControl,
+ "should select the text after color change");
+
+ // select the text
+ pText = $field.find('.note-editable p').first().contents()[2];
+ Wysiwyg.setRange(fontContent, 5, pText, 2);
+ // text is selected
+
+ $field.find('.note-toolbar .note-bg-color button:first').mousedown().click();
+ $field.find('.note-toolbar .note-bg-color button[data-value="bg-gamma"]').mousedown().click();
+
+ assert.strictEqual($field.find('.note-editable').html(),
+ '<p>t<font style="background-color: rgb(0, 255, 255);">oto t</font><font class="bg-gamma">oto&nbsp;to</font>to</p><p>tata</p>',
+ "should have rendered the field correctly in edit");
+
+ form.destroy();
+ done();
+ });
+});
+
+QUnit.test('media dialog: image', 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"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ 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(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();
+
+ var $editable = form.$('.oe_form_field[name="body"] .note-editable');
+
+ 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");
+
+ testUtils.mock.unpatch(MediaDialog);
+
+ form.destroy();
+ done();
+ });
+ });
+});
+
+QUnit.test('media dialog: icon', 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"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.model === 'ir.attachment') {
+ return $.when([]);
+ }
+ 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();
+
+ var $editable = form.$('.oe_form_field[name="body"] .note-editable');
+
+ 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");
+
+ testUtils.mock.unpatch(MediaDialog);
+
+ form.destroy();
+ done();
+ }, 200);
+ });
+});
+
+QUnit.test('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"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === "write") {
+ assert.strictEqual(args.args[1].body,
+ '<p>t<font class="bg-gamma">oto toto&nbsp;</font>toto</p><p>tata</p>',
+ "should save the content");
+
+ }
+ return this._super.apply(this, arguments);
+ },
+ }).then(function (form) {
+ form.$buttons.find('.o_form_button_edit').click();
+ var $field = form.$('.oe_form_field[name="body"]');
+
+ // select the text
+ var pText = $field.find('.note-editable p').first().contents()[0];
+ Wysiwyg.setRange(pText, 1, pText, 10);
+ // text is selected
+
+ $field.find('.note-toolbar .note-bg-color button:first').mousedown().click();
+ $field.find('.note-toolbar .note-bg-color button[data-value="bg-gamma"]').mousedown().click();
+
+ form.$buttons.find('.o_form_button_save').click();
+
+ form.destroy();
+ done();
+ });
+});
+
+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>';
+
+ testUtils.createAsyncView({
+ View: FormView,
+ model: 'note.note',
+ data: this.data,
+ 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 === "write") {
+ assert.strictEqual(args.args[1].body,
+ '<p class="pull-right" style="margin:0px;font-size:13px;font-family:&quot;Lucida Grande&quot;, Helvetica, Verdana, Arial, sans-serif;float:right;">' +
+ 'toto ' +
+ '<img ' +
+ 'data-class="fa fa-star" ' +
+ 'data-style="color: red;" ' +
+ 'style="border-style:none;vertical-align:middle;color: red; height: auto; width: auto;" ' +
+ 'data-src="/web_editor/font_to_img/61445/rgb(255,0,0)/13"' +
+ '>' +
+ 'toto toto' +
+ '</p>' +
+ '<p style="margin:0px;font-size:13px;font-family:&quot;Lucida Grande&quot;, Helvetica, Verdana, Arial, sans-serif;">' +
+ 'tata' +
+ '</p>',
+ "should save the content");
+ }
+ if (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();
+ });
+});
+
+QUnit.module('cssReadonly');
+
+QUnit.test('rendering with iframe for readonly mode', function (assert) {
+ var done = assert.async();
+ assert.expect(3);
+
+ 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.module('cssEdit');
+
+QUnit.test('rendering with iframe for edit mode', function (assert) {
+ var done = assert.async();
+ assert.expect(4);
+
+ 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,
+ }).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 === "write") {
+ assert.strictEqual(args.args[1].body,
+ '<p>t<font class="bg-gamma">oto toto&nbsp;</font>toto</p><p>tata</p>',
+ "should save the content");
+ }
+ return this._super.apply(this, arguments);
+ },
+ }).then(function (form) {
+ form.$buttons.find('.o_form_button_edit').click();
+ var $field = form.$('.oe_form_field[name="body"]');
+ var $iframe = $field.find('iframe');
+
+ $iframe.data('load-def').then(function () {
+ var doc = $iframe.contents()[0];
+ var $content = $('#iframe_target', doc);
+ var $editable = $content.find('.note-editable');
+
+ // select the text
+ var pText = $editable.find('p').first().contents()[0];
+ Wysiwyg.setRange(pText, 1, pText, 10);
+ // text is selected
+
+ $content.find('.note-toolbar .note-bg-color button:first').mousedown().click();
+ $content.find('.note-toolbar .note-bg-color button[data-value="bg-gamma"]').mousedown().click();
+
+
+ form.$buttons.find('.o_form_button_save').click();
+
+ form.destroy();
+ done();
+ });
+ });
+});
+
+QUnit.module('wrapper');
+
+// todo add test wrapper
+
+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('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\'}"/>' +
+ '</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');
+
+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\', \'cssEdit\': \'template.assets\'}"/>' +
+ '</form>',
+ res_id: 1,
+ }).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
+ }));
+ }
+
+ 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;
+
+ });
+});
+
+QUnit.test('field html translatable in iframe', function (assert) {
+ var done = assert.async();
+ assert.expect(2);
+
+ 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>' +
+ '<field name="body" widget="html" style="height: 100px" options="{\'cssEdit\': \'template.assets\'}"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_button' && args.method === 'translate_fields') {
+ assert.deepEqual(args.args, ['note.note', 1, 'body', {}], "should call 'call_button' route");
+ return $.when();
+ }
+ return this._super.apply(this, arguments);
+ },
+ }).then(function (form) {
+ var $field = form.$('.oe_form_field[name="body"]');
+ form.$buttons.find('.o_form_button_edit').click();
+ $field = form.$('.oe_form_field[name="body"]');
+ var $iframe = $field.find('iframe');
+
+ $iframe.data('load-def').then(function () {
+ var doc = $iframe.contents()[0];
+ var $content = $('#iframe_target', doc);
+
+ var $button = $content.find('.note-toolbar .o_field_translate');
+ assert.strictEqual($button.length, 1, "should have a translate button");
+ $button.click();
+
+ form.destroy();
+ _t.database.multi_lang = multiLang;
+ done();
+ });
+ });
+});
+
+});
+});
+});
diff --git a/addons/web_editor/static/tests/test_utils.js b/addons/web_editor/static/tests/test_utils.js
new file mode 100644
index 00000000000..ff740e52f36
--- /dev/null
+++ b/addons/web_editor/static/tests/test_utils.js
@@ -0,0 +1,674 @@
+odoo.define('web_editor.test_utils', function (require) {
+"use strict";
+
+var ajax = require('web.ajax');
+var testUtils = require('web.test_utils');
+var Widget = require('web.Widget');
+var Wysiwyg = require('web_editor.wysiwyg');
+var options = require('web_editor.snippets.options');
+
+
+/**
+ * Options with animation and edition for test.
+ */
+options.registry.option_test = options.Class.extend({
+ cleanForSave: function () {
+ this.$target.addClass('cleanForSave');
+ },
+ onBuilt: function () {
+ this.$target.addClass('built');
+ },
+ onBlur: function () {
+ this.$target.removeClass('focus');
+ },
+ onClone: function () {
+ this.$target.addClass('clone');
+ this.$target.removeClass('focus');
+ },
+ onFocus: function () {
+ this.$target.addClass('focus');
+ },
+ onMove: function () {
+ this.$target.addClass('move');
+ },
+ onRemove: function () {
+ this.$target.closest('.note-editable').addClass('snippet_has_removed');
+ },
+});
+
+
+/**
+ * Constructor WysiwygTest why editable and unbreakable node used in test.
+ */
+var WysiwygTest = Wysiwyg.extend({
+ _parentToDestroyForTest: null,
+ /**
+ * Override 'destroy' of discuss so that it calls 'destroy' on the parent.
+ *
+ * @override
+ */
+ destroy: function () {
+ unpatch();
+ this._super();
+ this.$target.remove();
+ this._parentToDestroyForTest.destroy();
+ },
+ /**
+ * @override
+ */
+ isUnbreakableNode: function (node) {
+ var Node = (node.tagName ? node : node.parentNode);
+ return (!this.options.useOnlyTestUnbreakable && this._super(node)) ||
+ !this.isEditableNode(node) ||
+ Node.tagName === "UNBREAKABLE" ||
+ (Node.className + '').indexOf('unbreakable') !== -1;
+ },
+ /**
+ * @override
+ */
+ isEditableNode: function (node) {
+ if (!$(node).closest(this.$el).length) {
+ return false;
+ }
+ while (node) {
+ if (node.tagName === "EDITABLE") {
+ return true;
+ }
+ if (node.tagName === "NOTEDITABLE") {
+ return false;
+ }
+ if ((node.className + '').indexOf('editable') !== -1) {
+ return true;
+ }
+ if (this.$el[0] === node) {
+ return true;
+ }
+ node = node.parentNode;
+ }
+ return false;
+ },
+});
+
+
+function patch() {
+ testUtils.mock.patch(ajax, {
+ loadAsset: function (xmlId) {
+ if (xmlId === 'template.assets') {
+ return $.when({
+ cssLibs: [],
+ cssContents: ['body {background-color: red;}']
+ });
+ }
+ if (xmlId === 'template.assets_all_style') {
+ return $.when({
+ cssLibs: $('head link[href]:not([type="image/x-icon"])').map(function () {
+ return $(this).attr('href');
+ }).get(),
+ cssContents: ['body {background-color: red;}']
+ });
+ }
+ throw 'Wrong template';
+ },
+ });
+}
+
+function unpatch() {
+ testUtils.mock.unpatch(ajax);
+}
+
+/**
+ * @param {object} data
+ * @returns {object}
+ */
+function wysiwygData(data) {
+ return _.defaults({}, data, {
+ 'ir.ui.view': {
+ fields: {
+ display_name: {
+ string: "Displayed name",
+ type: "char",
+ },
+ },
+ records: [],
+ read_template: function (args) {
+ if (args[0] === 'web_editor.colorpicker') {
+ return '<templates><t t-name="web_editor.colorpicker">' +
+ '<colorpicker>' +
+ ' <div class="o_colorpicker_section" data-name="theme" data-display="Theme Colors" data-icon-class="fa fa-flask">' +
+ ' <button data-color="alpha"></button>' +
+ ' <button data-color="beta"></button>' +
+ ' <button data-color="gamma"></button>' +
+ ' <button data-color="delta"></button>' +
+ ' <button data-color="epsilon"></button>' +
+ ' </div>' +
+ ' <div class="o_colorpicker_section" data-name="transparent_grayscale" data-display="Transparent Colors" data-icon-class="fa fa-eye-slash">' +
+ ' <button class="o_btn_transparent"></button>' +
+ ' <button data-color="black-25"></button>' +
+ ' <button data-color="black-50"></button>' +
+ ' <button data-color="black-75"></button>' +
+ ' <button data-color="white-25"></button>' +
+ ' <button data-color="white-50"></button>' +
+ ' <button data-color="white-75"></button>' +
+ ' </div>' +
+ ' <div class="o_colorpicker_section" data-name="common" data-display="Common Colors" data-icon-class="fa fa-paint-brush"></div>' +
+ '</colorpicker>' +
+ '</t></templates>';
+ }
+ },
+ render_template: function (args) {
+ if (args[0] === 'web_editor.snippets') {
+ return '<h2 id="snippets_menu">Add blocks</h2>' +
+ '<div id="o_scroll">' +
+ ' <div id="snippet_structure" class="o_panel">' +
+ ' <div class="o_panel_header">' +
+ ' <i class="fa fa-th-large"></i> First Panel' +
+ ' </div>' +
+ ' <div class="o_panel_body">' +
+ ' <div name="Separator" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_separator.png">' +
+ ' <div class="s_hr pt32 pb32">' +
+ ' <hr class="s_hr_1px s_hr_solid w-100 mx-auto"/>' +
+ ' </div>' +
+ ' </div>' +
+ ' <div name="Content" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_text_block.png">' +
+ ' <section name="Content+Options" class="test_option_all pt32 pb32" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_text_block.png">' +
+ ' <div class="container">' +
+ ' <div class="row">' +
+ ' <div class="col-lg-10 offset-lg-1 pt32 pb32">' +
+ ' <h2>Title</h2>' +
+ ' <p class="lead o_default_snippet_text">Content</p>' +
+ ' </div>' +
+ ' </div>' +
+ ' </div>' +
+ ' </section>' +
+ ' </div>' +
+ ' </div>' +
+ ' </div>' +
+ '</div>' +
+ '<div id="snippet_options" class="d-none">' +
+ ' <div data-js="many2one" data-selector="[data-oe-many2one-model]:not([data-oe-readonly])" data-no-check="true"></div>' +
+ ' <div data-js="content" data-selector=".s_hr, .test_option_all" ' +
+ 'data-drop-in=".note-editable" data-drop-near="p, h1, h2, h3, blockquote, .s_hr"></div>' +
+ ' <div data-js="sizing_y" data-selector=".s_hr, .test_option_all"></div>' +
+ ' <div data-js="colorpicker" data-selector=".test_option_all">' +
+ ' <div class="dropdown-submenu">' +
+ ' <a tabindex="-1" href="#" class="dropdown-item"><i class="fa fa-eyedropper"/>Background Color</a>' +
+ ' <div class="dropdown-menu" role="menu"/>' +
+ ' </div>' +
+ ' </div>' +
+ ' <div data-js="background" data-selector=".test_option_all">' +
+ ' <a tabindex="-1" href="#" class="dropdown-item" data-choose-image="true" data-no-preview="true"><i class="fa fa-picture-o"/>Background Image</a>' +
+ ' </div>' +
+ ' <div data-js="option_test" data-selector=".s_hr">' +
+ ' <div class="dropdown-submenu">' +
+ ' <a tabindex="-2" href="#" class="dropdown-item"><i class="fa fa-arrows-v"/>Alignment</a>' +
+ ' <div class="dropdown-menu" role="menu">' +
+ ' <a href="#" class="dropdown-item" data-select-class="align-items-start">Top</a>' +
+ ' <a href="#" class="dropdown-item" data-select-class="align-items-center">Middle</a>' +
+ ' <a href="#" class="dropdown-item" data-select-class="align-items-end">Bottom</a>' +
+ ' <div class="dropdown-divider"/>' +
+ ' <a href="#" class="dropdown-item" data-select-class="align-items-stretch">Equal height</a>' +
+ ' </div>' +
+ ' </div>' +
+ ' </div>' +
+ '</div>';
+ }
+ },
+ },
+ 'ir.attachment': {
+ fields: {
+ display_name: {
+ string: "display_name",
+ type: 'char',
+ },
+ datas_fname: {
+ string: "datas_fname",
+ type: 'char',
+ },
+ mimetype: {
+ string: "mimetype",
+ type: 'char',
+ },
+ checksum: {
+ string: "checksum",
+ type: 'char',
+ },
+ url: {
+ string: "url",
+ type: 'char',
+ },
+ type: {
+ string: "type",
+ type: 'char',
+ },
+ res_id: {
+ string: "res_id",
+ type: 'integer',
+ },
+ res_model: {
+ string: "res_model",
+ type: 'char',
+ },
+ access_token: {
+ string: "access_token",
+ type: 'char',
+ },
+ },
+ records: [{
+ id: 1,
+ public: true,
+ name: 'image',
+ datas_fname: 'image.png',
+ mimetype: 'image/png',
+ checksum: false,
+ url: '/web_editor/static/src/img/transparent.png',
+ type: 'url',
+ res_id: 0,
+ res_model: false,
+ access_token: false
+ }],
+ generate_access_token: function () {
+ return;
+ },
+ },
+ });
+}
+
+/**
+ * Create the wysiwyg instance for test (contains patch, usefull ir.ui.view, snippets).
+ *
+ * @param {object} params
+ */
+function createWysiwyg(params) {
+ patch();
+ params.data = wysiwygData(params.data);
+
+ var parent = new Widget();
+ testUtils.mock.addMockEnvironment(parent, params);
+
+ var wysiwygOptions = _.extend({}, params.wysiwygOptions, {
+ recordInfo: {
+ context: {},
+ res_model: 'module.test',
+ res_id: 1,
+ },
+ useOnlyTestUnbreakable: params.useOnlyTestUnbreakable,
+ });
+
+ var wysiwyg = new WysiwygTest(parent, wysiwygOptions);
+ wysiwyg._parentToDestroyForTest = parent;
+
+ var $textarea = $('<textarea/>');
+ if (wysiwygOptions.value) {
+ $textarea.val(wysiwygOptions.value);
+ }
+ var selector = params.debug ? 'body' : '#qunit-fixture';
+ $textarea.prependTo($(selector));
+ if (params.debug) {
+ $('body').addClass('debug');
+ }
+ return wysiwyg.attachTo($textarea).then(function () {
+ if (wysiwygOptions.snippets) {
+ var defSnippets = $.Deferred();
+ testUtils.mock.intercept(wysiwyg, "snippets_loaded", function () {
+ defSnippets.resolve(wysiwyg);
+ });
+ return defSnippets;
+ }
+ return wysiwyg;
+ });
+}
+
+
+/**
+ * Char codes.
+ */
+var dom = $.summernote.dom;
+var keyboardMap = {
+ "8": "BACKSPACE",
+ "9": "TAB",
+ "13": "ENTER",
+ "16": "SHIFT",
+ "17": "CONTROL",
+ "18": "ALT",
+ "19": "PAUSE",
+ "20": "CAPS_LOCK",
+ "27": "ESCAPE",
+ "32": "SPACE",
+ "33": "PAGE_UP",
+ "34": "PAGE_DOWN",
+ "35": "END",
+ "36": "HOME",
+ "37": "LEFT",
+ "38": "UP",
+ "39": "RIGHT",
+ "40": "DOWN",
+ "45": "INSERT",
+ "46": "DELETE",
+ "91": "OS_KEY", // 'left command': Windows Key (Windows) or Command Key (Mac)
+ "93": "CONTEXT_MENU", // 'right command'
+};
+_.each(_.range(40, 127), function (keyCode) {
+ if (!keyboardMap[keyCode]) {
+ keyboardMap[keyCode] = String.fromCharCode(keyCode);
+ }
+});
+
+/**
+ * Perform a series of tests (`keyboardTests`) for using keyboard inputs.
+ *
+ * @see wysiwyg_keyboard_tests.js
+ * @see wysiwyg_tests.js
+ *
+ * @param {jQuery} $editable
+ * @param {object} assert
+ * @param {object[]} keyboardTests
+ * @param {string} keyboardTests.name
+ * @param {string} keyboardTests.content
+ * @param {object[]} keyboardTests.steps
+ * @param {string} keyboardTests.steps.start
+ * @param {string} [keyboardTests.steps.end] default: steps.start
+ * @param {string} keyboardTests.steps.key
+ * @param {object} keyboardTests.test
+ * @param {string} [keyboardTests.test.content]
+ * @param {string} [keyboardTests.test.start]
+ * @param {string} [keyboardTests.test.end] default: steps.start
+ * @param {function($editable, assert)} [keyboardTests.test.check]
+ * @param {Number} addTests
+ */
+var testKeyboard = function ($editable, assert, keyboardTests, addTests) {
+ var tests = _.compact(_.pluck(keyboardTests, 'test'));
+ var testNumber = _.compact(_.pluck(tests, 'start')).length +
+ _.compact(_.pluck(tests, 'content')).length +
+ _.compact(_.pluck(tests, 'check')).length +
+ (addTests | 0);
+ assert.expect(testNumber);
+
+ function keydown(target, keypress) {
+ var $target = $(target.tagName ? target : target.parentNode);
+ if (!keypress.keyCode) {
+ keypress.keyCode = +_.findKey(keyboardMap, function (key) {
+ return key === keypress.key;
+ });
+ } else {
+ keypress.key = keyboardMap[keypress.keyCode] || String.fromCharCode(keypress.keyCode);
+ }
+ keypress.keyCode = keypress.keyCode;
+ var event = $.Event("keydown", keypress);
+ $target.trigger(event);
+ if (!event.isDefaultPrevented()) {
+ if (keypress.key.length === 1) {
+ document.execCommand("insertText", 0, keypress.key);
+ } else {
+ console.warn('Native "' + keypress.key + '" is not supported in test');
+ }
+ }
+ $target.trigger($.Event("keyup", keypress));
+ return $target;
+ }
+
+ function _select(selector) {
+ // eg: ".class:contents()[0]->1" selects the first contents of the 'class' class, with an offset of 1
+ var reDOMSelection = /^(.+?)(:contents(\(\)\[|\()([0-9]+)[\]|\)])?(->([0-9]+))?$/;
+ var sel = selector.match(reDOMSelection);
+ var $node = $editable.find(sel[1]);
+ var point = {
+ node: sel[3] ? $node.contents()[+sel[4]] : $node[0],
+ offset: sel[5] ? +sel[6] : 0,
+ };
+ if (!point.node || point.offset > (point.node.tagName ? point.node.childNodes : point.node.textContent).length) {
+ assert.notOk("Node not found: '" + selector + "' " + (point.node ? "(container: '" + (point.node.outerHTML || point.node.textContent) + "')" : ""));
+ }
+ return point;
+ }
+
+ function selectText(start, end) {
+ start = _select(start);
+ var target = start.node;
+ $(target.tagName ? target : target.parentNode).trigger("mousedown");
+ if (end) {
+ end = _select(end);
+ Wysiwyg.setRange(start.node, start.offset, end.node, end.offset);
+ } else {
+ Wysiwyg.setRange(start.node, start.offset);
+ }
+ target = end ? end.node : start.node;
+ $(target.tagName ? target : target.parentNode).trigger('mouseup');
+ }
+
+ function endOfAreaBetweenTwoNodes(point) {
+ // move the position because some browser make the caret on the end of the previous area after normalize
+ if (
+ !point.node.tagName &&
+ point.offset === point.node.textContent.length &&
+ !/\S|\u00A0/.test(point.node.textContent)
+ ) {
+ point = dom.nextPoint(dom.nextPoint(point));
+ while (point.node.tagName && point.node.textContent.length) {
+ point = dom.nextPoint(point);
+ }
+ }
+ return point;
+ }
+
+ var defPollTest = $.when();
+
+ function pollTest(test) {
+ var def = $.when();
+ $editable.data('wysiwyg').setValue(test.content);
+
+ function poll(step) {
+ var def = $.Deferred();
+ if (step.start) {
+ selectText(step.start, step.end);
+ if (!Wysiwyg.getRange($editable[0])) {
+ throw 'Wrong range! \n' +
+ 'Test: ' + test.name + '\n' +
+ 'Selection: ' + step.start + '" to "' + step.end + '"\n' +
+ 'DOM: ' + $editable.html();
+ }
+ }
+ setTimeout(function () {
+ if (step.keyCode || step.key) {
+ var target = Wysiwyg.getRange($editable[0]).ec;
+ if (window.location.search.indexOf('notrycatch') !== -1) {
+ keydown(target, {
+ key: step.key,
+ keyCode: step.keyCode,
+ ctrlKey: !!step.ctrlKey,
+ shiftKey: !!step.shiftKey,
+ altKey: !!step.altKey,
+ metaKey: !!step.metaKey,
+ });
+ } else {
+ try {
+ keydown(target, {
+ key: step.key,
+ keyCode: step.keyCode,
+ ctrlKey: !!step.ctrlKey,
+ shiftKey: !!step.shiftKey,
+ altKey: !!step.altKey,
+ metaKey: !!step.metaKey,
+ });
+ } catch (e) {
+ assert.notOk(e.name + '\n\n' + e.stack, test.name);
+ }
+ }
+ }
+ setTimeout(function () {
+ if (step.keyCode || step.key) {
+ var $target = $(target.tagName ? target : target.parentNode);
+ $target.trigger($.Event('keyup', {
+ key: step.key,
+ keyCode: step.keyCode,
+ ctrlKey: !!step.ctrlKey,
+ shiftKey: !!step.shiftKey,
+ altKey: !!step.altKey,
+ metaKey: !!step.metaKey,
+ }));
+ }
+ setTimeout(def.resolve.bind(def));
+ });
+ });
+ return def;
+ }
+ while (test.steps.length) {
+ def = def.then(poll.bind(null, test.steps.shift()));
+ }
+
+ return def.then(function () {
+ if (!test.test) {
+ return;
+ }
+
+ if (test.test.check) {
+ test.test.check($editable, assert);
+ }
+
+ // test content
+ if (test.test.content) {
+ var value = $editable.data('wysiwyg').getValue({
+ keepPopover: true,
+ });
+ var allInvisible = /\u200B/g;
+ value = value.replace(allInvisible, '&#8203;');
+ var result = test.test.content.replace(allInvisible, '&#8203;');
+ assert.strictEqual(value, result, test.name);
+
+ if (test.test.start && value !== result) {
+ assert.notOk("Wrong DOM (see previous assert)", test.name + " (carret position)");
+ return;
+ }
+ }
+
+ $editable[0].normalize();
+
+ // test carret position
+ if (test.test.start) {
+ var start = _select(test.test.start);
+ var range = Wysiwyg.getRange($editable[0]);
+ if ((range.sc !== range.ec || range.so !== range.eo) && !test.test.end) {
+ assert.ok(false, test.name + ": the carret is not colapsed and the 'end' selector in test is missing");
+ return;
+ }
+ var end = test.test.end ? _select(test.test.end) : start;
+ if (start.node && end.node) {
+ range = Wysiwyg.getRange($editable[0]);
+ var startPoint = endOfAreaBetweenTwoNodes({
+ node: range.sc,
+ offset: range.so,
+ });
+ var endPoint = endOfAreaBetweenTwoNodes({
+ node: range.ec,
+ offset: range.eo,
+ });
+ var sameDOM = (startPoint.node.outerHTML || startPoint.node.textContent) === (start.node.outerHTML || start.node.textContent);
+ var stringify = function (obj) {
+ if (!sameDOM) {
+ delete obj.sameDOMsameNode;
+ }
+ return JSON.stringify(obj, null, 2)
+ .replace(/"([^"\s-]+)":/g, "\$1:")
+ .replace(/([^\\])"/g, "\$1'")
+ .replace(/\\"/g, '"');
+ };
+ assert.deepEqual(stringify({
+ startNode: startPoint.node.outerHTML || startPoint.node.textContent,
+ startOffset: startPoint.offset,
+ endPoint: endPoint.node.outerHTML || endPoint.node.textContent,
+ endOffset: endPoint.offset,
+ sameDOMsameNode: sameDOM && startPoint.node === start.node,
+ }),
+ stringify({
+ startNode: start.node.outerHTML || start.node.textContent,
+ startOffset: start.offset,
+ endPoint: end.node.outerHTML || end.node.textContent,
+ endOffset: end.offset,
+ sameDOMsameNode: true,
+ }),
+ test.name + " (carret position)");
+ }
+ }
+ });
+ }
+ while (keyboardTests.length) {
+ defPollTest = defPollTest.then(pollTest.bind(null, keyboardTests.shift()));
+ }
+
+ return defPollTest;
+};
+
+
+/**
+ * Select a node in the dom with is offset.
+ *
+ * @param {String} startSelector
+ * @param {String} endSelector
+ * @param {jQuery} $editable
+ * @returns {Object} {sc, so, ec, eo}
+ */
+var select = (function () {
+ var __select = function (selector, $editable) {
+ var sel = selector.match(/^(.+?)(:contents\(\)\[([0-9]+)\]|:contents\(([0-9]+)\))?(->([0-9]+))?$/);
+ var $node = $editable.find(sel[1]);
+ return {
+ node: sel[2] ? $node.contents()[sel[3] ? +sel[3] : +sel[4]] : $node[0],
+ offset: sel[5] ? +sel[6] : 0,
+ };
+ };
+ return function (startSelector, endSelector, $editable) {
+ var start = __select(startSelector, $editable);
+ var end = endSelector ? __select(endSelector, $editable) : start;
+ return {
+ sc: start.node,
+ so: start.offset,
+ ec: end.node,
+ eo: end.offset,
+ };
+ };
+})();
+
+/**
+ * Trigger a keydown event.
+ *
+ * @param {String or Number} key (name or code)
+ * @param {jQuery} $editable
+ * @param {Object} [options]
+ * @param {Boolean} [options.firstDeselect] (default: false) true to deselect before pressing
+ */
+var keydown = function (key, $editable, options) {
+ var keyPress = {};
+ if (typeof key === 'string') {
+ keyPress.key = key;
+ keyPress.keyCode = +_.findKey(keyboardMap, function (k) {
+ return k === key;
+ });
+ } else {
+ keyPress.key = keyboardMap[key] || String.fromCharCode(key);
+ keyPress.keyCode = key;
+ }
+ var range = Wysiwyg.getRange($editable[0]);
+ if (!range) {
+ console.error("Editor have not any range");
+ return;
+ }
+ if (options && options.firstDeselect) {
+ range.sc = range.ec;
+ range.so = range.eo;
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ }
+ var target = range.ec;
+ var $target = $(target.tagName ? target : target.parentNode);
+ var event = $.Event("keydown", keyPress);
+ $target.trigger(event);
+};
+
+return {
+ wysiwygData: wysiwygData,
+ createWysiwyg: createWysiwyg,
+ testKeyboard: testKeyboard,
+ select: select,
+ keydown: keydown,
+};
+
+
+});
diff --git a/addons/web_editor/static/tests/web_editor_tests.js b/addons/web_editor/static/tests/web_editor_tests.js
deleted file mode 100644
index 1995add777d..00000000000
--- a/addons/web_editor/static/tests/web_editor_tests.js
+++ /dev/null
@@ -1,327 +0,0 @@
-odoo.define('web_editor.web_editor_tests', function (require) {
-"use strict";
-
-var FormView = require('web.FormView');
-var testUtils = require('web.test_utils');
-var core = require('web.core');
-var web_editor = require('web_editor.editor');
-
-var _t = core._t;
-
-QUnit.module('web_editor', {
- beforeEach: function() {
- this.data = {
- 'mass.mailing': {
- fields: {
- display_name: { string: "Displayed name", type: "char" },
- body: {string: "Message Body", type: "html"},
- },
- records: [{
- id: 1,
- display_name: "first record",
- body: "<div class='field_body'>yep</div>",
- }],
- onchanges: {},
- },
- };
- }
-});
-
-QUnit.test('field html widget', function (assert) {
- var done = assert.async();
- assert.expect(3);
-
- var form = testUtils.createView({
- View: FormView,
- model: 'mass.mailing',
- data: this.data,
- arch: '<form string="Partners">' +
- '<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>',
- "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) {
- var done = assert.async();
- assert.expect(3);
-
- var form = testUtils.createView({
- View: FormView,
- model: 'mass.mailing',
- data: this.data,
- arch: '<form string="Partners">' +
- '<field name="body" widget="html" style="height: 100px" options="{\'style-inline\': true}"/>' +
- '</form>',
- res_id: 1,
- });
-
- 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");
-
- testUtils.form.clickEdit(form);
-
- assert.strictEqual(form.$('.note-editable').html(), '<div class="field_body">yep</div>',
- "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;
-
- var form = testUtils.createView({
- View: FormView,
- model: 'mass.mailing',
- 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, ['mass.mailing',1,'body',{}], "should call 'call_button' route");
- return $.when();
- }
- return this._super.apply(this, arguments);
- },
- });
-
- assert.containsNone(form, '.oe_form_field_html_text .o_field_translate',
- "should not have a translate button in readonly mode");
-
- 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);
-
- form.destroy();
- _t.database.multi_lang = multiLang;
-});
-
-QUnit.test('field html_frame widget', function (assert) {
- assert.expect(6);
-
- var form = testUtils.createView({
- View: FormView,
- model: 'mass.mailing',
- data: this.data,
- arch: '<form string="Partners">' +
- '<field name="body" widget="html_frame" options="{\'editor_url\': \'/logo\'}"/>' +
- '</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();
- }
- return this._super.apply(this, arguments);
- },
- });
-
- assert.containsOnce(form, 'iframe', "should have rendered an iframe without crashing");
-
- testUtils.form.clickEdit(form);
-
- assert.containsOnce(form, 'iframe', "should have rendered an iframe without crashing");
-
- form.destroy();
-});
-
-QUnit.test('field htmlsimple does not crash when commitChanges is called in mode=readonly', function (assert) {
- assert.expect(1);
-
- var form = testUtils.createView({
- View: FormView,
- model: 'mass.mailing',
- 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>' +
- '</form>',
- res_id: 1,
- intercepts: {
- execute_action: function () {
- assert.step('execute_action');
- }
- },
- });
-
- testUtils.dom.click(form.$('button:contains(Do it)'));
- form.destroy();
-});
-
-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);
-
- var form = testUtils.createView({
- View: FormView,
- model: 'mass.mailing',
- data: this.data,
- arch: '<form string="Partners">' +
- '<sheet>' +
- '<field name="body" widget="html_frame" options="{\'editor_url\': \'/logo\'}"/>' +
- '</sheet>' +
- '</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 loaded (note: just the content, not the editor)
- window.odoo[$.deparam(route).callback + '_content'].call();
- return $.when();
- }
- return this._super.apply(this, arguments);
- },
- });
-
- form.saveRecord(); // before the fix done in this commit, it crashed here
-
- assert.verifySteps(['read']);
-
- form.destroy();
-});
-
-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);
-
- var form = testUtils.createView({
- View: FormView,
- model: 'mass.mailing',
- data: this.data,
- arch: '<form string="Partners">' +
- '<sheet>' +
- '<field name="display_name"/>' +
- '<field name="body" widget="html_frame" options="{\'editor_url\': \'/logo\'}"/>' +
- '</sheet>' +
- '</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();
- }
- return this._super.apply(this, arguments);
- },
- });
-
- testUtils.form.clickEdit(form);
- testUtils.fields.editInput(form.$('input.o_input.o_field_char'), 'trululu');
- testUtils.form.clickSave(form); // crash without editor fully loaded
-
- assert.verifySteps(['read']);
-
- form.destroy();
-});
-
-QUnit.test('html_frame saving in edit mode (editor and content fully loaded)', function (assert) {
- var done = assert.async();
- assert.expect(4);
-
- var editorBar = new web_editor.Class();
- var loadDeferred = $.Deferred();
- var writeDeferred = $.Deferred();
-
- var form = testUtils.createView({
- View: FormView,
- model: 'mass.mailing',
- data: this.data,
- arch: '<form string="Partners">' +
- '<sheet>' +
- '<field name="display_name"/>' +
- '<field name="body" widget="html_frame" options="{\'editor_url\': \'/logo\'}"/>' +
- '</sheet>' +
- '</form>',
- res_id: 1,
- mockRPC: function (route, args) {
- if (args.method) {
- assert.step(args.method);
- if (args.method === 'write') {
- writeDeferred.resolve();
- }
- }
- 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);
- }
- });
- }
- return this._super.apply(this, arguments);
- },
- });
-
- testUtils.form.clickEdit(form);
- testUtils.fields.editInput(form.$('input.o_input.o_field_char'), 'trululu');
- testUtils.form.clickSave(form);
-
- loadDeferred.resolve(); // simulate late loading of html frame
-
- assert.strictEqual(form.$('.o_field_char').val(), 'trululu',
- "should have saved the char field text");
-
- writeDeferred.then( function () { // html_frame is async with write
- assert.verifySteps(['read', 'write']);
- form.destroy();
- done();
- });
-
-});
-
-});
diff --git a/addons/web_editor/static/tests/wysiwyg_keyboard_tests.js b/addons/web_editor/static/tests/wysiwyg_keyboard_tests.js
new file mode 100644
index 00000000000..dfbddb2dc55
--- /dev/null
+++ b/addons/web_editor/static/tests/wysiwyg_keyboard_tests.js
@@ -0,0 +1,4179 @@
+odoo.define('web_editor.wysiwyg.keyboard_tests', function (require) {
+"use strict";
+
+var weTestUtils = require('web_editor.test_utils');
+
+//--------------------------------------------------------------------------
+// tests
+//--------------------------------------------------------------------------
+
+QUnit.module('web_editor', {}, function () {
+QUnit.module('wysiwyg', {}, function () {
+
+//--------------------------------------------------------------------------
+// Unbreakable tests
+//--------------------------------------------------------------------------
+QUnit.module('Unbreakable');
+
+var unbreakableTestDom = [
+ ' <noteditable id="a">',
+ ' content_a_0',
+ ' <noteditable id="b">content_b_0</noteditable>',
+ ' <editable id="c">',
+ ' content_c_0',
+ ' <noteditable id="d">content_d_0</noteditable>',
+ ' <noteditable id="e">',
+ ' content_e_0',
+ ' <editable id="f">content_f_0</editable>',
+ ' content_e_1',
+ ' </noteditable>',
+ ' content_c_4',
+ ' </editable>',
+ ' <noteditable id="g">content_g_0</noteditable>',
+ ' <editable id="h">',
+ ' content_h_0',
+ ' <noteditable id="i">content_i_0</noteditable>',
+ ' content_h_2',
+ ' </editable>',
+ ' <editable id="j">',
+ ' content_j_0',
+ ' <editable id="k">content_k_0</editable>',
+ ' <editable id="l">content_l_0</editable>',
+ ' content_j_4',
+ ' </editable>',
+ ' </noteditable>',
+ ' <editable id="m">content_m_0</editable>',
+ ' <editable id="n">content_n_0</editable>',
+ ''
+].join('\n');
+var UnbreakableTests = [{
+ name: "nothing to do",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#c:contents()[0]->13",
+ end: "#c:contents()[0]->15",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#c:contents()[0]->13",
+ end: "#c:contents()[0]->15",
+ },
+ },
+ {
+ name: "nothing to do 2",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#j:contents()[0]->13",
+ end: "#l:contents()[0]->5",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#j:contents()[0]->13",
+ end: "#l:contents()[0]->5",
+ },
+ },
+ {
+ name: "nothing to do 3",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#k:contents()[0]->3",
+ end: "#l:contents()[0]->5",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#k:contents()[0]->3",
+ end: "#l:contents()[0]->5",
+ },
+ },
+ {
+ name: "find the first allowed node",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#a:contents()[0]->13",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#c:contents()[0]->0",
+ end: "#c:contents()[0]->0",
+ },
+ },
+ {
+ name: "find the first allowed node and collapse the selection",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#a:contents()[0]->10",
+ end: "#b:contents()[0]->3",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#c:contents()[0]->0",
+ end: "#c:contents()[0]->0",
+ },
+ },
+ {
+ name: "resize the range to the allowed end",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#a:contents()[0]->10",
+ end: "#c:contents()[0]->14",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#c:contents()[0]->0",
+ end: "#c:contents()[0]->14",
+ },
+ },
+ {
+ name: "resize the range to the allowed start",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#c:contents()[0]->15",
+ end: "#d:contents()[0]->4",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#c:contents()[0]->15",
+ end: "#c:contents()[0]->37",
+ },
+ },
+ {
+ name: "resize the range to the allowed node that contains unbreakable node",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#g:contents()[0]->5",
+ end: "#h:contents()[2]->15",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#h:contents()[0]->0",
+ end: "#h:contents()[2]->15",
+ },
+ },
+ {
+ name: "resize the range to the allowed node between the start and the end",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#e:contents()[0]->15",
+ end: "#c:contents()[4]->15",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#f:contents()[0]->0",
+ end: "#f:contents()[0]->11",
+ },
+ },
+ {
+ name: "resize the range to the allowed start with the entirety of the unbreakable node",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#c:contents()[0]->15",
+ end: "#e:contents()[0]->15",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#c:contents()[0]->15",
+ end: "#d->1",
+ },
+ },
+ {
+ name: "resize the range to the allowed start and delete content",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#c:contents()[0]->15",
+ end: "#e:contents()[0]->15",
+ key: "DELETE",
+ }],
+ test: {
+ content: unbreakableTestDom
+ // when we remove, we clean the invisible space before and after the deleted area
+ .replace(/>[\s]+content_c_0[\s\S]+content_d_0<\/noteditable>\s+/, '>co')
+ .replace(/^\s+/, ''), // corner effect of the clean and normalize
+ start: "#c:contents()[0]->2",
+ end: "#c:contents()[0]->2",
+ },
+ },
+ {
+ name: "delete unbreakable nodes in a breakable",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#c:contents()[0]->15",
+ end: "#c:contents()[4]->19",
+ key: "DELETE",
+ }],
+ test: {
+ content: unbreakableTestDom
+ // when we remove, we clean the invisible space before and after the deleted area
+ .replace(/>[\s]+content_c_0[\s\S]*content_c_4\s*/, '>cot_c_4')
+ .replace(/^\s+/, ''), // corner effect of the clean and normalize
+ start: "#c:contents()[0]->2",
+ end: "#c:contents()[0]->2",
+ },
+ },
+ {
+ name: "select range with 2 nodes on the root",
+ content: unbreakableTestDom,
+ steps: [{
+ start: "#m:contents()[0]->5",
+ end: "#n:contents()[0]->5",
+ }],
+ test: {
+ content: unbreakableTestDom,
+ start: "#m:contents()[0]->5",
+ end: "#n:contents()[0]->5",
+ },
+ },
+ {
+ name: "ENTER in an unbreakable p",
+ content: "<p class='unbreakable'>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ end: "p:contents()[0]->1",
+ key: 'ENTER',
+ }],
+ test: {
+ content: '<p class="unbreakable">d<br>om to edit</p>',
+ start: "p:contents()[2]->0",
+ },
+ },
+];
+
+QUnit.test('Unbreakable selection and edition', function (assert) {
+ var done = assert.async();
+ weTestUtils.createWysiwyg({
+ data: this.data,
+ useOnlyTestUnbreakable: true,
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+ weTestUtils.testKeyboard($editable, assert, UnbreakableTests).then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+//--------------------------------------------------------------------------
+// Keyboard integration
+//--------------------------------------------------------------------------
+
+QUnit.module('Keyboard');
+
+var keyboardTestsChar = [{
+ name: "visible char in a p tag",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->3",
+ keyCode: 66,
+ }],
+ test: {
+ content: "<p>domB to edit</p>",
+ start: "p:contents()[0]->4",
+ end: null,
+ check: function ($editable, assert) {
+ assert.ok(true);
+ },
+ },
+ },
+ {
+ name: "visible char in a link tag entirely selected",
+ content: '<div><a href="#">dom to edit</a></div>',
+ steps: [{
+ start: "a:contents()[0]->0",
+ end: "a:contents()[0]->11",
+ key: 'a',
+ }],
+ test: {
+ content: '<div><a href="#">&#8203;a&#8203;</a></div>',
+ start: "a:contents()[0]->2",
+ },
+ },
+ {
+ name: "'a' on a selection of all the contents of a complex dom",
+ content: "<p><b>dom</b></p><p><b>to<br>completely</b>remov<i>e</i></p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "i:contents()[0]->1",
+ key: 'a',
+ }],
+ test: {
+ content: "<p><b>a</b></p>",
+ start: "b:contents()[0]->1",
+ },
+ },
+ {
+ name: "'a' on a selection of all the contents of a complex dom (2)",
+ content: "<h1 class=\"a\"><font style=\"font-size: 62px;\"><b>dom to</b>edit</font></h1>",
+ steps: [{
+ start: "font:contents()[0]->0",
+ end: "font:contents()[1]->4",
+ key: 'a',
+ }],
+ test: {
+ content: "<h1 class=\"a\"><font style=\"font-size: 62px;\"><b>a</b></font></h1>",
+ start: "b:contents()[0]->1",
+ },
+ },
+ {
+ name: "'a' before an image",
+ content: '<p>xxx <img src="/web_editor/static/src/img/transparent.png"> yyy</p>',
+ steps: [{
+ start: "p:contents()[0]->4",
+ key: 'a',
+ }],
+ test: {
+ content: '<p>xxx a<img data-src="/web_editor/static/src/img/transparent.png"> yyy</p>',
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "'a' before an image (2)",
+ content: '<p>xxx <img src="/web_editor/static/src/img/transparent.png"> yyy</p>',
+ steps: [{
+ start: "img->0",
+ key: 'a',
+ }],
+ test: {
+ content: '<p>xxx a<img data-src="/web_editor/static/src/img/transparent.png"> yyy</p>',
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "'a' before an image in table",
+ content: '<table><tbody><tr><td><p>xxx</p></td><td><p><img src="/web_editor/static/src/img/transparent.png"></p></td><td><p>yyy</p></td></tr></tbody></table>',
+ steps: [{
+ start: "img->0",
+ key: 'a',
+ }],
+ test: {
+ content: '<table><tbody><tr><td><p>xxx</p></td><td><p>a<img data-src="/web_editor/static/src/img/transparent.png"></p></td><td><p>yyy</p></td></tr></tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "'a' on invisible text before an image in table",
+ content: '<table><tbody><tr><td><p>xxx</p></td><td><p><img src="/web_editor/static/src/img/transparent.png"></p></td><td><p>yyy</p></td></tr></tbody></table>',
+ steps: [{
+ start: "p:eq(1)->0",
+ key: 'a',
+ }],
+ test: {
+ content: '<table><tbody><tr><td><p>xxx</p></td><td><p>a<img data-src="/web_editor/static/src/img/transparent.png"></p></td><td><p>yyy</p></td></tr></tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "'a' before an image in table without spaces",
+ content: '<table><tbody><tr><td><p>xxx</p></td><td><p><img src="/web_editor/static/src/img/transparent.png"></p></td><td><p>yyy</p></td></tr></tbody></table>',
+ steps: [{
+ start: "img->0",
+ key: 'a',
+ }],
+ test: {
+ content: '<table><tbody><tr><td><p>xxx</p></td><td><p>a<img data-src="/web_editor/static/src/img/transparent.png"></p></td><td><p>yyy</p></td></tr></tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "'a' before an image in table without spaces (2)",
+ content: '<table><tbody><tr><td><p>xxx</p></td><td><p><img src="/web_editor/static/src/img/transparent.png"></p></td><td><p>yyy</p></td></tr></tbody></table>',
+ steps: [{
+ start: "td:eq(1)->0",
+ key: 'a',
+ }],
+ test: {
+ content: '<table><tbody><tr><td><p>xxx</p></td><td><p>a<img data-src="/web_editor/static/src/img/transparent.png"></p></td><td><p>yyy</p></td></tr></tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "'a' before an image in table with spaces",
+ content:
+ '<table><tbody>\n' +
+ ' <tr>\n' +
+ ' <td>\n' +
+ ' <p>xxx</p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p><img src="/web_editor/static/src/img/transparent.png"></p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p>yyy</p>\n' +
+ ' </td>\n' +
+ ' </tr>\n' +
+ '</tbody></table>',
+ steps: [{
+ start: "img->0",
+ key: 'a',
+ }],
+ test: {
+ content:
+ '<table><tbody>\n' +
+ ' <tr>\n' +
+ ' <td>\n' +
+ ' <p>xxx</p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p>a<img data-src="/web_editor/static/src/img/transparent.png"></p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p>yyy</p>\n' +
+ ' </td>\n' +
+ ' </tr>\n' +
+ '</tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "'a' before an image in table with spaces (2)",
+ content:
+ '<table><tbody>\n' +
+ ' <tr>\n' +
+ ' <td>\n' +
+ ' <p>xxx</p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p><img src="/web_editor/static/src/img/transparent.png"></p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p>yyy</p>\n' +
+ ' </td>\n' +
+ ' </tr>\n' +
+ '</tbody></table>',
+ steps: [{
+ start: "td:eq(1)->1",
+ key: 'a',
+ }],
+ test: {
+ content:
+ '<table><tbody>\n' +
+ ' <tr>\n' +
+ ' <td>\n' +
+ ' <p>xxx</p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p>a<img data-src="/web_editor/static/src/img/transparent.png"></p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p>yyy</p>\n' +
+ ' </td>\n' +
+ ' </tr>\n' +
+ '</tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "'a' before an image in table with spaces (3)",
+ content:
+ '<table><tbody>\n' +
+ ' <tr>\n' +
+ ' <td>\n' +
+ ' <p>xxx</p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p><img src="/web_editor/static/src/img/transparent.png"></p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p>yyy</p>\n' +
+ ' </td>\n' +
+ ' </tr>\n' +
+ '</tbody></table>',
+ steps: [{
+ start: "td:eq(1):contents()[0]->12",
+ key: 'a',
+ }],
+ test: {
+ content:
+ '<table><tbody>\n' +
+ ' <tr>\n' +
+ ' <td>\n' +
+ ' <p>xxx</p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p>a<img data-src="/web_editor/static/src/img/transparent.png"></p>\n' +
+ ' </td>\n' +
+ ' <td>\n' +
+ ' <p>yyy</p>\n' +
+ ' </td>\n' +
+ ' </tr>\n' +
+ '</tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "'a' on begin of a span with fake_editable",
+ content:
+ '<div class="o_fake_not_editable" contentEditable="false">\n' +
+ ' <div>\n' +
+ ' <label>\n' +
+ ' <input type="checkbox"/>\n' +
+ ' <span class="o_fake_editable" contentEditable="true">\n' +
+ ' dom to edit\n' +
+ ' </span>\n' +
+ ' </label>\n' +
+ ' </div>\n' +
+ '</div>',
+ steps: [{
+ start: "span:contents(0)->10",
+ key: 'a',
+ }],
+ test: {
+ content:
+ '<div>\n' +
+ ' <div>\n' +
+ ' <label>\n' +
+ ' <input type="checkbox">\n' +
+ ' <span>\n' +
+ ' adom to edit\n' +
+ ' </span>\n' +
+ ' </label>\n' +
+ ' </div>\n' +
+ '</div>',
+ start: "span:contents(0)->11",
+ },
+ },
+ {
+ name: "'a' on all contents of p starting with an icon",
+ content: '<p><span class="fa fa-star"></span>bbb</p>',
+ steps: [{
+ start: "span->0",
+ end: 'p:contents(1)->3',
+ key: 'a',
+ }],
+ test: {
+ content: '<p>a</p>',
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "' ' at start of p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: ' ',
+ }],
+ test: {
+ content: '<p>&nbsp;dom to edit</p>',
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "' ' at end of p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->11",
+ key: ' ',
+ }],
+ test: {
+ content: '<p>dom to edit&nbsp;</p>',
+ start: "p:contents()[0]->12",
+ },
+ },
+ {
+ name: "' ' within p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->2",
+ key: ' ',
+ }],
+ test: {
+ content: '<p>do m to edit</p>',
+ start: "p:contents()[0]->3",
+ },
+ },
+ {
+ name: "' ' before space within p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->3",
+ key: ' ',
+ }],
+ test: {
+ content: '<p>dom&nbsp;&nbsp;to edit</p>',
+ start: "p:contents()[0]->4",
+ },
+ },
+ {
+ name: "' ' after space within p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->4",
+ key: ' ',
+ }],
+ test: {
+ content: '<p>dom&nbsp;&nbsp;to edit</p>',
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "3x ' ' at start of p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: ' ',
+ }, {
+ key: ' ',
+ }, {
+ key: ' ',
+ }],
+ test: {
+ content: '<p>&nbsp;&nbsp;&nbsp;dom to edit</p>',
+ start: "p:contents()[0]->3",
+ },
+ },
+ {
+ name: "3x ' ' at end of p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->11",
+ key: ' ',
+ }, {
+ key: ' ',
+ }, {
+ key: ' ',
+ }],
+ test: {
+ content: '<p>dom to edit&nbsp;&nbsp;&nbsp;</p>',
+ start: "p:contents()[0]->14",
+ },
+ },
+ {
+ name: "3x ' ' within p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->2",
+ key: ' ',
+ }, {
+ key: ' ',
+ }, {
+ key: ' ',
+ }],
+ test: {
+ content: '<p>do&nbsp;&nbsp;&nbsp;m to edit</p>',
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "3x ' ' before space in p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->3",
+ key: ' ',
+ }, {
+ key: ' ',
+ }, {
+ key: ' ',
+ }],
+ test: {
+ content: '<p>dom&nbsp;&nbsp;&nbsp;&nbsp;to edit</p>',
+ start: "p:contents()[0]->6",
+ },
+ },
+ {
+ name: "3x ' ' after space in p",
+ content: '<p>dom to edit</p>',
+ steps: [{
+ start: "p:contents()[0]->4",
+ key: ' ',
+ }, {
+ key: ' ',
+ }, {
+ key: ' ',
+ }],
+ test: {
+ content: '<p>dom&nbsp;&nbsp;&nbsp;&nbsp;to edit</p>',
+ start: "p:contents()[0]->7",
+ },
+ },
+ {
+ name: "'a' in unbreakable with font",
+ content: '<div class="unbreakable">dom <span class="fa fa-heart"></span>to edit</div>',
+ steps: [{
+ start: "div:contents(2)->2",
+ key: 'a',
+ }],
+ test: {
+ content: '<div class="unbreakable">dom <span class="fa fa-heart"></span>toa edit</div>',
+ start: "div:contents(2)->3",
+ },
+ },
+ {
+ name: "'a' on begin of unbreakable inline node",
+ content: 'dom <strong class="unbreakable">to</strong> edit',
+ steps: [{
+ start: "strong:contents(0)->0",
+ key: 'a',
+ }],
+ test: {
+ content: 'dom <strong class="unbreakable">ato</strong> edit',
+ start: "strong:contents(0)->1",
+ },
+ },
+ {
+ name: "'a' on end of unbreakable inline node",
+ content: '<div>dom <strong class="unbreakable">to</strong> edit</div>',
+ steps: [{
+ start: "strong:contents(0)->2",
+ key: 'a',
+ }],
+ test: {
+ content: '<div>dom <strong class="unbreakable">toa</strong> edit</div>',
+ start: "strong:contents(0)->3",
+ },
+ },
+ {
+ name: "'1' on begin of value of a field currency",
+ content:
+ '<noteditable>\n' +
+ '<b data-oe-type="monetary" class="oe_price editable">$&nbsp;<span class="oe_currency_value">750.00</span></b>\n' +
+ '</noteditable>',
+ steps: [{
+ start: "span:contents(0)->0",
+ key: '1',
+ }],
+ test: {
+ content:
+ '<noteditable>\n' +
+ '<b data-oe-type="monetary" class="oe_price editable">$&nbsp;<span class="oe_currency_value">1750.00</span></b>\n' +
+ '</noteditable>',
+ start: "span:contents(0)->1",
+ },
+ },
+ {
+ name: "'1' on end of value of a field currency",
+ content:
+ '<noteditable>\n' +
+ '<b data-oe-type="monetary" class="oe_price editable">$&nbsp;<span class="oe_currency_value">750.00</span></b>\n' +
+ '</noteditable>',
+ steps: [{
+ start: "span:contents(0)->6",
+ key: '1',
+ }],
+ test: {
+ content:
+ '<noteditable>\n' +
+ '<b data-oe-type="monetary" class="oe_price editable">$&nbsp;<span class="oe_currency_value">750.001</span></b>\n' +
+ '</noteditable>',
+ start: "span:contents(0)->7",
+ },
+ },
+ {
+ name: "'1' on begin of editable in noteditable",
+ content:
+ '<noteditable contenteditable="false">\n' +
+ '<b data-oe-type="monetary" class="oe_price editable" contenteditable="true">$&nbsp;<span class="oe_currency_value">750.00</span></b>\n' +
+ '</noteditable>',
+ steps: [{
+ start: "span:contents(0)->0",
+ key: '1',
+ }],
+ test: {
+ content:
+ '<noteditable>\n' +
+ '<b data-oe-type="monetary" class="oe_price editable">$&nbsp;<span class="oe_currency_value">1750.00</span></b>\n' +
+ '</noteditable>',
+ start: "span:contents(0)->1",
+ },
+ },
+ {
+ name: "'1' on end of editable in noteditable",
+ content:
+ '<noteditable contenteditable="false">\n' +
+ '<b data-oe-type="monetary" class="oe_price editable" contenteditable="true">$&nbsp;<span class="oe_currency_value">750.00</span></b>\n' +
+ '</noteditable>',
+ steps: [{
+ start: "span:contents(0)->6",
+ key: '1',
+ }],
+ test: {
+ content:
+ '<noteditable>\n' +
+ '<b data-oe-type="monetary" class="oe_price editable">$&nbsp;<span class="oe_currency_value">750.001</span></b>\n' +
+ '</noteditable>',
+ start: "span:contents(0)->7",
+ },
+ },
+ {
+ name: "'a' on editable with before&after in noteditable",
+ content:
+ '<style>#test-before-after:before { content: "placeholder";} #test-before-after:after { content: "\\00a0";}</style>\n' +
+ '<noteditable contenteditable="false">\n' +
+ '<b id="test-before-after" class="editable" contenteditable="true"></b>\n' +
+ '</noteditable>',
+ steps: [{
+ start: "b->0",
+ key: 'a',
+ }],
+ test: {
+ content:
+ '<style>#test-before-after:before { content: "placeholder";} #test-before-after:after { content: "\\00a0";}</style>\n' +
+ '<noteditable>\n' +
+ '<b id="test-before-after" class="editable">a</b>\n' +
+ '</noteditable>',
+ start: "b:contents(0)->1",
+ },
+ },
+];
+
+QUnit.test('Char', function (assert) {
+ var done = assert.async();
+ weTestUtils.createWysiwyg({
+ data: this.data,
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+ weTestUtils.testKeyboard($editable, assert, keyboardTestsChar).then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+var keyboardTestsEnter = [{
+ name: "in p: ENTER",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p>d</p><p>om to edit</p>",
+ start: "p:contents()[1]->0",
+ },
+ },
+ {
+ name: "in p: 2x ENTER",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ key: 'ENTER',
+ }, {
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p>d</p><p><br></p><p>om to edit</p>",
+ start: "p:contents()[2]->0",
+ },
+ },
+ {
+ name: "in p: SHIFT+ENTER",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p>d<br>om to edit</p>",
+ start: "p:contents()[2]->0",
+ },
+ },
+ {
+ name: "in empty-p: SHIFT+ENTER",
+ content: "<p><br></p>",
+ steps: [{
+ start: "p->1",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p><br><br></p>",
+ start: "br:eq(1)->0",
+ },
+ },
+ {
+ name: "in p: 2x SHIFT+ENTER",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p>d<br><br>om to edit</p>",
+ start: "p:contents()[3]->0",
+ },
+ },
+ {
+ name: "in empty-p: 2x SHIFT+ENTER",
+ content: "<p><br></p>",
+ steps: [{
+ start: "p->1",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p><br><br><br></p>",
+ start: "br:eq(2)->0",
+ },
+ },
+ {
+ name: "in p: ENTER -> SHIFT+ENTER",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ key: 'ENTER',
+ }, {
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p>d</p><p><br>om to edit</p>",
+ start: "p:eq(1):contents()[1]->0",
+ },
+ },
+ {
+ name: "in p: ENTER on selection",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ end: "p:contents()[0]->7",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p>d</p><p>edit</p>",
+ start: "p:contents()[1]->0",
+ end: "p:contents()[1]->0",
+ },
+ },
+ {
+ name: "in p: SHIFT+ENTER on selection",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ end: "p:contents()[0]->7",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p>d<br>edit</p>",
+ start: "p:contents()[2]->0",
+ },
+ },
+
+ // list UL / OL
+
+ {
+ name: "in li: SHIFT+ENTER at start",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<ul><li><br>dom to edit</li></ul>",
+ start: "li:contents()[1]->0",
+ },
+ },
+ {
+ name: "in li: SHIFT+ENTER within contents",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->5",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<ul><li>dom t<br>o edit</li></ul>",
+ start: "li:contents()[2]->0",
+ },
+ },
+ {
+ name: "in empty-li: SHIFT+ENTER",
+ content: "<ul><li><br></li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<ul><li><br><br></li></ul>",
+ start: "br:eq(1)->0",
+ },
+ },
+ {
+ name: "in li: SHIFT+ENTER on selection",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->1",
+ end: "li:contents()[0]->7",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<ul><li>d<br>edit</li></ul>",
+ start: "li:contents()[2]->0",
+ },
+ },
+ {
+ name: "in li: ENTER at start",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<ul><li><br></li><li>dom to edit</li></ul>",
+ start: "li:contents()[1]->0",
+ },
+ },
+ {
+ name: "in li: ENTER within contents",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->5",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<ul><li>dom t</li><li>o edit</li></ul>",
+ start: "li:eq(1):contents()[0]->0",
+ },
+ },
+ {
+ name: "in li: ENTER on selection",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->1",
+ end: "li:contents()[0]->7",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<ul><li>d</li><li>edit</li></ul>",
+ start: "li:eq(1):contents()[0]->0",
+ },
+ },
+ {
+ name: "in li: ENTER on selection of all contents",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ end: "li:contents()[0]->11",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p><br></p>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in li: ENTER -> 'a' on selection of all contents",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ end: "li:contents()[0]->11",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>a</p>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "across 2 li: ENTER on partial selection",
+ content: "<ul><li>dom to edit</li><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->1",
+ end: "li:eq(1):contents()[0]->4",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<ul><li>d</li><li>to edit</li></ul>",
+ start: "li:eq(1):contents()[0]->0", // we are after the <br>, the carret is on the li with an offset equal to the node length
+ },
+ },
+ {
+ name: "in li: ENTER at end",
+ content: "<ul><li><p>dom to edit</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->11",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<ul><li><p>dom to edit</p></li><li><p><br></p></li></ul>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in li: 2x ENTER at end",
+ content: "<ul><li><p>dom to edit</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->11",
+ key: 'ENTER',
+ }, {
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<ul><li><p>dom to edit</p></li></ul><p><br></p>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in ul.list-group > li: ENTER at end",
+ content: '<ul class="list-group"><li><p>dom to edit</p></li></ul>',
+ steps: [{
+ start: "p:contents()[0]->11",
+ key: 'ENTER',
+ }, {
+ key: 'ENTER',
+ }],
+ test: {
+ content: '<ul class="list-group"><li><p>dom to edit</p></li><li><p><br></p></li><li><p><br></p></li></ul>',
+ start: "p:eq(2) br->0",
+ },
+ },
+ {
+ name: "in indented-li: 2x ENTER at end",
+ content: "<ul><li><p>aaa</p></li><ul><li><p>dom to edit</p></li></ul><li><p>bbb</p></li></ul>",
+ steps: [{
+ start: "ul ul p:contents()[0]->11",
+ key: 'ENTER',
+ }, {
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<ul><li><p>aaa</p></li><ul><li><p>dom to edit</p></li></ul><li><p><br></p></li><li><p>bbb</p></li></ul>",
+ start: "p:eq(2) br->0",
+ },
+ },
+ {
+ name: "in indented-li with font: 2x ENTER at end",
+ content: "<ul><li><p>aaa</p></li><ul><li><p><font style=\"color\">dom to edit</font></p></li></ul><li><p>bbb</p></li></ul>",
+ steps: [{
+ start: "font:contents()[0]->11",
+ key: 'ENTER',
+ }, {
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<ul><li><p>aaa</p></li><ul><li><p><font style=\"color\">dom to edit</font></p></li></ul><li><p><font style=\"color\"><br></font></p></li><li><p>bbb</p></li></ul>",
+ start: "p:eq(2) br->0",
+ },
+ },
+ {
+ name: "in li > empty-p: ENTER",
+ content: "<ul><li><p><br></p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p><br></p>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in li > p > empty-b: ENTER",
+ content: "<ul><li><p><b><br></b></p></li></ul>",
+ steps: [{
+ start: "b:contents()[0]->0",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p><b><br></b></p>",
+ start: "br->0",
+ },
+ },
+
+ // end list UL / OL
+
+ {
+ name: "after p > b: SHIFT+ENTER",
+ content: "<p><b>dom</b> to edit</p>",
+ steps: [{
+ start: "p:contents()[1]->0",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p><b>dom</b><br>&nbsp;to edit</p>",
+ start: "p:contents()[2]->0",
+ },
+ },
+ {
+ name: "in p > b: ENTER -> 'a'",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->2",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><b>do</b></p><p><b>am to edit</b></p>",
+ start: "b:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "after p > b: ENTER -> 'a'",
+ content: "<p><b>dom</b> to edit</p>",
+ steps: [{
+ start: "p:contents()[1]->0",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><b>dom</b></p><p>a to edit</p>",
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "after p > b: SHIFT+ENTER -> 'a'",
+ content: "<p><b>dom</b>&nbsp;to edit</p>",
+ steps: [{
+ start: "p:contents()[1]->0",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><b>dom</b><br>a to edit</p>",
+ start: "p:contents()[2]->1",
+ },
+ },
+ {
+ name: "in p (other-p > span.a before - p > span.b after): ENTER at beginning",
+ content: "<p><span class=\"a\">dom to</span></p><p><span class=\"b\">edit</span></p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p><span class=\"a\">dom to</span></p><p><span class=\"b\"><br></span></p><p><span class=\"b\">edit</span></p>",
+ start: "span:eq(2):contents()[0]->0",
+ },
+ },
+ {
+ name: "in p (other-p > span.a before - p > span.b after): ENTER -> 'a' at beginning",
+ content: "<p><span class=\"a\">dom to</span></p><p><span class=\"b\">edit</span></p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><span class=\"a\">dom to</span></p><p><span class=\"b\"><br></span></p><p><span class=\"b\">aedit</span></p>",
+ start: "span:eq(2):contents()[0]->1",
+ },
+ },
+ {
+ name: "in p (other-p > span.a before - p > span.b after): SHIFT+ENTER at beginning",
+ content: "<p><span class=\"a\">dom to</span></p><p><span class=\"b\">edit</span></p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p><span class=\"a\">dom to</span></p><p><span class=\"b\"><br>edit</span></p>",
+ start: "span:eq(1):contents()[1]->0",
+ },
+ },
+ {
+ name: "in p (other-p > span.a before - p > span.b after): SHIFT+ENTER -> 'a' at beginning",
+ content: "<p><span class=\"a\">dom to</span></p><p><span class=\"b\">edit</span></p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><span class=\"a\">dom to</span></p><p><span class=\"b\"><br>aedit</span></p>",
+ start: "span:eq(1):contents()[1]->1",
+ },
+ },
+ {
+ name: "in p: ENTER on selection of all contents",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "p:contents()[0]->11",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p><br></p><p><br></p>",
+ start: "p:eq(1) br->0",
+ },
+ },
+ {
+ name: "in p: ENTER -> 'a' on selection of all contents",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "p:contents()[0]->11",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><br></p><p>a</p>",
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in p: SHIFT+ENTER on selection of all contents",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "p:contents()[0]->11",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p><br><br></p>",
+ start: "br:eq(1)->0",
+ },
+ },
+ {
+ name: "in p: SHIFT+ENTER -> 'a' on selection of all contents",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "p:contents()[0]->11",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><br>a</p>",
+ start: "p:contents()[1]->1",
+ },
+ },
+ {
+ name: "in p: 2x ENTER -> 'a' on selection of all contents",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->3",
+ key: 'ENTER',
+ }, {
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>dom</p><p><br></p><p>a to edit</p>",
+ start: "p:eq(2):contents()[0]->1",
+ },
+ },
+ {
+ name: "in p > b: ENTER at start",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->0",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p><b><br></b></p><p><b>dom to edit</b></p>",
+ start: "b:eq(1):contents()[0]->0",
+ },
+ },
+ {
+ name: "in p > b: ENTER",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p><b>dom</b></p><p><b>&nbsp;to edit</b></p>",
+ start: "b:eq(1):contents()[0]->0",
+ },
+ },
+ {
+ name: "in p > b: SHIFT+ENTER -> ENTER",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<p><b>dom<br>&#8203;</b></p><p><b>&nbsp;to edit</b></p>",
+ start: "b:eq(1):contents()[0]->0",
+ },
+ },
+ {
+ name: "in p > b: ENTER -> a'",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><b>dom</b></p><p><b>a to edit</b></p>",
+ start: "b:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in p > b: SHIFT+ENTER",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p><b>dom<br>&nbsp;to edit</b></p>",
+ start: "b:contents()[2]->0",
+ },
+ },
+ {
+ name: "in p > b: SHIFT+ENTER -> 'a'",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><b>dom<br>a to edit</b></p>",
+ start: "b:contents()[2]->1",
+ },
+ },
+ {
+ name: "in p > b: SHIFT+ENTER -> ENTER -> 'a'",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><b>dom<br>&#8203;</b></p><p><b>a to edit</b></p>",
+ start: "b:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in span > b: ENTER",
+ content: "<span><b>dom to edit</b></span>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<span><b>dom</b></span><br><span><b>&#8203;&nbsp;to edit</b></span>",
+ start: "b:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in span > b: SHIFT+ENTER -> ENTER",
+ content: "<span><b>dom to edit</b></span>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<span><b>dom<br></b></span><br><span><b>&#8203;&nbsp;to edit</b></span>",
+ start: "b:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in span > b: ENTER -> a'",
+ content: "<span><b>dom to edit</b></span>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<span><b>dom</b></span><br><span><b>a to edit</b></span>",
+ start: "b:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in span > b: SHIFT+ENTER",
+ content: "<span><b>dom to edit</b></span>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<span><b>dom<br>&nbsp;to edit</b></span>",
+ start: "b:contents()[2]->0",
+ },
+ },
+ {
+ name: "in span > b: SHIFT+ENTER -> 'a'",
+ content: "<span><b>dom to edit</b></span>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<span><b>dom<br>a to edit</b></span>",
+ start: "b:contents()[2]->1",
+ },
+ },
+ {
+ name: "in span > b: SHIFT+ENTER -> ENTER -> 'a'",
+ content: "<span><b>dom to edit</b></span>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<span><b>dom<br></b></span><br><span><b>a to edit</b></span>",
+ start: "b:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in p: 2x SHIFT+ENTER -> 'a'",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>dom<br><br>a to edit</p>",
+ start: "p:contents()[3]->1",
+ },
+ },
+ {
+ name: "in p: ENTER -> SHIFT+ENTER -> 'a'",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->3",
+ key: 'ENTER',
+ }, {
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>dom</p><p><br>a to edit</p>",
+ start: "p:eq(1):contents()[1]->1",
+ },
+ },
+ {
+ name: "in empty-p (p before and after): ENTER -> 'a'",
+ content: "<p>dom </p><p><br></p><p>to edit</p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>dom </p><p><br></p><p>a</p><p>to edit</p>",
+ start: "p:eq(2):contents()[0]->1",
+ },
+ },
+ {
+ name: "in p: SHIFT+ENTER at end",
+ content: "<p>dom </p><p>to edit</p>",
+ steps: [{
+ start: "p:first:contents()[0]->4",
+ key: 'ENTER',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p>dom <br>&#8203;</p><p>to edit</p>",
+ start: "p:first:contents()[2]->0",
+ },
+ },
+ {
+ name: "in p: SHIFT+ENTER at end -> '寺'",
+ content: "<p>dom </p><p>to edit</p>",
+ steps: [{
+ start: "p:first:contents()[0]->4",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ keyCode: 23546, /*temple in chinese*/
+ }],
+ test: {
+ content: "<p>dom <br>寺</p><p>to edit</p>",
+ start: "p:first:contents()[2]->1",
+ },
+ },
+ {
+ name: "in empty-p (div > a after): 3x SHIFT+ENTER -> 'a'",
+ content: "<p><br></p><div><a href='#'>dom to edit</a></div>",
+ steps: [{
+ start: "p->1",
+ key: 'ENTER',
+ shiftKey: true,
+ },
+ {
+ key: 'ENTER',
+ shiftKey: true,
+ },
+ {
+ key: 'ENTER',
+ shiftKey: true,
+ },
+ {
+ key: 'a',
+ }
+ ],
+ test: {
+ content: "<p><br><br><br>a</p><div><a href=\"#\">dom to edit</a></div>",
+ start: "p:contents()[3]->1",
+ },
+ },
+
+ // Buttons
+
+ {
+ name: "in div > a.btn: ENTER -> 'a' at start (before invisible space)",
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;dom to edit&#8203;</a></div>",
+ steps: [{
+ start: "a:contents()[0]->0",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;a&#8203;</a><a class=\"btn\" href=\"#\">dom to edit</a></div>",
+ // split button has no text so the placeholder text is selected then replaced by 'a'
+ start: "a:contents()[0]->2",
+ },
+ },
+ {
+ name: "in div > a.btn: ENTER -> 'a' at start (after invisible space)",
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;dom to edit&#8203;</a></div>",
+ steps: [{
+ start: "a:contents()[0]->1",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;a&#8203;</a><a class=\"btn\" href=\"#\">dom to edit</a></div>",
+ // split button has no text so the placeholder text is selected then replaced by 'a'
+ start: "a:contents()[0]->2",
+ },
+ },
+ {
+ name: "in div > a.btn: ENTER -> 'a' within contents",
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;dom to edit&#8203;</a></div>",
+ steps: [{
+ start: "a:contents()[0]->6",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">dom t</a><a class=\"btn\" href=\"#\">&#8203;ao edit&#8203;</a></div>",
+ start: "a:eq(1):contents()[0]->2",
+ },
+ },
+ {
+ name: "in div > a.btn: ENTER -> 'a' at end (before invisible space)",
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;dom to edit&#8203;</a></div>",
+ steps: [{
+ start: "a:contents()[0]->12",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">dom to edit</a><a class=\"btn\" href=\"#\">&#8203;a&#8203;</a></div>",
+ start: "a:eq(1):contents()[0]->2",
+ },
+ },
+ {
+ name: "in div > a.btn: ENTER -> 'a' at end (after invisible space)",
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;dom to edit&#8203;</a></div>",
+ steps: [{
+ start: "a:contents()[0]->13",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">dom to edit</a><a class=\"btn\" href=\"#\">&#8203;a&#8203;</a></div>",
+ start: "a:eq(1):contents()[0]->2",
+ },
+ },
+ {
+ name: "in div > button.btn: ENTER -> 'a' at end (after invisible space)",
+ content: "<div class=\"unbreakable\"><button class=\"btn\" href=\"#\">&#8203;dom to edit&#8203;</button></div>",
+ steps: [{
+ start: "button:contents()[0]->13",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<div class=\"unbreakable\"><button class=\"btn\" href=\"#\">dom to edit</button><button class=\"btn\" href=\"#\">&#8203;a&#8203;</button></div>",
+ start: "button:eq(1):contents()[0]->2",
+ },
+ },
+ {
+ name: "in div > a.btn: ENTER -> 'a' on partial selection",
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;dom to edit&#8203;</a></div>",
+ steps: [{
+ start: "a:contents()[0]->4",
+ end: "a:contents()[0]->8",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">dom</a><a class=\"btn\" href=\"#\">&#8203;aedit&#8203;</a></div>",
+ start: "a:eq(1):contents()[0]->2",
+ },
+ },
+ {
+ name: "in div > a.btn: ENTER -> 'a' on selection of all visible text",
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">dom to edit</a></div>",
+ steps: [{
+ start: "a:contents()[0]->0",
+ end: "a:contents()[0]->11",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;a&#8203;</a><a class=\"btn\" href=\"#\">Label</a></div>",
+ // Removing all text in a link replaces that text with "Label"
+ start: "a:contents()[0]->2",
+ },
+ },
+ {
+ name: "across 2 a.btn: ENTER on selection across two a.btn",
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">dom not to edit</a><a class=\"btn\" href=\"#\">other dom not to edit</a></div>",
+ steps: [{
+ start: "a:contents()[0]->0",
+ end: "a:eq(1):contents()[0]->11",
+ key: 'ENTER',
+ }],
+ test: {
+ content: "<div class=\"unbreakable\"><a class=\"btn\" href=\"#\">&#8203;Label&#8203;</a><a class=\"btn\" href=\"#\">ot to edit</a></div>",
+ start: "a:contents()[0]->1",
+ end: "a:contents()[0]->6",
+ },
+ },
+];
+
+QUnit.test('Enter', function (assert) {
+ var done = assert.async();
+ weTestUtils.createWysiwyg({
+ data: this.data,
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+ weTestUtils.testKeyboard($editable, assert, keyboardTestsEnter).then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+var keyboardTestsComplex = [{
+ name: "in span > b: SHIFT+ENTER -> BACKSPACE -> 'a'",
+ content: "<span><b>dom to edit</b></span>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<span><b>doma to edit</b></span>",
+ start: "b:contents()[0]->4",
+ },
+ },
+ {
+ name: "in span > b: SHIFT+ENTER -> ENTER -> BACKSPACE",
+ content: "<span><b>dom to edit</b></span>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<span><b>dom<br>&nbsp;to edit</b></span>",
+ start: "b:contents(2)->0",
+ },
+ },
+ {
+ name: "in span > b: SHIFT+ENTER -> ENTER -> BACKSPACE -> 'a'",
+ content: "<span><b>dom to edit</b></span>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<span><b>dom<br>a to edit</b></span>",
+ start: "b:contents()[2]->1",
+ },
+ },
+ {
+ name: "in p > b: 2x SHIFT+ENTER -> BACKSPACE",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><b>dom<br>&nbsp;to edit</b></p>",
+ start: "b:contents()[2]->0",
+ },
+ },
+ {
+ name: "in p > b: 2x ENTER -> 2x BACKSPACE",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: false
+ }, {
+ key: 'ENTER',
+ shiftKey: false
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><b>dom&nbsp;to edit</b></p>",
+ start: "b:contents()[0]->3",
+ },
+ },
+ {
+ name: "in empty-p: 2x ENTER -> 2x BACKSPACE",
+ content: "<p><br></p>",
+ steps: [{
+ start: "p->1",
+ key: 'ENTER',
+ shiftKey: false
+ }, {
+ key: 'ENTER',
+ shiftKey: false
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><br></p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in empty-p (p before): ENTER -> 2x BACKSPACE",
+ content: "<p>dom not to edit</p><p><br></p>",
+ steps: [{
+ start: "p:eq(1)->1",
+ key: 'ENTER',
+ shiftKey: false
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom not to edit</p>",
+ start: "p:first:contents()[0]->15",
+ },
+ },
+ {
+ name: "in p > b: 2x SHIFT+ENTER -> BACKSPACE -> 'a'",
+ content: "<p><b>dom to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'ENTER',
+ shiftKey: true,
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><b>dom<br>a to edit</b></p>",
+ start: "b:contents()[2]->1",
+ },
+ },
+ {
+ name: "in li -> ENTER before br -> 'a'",
+ content: "<ul><li><p>dom<br/>&nbsp;to edit</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->3",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<ul><li><p>dom</p></li><li><p>a<br>&nbsp;to edit</p></li></ul>",
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in li -> ENTER after br -> 'a'",
+ content: "<ul><li><p>dom<br/>&nbsp;to edit</p></li></ul>",
+ steps: [{
+ start: "p:contents()[2]->0",
+ key: 'ENTER',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<ul><li><p>dom<br>&#8203;</p></li><li><p>a to edit</p></li></ul>",
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "within p (p before and after): 3x SHIFT+ENTER -> 3x BACKSPACE",
+ content: "<p>dom not to edit</p><p>dom to edit</p><p>dom not to edit</p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->5",
+ key: 'ENTER',
+ shiftKey: true,
+ },
+ {
+ key: 'ENTER',
+ shiftKey: true,
+ },
+ {
+ key: 'ENTER',
+ shiftKey: true,
+ },
+ {
+ key: 'BACKSPACE',
+ },
+ {
+ key: 'BACKSPACE',
+ },
+ {
+ key: 'BACKSPACE',
+ }
+ ],
+ test: {
+ content: "<p>dom not to edit</p><p>dom to edit</p><p>dom not to edit</p>",
+ start: "p:eq(1):contents()[0]->5",
+ },
+ },
+ {
+ name: "in h1.a > font: 'a' on selection of all contents",
+ content: "<h1 class=\"a\"><font style=\"font-size: 62px;\">dom to edit</font></h1>",
+ steps: [{
+ start: "h1->0",
+ end: "h1->1",
+ key: 'a',
+ }],
+ test: {
+ content: "<h1 class=\"a\"><font style=\"font-size: 62px;\">a</font></h1>",
+ start: "font:contents()[0]->1",
+ },
+ },
+ {
+ name: "in complex-dom: BACKSPACE on partial selection (requires merging non-similar blocks)",
+ content: "<p class=\"a\">pif</p><p><span><b>paf</b></span></p><ul><li><p>p<i>ouf</i></p></li></ul>",
+ steps: [{
+ start: 'p:contents()[0]->2',
+ end: 'i:contents()[0]->2',
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p class=\"a\">pi<i>f</i></p>',
+ start: "p:contents()[0]->2",
+ },
+ },
+ {
+ name: "in complex-dom: DELETE on partial selection (requires merging non-similar blocks)",
+ content: "<p class=\"a\">pif</p><p><span><b>paf</b></span></p><ul><li><p>p<i>ouf</i></p></li></ul>",
+ steps: [{
+ start: 'p:contents()[0]->2',
+ end: 'i:contents()[0]->2',
+ key: 'DELETE',
+ }],
+ test: {
+ content: '<p class=\"a\">pi<i>f</i></p>',
+ start: "p:contents()[0]->2",
+ },
+ },
+
+ // paragraph outdent
+
+ {
+ name: "in p (h1 before): TAB -> BACKSPACE at start (must indent then outdent)",
+ content: "<h1>dom not to edit</h1><p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'TAB',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<h1>dom not to edit</h1><p>dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in p (h1 before): TAB -> 2x BACKSPACE at start (must indent then outdent, then merge blocks)",
+ content: "<h1>dom not to edit</h1><p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'TAB',
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<h1>dom not to editdom to edit</h1>",
+ start: "h1:contents()[0]->15",
+ },
+ },
+];
+
+QUnit.test('Complex', function (assert) {
+ var done = assert.async();
+ weTestUtils.createWysiwyg({
+ data: this.data,
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+ weTestUtils.testKeyboard($editable, assert, keyboardTestsComplex).then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+var keyboardTestsTab = [{
+ name: "in li: TAB at start",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'TAB',
+ }],
+ test: {
+ content: "<ul><li class=\"o_indent\"><ul><li>dom to edit</li></ul></li></ul>",
+ start: "ul ul li:contents()[0]->0",
+ },
+ },
+ {
+ name: "in indented-li: SHIFT+TAB at start",
+ content: "<ul><ul><li>dom to edit</li></ul></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'TAB',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<ul><li>dom to edit</li></ul>",
+ start: "li:contents()[0]->0",
+ },
+ },
+ {
+ name: "in li: TAB within contents",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->5",
+ key: 'TAB',
+ }],
+ test: {
+ content: "<ul><li>dom t&nbsp;&nbsp;&nbsp;&nbsp;o edit</li></ul>",
+ start: "li:contents()[0]->9",
+ },
+ },
+ {
+ name: "in li: SHIFT+TAB within contents (should do nothing)",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->5",
+ key: 'TAB',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<ul><li>dom to edit</li></ul>",
+ start: "li:contents()[0]->5",
+ },
+ },
+ {
+ name: "in td > p: TAB within contents",
+ content: "<table><tbody><tr><td><p>dom to edit</p></td><td><p>node</p></td></tr></tbody></table>",
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'TAB',
+ }],
+ test: {
+ content: "<table><tbody><tr><td><p>dom to edit</p></td><td><p>node</p></td></tr></tbody></table>",
+ start: "td:eq(1) p:contents(0)->0",
+ },
+ },
+ {
+ name: "in p: TAB at start",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'TAB',
+ }],
+ test: {
+ content: "<p style=\"margin-left: 1.5em;\">dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in indented-p: SHIFT+TAB at start",
+ content: "<p style=\"margin-left: 1.5em;\">dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'TAB',
+ shiftKey: true,
+ }],
+ test: {
+ content: "<p>dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in p: TAB -> BACKSPACE at start",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'TAB',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in p > b: TAB at start",
+ content: "<p>dom <b>to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->0",
+ key: 'TAB',
+ }],
+ test: {
+ content: "<p>dom <b>&nbsp;&nbsp;&nbsp;&nbsp;to edit</b></p>",
+ start: "b:contents()[0]->4",
+ },
+ },
+ {
+ name: "in p > b: TAB -> BACKSPACE at start",
+ content: "<p>dom <b>to edit</b></p>",
+ steps: [{
+ start: "b:contents()[0]->0",
+ key: 'TAB',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom <b>&nbsp;&nbsp;&nbsp;to edit</b></p>",
+ start: "b:contents()[0]->3",
+ },
+ },
+];
+
+QUnit.test('Tab', function (assert) {
+ var done = assert.async();
+ weTestUtils.createWysiwyg({
+ data: this.data,
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+ weTestUtils.testKeyboard($editable, assert, keyboardTestsTab).then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+var keyboardTestsBackspace = [{
+ name: "in p: BACKSPACE after span.fa",
+ content: '<p>aaa<span class="fa fa-star"></span>bbb</p>',
+ steps: [{
+ start: "p:contents()[2]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>aaabbb</p>',
+ start: "p:contents()[0]->3",
+ },
+ },
+ {
+ name: "in empty-p (no br): BACKSPACE (must insert br)",
+ content: "<p></p>",
+ steps: [{
+ start: "p->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><br></p>", // The br is there to ensure the carret can enter the p tag
+ start: "br->0",
+ },
+ },
+ {
+ name: "in empty-p: BACKSPACE (must leave it unchanged)",
+ content: "<p><br></p>",
+ steps: [{
+ start: "p->1",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><br></p>", // The br is there to ensure the carret can enter the p tag
+ start: "br->0",
+ },
+ },
+ {
+ name: "in p (empty-p before): BACKSPACE",
+ content: "<p><br></p><p>dom to edit</p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in p (empty-p.a before): BACKSPACE",
+ content: "<p class=\"a\"><br></p><p>dom to edit</p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in p: BACKSPACE within text",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom o edit</p>", // The br is there to ensure the carret can enter the p tag
+ start: "p:contents()[0]->4",
+ },
+ },
+ {
+ name: "in p: 2x BACKSPACE within text",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>domo edit</p>", // The br is there to ensure the carret can enter the p tag
+ start: "p:contents()[0]->3",
+ },
+ },
+ {
+ name: "in p (p > span.a before - span.b after): BACKSPACE at beginning (must attach them)",
+ content: "<p><span class=\"a\">dom to</span></p><p><span class=\"b\">edit</span></p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><span class=\"a\">dom to</span><span class=\"b\">edit</span></p>",
+ start: "span:eq(0):contents()[0]->6",
+ },
+ },
+ {
+ name: "in p (p > span.a before - span.a after): BACKSPACE (must merge them)",
+ content: "<p><span class=\"a\">dom to&nbsp;</span></p><p><span class=\"a\">edit</span></p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><span class=\"a\">dom to&nbsp;edit</span></p>",
+ start: "span:contents()[0]->7",
+ },
+ },
+ {
+ name: "in p (b before): BACKSPACE",
+ content: "<p><b>dom</b> to edit</p>",
+ steps: [{
+ start: "p:contents()[1]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><b>do</b>&nbsp;to edit</p>",
+ start: "b:contents()[0]->2",
+ },
+ },
+ {
+ name: "in p (div > span.a before - span.a after): BACKSPACE at beginning (must do nothing)",
+ content: "<div><span class=\"a\">dom to&nbsp;</span></div><p><span class=\"a\">edit</span></p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<div><span class=\"a\">dom to&nbsp;</span></div><p><span class=\"a\">edit</span></p>",
+ start: "span:eq(1):contents()[0]->0",
+ },
+ },
+ {
+ name: "in p: BACKSPACE on partial selection",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ end: "p:contents()[0]->7",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dedit</p>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "across 2 p's: BACKSPACE on partial selection",
+ content: "<p>dom</p><p>to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ end: "p:eq(1):contents()[0]->3",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dedit</p>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "in p: BACKSPACE within text, with space at beginning",
+ content: "<p> dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->10",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom o edit</p>",
+ start: "p:contents()[0]->4",
+ },
+ },
+ {
+ name: "in p: BACKSPACE within text, with one space at beginning",
+ content: "<p> dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->6",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>&nbsp;dom o edit</p>",
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "in p: BACKSPACE within text, with space at end",
+ content: "<p>dom to edit </p>",
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom o edit</p>",
+ start: "p:contents()[0]->4",
+ },
+ },
+ {
+ name: "in p: BACKSPACE within text, with one space at end",
+ content: "<p>dom to edit </p>",
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom o edit&nbsp;</p>",
+ start: "p:contents()[0]->4",
+ },
+ },
+ {
+ name: "in p: BACKSPACE within text, with space at beginning and end",
+ content: "<p> dom to edit </p>",
+ steps: [{
+ start: "p:contents()[0]->10",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom o edit</p>",
+ start: "p:contents()[0]->4",
+ },
+ },
+ {
+ name: "in p: BACKSPACE within text, with one space at beginning and one at end",
+ content: "<p> dom to edit </p>",
+ steps: [{
+ start: "p:contents()[0]->6",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>&nbsp;dom o edit&nbsp;</p>",
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "in p: BACKSPACE after \\w<br>\\w",
+ content: "<p>dom to edi<br>t</p>",
+ steps: [{
+ start: "p:contents()[2]->1",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom to edi<br>&#8203;</p>",
+ start: "p:contents()[2]->1",
+ },
+ },
+ {
+ name: "in p: BACKSPACE -> 'a' within text, after \\s\\w",
+ content: "<p>dom t</p>",
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>dom a</p>",
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "in pre: BACKSPACE within text, with space at beginning",
+ content: "<pre> dom to edit</pre>",
+ steps: [{
+ start: "pre:contents()[0]->10",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<pre> dom o edit</pre>",
+ start: "pre:contents()[0]->9",
+ },
+ },
+ {
+ name: "in pre: BACKSPACE within text, with one space at beginning",
+ content: "<pre> dom to edit</pre>",
+ steps: [{
+ start: "pre:contents()[0]->6",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<pre> dom o edit</pre>",
+ start: "pre:contents()[0]->5",
+ },
+ },
+ {
+ name: "in pre: BACKSPACE within text, with space at end",
+ content: "<pre>dom to edit </pre>",
+ steps: [{
+ start: "pre:contents()[0]->5",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<pre>dom o edit </pre>",
+ start: "pre:contents()[0]->4",
+ },
+ },
+ {
+ name: "in pre: BACKSPACE within text, with one space at end",
+ content: "<pre>dom to edit </pre>",
+ steps: [{
+ start: "pre:contents()[0]->5",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<pre>dom o edit </pre>",
+ start: "pre:contents()[0]->4",
+ },
+ },
+ {
+ name: "in pre: BACKSPACE within text, with space at beginning and end",
+ content: "<pre> dom to edit </pre>",
+ steps: [{
+ start: "pre:contents()[0]->10",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<pre> dom o edit </pre>",
+ start: "pre:contents()[0]->9",
+ },
+ },
+ {
+ name: "in pre: BACKSPACE within text, with one space at beginning and one at end",
+ content: "<pre> dom to edit </pre>",
+ steps: [{
+ start: "pre:contents()[0]->6",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<pre> dom o edit </pre>",
+ start: "pre:contents()[0]->5",
+ },
+ },
+
+ // list UL / OL
+
+ {
+ name: "from p to ul > li: BACKSPACE on whole list",
+ content: "<p>dom not to edit</p><ul><li><p>dom to edit</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->15",
+ end: "p:eq(1):contents()[0)->11",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom not to edit</p>",
+ start: "p:contents()[0]->15",
+ },
+ },
+ {
+ name: "in ul > second-li > p: BACKSPACE within text",
+ content: "<ul><li><p>dom to</p></li><li><p>edit</p></li></ul>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->4",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li><p>dom to</p></li><li><p>edi</p></li></ul>",
+ start: "p:eq(1):contents()[0]->3",
+ },
+ },
+ {
+ name: "in ul > second-li > empty-p: BACKSPACE at beginning",
+ content: "<ul><li><p><br></p></li><li><p><br></p></li></ul>",
+ steps: [{
+ start: "p:eq(1)->1",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li><p><br></p></li></ul>",
+ start: "p:first:contents()[0]->0",
+ },
+ },
+ {
+ name: "in ul > indented-li (no other li - p before): BACKSPACE at beginning",
+ content: "<p>dom to</p><ul><ul><li>edit</li></ul></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom to</p><ul><li>edit</li></ul>",
+ start: "li:contents()[0]->0",
+ },
+ },
+ {
+ name: "in ul > indented-li (no other li - p before): BACKSPACE -> 'a' at beginning",
+ content: "<p>dom to</p><ul><ul><li>edit</li></ul></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>dom to</p><ul><li>aedit</li></ul>",
+ start: "li:contents()[0]->1",
+ },
+ },
+ {
+ name: "in ul > indented-li (no other li - none before): BACKSPACE at beginning",
+ content: "<ul><ul><li>dom to edit</li></ul></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li>dom to edit</li></ul>",
+ start: "li:contents()[0]->0",
+ },
+ },
+ {
+ name: "in li: BACKSPACE on partial selection",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->1",
+ end: "li:contents()[0]->7",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li>dedit</li></ul>",
+ start: "li:contents()[0]->1",
+ },
+ },
+ {
+ name: "across 2 li: BACKSPACE on partial selection",
+ content: "<ul><li>dom to edit</li><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->1",
+ end: "li:eq(1):contents()[0]->7",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li>dedit</li></ul>",
+ start: "li:contents()[0]->1",
+ },
+ },
+ {
+ name: "in li (no other li): BACKSPACE on selection of all contents",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ end: "li:contents()[0]->11",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li><br></li></ul>",
+ start: "li->0",
+ },
+ },
+ {
+ name: "in li (no other li): BACKSPACE -> 'a' on selection of all contents",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ end: "li:contents()[0]->11",
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<ul><li><p>a</p></li></ul>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "in empty-li: BACKSPACE (must remove list)",
+ content: "<ul><li><br></li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><br></p>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in empty-li (no other li - empty-p before): BACKSPACE -> 'a'",
+ content: "<p><br></p><ul><li><br></li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p><br></p><p>a</p>",
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in empty-li (no other li - p before): BACKSPACE",
+ content: "<p>toto</p><ul><li><br></li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>toto</p><p><br></p>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in li (no other li - p before): BACKSPACE at start",
+ content: "<p>toto</p><ul><li>&nbsp;<img src='/web_editor/static/src/img/transparent.png'></li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>toto</p><p>&nbsp;<img data-src=\"/web_editor/static/src/img/transparent.png\"></p>",
+ start: "p:eq(1):contents()[0]->0",
+ },
+ },
+ {
+ name: "in empty-indented-li (other li - no other indented-li): BACKSPACE",
+ content: "<ul><li><p>toto</p></li><ul><li><br></li></ul><li><p>tutu</p></li></ul>",
+ steps: [{
+ start: "ul ul li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li><p>toto</p></li><li><br></li><li><p>tutu</p></li></ul>",
+ start: "li:eq(1) br->0",
+ },
+ },
+ {
+ name: "in empty-indented-li (other li - other indented-li): BACKSPACE",
+ content: "<ul><li><p>toto</p></li><ul><li><br></li><li><br></li></ul><li><p>tutu</p></li></ul>",
+ steps: [{
+ start: "ul ul li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li><p>toto</p></li><li><br></li><ul><li><br></li></ul><li><p>tutu</p></li></ul>",
+ start: "li:eq(1) br->0",
+ },
+ },
+ {
+ name: "in empty-indented-li (no other li, no other indented-li): BACKSPACE",
+ content: "<ul><ul><li><br></li></ul></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li><br></li></ul>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in indented-li (other li, other indented-li): BACKSPACE at start",
+ content: "<ul><li><p>toto</p></li><ul><li><p>xxx</p></li><li><p>yyy</p></li></ul><li><p>tutu</p></li></ul>",
+ steps: [{
+ start: "ul ul li:contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li><p>toto</p></li><li><p>xxx</p></li><ul><li><p>yyy</p></li></ul><li><p>tutu</p></li></ul>",
+ start: "p:eq(1):contents()[0]->0",
+ },
+ },
+ {
+ name: "in li > second-p: BACKSPACE at start",
+ content: "<ul><li><p>toto</p></li><li><p>xxx</p><p>yyy</p></li><li><p>tutu</p></li></ul>",
+ steps: [{
+ start: "li:eq(1) p:eq(1):contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li><p>toto</p></li><li><p>xxxyyy</p></li><li><p>tutu</p></li></ul>",
+ start: "li:eq(1) p:contents()[0]->3",
+ },
+ },
+ {
+ name: "in li (li after): BACKSPACE at start, with spaces",
+ content: "<p>dom to edit&nbsp; </p><ul><li><p> &nbsp; dom to edit</p></li><li><p>dom not to edit</p></li></ul>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->6",
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom to edit&nbsp;dom to edit</p><ul><li><p>dom not to edit</p></li></ul>",
+ start: "p:contents()[0]->12",
+ },
+ },
+ {
+ name: "in li > p: BACKSPACE after single character",
+ content: "<ul><li><p>a</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<ul><li><p><br></p></li></ul>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in li > p: BACKSPACE -> 'a' after single character",
+ content: "<ul><li><p>a</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<ul><li><p>a</p></li></ul>",
+ start: "p:contents()[0]->1",
+ },
+ },
+
+ // end list UL / OL
+
+ {
+ name: "in p: BACKSPACE on selection of all contents",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "p:contents()[0]->11",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><br></p>",
+ start: "p->0",
+ },
+ },
+ {
+ name: "in p: BACKSPACE -> 'a' on selection of all contents",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "p:contents()[0]->11",
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>a</p>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "in complex-dom: BACKSPACE on selection of most contents",
+ content: "<p><b>dom</b></p><p><b>to<br>partially</b>re<i>m</i>ove</p>",
+ steps: [{
+ start: "b:contents()[0]->2",
+ end: "i:contents()[0]->1",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><b>do</b>ove</p>",
+ start: "b:contents()[0]->2",
+ },
+ },
+ {
+ name: "in complex-dom: BACKSPACE on selection of all contents",
+ content: "<p><b>dom</b></p><p><b>to<br>completely</b>remov<i>e</i></p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "i:contents()[0]->1",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p><br></p>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in p: BACKSPACE after br",
+ content: "<p>dom <br>to edit</p>",
+ steps: [{
+ start: "p:contents()[2]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p>dom to edit</p>",
+ start: "p:contents()[0]->4",
+ },
+ },
+ {
+ name: "in complex-dom (span > b -> ENTER in contents)",
+ content: "<span><b>dom<br></b></span><br><span><b>&nbsp;to edit</b></span>",
+ steps: [{
+ start: "b:eq(1):contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<span><b>dom<br>&nbsp;to edit</b></span>",
+ start: "b:contents(2)->0",
+ },
+ },
+ {
+ name: "in complex-dom (span > b -> ENTER in contents): 2 x BACKSPACE",
+ content: "<span><b>dom<br></b></span><br><span><b>a to edit</b></span>",
+ steps: [{
+ start: "b:eq(1):contents()[0]->1",
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<span><b>dom<br>&nbsp;to edit</b></span>",
+ start: "b:contents(2)->0",
+ },
+ },
+ {
+ name: "in p (hr before): BACKSPACE",
+ content: '<p>aaa</p><hr><p>bbb</p>',
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>aaabbb</p>',
+ start: "p:contents()[0]->3",
+ },
+ },
+ {
+ name: "in p with multi br[id] (p before and after) (1)",
+ content: '<p>dom not to edit</p><p><br id="br-1"><br id="br-2"><br id="br-3"><br id="br-4"></p><p>dom not to edit</p>',
+ steps: [{
+ start: "p:eq(1)->2",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>dom not to edit</p><p><br id="br-1"><br id="br-3"><br id="br-4"></p><p>dom not to edit</p>',
+ start: "p:eq(1)->1",
+ },
+ },
+ {
+ name: "in p with multi br[id] (p before and after) (2)",
+ content: '<p>dom not to edit</p><p><br id="br-1"><br id="br-2"><br id="br-3"><br id="br-4"></p><p>dom not to edit</p>',
+ steps: [{
+ start: "br:eq(2)->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>dom not to edit</p><p><br id="br-1"><br id="br-3"><br id="br-4"></p><p>dom not to edit</p>',
+ start: "p:eq(1)->1",
+ },
+ },
+ {
+ name: "in p with multi br[id] (p before and after): 2x BACKSPACE",
+ content: '<p>dom not to edit</p><p><br id="br-1"><br id="br-2"><br id="br-3"><br id="br-4"></p><p>dom not to edit</p>',
+ steps: [{
+ start: "p:eq(1)->2",
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>dom not to edit</p><p><br id="br-3"><br id="br-4"></p><p>dom not to edit</p>',
+ start: "p:eq(1)->0",
+ },
+ },
+ {
+ name: "in p with multi br (p before and after) (1)",
+ content: '<p>dom not to edit</p><p><br><br><br><br></p><p>dom not to edit</p>',
+ steps: [{
+ start: "p:eq(1)->2",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>dom not to edit</p><p><br><br><br></p><p>dom not to edit</p>',
+ start: "p:eq(1)->1",
+ },
+ },
+ {
+ name: "in p with multi br (p before and after) (2)",
+ content: '<p>dom not to edit</p><p><br><br><br><br></p><p>dom not to edit</p>',
+ steps: [{
+ start: "br:eq(2)->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>dom not to edit</p><p><br><br><br></p><p>dom not to edit</p>',
+ start: "p:eq(1)->1",
+ },
+ },
+ {
+ name: "in p with multi br (p before and after): 2x BACKSPACE",
+ content: '<p>dom not to edit</p><p><br><br><br><br></p><p>dom not to edit</p>',
+ steps: [{
+ start: "p:eq(1)->2",
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>dom not to edit</p><p><br><br></p><p>dom not to edit</p>',
+ start: "p:eq(1)->0",
+ },
+ },
+ {
+ name: "in p: BACKSPACE within text after \w+<br>",
+ content: '<p>dom to<br>edit</p>',
+ steps: [{
+ start: "p:contents()[2]->3",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>dom to<br>edt</p>',
+ start: "p:contents()[2]->2",
+ },
+ },
+
+ // table
+
+ {
+ name: "in empty-td (td before): BACKSPACE -> 'a' at start",
+ content: '<table class="table table-bordered"><tbody><tr><td><p><br></p></td><td><p><br></p></td></tr></tbody></table>',
+ steps: [{
+ start: "p:eq(1)->1",
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: '<table class="table table-bordered"><tbody><tr><td><p><br></p></td><td><p>a</p></td></tr></tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in td (td before): 2x BACKSPACE -> 'a' after first character",
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom not to edit</p></td><td><p>dom to edit</p></td></tr></tbody></table>',
+ steps: [{
+ start: "p:eq(1):contents()[0]->1",
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom not to edit</p></td><td><p>aom to edit</p></td></tr></tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+ {
+ name: "in td (no other td): BACKSPACE within text",
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom to edit</p></td></tr></tbody></table>',
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom o edit</p></td></tr></tbody></table>',
+ start: "p:contents()[0]->4",
+ },
+ },
+ {
+ name: "in complex-dom (empty-td (td before) -> 2x SHIFT-ENTER): 3x BACKSPACE",
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom not to edit</p></td><td><p><br><br><br></p></td></tr></tbody></table>',
+ steps: [{
+ start: 'p:eq(1)->3',
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom not to edit</p></td><td><p><br></p></td></tr></tbody></table>',
+ start: "p:eq(1)->0",
+ },
+ },
+ {
+ name: "in complex-dom (empty-td (td before) -> 2x SHIFT-ENTER): 3x BACKSPACE -> 'a'",
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom not to edit</p></td><td><p><br><br><br></p></td></tr></tbody></table>',
+ steps: [{
+ start: 'p:eq(1)->3',
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom not to edit</p></td><td><p>a</p></td></tr></tbody></table>',
+ start: "p:eq(1):contents()[0]->1",
+ },
+ },
+
+ // merging non-similar blocks
+
+ {
+ name: "in p (h1 before): BACKSPACE at start",
+ content: '<h1>node to merge with</h1><p>node to merge</p>',
+ steps: [{
+ start: 'p:contents()[0]->0',
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<h1>node to merge withnode to merge</h1>',
+ start: "h1:contents()[0]->18",
+ },
+ },
+ {
+ name: "in empty-p (h1 before): BACKSPACE",
+ content: "<h1>dom to edit</h1><p><br></p>",
+ steps: [{
+ start: "p->1",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<h1>dom to edit</h1>",
+ start: "h1:contents()[0]->11",
+ },
+ },
+ {
+ name: "in empty-p (h1 before): 2x BACKSPACE",
+ content: "<h1>dom to edit</h1><p><br></p>",
+ steps: [{
+ start: "p->1",
+ key: 'BACKSPACE',
+ }, {
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<h1>dom to edi</h1>",
+ start: "h1:contents()[0]->10",
+ },
+ },
+ {
+ name: "in p (ul before): BACKSPACE at start",
+ content: '<ul><li><p>node to merge with</p></li></ul><p>node to merge</p>',
+ steps: [{
+ start: 'p:eq(1):contents()[0]->0',
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<ul><li><p>node to merge withnode to merge</p></li></ul>',
+ start: "p:contents()[0]->18",
+ },
+ },
+ {
+ name: "in p > b (ul before, i after): BACKSPACE at start",
+ content: '<ul><li><p>node to merge with</p></li></ul><p><b>node</b><i> to merge</i></p>',
+ steps: [{
+ start: 'b:contents()[0]->0',
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<ul><li><p>node to merge with<b>node</b><i> to merge</i></p></li></ul>',
+ start: "p:contents()[0]->18",
+ },
+ },
+ {
+ name: "in p > b (ul > i before, i after): BACKSPACE at start",
+ content: '<ul><li><p><i>node to merge with</i></p></li></ul><p><b>node</b><i> to merge</i></p>',
+ steps: [{
+ start: 'b:contents()[0]->0',
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<ul><li><p><i>node to merge with</i><b>node</b><i> to merge</i></p></li></ul>',
+ start: "i:contents()[0]->18",
+ },
+ },
+ {
+ name: "in p.c (p.a > span.b before - span.b after): BACKSPACE at beginning",
+ content: "<p class=\"a\"><span class=\"b\">dom to&nbsp;</span></p><p class=\"c\"><span class=\"b\">edit</span></p>",
+ steps: [{
+ start: "p:eq(1):contents()[0]->0",
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: "<p class=\"a\"><span class=\"b\">dom to&nbsp;edit</span></p>",
+ start: "span:eq(0):contents()[0]->7",
+ },
+ },
+ {
+ name: "from h1 to p: BACKSPACE",
+ content: '<h1>node to merge with</h1><p>node to merge</p>',
+ steps: [{
+ start: 'h1:contents()[0]->5',
+ end: 'p:contents()[0]->2',
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<h1>node de to merge</h1>',
+ start: "h1:contents()[0]->5",
+ },
+ },
+ {
+ name: "from h1 to p: BACKSPACE at start",
+ content: '<h1><b>node to merge with</b></h1><p>node to merge</p>',
+ steps: [{
+ start: 'b:contents()[0]->0',
+ end: 'p:contents()[0]->2',
+ key: 'BACKSPACE',
+ }],
+ test: {
+ content: '<p>de to merge</p>',
+ start: "p:contents()[0]->0",
+ },
+ },
+];
+
+QUnit.test('Backspace', function (assert) {
+ var done = assert.async();
+ weTestUtils.createWysiwyg({
+ data: this.data,
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+ weTestUtils.testKeyboard($editable, assert, keyboardTestsBackspace).then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+var keyboardTestsDelete = [{
+ name: "in empty-p (no br): DELETE (must insert br)",
+ content: "<p></p>",
+ steps: [{
+ start: "p->0",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p><br></p>", // The br is there to ensure the carret can enter the p tag
+ start: "br->0",
+ },
+ },
+ {
+ name: "in empty-p: DELETE (must leave it unchanged)",
+ content: "<p><br></p>",
+ steps: [{
+ start: "p->0",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p><br></p>", // The br is there to ensure the carret can enter the p tag
+ start: "p->0",
+ },
+ },
+ {
+ name: "in empty-p: DELETE (must leave it unchanged) (2)",
+ content: "<p>\n <br>\n</p>",
+ steps: [{
+ start: "p->1",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>\n <br>\n</p>", // The br is there to ensure the carret can enter the p tag
+ start: "p:contents()[1]->0",
+ },
+ },
+ {
+ name: "in empty-p (p after): DELETE",
+ content: "<p><br></p><p>dom to edit</p>",
+ steps: [{
+ start: "p->0",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in empty-p (p after): DELETE (2)",
+ content: "<p>\n <br>\n</p><p>\n dom to edit</p>",
+ steps: [{
+ start: "p:contents()[1]->0",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in empty-p.a (p after): DELETE",
+ content: "<p class=\"a\"><br></p><p>dom to edit</p>",
+ steps: [{
+ start: "p->0",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in empty-p.a (p after): DELETE (2)",
+ content: "<p class=\"a\"><br></p><p>dom to edit</p>",
+ steps: [{
+ start: "p->1",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom to edit</p>",
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in p: DELETE at start",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>om to edit</p>", // The br is there to ensure the carret can enter the p tag
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in p: 2x DELETE at start",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'DELETE',
+ }, {
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>m to edit</p>", // The br is there to ensure the carret can enter the p tag
+ start: "p:contents()[0]->0",
+ },
+ },
+ {
+ name: "in p: DELETE within text",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom t edit</p>",
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "in p: DELETE within text, with space at beginning",
+ content: "<p> dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->10",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom t edit</p>",
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "in p: DELETE within text, with one space at beginning",
+ content: "<p> dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->6",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>&nbsp;dom t edit</p>",
+ start: "p:contents()[0]->6",
+ },
+ },
+ {
+ name: "in p: DELETE within text, with space at end",
+ content: "<p>dom to edit </p>",
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom t edit</p>",
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "in p: DELETE within text, with one space at end",
+ content: "<p>dom to edit </p>",
+ steps: [{
+ start: "p:contents()[0]->5",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom t edit&nbsp;</p>",
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "in p: DELETE within text, with space at beginning and end",
+ content: "<p> dom to edit </p>",
+ steps: [{
+ start: "p:contents()[0]->10",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom t edit</p>",
+ start: "p:contents()[0]->5",
+ },
+ },
+ {
+ name: "in p: DELETE within text, with one space at beginning and one at end",
+ content: "<p> dom to edit </p>",
+ steps: [{
+ start: "p:contents()[0]->6",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>&nbsp;dom t edit&nbsp;</p>",
+ start: "p:contents()[0]->6",
+ },
+ },
+ {
+ name: "in p: DELETE -> 'a' within text, before \\w\\s",
+ content: "<p>dom t</p>",
+ steps: [{
+ start: "p:contents()[0]->2",
+ key: 'DELETE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>doa t</p>",
+ start: "p:contents()[0]->3",
+ },
+ },
+ {
+ name: "in pre: DELETE within text, with space at beginning",
+ content: "<pre> dom to edit</pre>",
+ steps: [{
+ start: "pre:contents()[0]->10",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<pre> dom t edit</pre>",
+ start: "pre:contents()[0]->10",
+ },
+ },
+ {
+ name: "in pre: DELETE within text, with one space at beginning",
+ content: "<pre> dom to edit</pre>",
+ steps: [{
+ start: "pre:contents()[0]->6",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<pre> dom t edit</pre>",
+ start: "pre:contents()[0]->6",
+ },
+ },
+ {
+ name: "in pre: DELETE within text, with space at end",
+ content: "<pre>dom to edit </pre>",
+ steps: [{
+ start: "pre:contents()[0]->5",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<pre>dom t edit </pre>",
+ start: "pre:contents()[0]->5",
+ },
+ },
+ {
+ name: "in pre: DELETE within text, with one space at end",
+ content: "<pre>dom to edit </pre>",
+ steps: [{
+ start: "pre:contents()[0]->5",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<pre>dom t edit </pre>",
+ start: "pre:contents()[0]->5",
+ },
+ },
+ {
+ name: "in pre: DELETE within text, with space at beginning and end",
+ content: "<pre> dom to edit </pre>",
+ steps: [{
+ start: "pre:contents()[0]->10",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<pre> dom t edit </pre>",
+ start: "pre:contents()[0]->10",
+ },
+ },
+ {
+ name: "in pre: DELETE within text, with one space at beginning and one at end",
+ content: "<pre> dom to edit </pre>",
+ steps: [{
+ start: "pre:contents()[0]->6",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<pre> dom t edit </pre>",
+ start: "pre:contents()[0]->6",
+ },
+ },
+
+ // list UL / OL
+
+ {
+ name: "in empty-li (no other li): DELETE (must do nothing)",
+ content: "<ul><li><br></li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li><br></li></ul>",
+ start: "li->0",
+ },
+ },
+ {
+ name: "from p to li > p: DELETE on whole list",
+ content: "<p>dom not to edit</p><ul><li><p>dom to edit</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->15",
+ end: "p:eq(1):contents()[0)->11",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom not to edit</p>",
+ start: "p:contents()[0]->15",
+ },
+ },
+ {
+ name: "in empty-li (no other li): DELETE -> 'a' (must write into it)",
+ content: "<ul><li><br></li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ key: 'DELETE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<ul><li><p>a</p></li></ul>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "in li (li after): DELETE at end (must move contents of second li to carret)",
+ content: "<ul><li>dom to&nbsp;</li><li>edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->7",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li>dom to&nbsp;edit</li></ul>",
+ start: "li:contents()[0]->7",
+ },
+ },
+ {
+ name: "in li (li after): DELETE -> 'a' at end (must move contents of second li to carret, then write)",
+ content: "<ul><li>dom to&nbsp;</li><li>edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->7",
+ key: 'DELETE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<ul><li>dom to aedit</li></ul>",
+ start: "li:contents()[0]->8",
+ },
+ },
+ {
+ name: "in li > p: DELETE before single character",
+ content: "<ul><li><p>a</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li><p><br></p></li></ul>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in li > p: DELETE -> 'a' before single character",
+ content: "<ul><li><p>a</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ key: 'DELETE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<ul><li><p>a</p></li></ul>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "in li > p (p after): DELETE at end",
+ content: "<ul><li><p>toto</p></li><li><p>xxx</p><p>yyy</p></li><li><p>tutu</p></li></ul>",
+ steps: [{
+ start: "li:eq(1) p:eq(0):contents()[0]->3",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li><p>toto</p></li><li><p>xxxyyy</p></li><li><p>tutu</p></li></ul>",
+ start: "li:eq(1) p:eq(0):contents()[0]->3",
+ },
+ },
+ {
+ name: "in li > p (b before - p after): DELETE at end",
+ content: "<ul><li><p>toto</p></li><li><p><b>x</b>xx</p><p><b>y</b>yy</p></li><li><p>tutu</p></li></ul>",
+ steps: [{
+ start: "li:eq(1) p:eq(0):contents()[1]->2",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li><p>toto</p></li><li><p><b>x</b>xx<b>y</b>yy</p></li><li><p>tutu</p></li></ul>",
+ start: "li:eq(1) p:eq(0):contents()[1]->2",
+ },
+ },
+ {
+ name: "in li > p (p.o_default_snippet_text after): DELETE at end",
+ content: '<ul><li><p>toto</p></li><li><p>xxx</p><p class="o_default_snippet_text">yyy</p></li><li><p>tutu</p></li></ul>',
+ steps: [{
+ start: "li:eq(1) p:eq(0):contents()[0]->3",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li><p>toto</p></li><li><p>xxxyyy</p></li><li><p>tutu</p></li></ul>",
+ start: "li:eq(1) p:eq(0):contents()[0]->3",
+ },
+ },
+ {
+ name: "in indented-ul > li: DELETE at end (must do nothing)",
+ content: "<ul><ul><li>dom to edit</li></ul></ul>",
+ steps: [{
+ start: "li:contents()[0]->11",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><ul><li>dom to edit</li></ul></ul>",
+ start: "li:contents()[0]->11",
+ },
+ },
+ {
+ name: "in indented-ul > li: DELETE at end -> 'a' at end (must write)",
+ content: "<ul><ul><li>dom to edit</li></ul></ul>",
+ steps: [{
+ start: "li:contents()[0]->11",
+ key: 'DELETE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<ul><ul><li>dom to edita</li></ul></ul>",
+ start: "li:contents()[0]->12",
+ },
+ },
+ {
+ name: "in indented-ul > li (non-indented-li after): DELETE at end",
+ content: "<ul><ul><li>dom to edit</li></ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->11",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><ul><li>dom to editdom to edit</li></ul></ul>",
+ start: "li:contents()[0]->11",
+ },
+ },
+ {
+ name: "in ul > li (indented-li after): DELETE at end",
+ content: "<ul><li>dom to edit</li><ul><li>dom to edit</li></ul></ul>",
+ steps: [{
+ start: "li:contents()[0]->11",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li>dom to editdom to edit</li></ul>",
+ start: "li:contents()[0]->11",
+ },
+ },
+ {
+ name: "in li: DELETE on partial selection",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->1",
+ end: "li:contents()[0]->7",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li>dedit</li></ul>",
+ start: "li:contents()[0]->1",
+ },
+ },
+ {
+ name: "across 2 li: DELETE on partial selection",
+ content: "<ul><li>dom to edit</li><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->1",
+ end: "li:eq(1):contents()[0]->7",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li>dedit</li></ul>",
+ start: "li:contents()[0]->1",
+ },
+ },
+ {
+ name: "in li: DELETE on selection of all contents",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ end: "li:contents()[0]->11",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<ul><li><br></li></ul>",
+ start: "li->0",
+ },
+ },
+ {
+ name: "in li: DELETE -> 'a' on selection of all contents",
+ content: "<ul><li>dom to edit</li></ul>",
+ steps: [{
+ start: "li:contents()[0]->0",
+ end: "li:contents()[0]->11",
+ key: 'DELETE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<ul><li><p>a</p></li></ul>",
+ start: "p:contents()[0]->1",
+ },
+ },
+
+ // end list UL / OL
+
+ {
+ name: "in p (b before): DELETE",
+ content: "<p><b>dom</b>to edit</p>",
+ steps: [{
+ start: "b:contents()[0]->3",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p><b>dom</b>o edit</p>",
+ start: "p:contents(1)->0",
+ },
+ },
+ {
+ name: "in p > span.a (div > span.a after): DELETE at end (must do nothing)",
+ content: "<p><span class=\"a\">dom to</span></p><div><span class=\"a\">edit</span></div>",
+ steps: [{
+ start: "span:contents()[0]->6",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p><span class=\"a\">dom to</span></p><div><span class=\"a\">edit</span></div>",
+ start: "span:contents()[0]->6",
+ },
+ },
+ {
+ name: "in p: DELETE on partial selection",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ end: "p:contents()[0]->7",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dedit</p>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "across 2 p: DELETE on partial selection",
+ content: "<p>dom</p><p>to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->1",
+ end: "p:eq(1):contents()[0]->3",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dedit</p>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "in p: DELETE on selection of all contents",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "p:contents()[0]->11",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p><br></p>",
+ start: "p->0",
+ },
+ },
+ {
+ name: "in p: DELETE -> 'a' on selection of all contents",
+ content: "<p>dom to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "p:contents()[0]->11",
+ key: 'DELETE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: "<p>a</p>",
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "in p: DELETE before br",
+ content: "<p>dom <br>to edit</p>",
+ steps: [{
+ start: "p:contents()[0]->4",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom&nbsp;to edit</p>",
+ start: "p:contents()[0]->4",
+ },
+ },
+ {
+ name: "in complex-dom: DELETE on selection of most contents",
+ content: "<p><b>dom</b></p><p><b>to<br>partially</b>re<i>m</i>ove</p>",
+ steps: [{
+ start: "b:contents()[0]->2",
+ end: "i:contents()[0]->1",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p><b>do</b>ove</p>",
+ start: "b:contents()[0]->2",
+ },
+ },
+ {
+ name: "in complex-dom: DELETE on selection of all contents",
+ content: "<p><b>dom</b></p><p><b>to<br>completely</b>remov<i>e</i></p>",
+ steps: [{
+ start: "p:contents()[0]->0",
+ end: "i:contents()[0]->1",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p><br></p>",
+ start: "br->0",
+ },
+ },
+ {
+ name: "in complex-dom (span > b -> ENTER): DELETE",
+ content: "<span><b>dom</b></span><br><span><b> to edit</b></span>",
+ steps: [{
+ start: "b:eq(0):contents()[0]->3",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<span><b>dom&nbsp;to edit</b></span>",
+ start: "b:contents()[0]->3",
+ },
+ },
+ {
+ name: "in p (span.fa after): DELETE",
+ content: '<p>aaa<span class="fa fa-star"></span>bbb</p>',
+ steps: [{
+ start: "p:contents()[0]->3",
+ key: 'DELETE',
+ }],
+ test: {
+ content: '<p>aaabbb</p>',
+ start: "p:contents()[0]->3",
+ },
+ },
+ {
+ name: "in p (hr after): DELETE",
+ content: '<p>aaa</p><hr><p>bbb</p>',
+ steps: [{
+ start: "p:eq(0):contents()[0]->3",
+ key: 'DELETE',
+ }],
+ test: {
+ content: '<p>aaabbb</p>',
+ start: "p:contents()[0]->3",
+ },
+ },
+
+ // table
+
+ {
+ name: "in empty-td (td after): DELETE -> 'a' at start",
+ content: '<table class="table table-bordered"><tbody><tr><td><p><br></p></td><td><p><br></p></td></tr></tbody></table>',
+ steps: [{
+ start: "p->1",
+ key: 'DELETE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: '<table class="table table-bordered"><tbody><tr><td><p>a</p></td><td><p><br></p></td></tr></tbody></table>',
+ start: "p:contents()[0]->1",
+ },
+ },
+ {
+ name: "in td (td after): 2x DELETE -> 'a' after first character",
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom to edit</p></td><td><p>dom not to edit</p></td></tr></tbody></table>',
+ steps: [{
+ start: "p:contents()[0]->1",
+ key: 'DELETE',
+ }, {
+ key: 'DELETE',
+ }, {
+ key: 'a',
+ }],
+ test: {
+ content: '<table class="table table-bordered"><tbody><tr><td><p>da to edit</p></td><td><p>dom not to edit</p></td></tr></tbody></table>',
+ start: "p:contents()[0]->2",
+ },
+ },
+ {
+ name: "in td: DELETE within text",
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom to edit</p></td></tr></tbody></table>',
+ steps: [{
+ start: "p:contents()[0]->4",
+ key: 'DELETE',
+ }],
+ test: {
+ content: '<table class="table table-bordered"><tbody><tr><td><p>dom o edit</p></td></tr></tbody></table>',
+ start: "p:contents()[0]->4",
+ },
+ },
+
+ // merging non-similar blocks
+
+ {
+ name: "in p (ul after, with spaces): DELETE at end (must bring contents of first li to end of p)",
+ content: "<p>dom to edit&nbsp; </p><ul><li><p> &nbsp; dom to edit</p></li><li><p>dom not to edit</p></li></ul>",
+ steps: [{
+ start: "p:contents()[0]->11",
+ key: 'DELETE',
+ }, {
+ key: 'DELETE',
+ }, {
+ key: 'DELETE',
+ }, {
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p>dom to editdom to edit</p><ul><li><p>dom not to edit</p></li></ul>",
+ start: "p:contents()[0]->11",
+ },
+ },
+ {
+ name: "in h1 (p after): DELETE at end",
+ content: '<h1>node to merge with</h1><p>node to merge</p>',
+ steps: [{
+ start: 'h1:contents()[0]->18',
+ key: 'DELETE',
+ }],
+ test: {
+ content: '<h1>node to merge withnode to merge</h1>',
+ start: "h1:contents()[0]->18",
+ },
+ },
+ {
+ name: "in h1 (empty-p after): DELETE at end",
+ content: "<h1>dom to edit</h1><p><br></p>",
+ steps: [{
+ start: "h1:contents()[0]->11",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<h1>dom to edit</h1>",
+ start: "h1:contents()[0]->11",
+ },
+ },
+ {
+ name: "in h1 (empty-p after): 2x DELETE at end",
+ content: "<h1>dom to edit</h1><p><br></p>",
+ steps: [{
+ start: "h1:contents()[0]->11",
+ key: 'DELETE',
+ }, {
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<h1>dom to edit</h1>",
+ start: "h1:contents()[0]->11",
+ },
+ },
+ {
+ name: "in li > p (p after): DELETE at end",
+ content: '<ul><li><p>node to merge with</p></li></ul><p>node to merge</p>',
+ steps: [{
+ start: 'p:contents()[0]->18',
+ key: 'DELETE',
+ }],
+ test: {
+ content: '<ul><li><p>node to merge withnode to merge</p></li></ul>',
+ start: "p:contents()[0]->18",
+ },
+ },
+ {
+ name: "in li > p (i before - p > b after): DELETE at end",
+ content: '<ul><li><p><i>node to merge</i> with</p></li></ul><p><b>node</b> to merge</p>',
+ steps: [{
+ start: 'p:contents()[1]->5',
+ key: 'DELETE',
+ }],
+ test: {
+ content: '<ul><li><p><i>node to merge</i>&nbsp;with<b>node</b> to merge</p></li></ul>',
+ start: "p:contents()[1]->5",
+ },
+ },
+ {
+ name: "in li > p > i (b before - p > b after): DELETE at end",
+ content: '<ul><li><p><b>node to </b><i>merge with</i></p></li></ul><p><b>node</b> to merge</p>',
+ steps: [{
+ start: 'i:contents()[0]->10',
+ key: 'DELETE',
+ }],
+ test: {
+ content: '<ul><li><p><b>node to </b><i>merge with</i><b>node</b> to merge</p></li></ul>',
+ start: "i:contents()[0]->10",
+ },
+ },
+ {
+ name: "in p.a > span.b (p.c > span.b after): DELETE at end",
+ content: "<p class=\"a\"><span class=\"b\">dom to&nbsp;</span></p><p class=\"c\"><span class=\"b\">edit</span></p>",
+ steps: [{
+ start: "span:contents()[0]->7",
+ key: 'DELETE',
+ }],
+ test: {
+ content: "<p class=\"a\"><span class=\"b\">dom to&nbsp;edit</span></p>",
+ start: "span:contents()[0]->7",
+ },
+ },
+];
+
+var keyboardTestsDeleteDOM = [
+ '<div style="margin:0px; padding:0px;">',
+ ' <p style="margin:0px; padding:0px;">',
+ ' Dear Ready Mat',
+ ' <br><br>',
+ ' Here is',
+ ' the quotation <strong>SO003</strong>',
+ ' amounting in <strong>$&nbsp;1,127.50</strong>',
+ ' from YourCompany.',
+ ' <br><br>',
+ ' Do not hesitate to contact us if you have any question.',
+ ' <br><br>',
+ ' </p>',
+ ' <table width="100%">',
+ ' <tbody><tr>',
+ ' <td>',
+ ' <strong>Virtual Interior Design</strong><br>On Site ',
+ ' </td>',
+ ' <td width="100px" align="right">',
+ ' 10 Hour(s)',
+ ' </td>',
+ ' </tr>',
+ ' </tbody></table>',
+ '</div>',
+ ' '
+].join('\n');
+keyboardTestsDelete.push({
+ name: "in complex-dom: DELETE text selection",
+ content: keyboardTestsDeleteDOM,
+ steps: [{
+ start: "p:contents()[0]->20",
+ end: 'p:contents()[0]->23',
+ key: 'DELETE',
+ },
+ {
+ start: "p:contents()[10]->55",
+ end: 'p:contents()[10]->63',
+ key: 'DELETE',
+ },
+ ],
+ test: {
+ content: keyboardTestsDeleteDOM.replace('question', '').replace('Mat', ''),
+ start: "p:contents()[10]->55",
+ },
+}, {
+ name: "in complex-dom: DELETE br",
+ content: keyboardTestsDeleteDOM,
+ steps: [{
+ start: "p->2",
+ key: 'DELETE',
+ }, ],
+ test: {
+ content: keyboardTestsDeleteDOM.replace('<br>', ''),
+ start: "p:contents()[2]->0",
+ },
+}, {
+ name: "in complex-dom: DELETE selection (all strong text content)",
+ content: keyboardTestsDeleteDOM,
+ steps: [{
+ start: "strong:eq(0):contents()[0]->0",
+ end: 'strong:eq(0):contents()[0]->5',
+ key: 'DELETE',
+ }, ],
+ test: {
+ content: keyboardTestsDeleteDOM.replace(/<strong>SO003<\/strong>\s+/, '&nbsp;'),
+ start: "p:contents()[3]->43",
+ },
+}, {
+ name: "in complex-dom: DELETE selection (in strong text content before br)",
+ content: keyboardTestsDeleteDOM,
+ steps: [{
+ start: "strong:eq(2):contents()[0]->17",
+ end: 'strong:eq(2):contents()[0]->23',
+ key: 'DELETE',
+ }, ],
+ test: {
+ content: keyboardTestsDeleteDOM.replace(/Design/, ''),
+ start: "strong:eq(2):contents()[0]->17",
+ },
+});
+
+QUnit.test('Delete', function (assert) {
+ var done = assert.async();
+ weTestUtils.createWysiwyg({
+ data: this.data,
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+ weTestUtils.testKeyboard($editable, assert, keyboardTestsDelete).then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+});
+});
+});
diff --git a/addons/web_editor/static/tests/wysiwyg_snippets_tests.js b/addons/web_editor/static/tests/wysiwyg_snippets_tests.js
new file mode 100644
index 00000000000..2441897b36d
--- /dev/null
+++ b/addons/web_editor/static/tests/wysiwyg_snippets_tests.js
@@ -0,0 +1,403 @@
+odoo.define('web_editor.wysiwyg_snippets_tests', function (require) {
+"use strict";
+
+var testUtils = require('web.test_utils');
+var weTestUtils = require('web_editor.test_utils');
+var MediaDialog = require('wysiwyg.widgets.MediaDialog');
+
+
+QUnit.module('web_editor', {}, function () {
+QUnit.module('wysiwyg', {}, function () {
+QUnit.module('Snippets', {}, function () {
+
+QUnit.test('drag&drop', function (assert) {
+ var done = assert.async();
+ assert.expect(2);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value: '<p>toto toto toto</p><p>tata</p>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ var $hr = wysiwyg.snippets.$('.oe_snippet_thumbnail:first');
+ testUtils.dom.dragAndDrop($hr, $editable.find('p'));
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>toto toto toto</p><div class=\"s_hr pt32 pb32\"> <hr class=\"s_hr_1px s_hr_solid w-100 mx-auto\"> </div><p>tata</p>',
+ "should drop the snippet");
+
+ testUtils.mock.intercept(wysiwyg, "snippet_focused", function () {
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>toto toto toto</p><div class=\"s_hr pt32 pb32 built focus\"> <hr class=\"s_hr_1px s_hr_solid w-100 mx-auto\"> </div><p>tata</p>',
+ "should build and focus the snippet");
+
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+QUnit.test('begin drag&drop must hide popovers', function (assert) {
+ var done = assert.async();
+ assert.expect(2);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value:
+ '<div class="s_hr pt32 pb32"> ' +
+ '<hr class="s_hr_1px s_hr_solid w-100 mx-auto"> ' +
+ '<img class="img-fluid o_we_custom_image" data-src="/web_editor/static/src/img/transparent.png"> ' +
+ '</div>' +
+ '<h1>test</h1>' +
+ '<p><b>test</b></p>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ $editable.find('.s_hr img').mousedown().click();
+ assert.strictEqual($('.note-popover:visible').length, 1, "should display the image popover");
+
+ var $hr = wysiwyg.snippets.$('.oe_snippet_thumbnail:first');
+ testUtils.dom.dragAndDrop($hr, $editable.find('h1'), {
+ disableDrop: true,
+ });
+ assert.strictEqual($('.note-popover:visible').length, 0, "should hide the image popover");
+ testUtils.dom.dragAndDrop($hr, $editable.find('h1'), {
+ continueMove: true,
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('change size option must hide popovers', function (assert) {
+ var done = assert.async();
+ assert.expect(4);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value:
+ '<div class="s_hr pt32 pb32"> ' +
+ '<hr class="s_hr_1px s_hr_solid w-100 mx-auto"> ' +
+ '<img class="img-fluid o_we_custom_image" data-src="/web_editor/static/src/img/transparent.png"> ' +
+ '</div>' +
+ '<h1>test</h1>' +
+ '<p><b>test</b></p>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ $editable.find('.s_hr img').mousedown().click();
+ assert.strictEqual($('.note-popover:visible').length, 1, "should display the image popover");
+
+ var $handle = $('#oe_manipulators .o_handle.s');
+ testUtils.dom.dragAndDrop($handle, $editable.find('b'), {
+ disableDrop: true,
+ });
+ assert.ok($handle.hasClass('o_active'), "should active the handle");
+ assert.notOk($handle.hasClass('pb32'), "should change the padding bottom");
+ assert.strictEqual($('.note-popover:visible').length, 0, "should hide the image popover");
+ testUtils.dom.dragAndDrop($handle, $editable.find('b'), {
+ continueMove: true,
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('clean the dom before save, after drag&drop', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value: '<p>toto toto toto</p><p>tata</p>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ var $hr = wysiwyg.snippets.$('.oe_snippet_thumbnail:first');
+ testUtils.dom.dragAndDrop($hr, $editable.find('p'));
+
+ testUtils.mock.intercept(wysiwyg, "snippet_focused", function () {
+ wysiwyg.save().then(function (isDirty, html) {
+ assert.strictEqual(html.replace(/\s+/g, ' '),
+ '<p>toto toto toto</p><div class=\"s_hr pt32 pb32 built cleanForSave\"> <hr class=\"s_hr_1px s_hr_solid w-100 mx-auto\"> </div><p>tata</p>',
+ "should clean the snippet");
+
+ wysiwyg.destroy();
+ done();
+ });
+ });
+ });
+});
+
+QUnit.test('clean the dom before save', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value: '<div class="s_hr pt32 pb32"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ testUtils.mock.intercept(wysiwyg, "snippet_focused", function () {
+ // add dirty flag to remove warning because the cleaned dom is different of the initial value and no dirty flag
+ $editable.find('.s_hr').keydown();
+
+ // trigger change to avoid warning because the DOM change on clean without trigger onchange by snippet or edition
+ $editable.trigger('content_changed');
+
+ wysiwyg.save().then(function (isDirty, html) {
+ assert.strictEqual(html.replace(/\s+/g, ' '),
+ '<div class=\"s_hr pt32 pb32 cleanForSave\"> <hr class=\"s_hr_1px s_hr_solid w-100 mx-auto\"> </div>',
+ "should clean the snippet");
+
+ wysiwyg.destroy();
+ done();
+ });
+ });
+
+ $editable.find('.s_hr').mousedown().click();
+ });
+});
+
+QUnit.test('remove snippet', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value: '<div class="s_hr pt32 pb32"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ testUtils.mock.intercept(wysiwyg, "snippet_focused", function () {
+ $('#oe_manipulators .oe_overlay_options .oe_snippet_remove').click();
+
+ wysiwyg.save().then(function (isDirty, html) {
+ assert.strictEqual(html.replace(/\s+/g, ' '), '', "should remove the snippet");
+
+ wysiwyg.destroy();
+ done();
+ });
+ });
+
+ $editable.find('.s_hr').mousedown().click();
+ });
+});
+
+QUnit.test('move a snippet', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value: '<div class="s_hr pt32 pb32"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><h1>test</h1><p><b>test</b></p>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ var first = true;
+ testUtils.mock.intercept(wysiwyg, "snippet_focused", function () {
+ if (first) {
+ first = false;
+ var $hr = $('#oe_manipulators .oe_overlay_options .oe_snippet_move');
+ testUtils.dom.dragAndDrop($hr, $editable.find('b'));
+ } else {
+ wysiwyg.save().then(function (isDirty, html) {
+ assert.strictEqual(html.replace(/\s+/g, ' '),
+ '<h1>test</h1><div class="s_hr pt32 pb32 move cleanForSave"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><p><b>test</b></p>',
+ "should move the snippet on the bottom");
+
+ wysiwyg.destroy();
+ done();
+ });
+ }
+ });
+
+ $editable.find('.s_hr').mousedown().click();
+ });
+});
+
+QUnit.test('clone a snippet', function (assert) {
+ var done = assert.async();
+ assert.expect(2);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value: '<div class="s_hr pt32 pb32"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><h1>test</h1><p><b>test</b></p>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ testUtils.mock.intercept(wysiwyg, "snippet_focused", function () {
+ $('#oe_manipulators .oe_overlay_options .oe_snippet_clone').click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<div class="s_hr pt32 pb32 focus"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><div class="s_hr pt32 pb32 clone"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><h1>test</h1><p><b>test</b></p>',
+ "should duplicate the snippet");
+
+ wysiwyg.save().then(function (isDirty, html) {
+ assert.strictEqual(html.replace(/\s+/g, ' '),
+ '<div class="s_hr pt32 pb32 cleanForSave"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><div class="s_hr pt32 pb32 clone cleanForSave"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><h1>test</h1><p><b>test</b></p>',
+ "should duplicate the snippet");
+
+ wysiwyg.destroy();
+ done();
+ });
+ });
+
+ $editable.find('.s_hr').mousedown().click();
+ });
+});
+
+QUnit.test('customize snippet', function (assert) {
+ var done = assert.async();
+ assert.expect(3);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value: '<h1>test</h1><div class="s_hr pt32 pb32"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><p><b>test</b></p>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ testUtils.mock.intercept(wysiwyg, "snippet_focused", function () {
+ $('#oe_manipulators .oe_overlay_options .oe_options a:first').click();
+ var $option = $('#oe_manipulators .oe_overlay_options a[data-select-class="align-items-center"]');
+
+ assert.strictEqual($option.size(), 1, "should display the snippet option");
+
+ $option.click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<h1>test</h1><div class="s_hr pt32 pb32 focus align-items-center"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><p><b>test</b></p>',
+ "should customized the snippet");
+
+ $('#oe_manipulators .oe_overlay_options .oe_options a:first').click();
+ $('#oe_manipulators .oe_overlay_options a[data-select-class="align-items-end"]').click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<h1>test</h1><div class="s_hr pt32 pb32 focus align-items-end"> <hr class="s_hr_1px s_hr_solid w-100 mx-auto"> </div><p><b>test</b></p>',
+ "should twice customized the snippet");
+
+ wysiwyg.destroy();
+ done();
+ });
+
+ $editable.find('.s_hr').mousedown().click();
+ });
+});
+
+QUnit.test('background-image', function (assert) {
+ var done = assert.async();
+ assert.expect(2);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value:
+ '<h1>Big Title</h1>' +
+ '<section class="test_option_all pt32 pb32">' +
+ ' <div class="container">' +
+ ' <div class="row">' +
+ ' <div class="col-lg-10 offset-lg-1 pt32 pb32">' +
+ ' <h2>Title</h2>' +
+ ' <p class="lead o_default_snippet_text">Content</p>' +
+ ' </div>' +
+ ' </div>' +
+ ' </div>' +
+ '</section>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ var defMediaDialogInit = $.Deferred();
+ var defMediaDialogSave = $.Deferred();
+ testUtils.mock.patch(MediaDialog, {
+ init: function () {
+ this._super.apply(this, arguments);
+ this.opened(defMediaDialogInit.resolve.bind(defMediaDialogInit));
+ },
+ save: function () {
+ $.when(this._super.apply(this, arguments)).then(function () {
+ defMediaDialogSave.resolve();
+ });
+ },
+ });
+
+ testUtils.mock.intercept(wysiwyg, "snippet_focused", function () {
+ $('#oe_manipulators .oe_overlay_options .oe_options a:first').click();
+ $('#oe_manipulators .oe_overlay_options a.snippet-option-background[data-choose-image]').click();
+
+ defMediaDialogInit.then(function () {
+ $('.modal.note-image-dialog .existing-attachments .o_image').click();
+ $('.modal.note-image-dialog .modal-footer .btn-primary').click();
+ });
+
+ defMediaDialogSave.then(function () {
+ var $snippet = $editable.find('.test_option_all');
+ assert.ok($snippet.hasClass('oe_custom_bg'), "should use custom background");
+ var background = $snippet.css('background-image') || '';
+ assert.strictEqual(background.replace(/[^(\("'\/]+\/\/[^\("'\/]+/, ''),
+ 'url("/web_editor/static/src/img/transparent.png")',
+ "should change the background image");
+
+ testUtils.mock.unpatch(MediaDialog);
+ wysiwyg.destroy();
+ done();
+ });
+ });
+
+ $editable.find('h2').mousedown().click();
+ });
+});
+
+QUnit.test('update content', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ return weTestUtils.createWysiwyg({
+ wysiwygOptions: {
+ snippets: true,
+ value: '<div class="s_hr pt32 pb32"> ' +
+ '<img class="img-fluid o_we_custom_image test_option_all" data-src="/web_editor/static/src/img/transparent.png"> ' +
+ '</div>',
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ testUtils.mock.intercept(wysiwyg, "snippet_focused", function () {
+ var $snippetOption = $('#oe_manipulators .oe_overlay_options:visible');
+ wysiwyg.$el.siblings('.note-image-popover').find('.note-media .note-icon-trash').mousedown().click();
+ assert.ok($snippetOption.is(':hidden'), "should remove the snippet editor because the target are removed");
+
+ wysiwyg.destroy();
+ done();
+ });
+
+ $editable.find('img').mousedown().click();
+ });
+});
+
+});
+});
+});
+});
diff --git a/addons/web_editor/static/tests/wysiwyg_tests.js b/addons/web_editor/static/tests/wysiwyg_tests.js
new file mode 100644
index 00000000000..c2033848e67
--- /dev/null
+++ b/addons/web_editor/static/tests/wysiwyg_tests.js
@@ -0,0 +1,3859 @@
+odoo.define('web_editor.wysiwyg_tests', function (require) {
+"use strict";
+
+var AltDialog = require('wysiwyg.widgets.AltDialog');
+var ColorpickerDialog = require('wysiwyg.widgets.ColorpickerDialog');
+var CropDialog = require('wysiwyg.widgets.CropImageDialog');
+var LinkDialog = require('wysiwyg.widgets.LinkDialog');
+var MediaDialog = require('wysiwyg.widgets.MediaDialog');
+var testUtils = require('web.test_utils');
+var weTestUtils = require('web_editor.test_utils');
+var Wysiwyg = require('web_editor.wysiwyg');
+
+var testName = "";
+var carretTestSuffix = " (carret position)";
+
+QUnit.module('web_editor', {
+ afterEach: function () {
+ $('body').removeClass('modal-open');
+ $('.note-popover, .modal, .modal-backdrop').remove();
+ },
+}, function () {
+QUnit.module('wysiwyg', {}, function () {
+QUnit.module('toolbar', {}, function () {
+
+QUnit.test('Magic wand', function (assert) {
+ var done = assert.async();
+ assert.expect(8);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var $dropdownStyle = wysiwyg.$('.note-style .dropdown-toggle');
+ var $btnsStyle = wysiwyg.$('.note-style .dropdown-menu .dropdown-item');
+
+ var wandTests = [{
+ name: "Click H1: p -> h1",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ do: function () {
+ $dropdownStyle.mousedown().click();
+ $btnsStyle.find('h1').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><h1>dom to edit</h1>',
+ start: 'h1:contents()[0]->0',
+ end: 'h1:contents()[0]->11',
+ },
+ },
+ {
+ name: "Click CODE: h1 -> pre",
+ content: '<p>dom not to edit</p><h1>dom to edit</h1>',
+ start: 'h1:contents()[0]->1',
+ do: function () {
+ $dropdownStyle.mousedown().click();
+ $btnsStyle.find('pre').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><pre>dom to edit</pre>',
+ start: 'pre:contents()[0]->0',
+ end: 'pre:contents()[0]->11',
+ },
+ },
+ {
+ name: "Click NORMAL: pre -> p",
+ content: '<p>dom not to edit</p><pre>dom to edit</pre>',
+ start: 'pre:contents()[0]->1',
+ do: function () {
+ $dropdownStyle.mousedown().click();
+ $btnsStyle.find('p').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->0',
+ end: 'p:eq(1):contents()[0]->11',
+ },
+ },
+ {
+ name: "Click H1 in empty p: empty p -> empty h1",
+ content: '<p><br></p>',
+ start: 'p->1',
+ do: function () {
+ $dropdownStyle.mousedown().click();
+ $btnsStyle.find('h1').mousedown().click();
+ },
+ test: {
+ content: '<h1><br></h1>',
+ start: 'h1:contents()[0]->0',
+ },
+ },
+ ];
+
+ _.each(wandTests, function (test) {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ test.do();
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), weTestUtils.select(test.test.start, test.test.end, $editable), testName + carretTestSuffix);
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('Font style', function (assert) {
+ var done = assert.async();
+ assert.expect(56);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+ generateOptions: function (options) {
+ options.toolbar[1][1] = ['bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', 'clear'];
+ },
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var $btnBold = wysiwyg.$('.note-font .note-btn-bold');
+ var $btnItalic = wysiwyg.$('.note-font .note-btn-italic');
+ var $btnUnderline = wysiwyg.$('.note-font .note-btn-underline');
+ var $strikethrough = wysiwyg.$('.note-font .note-icon-strikethrough');
+ var $superscript = wysiwyg.$('.note-font .note-icon-superscript');
+ var $subscript = wysiwyg.$('.note-font .note-icon-subscript');
+ var $btnRemoveStyles = wysiwyg.$('.note-font .btn-sm .note-icon-eraser');
+
+ var styleTests = [
+ /* BOLD */
+ {
+ name: "Click BOLD: normal -> bold",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>d<b>om t</b>o edit</p>',
+ start: 'b:contents()[0]->0',
+ end: 'b:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click BOLD then 'a': normal -> bold (empty p)",
+ content: '<p><br></p>',
+ start: 'p->1',
+ do: function () {
+ $btnBold.mousedown().click();
+ weTestUtils.keydown('a', $editable);
+ },
+ test: {
+ content: '<p><b>a</b></p>',
+ start: 'b:contents()[0]->1',
+ },
+ },
+ {
+ name: "Click BOLD: normal -> bold (across paragraphs)",
+ content: '<p>dom to edit</p><p>dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p>d<b>om to edit</b></p><p><b>dom t</b>o edit</p>',
+ start: 'b:contents()[0]->0',
+ end: 'b:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click BOLD then 'a': normal -> bold (no selection)",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->4',
+ do: function () {
+ $btnBold.mousedown().click();
+ weTestUtils.keydown('a', $editable);
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom <b>a</b>to edit</p>',
+ start: 'b:contents()[0]->1',
+ },
+ },
+ {
+ name: "Click BOLD: bold -> normal",
+ content: '<p>dom not to edit</p><p><b>dom to edit</b></p>',
+ start: 'b:contents()[0]->0',
+ end: 'b:contents()[0]->11',
+ do: function () {
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->0',
+ end: 'p:eq(1):contents()[0]->11',
+ },
+ },
+ {
+ name: "Click BOLD: bold -> normal (partial selection)",
+ content: '<p>dom not to edit</p><p><b>dom to edit</b></p>',
+ start: 'b:contents()[0]->4',
+ end: 'b:contents()[0]->6',
+ do: function () {
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><b>dom </b>to<b>&nbsp;edit</b></p>',
+ start: 'p:eq(1):contents()[1]->0',
+ end: 'p:eq(1):contents()[1]->2',
+ },
+ },
+ {
+ name: "Click BOLD: bold -> normal (no selection)",
+ content: '<p>dom not to edit</p><p><b>dom to edit</b></p>',
+ start: 'b:contents()[0]->4',
+ do: function () {
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><b>dom </b>\u200B<b>to edit</b></p>',
+ start: 'p:eq(1):contents()[1]->1',
+ },
+ },
+ {
+ name: "Click BOLD: bold + normal -> normal",
+ content: '<p><b>dom </b>to edit</p>',
+ start: 'b:contents()[0]->1',
+ end: 'p:contents()[1]->4',
+ do: function () {
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p><b>dom to e</b>dit</p>',
+ start: 'b:contents()[0]->1',
+ end: 'b:contents()[0]->8',
+ },
+ },
+ {
+ name: "Click BOLD: normal -> bold (with fontawesome)",
+ content: '<p>aaa<span class="fa fa-heart"></span>bbb</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[2]->2',
+ do: function () {
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p>a<b>aa<span class="fa fa-heart"></span>bb</b>b</p>',
+ start: 'b:contents()[0]->0',
+ end: 'b:contents()[2]->2',
+ },
+ },
+ {
+ name: "Click BOLD + click BOLD: normal -> bold -> normal (with fontawesome)",
+ content: '<p>aaa<span class="fa fa-heart"></span>bbb</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[2]->2',
+ do: function () {
+ $btnBold.mousedown().click();
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p>aaa<span class="fa fa-heart"></span>bbb</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[2]->2',
+ },
+ },
+ {
+ name: "Click BOLD: bold -> normal (with fontawesome)",
+ content: '<p><b>aaa<span class="fa fa-heart"></span>bbb</b></p>',
+ start: 'b:contents()[0]->1',
+ end: 'b:contents()[2]->2',
+ do: function () {
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p><b>a</b>aa<span class="fa fa-heart"></span>bb<b>b</b></p>',
+ start: 'p:contents()[1]->0',
+ end: 'p:contents()[3]->2',
+ },
+ },
+ /* ITALIC */
+ {
+ name: "Click ITALIC: bold -> bold + italic",
+ content: '<p>dom not to edit</p><p><b>dom to edit</b></p>',
+ start: 'b:contents()[0]->1',
+ end: 'b:contents()[0]->5',
+ do: function () {
+ $btnItalic.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><b>d<i>om t</i>o edit</b></p>',
+ start: 'i:contents()[0]->0',
+ end: 'i:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click ITALIC: bold & normal -> italic & bold + italic (across paragraphs)",
+ content: '<p>dom <b>to</b> edit</p><p><b>dom to edit</b></p>',
+ start: 'p:contents()[0]->1',
+ end: 'b:eq(1):contents()[0]->5',
+ do: function () {
+ $btnItalic.mousedown().click();
+ },
+ test: {
+ content: '<p>d<i>om <b>to</b> edit</i></p><p><b><i>dom t</i>o edit</b></p>',
+ start: 'i:contents()[0]->0',
+ end: 'i:eq(1):contents()[0]->5',
+ },
+ },
+ /* UNDERLINE */
+ {
+ name: "Click UNDERLINE: bold -> bold + underlined",
+ content: '<p>dom not to edit</p><p><b>dom to edit</b></p>',
+ start: 'b:contents()[0]->1',
+ end: 'b:contents()[0]->5',
+ do: function () {
+ $btnUnderline.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><b>d<u>om t</u>o edit</b></p>',
+ start: 'u:contents()[0]->0',
+ end: 'u:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click UNDERLINE: bold & normal -> underlined & bold + underlined (across paragraphs)",
+ content: '<p>dom <b>to</b> edit</p><p><b>dom to edit</b></p>',
+ start: 'p:contents()[0]->1',
+ end: 'b:eq(1):contents()[0]->5',
+ do: function () {
+ $btnUnderline.mousedown().click();
+ },
+ test: {
+ content: '<p>d<u>om <b>to</b> edit</u></p><p><b><u>dom t</u>o edit</b></p>',
+ start: 'u:contents()[0]->0',
+ end: 'u:eq(1):contents()[0]->5',
+ },
+ },
+ /* strikethrough */
+ {
+ name: "Click strikethrough: bold -> bold + strikethrough",
+ content: '<p>dom not to edit</p><p><b>dom to edit</b></p>',
+ start: 'b:contents()[0]->1',
+ end: 'b:contents()[0]->5',
+ do: function () {
+ $strikethrough.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><b>d<s>om t</s>o edit</b></p>',
+ start: 's:contents()[0]->0',
+ end: 's:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click strikethrough: bold & normal -> strikethrough & bold + strikethrough (across paragraphs)",
+ content: '<p>dom <b>to</b> edit</p><p><b>dom to edit</b></p>',
+ start: 'p:contents()[0]->1',
+ end: 'b:eq(1):contents()[0]->5',
+ do: function () {
+ $strikethrough.mousedown().click();
+ },
+ test: {
+ content: '<p>d<s>om <b>to</b> edit</s></p><p><b><s>dom t</s>o edit</b></p>',
+ start: 's:contents()[0]->0',
+ end: 's:eq(1):contents()[0]->5',
+ },
+ },
+ /* superscript */
+ {
+ name: "Click superscript: bold -> bold + superscript",
+ content: '<p>dom not to edit</p><p><b>dom to edit</b></p>',
+ start: 'b:contents()[0]->1',
+ end: 'b:contents()[0]->5',
+ do: function () {
+ $superscript.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><b>d<sup>om t</sup>o edit</b></p>',
+ start: 'sup:contents()[0]->0',
+ end: 'sup:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click superscript: bold & normal -> superscript & bold + superscript (across paragraphs)",
+ content: '<p>dom <b>to</b> edit</p><p><b>dom to edit</b></p>',
+ start: 'p:contents()[0]->1',
+ end: 'b:eq(1):contents()[0]->5',
+ do: function () {
+ $superscript.mousedown().click();
+ },
+ test: {
+ content: '<p>d<sup>om <b>to</b> edit</sup></p><p><b><sup>dom t</sup>o edit</b></p>',
+ start: 'sup:contents()[0]->0',
+ end: 'sup:eq(1):contents()[0]->5',
+ },
+ },
+ /* subscript */
+ {
+ name: "Click subscript: bold -> bold + subscript",
+ content: '<p>dom not to edit</p><p><b>dom to edit</b></p>',
+ start: 'b:contents()[0]->1',
+ end: 'b:contents()[0]->5',
+ do: function () {
+ $subscript.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><b>d<sub>om t</sub>o edit</b></p>',
+ start: 'sub:contents()[0]->0',
+ end: 'sub:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click subscript: bold & normal -> subscript & bold + subscript (across paragraphs)",
+ content: '<p>dom <b>to</b> edit</p><p><b>dom to edit</b></p>',
+ start: 'p:contents()[0]->1',
+ end: 'b:eq(1):contents()[0]->5',
+ do: function () {
+ $subscript.mousedown().click();
+ },
+ test: {
+ content: '<p>d<sub>om <b>to</b> edit</sub></p><p><b><sub>dom t</sub>o edit</b></p>',
+ start: 'sub:contents()[0]->0',
+ end: 'sub:eq(1):contents()[0]->5',
+ },
+ },
+ /* REMOVE FONT STYLE */
+ {
+ name: "Click REMOVE FONT STYLE: bold -> normal",
+ content: '<p>dom not to edit</p><p><b>dom to edit</b></p>',
+ start: 'b:contents()[0]->1',
+ end: 'b:contents()[0]->5',
+ do: function () {
+ $btnRemoveStyles.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><b>d</b>om t<b>o edit</b></p>',
+ start: 'p:eq(1):contents()[1]->0',
+ end: 'p:eq(1):contents()[1]->4',
+ },
+ },
+ {
+ name: "Click REMOVE FONT STYLE: bold, italic, underlined & normal -> normal (across paragraphs)",
+ content: '<p>dom <b>t<i>o</i></b> e<u>dit</u></p><p><b><u>dom</u> to edit</b></p>',
+ start: 'p:contents()[0]->1',
+ end: 'u:eq(1):contents()[0]->3',
+ do: function () {
+ $btnRemoveStyles.mousedown().click();
+ },
+ test: {
+ content: '<p>dom to edit</p><p>dom<b>&nbsp;to edit</b></p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->3',
+ },
+ },
+ {
+ name: "Click REMOVE FONT STYLE: complex -> normal",
+ content: '<p>aaa<font style="background-color: rgb(255, 255, 0);">bbb</font></p><p><font style="color: rgb(255, 0, 0);">ccc</font></p>',
+ start: 'p:contents()[0]->1',
+ end: 'font:eq(1):contents()[0]->1',
+ do: function () {
+ $btnRemoveStyles.mousedown().click();
+ },
+ test: {
+ content: '<p>aaabbb</p><p>c<font style="color: rgb(255, 0, 0);">cc</font></p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->1',
+ },
+ },
+ {
+ name: "Click REMOVE FONT STYLE: complex -> normal (with icon)",
+ content: '<p>a<b>a</b>a<span class="bg-alpha text-alpha fa fa-heart" style="font-size: 10px;"></span>b<b><i>b</i>b</b></p>',
+ start: 'p:contents()[0]->0',
+ end: 'i:contents()[0]->1',
+ do: function () {
+ $btnRemoveStyles.mousedown().click();
+ },
+ test: {
+ content: '<p>aaa<span class="fa fa-heart"></span>bb<b>b</b></p>',
+ start: 'p:contents()[0]->0',
+ end: 'p:contents()[2]->2',
+ },
+ },
+ /* COMPLEX */
+ {
+ name: "COMPLEX Click BOLD: italic -> italic bold (partial selection)",
+ content: '<p>dom not to edit</p><p><i>dom to edit</i></p>',
+ start: 'i:contents()[0]->1',
+ end: 'i:contents()[0]->5',
+ do: function () {
+ $btnBold.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><i>d<b>om t</b>o edit</i></p>',
+ start: 'b:contents()[0]->0',
+ end: 'b:contents()[0]->4',
+ },
+ },
+ {
+ name: "COMPLEX Click BOLD then 'a': italic bold -> italic (across paragraphs)",
+ content: '<p><b><i>dom to edit</i></b></p><p><i><b>dom to edit</b></i></p>',
+ start: 'i:contents()[0]->1',
+ end: 'b:eq(1):contents()[0]->5',
+ do: function () {
+ $btnBold.mousedown().click();
+ weTestUtils.keydown('a', $editable, {
+ firstDeselect: true,
+ });
+ },
+ test: {
+ content: '<p><b><i>d</i></b><i>om to edit</i></p><p><i>dom ta<b>o edit</b></i></p>',
+ },
+ },
+ {
+ name: "COMPLEX Click BOLD then 'a': bold italic -> italic (no selection)",
+ content: '<p><b><i>dom to edit</i></b></p>',
+ start: 'i:contents()[0]->4',
+ do: function () {
+ $btnBold.mousedown().click();
+ weTestUtils.keydown('a', $editable, {
+ firstDeselect: true,
+ });
+ },
+ test: {
+ content: '<p><b><i>dom </i></b><i>a</i><b><i>to edit</i></b></p>',
+ },
+ },
+ {
+ name: "COMPLEX Click BOLD then 'a': underlined italic -> underlined italic bold (across paragraphs)",
+ content: '<p><u><i>dom to edit</i></u></p><p><i><u>dom to edit</u></i></p>',
+ start: 'i:contents()[0]->1',
+ end: 'u:eq(1):contents()[0]->5',
+ do: function () {
+ $btnBold.mousedown().click();
+ weTestUtils.keydown('a', $editable, {
+ firstDeselect: true,
+ });
+ },
+ test: {
+ content: '<p><u><i>d<b>om to edit</b></i></u></p><p><i><u><b>dom ta</b>o edit</u></i></p>',
+ },
+ },
+ {
+ name: "COMPLEX Click BOLD then 'a': underlined italic -> underlined italic bold (no selection)",
+ content: '<p><u><i>dom to edit</i></u></p>',
+ start: 'i:contents()[0]->1',
+ do: function () {
+ $btnBold.mousedown().click();
+ weTestUtils.keydown('a', $editable, {
+ firstDeselect: true,
+ });
+ },
+ test: {
+ content: '<p><u><i>d<b>a</b>om to edit</i></u></p>',
+ },
+ },
+ ];
+
+ _.each(styleTests, function (test) {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ test.end = test.end || test.start;
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ test.do();
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ $editable[0].normalize();
+ if (test.test.start) {
+ test.test.end = test.test.end || test.test.start;
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), weTestUtils.select(test.test.start, test.test.end, $editable), testName + carretTestSuffix);
+ }
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('Font size', function (assert) {
+ var done = assert.async();
+ assert.expect(10);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var $dropdownFontSize = wysiwyg.$('.note-fontsize .dropdown-toggle');
+ var $linksFontSize = wysiwyg.$('.note-fontsize .dropdown-menu .dropdown-item');
+
+ var sizeTests = [{
+ name: "Click 18: default -> 18px",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $dropdownFontSize.mousedown().click();
+ $linksFontSize.filter(':contains("18")').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>d<font style="font-size: 18px;">om t</font>o edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click DEFAULT: 18px -> default",
+ content: '<p>dom not to edit</p><p><font style="font-size: 18px;">dom to edit</font></p>',
+ start: 'font:contents()[0]->1',
+ end: 'font:contents()[0]->5',
+ do: function () {
+ $dropdownFontSize.mousedown().click();
+ $linksFontSize.filter(':contains("Default")').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p><font style="font-size: 18px;">d</font>om t<font style="font-size: 18px;">o edit</font></p>',
+ start: 'p:eq(1):contents()[1]->0',
+ end: 'p:eq(1):contents()[1]->4',
+ },
+ },
+ {
+ name: "Click 18: 26px -> 18px",
+ content: '<p><font style="font-size: 26px;">dom to edit</font></p>',
+ start: 'font:contents()[0]->1',
+ end: 'font:contents()[0]->5',
+ do: function () {
+ $dropdownFontSize.mousedown().click();
+ $linksFontSize.filter(':contains("18")').mousedown().click();
+ },
+ test: {
+ content: '<p>' +
+ '<font style="font-size: 26px;">d</font>' +
+ '<font style="font-size: 18px;">om t</font>' +
+ '<font style="font-size: 26px;">o edit</font></p>',
+ start: 'font:eq(1):contents()[0]->0',
+ end: 'font:eq(1):contents()[0]->4',
+ },
+ },
+ {
+ name: "Click 18 then 'a' (no selection): default -> 18px",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ do: function () {
+ $dropdownFontSize.mousedown().click();
+ $linksFontSize.filter(':contains("18")').mousedown().click();
+ weTestUtils.keydown('a', $editable, {
+ firstDeselect: true,
+ });
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>d<font style="font-size: 18px;">a</font>om to edit</p>',
+ start: 'font:contents()[0]->1',
+ },
+ },
+ {
+ name: "Click 18 then 'a' (no selection): 26px -> 18px",
+ content: '<p><font style="font-size: 26px;">dom to edit</font></p>',
+ start: 'font:contents()[0]->1',
+ do: function () {
+ $dropdownFontSize.mousedown().click();
+ $linksFontSize.filter(':contains("18")').mousedown().click();
+ weTestUtils.keydown('a', $editable, {
+ firstDeselect: true,
+ });
+ },
+ test: {
+ content: '<p>' +
+ '<font style="font-size: 26px;">d</font>' +
+ '<font style="font-size: 18px;">a</font>' +
+ '<font style="font-size: 26px;">om to edit</font></p>',
+ start: 'font:eq(1):contents()[0]->1',
+ },
+ },
+ ];
+
+ _.each(sizeTests, function (test) {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ test.do();
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), weTestUtils.select(test.test.start, test.test.end, $editable), testName + carretTestSuffix);
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('Text forecolor', function (assert) {
+ var done = assert.async();
+ assert.expect(40);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var $dropdownForeColor = wysiwyg.$('.note-color .note-fore-color .dropdown-toggle');
+ var $btnsForeColor = wysiwyg.$('.note-color .note-fore-color .dropdown-menu .note-palette .note-color-btn');
+
+ var defColorpickerDialogInit;
+ testUtils.mock.patch(ColorpickerDialog, {
+ init: function () {
+ this._super.apply(this, arguments);
+ defColorpickerDialogInit = $.Deferred();
+ this.opened(defColorpickerDialogInit.resolve.bind(defColorpickerDialogInit));
+ },
+ });
+
+ var forecolorTests = [{
+ name: "Click THEME COLORS - ALPHA: default -> alpha theme color",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $dropdownForeColor.mousedown().click();
+ $btnsForeColor.filter('.bg-alpha').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>d<font class="text-alpha">om t</font>o edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click THEME COLORS - BLACK 25: alpha theme color & default -> black 25",
+ content: '<p>dom not to edit</p><p>do<font class="text-alpha">m to </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'font:contents()[0]->3',
+ do: function () {
+ $dropdownForeColor.mousedown().click();
+ $btnsForeColor.filter('.bg-black-25').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>d<font class="text-black-25">om t</font><font class="text-alpha">o </font>edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click COMMON COLORS - BLUE #0000FF: black 25 & default -> blue #0000FF",
+ content: '<p>dom not to edit</p><p>do<font class="text-black-25">m to </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'font:contents()[0]->3',
+ do: function () {
+ $dropdownForeColor.mousedown().click();
+ $btnsForeColor.filter('[style="background-color:#0000FF"]').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>d<font style="color: rgb(0, 0, 255);">om t</font><font class="text-black-25">o </font>edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click RESET TO DEFAULT: black 25 & default -> default",
+ content: '<p>dom not to edit</p><p>do<font class="text-black-25">m to </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'font:contents()[0]->3',
+ do: function () {
+ $dropdownForeColor.mousedown().click();
+ $btnsForeColor.filter('.note-color-reset').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom t<font class="text-black-25">o </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click CUSTOM COLORS then CUSTOM COLOR: blue #0000FF & default -> #875A7B",
+ async: true,
+ content: '<p>dom not to edit</p><p>do<font style="color: rgb(0, 0, 255);">m to </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'font:contents()[0]->3',
+ do: function () {
+ var self = this;
+ var def = $.Deferred();
+
+ $dropdownForeColor.mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color').mousedown().click();
+
+ defColorpickerDialogInit.then(function () {
+ $('.modal-dialog .o_hex_input').val('#875A7B').change();
+ $('.o_technical_modal .modal-footer .btn-primary:contains("Choose")').mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color-btn:last').mousedown().click();
+
+ assert.deepEqual(wysiwyg.getValue(),
+ '<p>dom not to edit</p><p>d<font style="color: rgb(135, 90, 123);">om t</font><font style="color: rgb(0, 0, 255);">o </font>edit</p>',
+ self.name);
+ var range = weTestUtils.select('font:contents()[0]->0', 'font:contents()[0]->4', $editable);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), range, self.name + carretTestSuffix);
+ def.resolve();
+ });
+ return def;
+ },
+ },
+ {
+ name: "Click CUSTOM COLORS then CUSTOM COLOR: change blue input",
+ content: '<p>dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[0]->6',
+ do: function () {
+ var self = this;
+ var def = $.Deferred();
+
+ $dropdownForeColor.mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color').mousedown().click();
+
+ defColorpickerDialogInit.then(function () {
+ $('.modal-dialog .o_blue_input').val('100').change();
+
+ assert.deepEqual($('.modal-dialog .o_hex_input').val(), '#ff0064', self.name + ' (hex)');
+ assert.deepEqual($('.modal-dialog .o_hue_input').val(), '337', self.name + ' (hue)');
+
+ $('.o_technical_modal .modal-footer .btn-primary:contains("Choose")').mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color-btn:last').mousedown().click();
+ def.resolve();
+ });
+ return def;
+ },
+ test: {
+ content: '<p>d<font style="color: rgb(255, 0, 100);">om to</font> edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->5',
+ },
+ },
+ {
+ name: "CUSTOM COLOR: change hue, saturation and lightness inputs",
+ content: '<p>dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[0]->6',
+ do: function () {
+ var self = this;
+ var def = $.Deferred();
+
+ $dropdownForeColor.mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color').mousedown().click();
+
+ defColorpickerDialogInit.then(function () {
+ $('.modal-dialog .o_hue_input').val('337').change();
+ $('.modal-dialog .o_saturation_input').val('50').change();
+ $('.modal-dialog .o_lightness_input').val('40').change();
+
+ assert.deepEqual($('.modal-dialog .o_hex_input').val(), '#99335a', self.name + ' (hex)');
+ assert.deepEqual($('.modal-dialog .o_green_input').val(), '51', self.name + ' (green)');
+
+ $('.o_technical_modal .modal-footer .btn-primary:contains("Choose")').mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color-btn:last').mousedown().click();
+ def.resolve();
+ });
+ return def;
+ },
+ test: {
+ content: '<p>d<font style="color: rgb(153, 51, 90);">om to</font> edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->5',
+ },
+ },
+ {
+ name: "CUSTOM COLOR: mousedown on area",
+ content: '<p>dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[0]->6',
+ do: function () {
+ var self = this;
+ var def = $.Deferred();
+
+ $dropdownForeColor.mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color').mousedown().click();
+
+ defColorpickerDialogInit.then(function () {
+ var $area = $('.modal-dialog .o_color_pick_area');
+ var pos = $area.offset();
+ $area.trigger($.Event("mousedown", {
+ which: 1,
+ pageX: pos.left + 50,
+ pageY: pos.top + 50
+ }));
+ $area.trigger('mouseup');
+
+ assert.deepEqual($('.modal-dialog .o_hex_input').val(), '#cfafaf', self.name + ' (hex)');
+ assert.deepEqual($('.modal-dialog .o_red_input').val(), '207', self.name + ' (red)');
+ assert.deepEqual($('.modal-dialog .o_green_input').val(), '175', self.name + ' (green)');
+ assert.deepEqual($('.modal-dialog .o_blue_input').val(), '175', self.name + ' (blue)');
+ assert.deepEqual($('.modal-dialog .o_hue_input').val(), '0', self.name + ' (hue)');
+ assert.deepEqual($('.modal-dialog .o_saturation_input').val(), '25', self.name + ' (saturation)');
+ assert.deepEqual($('.modal-dialog .o_lightness_input').val(), '75', self.name + ' (lightness)');
+
+ $('.o_technical_modal .modal-footer .btn-primary:contains("Choose")').mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color-btn:last').mousedown().click();
+ def.resolve();
+ });
+ return def;
+ },
+ test: {
+ content: '<p>d<font style="color: rgb(207, 175, 175);">om to</font> edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->5',
+ },
+ },
+ {
+ name: "CUSTOM COLOR: mousedow on sliders",
+ content: '<p>dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[0]->6',
+ do: function () {
+ var self = this;
+ var def = $.Deferred();
+
+ $dropdownForeColor.mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color').mousedown().click();
+
+ defColorpickerDialogInit.then(function () {
+ var $slider1 = $('.modal-dialog .o_slider_pointer');
+ var pos1 = $slider1.offset();
+ $slider1.trigger($.Event("mousedown", {
+ which: 1,
+ pageX: pos1.left,
+ pageY: pos1.top + 50
+ }));
+ $slider1.trigger('mouseup');
+
+ assert.deepEqual($('.modal-dialog .o_hex_input').val(), '#83ff00', self.name + ' (hex)');
+
+ var $slider2 = $('.modal-dialog .o_opacity_slider');
+ var pos2 = $slider2.offset();
+ $slider2.trigger($.Event("mousedown", {
+ which: 1,
+ pageX: pos2.left,
+ pageY: pos2.top + 80
+ }));
+ $slider2.trigger('mouseup');
+
+ assert.deepEqual($('.modal-dialog .o_hue_input').val(), '89', self.name + ' (hue)');
+ assert.deepEqual($('.modal-dialog .o_opacity_input').val(), '60', self.name + ' (opacity)');
+
+ $('.o_technical_modal .modal-footer .btn-primary:contains("Choose")').mousedown().click();
+ wysiwyg.$('.note-color .note-fore-color .note-custom-color-btn:last').mousedown().click();
+ def.resolve();
+ });
+ return def;
+ },
+ test: {
+ content: '<p>d<font style="color: rgba(131, 255, 0, 0.6);">om to</font> edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->5',
+ },
+ },
+ {
+ name: "Apply a color on a fontawesome",
+ content: '<p>dom <i class="fa fa-glass"/>not to edit</p>',
+ start: 'i->0',
+ do: function () {
+ $dropdownForeColor.mousedown().click();
+ $btnsForeColor.filter('[style="background-color:#0000FF"]').mousedown().click();
+ },
+ test: {
+ content: '<p>dom <i class="fa fa-glass" style="color: rgb(0, 0, 255);"></i>not to edit</p>',
+ start: 'p->2',
+ },
+ },
+ {
+ name: "Apply a color on a font with text",
+ content: '<p>dom <i class="fa fa-glass"/>not to edit</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[2]->6',
+ do: function () {
+ $dropdownForeColor.mousedown().click();
+ $btnsForeColor.filter('[style="background-color:#0000FF"]').mousedown().click();
+ },
+ test: {
+ content: '<p>d<font style="color: rgb(0, 0, 255);">om&nbsp;</font><i class="fa fa-glass" style="color: rgb(0, 0, 255);"></i><font style="color: rgb(0, 0, 255);">not to</font> edit</p>',
+ start: 'font:eq(0):contents()[0]->0',
+ end: 'font:eq(1):contents()[0]->6',
+ },
+ },
+ {
+ name: "Apply color, then 'a' (no selection)",
+ content: '<p>dom not to edit</p>',
+ start: 'p:contents()[0]->1',
+ do: function () {
+ $dropdownForeColor.mousedown().click();
+ $btnsForeColor.filter('[style="background-color:#0000FF"]').mousedown().click();
+ weTestUtils.keydown('a', $editable, {
+ firstDeselect: true,
+ });
+ },
+ test: {
+ content: '<p>d<font style="color: rgb(0, 0, 255);">a</font>om not to edit</p>',
+ start: 'font:contents()[0]->1',
+ },
+ },
+ {
+ name: "Apply color on two ranges with the same color",
+ content: '<p>do<br><span class="toto"> </span>m not to edit</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[3]->4',
+ do: function ($editable) {
+ $dropdownForeColor.mousedown().click();
+ $btnsForeColor.filter('[style="background-color:#0000FF"]').mousedown().click();
+
+ var range = weTestUtils.select('p:contents()[5]->3', 'p:contents()[5]->6', $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+
+ $dropdownForeColor.mousedown().click();
+ $btnsForeColor.filter('[style="background-color:#0000FF"]').mousedown().click();
+ },
+ test: {
+ content: '<p>d<font style="color: rgb(0, 0, 255);">o</font><br><span class="toto"> </span><font style="color: rgb(0, 0, 255);">m no</font>t t<font style=\"color: rgb(0, 0, 255);\">o e</font>dit</p>',
+ start: 'font:eq(2):contents()[0]->0',
+ end: 'font:eq(2):contents()[0]->3',
+ },
+ },
+ ];
+
+ var def = $.when();
+ _.each(forecolorTests, function (test) {
+ def = def.then(function () {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ return $.when(test.do($editable)).then(function () {
+ if (!test.async) {
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), weTestUtils.select(test.test.start, test.test.end, $editable), testName + carretTestSuffix);
+ }
+ });
+ });
+ });
+ def.then(function () {
+ testUtils.mock.unpatch(ColorpickerDialog);
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+QUnit.test('Text bgcolor', function (assert) {
+ var done = assert.async();
+ assert.expect(10);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+ var testName = "";
+ var carretTestSuffix = " (carret position)";
+
+ var $dropdownBgColor = wysiwyg.$('.note-color .note-bg-color .dropdown-toggle');
+ var $btnsBgColor = wysiwyg.$('.note-color .note-bg-color .dropdown-menu .note-palette .note-color-btn');
+
+ var defColorpickerDialogInit;
+ testUtils.mock.patch(ColorpickerDialog, {
+ init: function () {
+ this._super.apply(this, arguments);
+ defColorpickerDialogInit = $.Deferred();
+ this.opened(defColorpickerDialogInit.resolve.bind(defColorpickerDialogInit));
+ },
+ });
+
+ var bgcolorTests = [{
+ name: "Click THEME COLORS - ALPHA: default -> alpha theme color",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $dropdownBgColor.mousedown().click();
+ $btnsBgColor.filter('.bg-alpha').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>d<font class="bg-alpha">om t</font>o edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click THEME COLORS - BLACK 25: alpha theme color & default -> black 25",
+ content: '<p>dom not to edit</p><p>do<font class="bg-alpha">m to </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'font:contents()[0]->3',
+ do: function () {
+ $dropdownBgColor.mousedown().click();
+ $btnsBgColor.filter('.bg-black-25').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>d<font class="bg-black-25">om t</font><font class="bg-alpha">o </font>edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click COMMON COLORS - BLUE #0000FF: black 25 & default -> blue #0000FF",
+ content: '<p>dom not to edit</p><p>do<font class="bg-black-25">m to </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'font:contents()[0]->3',
+ do: function () {
+ $dropdownBgColor.mousedown().click();
+ $btnsBgColor.filter('[style="background-color:#0000FF"]').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>d<font style="background-color: rgb(0, 0, 255);">om t</font><font class="bg-black-25">o </font>edit</p>',
+ start: 'font:contents()[0]->0',
+ end: 'font:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click RESET TO DEFAULT: black 25 & default -> default",
+ content: '<p>dom not to edit</p><p>do<font class="bg-black-25">m to </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'font:contents()[0]->3',
+ do: function () {
+ $dropdownBgColor.mousedown().click();
+ $btnsBgColor.filter('.note-color-reset').mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom t<font class="bg-black-25">o </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click CUSTOM COLORS then CUSTOM COLOR: blue #0000FF & default -> #875A7B",
+ async: true,
+ content: '<p>dom not to edit</p><p>do<font style="background-color: rgb(0, 0, 255);">m to </font>edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'font:contents()[0]->3',
+ do: function () {
+ var def = $.Deferred();
+ testName = "Click CUSTOM COLORS then CUSTOM COLOR: blue #0000FF & default -> #875A7B";
+
+ $dropdownBgColor.mousedown().click();
+ wysiwyg.$('.note-color .note-bg-color .note-custom-color').mousedown().click();
+
+ defColorpickerDialogInit.then(function () {
+ $('.modal-dialog .o_hex_input').val('#875A7B').change();
+ $('.o_technical_modal .modal-footer .btn-primary:contains("Choose")').mousedown().click();
+ wysiwyg.$('.note-color .note-bg-color .note-custom-color-btn:last').mousedown().click();
+
+ assert.deepEqual(wysiwyg.getValue(),
+ '<p>dom not to edit</p><p>d<font style="background-color: rgb(135, 90, 123);">om t</font><font style="background-color: rgb(0, 0, 255);">o </font>edit</p>',
+ testName);
+ var range = weTestUtils.select('font:contents()[0]->0',
+ 'font:contents()[0]->4',
+ $editable);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), range, testName + carretTestSuffix);
+ def.resolve();
+ });
+ return def;
+ },
+ },
+ ];
+
+ var def = $.when();
+ _.each(bgcolorTests, function (test) {
+ def = def.then(function () {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ return $.when(test.do()).then(function () {
+ if (!test.async) {
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), weTestUtils.select(test.test.start, test.test.end, $editable), testName + carretTestSuffix);
+ }
+ });
+ });
+ });
+ def.then(function () {
+ testUtils.mock.unpatch(ColorpickerDialog);
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+QUnit.test('Unordered list', function (assert) {
+ var done = assert.async();
+ assert.expect(10);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var $btnUL = wysiwyg.$('.note-para .note-icon-unorderedlist');
+
+ var ulTests = [{
+ name: "Click UL: p -> ul",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $btnUL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><ul><li><p>dom to edit</p></li></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click UL: p -> ul (across paragraphs)",
+ content: '<p>dom not to edit</p><p>dom to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ do: function () {
+ $btnUL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><ul><li><p>dom to edit</p></li><li><p>dom to edit</p></li></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click UL: ul -> p",
+ content: '<p>dom not to edit</p><ul><li><p>dom to edit</p></li></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $btnUL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click UL: p -> ul (across li's)",
+ content: '<p>dom not to edit</p><ul><li><p>dom to edit</p></li><li><p>dom to edit</p></li></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ do: function () {
+ $btnUL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click UL: ul -> p (from second li)",
+ content: '<p>dom not to edit</p><ul><li><p>xxx</p></li><li><p>dom to edit</p></li></ul>',
+ start: 'li:eq(1) p:contents()[0]->1',
+ end: 'li:eq(1) p:contents()[0]->5',
+ do: function () {
+ $btnUL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><ul><li><p>xxx</p></li></ul><p>dom to edit</p>',
+ start: 'p:eq(2):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ },
+ },
+ ];
+
+ _.each(ulTests, function (test) {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ test.do();
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), weTestUtils.select(test.test.start, test.test.end, $editable), testName + carretTestSuffix);
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('Ordered list', function (assert) {
+ var done = assert.async();
+ assert.expect(32);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var $btnOL = wysiwyg.$('.note-para .note-icon-orderedlist');
+
+ var olTests = [{
+ name: "Click OL: p -> ol",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><ol><li><p>dom to edit</p></li></ol>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click OL: p -> ol (across paragraphs)",
+ content: '<p>dom not to edit</p><p>dom to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><ol><li><p>dom to edit</p></li><li><p>dom to edit</p></li></ol>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click OL: ol -> p",
+ content: '<p>dom not to edit</p><ol><li><p>dom to edit</p></li></ol>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click OL: p -> ol (across li's)",
+ content: '<p>dom not to edit</p><ol><li><p>dom to edit</p></li><li><p>dom to edit</p></li></ol>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click OL: ol -> p (from second li) (2)",
+ content: '<p>dom not to edit</p><ol><li><p>dom to edit</p></li><li><p>dom to edit</p></li></ol>',
+ start: 'li:eq(0) p:contents()[0]->1',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom to edit</p><ol><li><p>dom to edit</p></li></ol>',
+ start: 'p:eq(1):contents()[0]->1',
+ },
+ },
+ {
+ name: "Click OL: ol -> p (from second li)",
+ content: '<p>dom not to edit</p><ol><li><p>dom to edit</p></li><li><p>dom to edit</p></li></ol>',
+ start: 'li:eq(0) p:contents()[0]->1',
+ end: 'li:eq(0) p:contents()[0]->5',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom to edit</p><ol><li><p>dom to edit</p></li></ol>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click OL + Click OL: ul ul -> ul ol -> ul",
+ content: '<p>a</p>' +
+ '<ul>' +
+ '<li><p>b</p></li>' +
+ '<ul>' +
+ '<li><p>c</p></li>' +
+ '<li><p>d</p></li>' +
+ '</ul>' +
+ '<li><p>e</p></li>' +
+ '</ul>',
+ start: 'ul ul li:first:contents()[0]->1',
+ do: function () {
+ $btnOL.mousedown().click();
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<p>a</p>' +
+ '<ul>' +
+ '<li><p>b</p></li>' +
+ '<li><p>c</p></li>' +
+ '<ul>' +
+ '<li><p>d</p></li>' +
+ '</ul>' +
+ '<li><p>e</p></li>' +
+ '</ul>',
+ start: 'ul li:eq(1) p:contents()[0]->1',
+ },
+ },
+ {
+ name: "Click OL in empty table cell in div",
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'tr:eq(0) td:eq(1)->1',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><ol><li><p><br></p></li></ol></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'tr:eq(0) td:eq(1) br->0',
+ },
+ },
+ {
+ name: "Click OL in empty table cell in div (2)",
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'tr:eq(0) td:eq(1) br->0',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><ol><li><p><br></p></li></ol></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'tr:eq(0) td:eq(1) br->0',
+ },
+ },
+ {
+ name: "Click OL in empty table cell in div (3)",
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'tr:eq(0) td:eq(1)->0',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><ol><li><p><br></p></li></ol></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'tr:eq(0) td:eq(1) br->0',
+ },
+ },
+ {
+ name: "Click OL in table cell in div",
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td>aaa</td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'tr:eq(0) td:eq(1):contents(0)->1',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><ol><li><p>aaa</p></li></ol></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'td li p:contents(0)->1',
+ },
+ },
+ {
+ name: "Click OL on image in table cell in div",
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><img data-src="/web_editor/static/src/img/transparent.png"></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'img->0',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<div>' +
+ '<p>a</p>' +
+ '<table>' +
+ '<tbody>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><ol><li><img data-src="/web_editor/static/src/img/transparent.png"></li></ol></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '<tr>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '<td><br></td>' +
+ '</tr>' +
+ '</tbody>' +
+ '</table>' +
+ '</div>',
+ start: 'td li->1',
+ },
+ },
+ {
+ name: "Click OL with selected LI in OL",
+ content: '<p>x</p>' +
+ '<ol>' +
+ '<li><p>aaa</p></li>' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ol>' +
+ '<p>y</p>',
+ start: 'p:eq(2):contents(0)->1',
+ end: 'p:eq(3):contents(0)->1',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ol>' +
+ '<li><p>aaa</p></li>' +
+ '</ol>' +
+ '<p>bbb</p>' +
+ '<p>ccc</p>' +
+ '<ol>' +
+ '<li><p>ddd</p></li>' +
+ '</ol>' +
+ '<p>y</p>',
+ start: 'p:eq(2):contents(0)->1',
+ end: 'p:eq(3):contents(0)->1',
+ },
+ },
+ {
+ name: "Click OL with selected LI in UL",
+ content: '<p>x</p>' +
+ '<ul>' +
+ '<li><p>aaa</p></li>' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ start: 'p:eq(2):contents(0)->1',
+ end: 'p:eq(3):contents(0)->1',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul>' +
+ '<li><p>aaa</p></li>' +
+ '</ul>' +
+ '<ol>' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '</ol>' +
+ '<ul>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ start: 'p:eq(2):contents(0)->1',
+ end: 'p:eq(3):contents(0)->1',
+ },
+ },
+ {
+ name: "Click OL after ENTER in p > b",
+ content: "<p><b>dom to edit</b></p>",
+ start: 'b:contents(0)->11',
+ do: function () {
+ weTestUtils.keydown(13, $editable); // ENTER
+ $btnOL.mousedown().click();
+ weTestUtils.keydown('a', $editable); // ENTER
+ },
+ test: {
+ content: "<p><b>dom to edit</b></p><ol><li><p><b>a</b></p></li></ol>",
+ start: 'b:eq(1):contents(0)->1',
+ },
+ },
+ {
+ name: "Click OL in font in H1 (with link) in div",
+ content: '<div><h1><font style="font-size: 62px;">table of contents <a href="p23">p23</a> (cfr: 34)</font></h1></div>',
+ start: 'font:contents(0)->11',
+ do: function () {
+ $btnOL.mousedown().click();
+ },
+ test: {
+ content: '<div><ol><li><h1><font style="font-size: 62px;">table of contents <a href="p23">p23</a> (cfr: 34)</font></h1></li></ol></div>',
+ start: 'font:contents(0)->11',
+ },
+ },
+ ];
+
+ _.each(olTests, function (test) {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ $(range.sc).mousedown();
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ test.do();
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ if (wysiwyg.getValue() === test.test.content) {
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), weTestUtils.select(test.test.start, test.test.end, $editable), testName + carretTestSuffix);
+ } else {
+ assert.notOk(true, testName + ' (Wrong DOM)');
+ }
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('Align', function (assert) {
+ var done = assert.async();
+ assert.expect(20);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var $dropdownPara = wysiwyg.$('.note-para .dropdown-toggle');
+ var $btnAlignLeft = wysiwyg.$('.note-align .note-icon-align-left');
+ var $btnAlignCenter = wysiwyg.$('.note-align .note-icon-align-center');
+ var $btnAlignRight = wysiwyg.$('.note-align .note-icon-align-right');
+ var $btnAlignJustify = wysiwyg.$('.note-align .note-icon-align-justify');
+
+ var alignTests = [
+ /* ALIGN LEFT */
+ {
+ name: "Click ALIGN LEFT: p -> p align left (does nothing)",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignLeft.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ },
+ },
+ {
+ name: "Click ALIGN LEFT: p (parent align right) -> p align left (does nothing)",
+ content: '<div style="text-align: right;"><p>dom not to edit</p><p>dom to edit</p></div>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignLeft.mousedown().click();
+ },
+ test: {
+ content: '<div style="text-align: right;"><p>dom not to edit</p><p style="text-align: left;">dom to edit</p></div>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click ALIGN LEFT: p (parent align left) -> p align left (does nothing)",
+ content: '<div style="text-align: left;"><p>dom not to edit</p><p>dom to edit</p></div>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignLeft.mousedown().click();
+ },
+ test: {
+ content: '<div style="text-align: left;"><p>dom not to edit</p><p>dom to edit</p></div>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(1):contents()[0]->5',
+ },
+ },
+ {
+ name: "Click ALIGN LEFT: p align justify & default -> p align right (across paragraphs)",
+ content: '<p>dom not to edit</p><p style="text-align: justify;">dom to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignRight.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p style="text-align: right;">dom to edit</p><p style="text-align: right;">dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ },
+ },
+ /* ALIGN CENTER */
+ {
+ name: "Click ALIGN CENTER: p -> p align center",
+ content: '<p>dom not to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignCenter.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p style="text-align: center;">dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ },
+ },
+ {
+ name: "Click ALIGN CENTER: p align left & default -> p align center (across paragraphs)",
+ content: '<p>dom not to edit</p><p style="text-align: left;">dom to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignCenter.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p style="text-align: center;">dom to edit</p><p style="text-align: center;">dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ },
+ },
+ /* ALIGN RIGHT */
+ {
+ name: "Click ALIGN RIGHT: p align center -> p align right",
+ content: '<p>dom not to edit</p><p style="text-align: center;">dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignRight.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p style="text-align: right;">dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ },
+ },
+ {
+ name: "Click ALIGN RIGHT: p align center & default -> p align right (across paragraphs)",
+ content: '<p>dom not to edit</p><p style="text-align: center;">dom to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignRight.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p style="text-align: right;">dom to edit</p><p style="text-align: right;">dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ },
+ },
+ /* ALIGN JUSTIFY */
+ {
+ name: "Click ALIGN JUSTIFY: p align right -> p align justify",
+ content: '<p>dom not to edit</p><p style="text-align: right;">dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignJustify.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p style="text-align: justify;">dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ },
+ },
+ {
+ name: "Click ALIGN JUSTIFY: p align right & default -> p align justify (across paragraphs)",
+ content: '<p>dom not to edit</p><p style="text-align: right;">dom to edit</p><p>dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnAlignJustify.mousedown().click();
+ },
+ test: {
+ content: '<p>dom not to edit</p><p style="text-align: justify;">dom to edit</p><p style="text-align: justify;">dom to edit</p>',
+ start: 'p:eq(1):contents()[0]->1',
+ end: 'p:eq(2):contents()[0]->5',
+ },
+ },
+ ];
+
+ _.each(alignTests, function (test) {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ test.do();
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), weTestUtils.select(test.test.start, test.test.end, $editable), testName + carretTestSuffix);
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('Indent/outdent', function (assert) {
+ var done = assert.async();
+ assert.expect(18);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var $dropdownPara = wysiwyg.$('.note-para .dropdown-toggle');
+ var $btnIndent = wysiwyg.$('.note-list .note-btn:eq(1)');
+ var $btnOutdent = wysiwyg.$('.note-list .note-btn:first');
+
+ var indentTests = [
+ /* INDENT */
+ {
+ name: "Click INDENT: p -> indented p",
+ content: '<p>dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnIndent.mousedown().click();
+ },
+ test: {
+ content: '<p style="margin-left: 1.5em;">dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ },
+ },
+ {
+ name: "Click INDENT: li -> indented li",
+ content: '<ul><li><p>dom</p></li><li><p>to edit</p></li></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnIndent.mousedown().click();
+ },
+ test: {
+ content: '<ul><li><p>dom</p></li><li class="o_indent"><ul><li><p>to edit</p></li></ul></li></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ },
+ },
+ /* OUTDENT */
+ {
+ name: "Click OUTDENT: indented p -> p",
+ content: '<p style="margin-left: 1.5em;">dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnOutdent.mousedown().click();
+ },
+ test: {
+ content: '<p>dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ },
+ },
+ {
+ name: "Click OUTDENT: indented li -> li",
+ content: '<ul><li><p>dom</p></li><li><ul><li><p>to edit</p></li></ul></li></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnOutdent.mousedown().click();
+ },
+ test: {
+ content: '<ul><li><p>dom</p></li><li><p>to edit</p></li></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ },
+ },
+ {
+ name: "Click OUTDENT: indented li -> li (2)",
+ content: '<ul><li><p>dom</p></li><ul><li><p>to edit</p></li></ul></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnOutdent.mousedown().click();
+ },
+ test: {
+ content: '<ul><li><p>dom</p></li><li><p>to edit</p></li></ul>',
+ start: 'p:eq(1):contents()[0]->1',
+ },
+ },
+ {
+ name: "Click OUTDENT on LI in OL in OL",
+ content: '<p>x</p>' +
+ '<ol>' +
+ '<li><p>aaa</p></li>' +
+ '<li><ol>' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ol></li>' +
+ '<li><p>eee</p></li>' +
+ '</ol>' +
+ '<p>y</p>',
+ start: 'ol ol p:eq(1):contents(0)->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnOutdent.mousedown().click();
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ol>' +
+ '<li><p>aaa</p></li>' +
+ '<li><ol>' +
+ '<li><p>bbb</p></li>' +
+ '</ol></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><ol>' +
+ '<li><p>ddd</p></li>' +
+ '</ol></li>' +
+ '<li><p>eee</p></li>' +
+ '</ol>' +
+ '<p>y</p>',
+ start: 'ol:first > li:eq(2) p:contents(0)->1',
+ },
+ },
+ {
+ name: "Click OUTDENT on LI in OL in OL (2)",
+ content: '<p>x</p>' +
+ '<ol>' +
+ '<li><p>aaa</p></li>' +
+ '<ol>' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ol>' +
+ '<li><p>eee</p></li>' +
+ '</ol>' +
+ '<p>y</p>',
+ start: 'ol ol p:eq(1):contents(0)->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnOutdent.mousedown().click();
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ol>' +
+ '<li><p>aaa</p></li>' +
+ '<ol>' +
+ '<li><p>bbb</p></li>' +
+ '</ol>' +
+ '<li><p>ccc</p></li>' +
+ '<ol>' +
+ '<li><p>ddd</p></li>' +
+ '</ol>' +
+ '<li><p>eee</p></li>' +
+ '</ol>' +
+ '<p>y</p>',
+ start: 'li:eq(2) p:contents(0)->1',
+ },
+ },
+ {
+ name: "Click OUTDENT on LI in OL",
+ content:
+ '<ol>' +
+ '<li><p>aaa</p></li>' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '</ol>',
+ start: 'p:eq(1):contents(0)->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnOutdent.mousedown().click();
+ },
+ test: {
+ content:
+ '<ol>' +
+ '<li><p>aaa</p></li>' +
+ '</ol>' +
+ '<p>bbb</p>' +
+ '<ol>' +
+ '<li><p>ccc</p></li>' +
+ '</ol>',
+ start: 'p:eq(1):contents(0)->1',
+ },
+ },
+ {
+ name: "Click OUTDENT on P with indent in a LI (must outdent the p)",
+ content:
+ '<ul>' +
+ '<li>' +
+ '<ul>' +
+ '<li>' +
+ '<p style="margin-left: 1.5em;">dom</p>' +
+ '</li>' +
+ '</ul>' +
+ '</li>' +
+ '</ul>',
+ start: 'p:contents(0)->1',
+ do: function () {
+ $dropdownPara.mousedown().click();
+ $btnOutdent.mousedown().click();
+ },
+ test: {
+ content:
+ '<ul>' +
+ '<li>' +
+ '<ul>' +
+ '<li>' +
+ '<p>dom</p>' +
+ '</li>' +
+ '</ul>' +
+ '</li>' +
+ '</ul>',
+ start: 'p:contents(0)->1',
+ },
+ },
+ ];
+
+ _.each(indentTests, function (test) {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ test.do();
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), weTestUtils.select(test.test.start, test.test.end, $editable), testName + carretTestSuffix);
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('checklist', function (assert) {
+ var done = assert.async();
+ assert.expect(10);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var checklistTests = [
+ {
+ name: "check checkbox in checklist with children",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('li:first');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ {
+ name: "check checkbox in checklist with children (2)",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('li:first');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ {
+ name: "uncheck checkbox in checklist with children",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('li:first');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ {
+ name: "uncheck checkbox in checklist with children (2)",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('li:first');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ {
+ name: "uncheck checkbox in checklist in checklist",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('ul ul li:eq(1)');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ {
+ name: "uncheck checkbox in checklist in checklist (2)",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('ul ul li:eq(1)');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ {
+ name: "check checkbox in checklist in checklist",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('ul ul li:eq(1)');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ {
+ name: "check checkbox in checklist in checklist (2)",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('ul ul li:eq(1)');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ {
+ name: "check checkbox in checklist in checklist (full)",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('ul ul li:eq(1)');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>aaa</p></li>' +
+ '<li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '</li>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ {
+ name: "check checkbox in checklist in checklist (full) (2)",
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ do: function () {
+ var $li = $editable.find('ul ul li:eq(1)');
+ var pos = $li.offset();
+ $li.trigger($.Event('mousedown', {
+ offsetX: -10,
+ }));
+ },
+ test: {
+ content: '<p>x</p>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>aaa</p></li>' +
+ '<ul class="o_checklist">' +
+ '<li class="o_checked"><p>bbb</p></li>' +
+ '<li class="o_checked"><p>ccc</p></li>' +
+ '<li class="o_checked"><p>ddd</p></li>' +
+ '</ul>' +
+ '<li><p>eee</p></li>' +
+ '</ul>' +
+ '<p>y</p>',
+ },
+ },
+ ];
+
+ _.each(checklistTests, function (test) {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ test.do();
+ assert.deepEqual(wysiwyg.getValue(), test.test.content, testName);
+ });
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('Link', function (assert) {
+ var done = assert.async();
+ assert.expect(19);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var $btnLink = wysiwyg.$('.note-insert .note-icon-link');
+
+ var defLinkDialogInit;
+ testUtils.mock.patch(LinkDialog, {
+ init: function () {
+ this._super.apply(this, arguments);
+ defLinkDialogInit = $.Deferred();
+ this.opened(defLinkDialogInit.resolve.bind(defLinkDialogInit));
+ },
+ save: function () {
+ defLinkDialogInit = null;
+ return this._super.apply(this, arguments);
+ },
+ });
+ var _clickLink = function (callbackInit, test) {
+ $btnLink.mousedown().click();
+ defLinkDialogInit.then(callbackInit).then(function () {
+ $('.modal-dialog .btn-primary:contains("Save")').mousedown().click();
+ if (test.check) {
+ test.check();
+ }
+ if (test.content) {
+ assert.deepEqual(wysiwyg.getValue(), test.content, testName);
+ }
+ if (test.start) {
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), range, testName + carretTestSuffix);
+ }
+ });
+ return defLinkDialogInit;
+ };
+
+ var linkTests = [{
+ name: "Click LINK: p -> a in p (w/ URL)",
+ async: true,
+ content: '<p>dom to edit</p>',
+ start: "p:contents()[0]->1",
+ end: "p:contents()[0]->5",
+ do: function () {
+ assert.strictEqual($('.modal-dialog #o_link_dialog_label_input').val(), 'om t', testName + ' (label)');
+ $('.modal-dialog #o_link_dialog_url_input').val('#');
+ },
+ test: {
+ content: '<p>d<a href="#">om t</a>o edit</p>',
+ start: 'a:contents()[0]->0',
+ end: 'a:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click LINK: p -> a in p (w/ URL) (no selection)",
+ async: true,
+ content: '<p>do edit</p>',
+ start: 'p:contents()[0]->1',
+ do: function () {
+ $('.modal-dialog #o_link_dialog_label_input').val('om t');
+ $('.modal-dialog #o_link_dialog_url_input').val('#');
+ },
+ test: {
+ content: '<p>d<a href="#">om t</a>o edit</p>',
+ start: 'p->2', // link not selected, the user can continue to write
+ end: 'p->2',
+ },
+ },
+ {
+ name: "Click LINK: a.btn in div -> a.btn.btn-outline-alpha in div (edit) (no selection)",
+ content: '<div><a href="#" class="btn btn-outline-alpha btn-lg">dom to edit</a></div>',
+ start: 'a:contents()[0]->5',
+ do: function () {
+ assert.strictEqual($('.modal-dialog #o_link_dialog_label_input').val(), 'dom to edit', testName + ' (label)');
+ $('.modal-dialog #o_link_dialog_url_input').val('#newlink');
+ },
+ test: {
+ content: '<div><a href="#newlink" class="btn btn-outline-alpha btn-lg">dom to edit</a></div>',
+ start: 'a->0',
+ end: 'a->1',
+ },
+ },
+ {
+ name: "Click LINK: p -> a in p (w/ Email)",
+ async: true,
+ content: '<p>dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[0]->5',
+ do: function () {
+ $('.modal-dialog #o_link_dialog_url_input').val('john.coltrane@example.com');
+ },
+ test: {
+ content: '<p>d<a href="mailto:john.coltrane@example.com">om t</a>o edit</p>',
+ start: 'a:contents()[0]->0',
+ end: 'a:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click LINK: p -> a in p (w/ URL & Size Large)",
+ async: true,
+ content: '<p>dom to edit</p>',
+ start: 'p:contents()[0]->1',
+ end: 'p:contents()[0]->5',
+ do: function () {
+ $('.modal-dialog #o_link_dialog_url_input').val('#');
+ $('.modal-dialog [name="link_style_size"]').val("lg");
+ },
+ test: {
+ content: '<p>d<a href="#" class="btn-lg">om t</a>o edit</p>',
+ start: 'a:contents()[0]->0',
+ end: 'a:contents()[0]->4',
+ },
+ },
+ {
+ name: "Click LINK: a in p -> a.btn-outline-alpha in p with alpha color and target=\"_blank\"",
+ async: true,
+ content: '<p><a href="#">dom to edit</a></p>',
+ start: 'a:contents()[0]->1',
+ do: function () {
+ $('.modal-dialog #o_link_dialog_url_input').val('#');
+ $('.modal-dialog [name="link_style_shape"]').val("outline");
+ $('.modal-dialog .o_link_dialog_color .o_link_dialog_color_item.btn-alpha').mousedown().click();
+ $('.modal-dialog .o_switch [name="is_new_window"]').mousedown().click();
+ },
+ test: {
+ content: '<p><a href="#" target="_blank" class="btn btn-outline-alpha">dom to edit</a></p>',
+ start: 'a->0',
+ end: 'a->1',
+ },
+ },
+ /* POPOVER */
+ {
+ name: "Click LINK in popover after adding link in p",
+ async: true,
+ content: '<p>dom to edit</p>',
+ start: "p:contents()[0]->1",
+ end: "p:contents()[0]->5",
+ do: function () {
+ $('.modal-dialog #o_link_dialog_url_input').val('/link');
+ },
+ test: {
+ check: function () {
+ $('.note-link-popover .note-btn .note-icon-link').mousedown().click();
+ defLinkDialogInit.then(function () {
+ assert.strictEqual($('.modal-dialog #o_link_dialog_label_input').val(), 'om t', testName + ' (label)');
+ assert.strictEqual($('.modal-dialog #o_link_dialog_url_input').val(), '/link', testName + ' (url)');
+ $('.modal-dialog #o_link_dialog_url_input').val('/newlink');
+ $('.modal-dialog .modal-footer .btn.btn-primary:contains("Save")').mousedown().click();
+ assert.deepEqual(wysiwyg.getValue(), '<p>d<a href="/newlink">om t</a>o edit</p>', testName);
+ });
+ },
+ },
+ },
+ {
+ name: "Click UNLINK in popover after adding link in p",
+ async: true,
+ content: '<p>dom to edit</p>',
+ start: "p:contents()[0]->1",
+ end: "p:contents()[0]->5",
+ do: function () {
+ $('.modal-dialog #o_link_dialog_url_input').val('/link');
+ },
+ test: {
+ content: '<p>dom to edit</p>',
+ check: function () {
+ $('.note-link-popover .note-btn .note-icon-chain-broken').mousedown().click();
+ var range = weTestUtils.select('p:contents()[0]->1', 'p:contents()[0]->5', $editable);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), range, testName + carretTestSuffix);
+ },
+ },
+ },
+ ];
+
+ var def = $.when();
+ _.each(linkTests, function (test) {
+ def = def.then(function () {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ return _clickLink(test.do, test.test);
+ });
+ });
+ def.then(function () {
+ testUtils.mock.unpatch(LinkDialog);
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+QUnit.test('Table', function (assert) {
+ var done = assert.async();
+ assert.expect(15);
+
+ function createTable(wysiwyg) {
+ wysiwyg.$('.note-table button:first').mousedown().click();
+
+ var $grid = wysiwyg.$('.note-table .note-dimension-picker-mousecatcher');
+ var pos = $grid.offset();
+ $grid.trigger($.Event('mousemove', {
+ pageX: pos.left + 40,
+ pageY: pos.top + 40,
+ }));
+ $grid.mousedown().click();
+ }
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {},
+ }).then(function (wysiwyg) {
+ var $editable = wysiwyg.getEditable();
+
+ // create a table in an empty dom
+
+ wysiwyg.setValue('<p><br></p>');
+ var range = weTestUtils.select('p->1', 'p->1', $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+
+ createTable(wysiwyg);
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p><br></p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '</tbody></table>' +
+ '<p><br></p>',
+ "should create a table in an empty dom (p > br)");
+
+ // create a table at start of p with content
+
+ wysiwyg.setValue('<p>dom to edit</p>');
+ range = weTestUtils.select('p:contents()[0]->0', 'p:contents()[0]->0', $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+
+ createTable(wysiwyg);
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p><br></p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '</tbody></table>' +
+ '<p>dom to edit</p>',
+ "should create a table at start of single p with content");
+
+ // create a table at end of p with content
+
+ wysiwyg.setValue('<p>dom to edit</p>');
+ range = weTestUtils.select('p:contents()[0]->11', 'p:contents()[0]->11', $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+
+ createTable(wysiwyg);
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom to edit</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '</tbody></table>' +
+ '<p><br></p>',
+ "should create a table at end of single p with content");
+
+ // create a table within p
+
+ wysiwyg.setValue('<p>dom to edit</p>');
+ range = weTestUtils.select('p:contents()[0]->5', 'p:contents()[0]->5', $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+
+ createTable(wysiwyg);
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>',
+ "should create a table");
+
+ // remove a table
+
+ range = weTestUtils.select('td:first->0', 'td:first->0', $editable);
+ $(range.sc).mousedown();
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+
+ var $trash = $('.note-table-popover:visible button:has(.note-icon-trash)');
+ assert.strictEqual($trash.size(), 1, "should display the table popover");
+
+ $trash.mousedown().click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom to edit</p>',
+ "should remove the table");
+
+ // re create a table and table in table
+
+ createTable(wysiwyg);
+ createTable(wysiwyg);
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '</tbody></table>' +
+ '<p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>',
+ "should create a table inside the table");
+
+ // remove inner table
+
+ range = weTestUtils.select('td td:eq(1)->0', 'td td:eq(1)->0', $editable);
+ $(range.sc).mousedown();
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+
+ $trash = $('.note-table-popover:visible button:has(.note-icon-trash)');
+ assert.strictEqual($trash.size(), 1, "should display the table popover");
+ $trash.mousedown().click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>',
+ "should remove the inner table");
+
+ // use popover to change row and line
+
+ wysiwyg.setValue('<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p>0-0</p></td><td><p>0-1</p></td><td><p>0-2</p></td></tr>' +
+ '<tr><td><p>1-0</p></td><td><p>1-1</p></td><td><p>1-2</p></td></tr>' +
+ '<tr><td><p>2-0</p></td><td><p>2-1</p></td><td><p>2-2</p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>');
+
+ // remove a row
+
+ range = weTestUtils.select('td:eq(1)->0', 'td:eq(1)->0', $editable);
+ $(range.sc).mousedown();
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ $('.note-table-popover:visible button:has(.note-icon-col-remove)').mousedown().click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p>0-0</p></td><td><p>0-2</p></td></tr>' +
+ '<tr><td><p>1-0</p></td><td><p>1-2</p></td></tr>' +
+ '<tr><td><p>2-0</p></td><td><p>2-2</p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>',
+ "should remove a row");
+
+ // remove a line
+
+ range = weTestUtils.select('tr:eq(1) td:eq(1)->0', 'tr:eq(1) td:eq(1)->0', $editable);
+ $(range.sc).mousedown();
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ $('.note-table-popover:visible button:has(.note-icon-row-remove)').mousedown().click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p>0-0</p></td><td><p>0-2</p></td></tr>' +
+ '<tr><td><p>2-0</p></td><td><p>2-2</p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>',
+ "should remove a line");
+
+ // add a row after
+
+ range = weTestUtils.select('tr:eq(1) td:first->0', 'tr:eq(1) td:first->0', $editable);
+ $(range.sc).mousedown();
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ $('.note-table-popover:visible button:has(.note-icon-col-after)').mousedown().click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p>0-0</p></td><td><p><br></p></td><td><p>0-2</p></td></tr>' +
+ '<tr><td><p>2-0</p></td><td><p><br></p></td><td><p>2-2</p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>',
+ "should add a row after");
+
+ // add a row before
+
+ $(range.sc).mousedown();
+ $('.note-table-popover:visible button:has(.note-icon-col-before)').mousedown().click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p></td><td><p>0-0</p></td><td><p><br></p></td><td><p>0-2</p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p>2-0</p></td><td><p><br></p></td><td><p>2-2</p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>',
+ "should add a row before");
+
+ // add a line after
+
+ $(range.sc).mousedown();
+ $('.note-table-popover:visible button:has(.note-icon-row-below)').mousedown().click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p></td><td><p>0-0</p></td><td><p><br></p></td><td><p>0-2</p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p>2-0</p></td><td><p><br></p></td><td><p>2-2</p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>',
+ "should add a line after");
+
+ // add a line before
+
+ $(range.sc).mousedown();
+ $('.note-table-popover:visible button:has(.note-icon-row-above)').mousedown().click();
+
+ assert.strictEqual($editable.html().replace(/\s+/g, ' '),
+ '<p>dom t</p>' +
+ '<table class=\"table table-bordered\"><tbody>' +
+ '<tr><td><p><br></p></td><td><p>0-0</p></td><td><p><br></p></td><td><p>0-2</p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p>2-0</p></td><td><p><br></p></td><td><p>2-2</p></td></tr>' +
+ '<tr><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td><td><p><br></p></td></tr>' +
+ '</tbody></table>' +
+ '<p>o edit</p>',
+ "should add a line before");
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+QUnit.test('CodeView', function (assert) {
+ var done = assert.async();
+ assert.expect(8);
+
+ return weTestUtils.createWysiwyg({
+ debug: false,
+ wysiwygOptions: {
+ codeview: true,
+ },
+ }).then(function (wysiwyg) {
+ wysiwyg.setValue('<p>dom to edit <img src="/web_editor/static/src/img/transparent.png"></p>');
+
+ var $buttonCodeView = wysiwyg.$('button:has(.note-icon-code)');
+
+ // hide popover in CodeView mode and no range error when edit the content
+ var $editable = wysiwyg.getEditable();
+ $editable.find('img').mousedown().click();
+ assert.strictEqual($('.note-popover.note-image-popover:visible').length, 1, "should display the image popover");
+ $buttonCodeView.mousedown().click();
+ assert.strictEqual($('textarea.note-codable:visible').length, 1, "should show the CodeView textarea");
+ assert.strictEqual($editable.is(':visible'), false, "should hide the editable area");
+ assert.strictEqual($('.note-popover:visible').length, 0, "should hide all popovers");
+ assert.strictEqual($('.note-toolbar button:not(.disabled)').length, 1, "should disabled all buttons expect the codeview button");
+ wysiwyg.$('textarea.note-codable').val('<p>dom to edit a <img src="/web_editor/static/src/img/transparent.png"></p>');
+ $buttonCodeView.mousedown().click();
+ assert.strictEqual($('textarea.note-codable:visible').length, 0, "should hide the CodeView textarea");
+ assert.strictEqual($editable.is(':visible'), true, "should show the editable area");
+ assert.strictEqual($editable[0].style.height, '', "should reset the height (not auto or sizing)");
+ // end
+
+ wysiwyg.destroy();
+ done();
+ });
+});
+
+});
+
+var defMediaDialogInit;
+var defMediaDialogSave;
+var defCropDialogInit;
+var defCropDialogSave;
+var defAltDialogInit;
+var defAltDialogSave;
+var imgWidth = 10;
+var imgHeight = 10;
+
+QUnit.module('Media', {
+ beforeEach: function () {
+ testUtils.mock.patch(MediaDialog, {
+ init: function () {
+ this._super.apply(this, arguments);
+ defMediaDialogInit = $.Deferred();
+ defMediaDialogSave = $.Deferred();
+ this.opened(defMediaDialogInit.resolve.bind(defMediaDialogInit));
+ },
+ save: function () {
+ $.when(this._super.apply(this, arguments)).then(function () {
+ var def = defMediaDialogSave;
+ defMediaDialogInit = null;
+ defMediaDialogSave = null;
+ def.resolve();
+ });
+ },
+ });
+ testUtils.mock.patch(CropDialog, {
+ init: function () {
+ $(arguments[2]).attr('src', $(arguments[2]).data('src'));
+ this._super.apply(this, arguments);
+ var self = this;
+ defCropDialogInit = $.Deferred();
+ defCropDialogSave = $.Deferred();
+ this.opened(function () {
+ var cropper = self.$cropperImage.data('cropper');
+ cropper.clone();
+ $.extend(cropper.image, {
+ naturalWidth: imgWidth,
+ naturalHeight: imgHeight,
+ aspectRatio: imgWidth / imgHeight,
+ });
+ cropper.loaded = true;
+ cropper.build();
+ cropper.render();
+ defCropDialogInit.resolve();
+ });
+ },
+ save: function () {
+ $.when(this._super.apply(this, arguments)).then(function () {
+ var def = defCropDialogSave;
+ defCropDialogInit = null;
+ defCropDialogSave = null;
+ def.resolve();
+ });
+ },
+ });
+ testUtils.mock.patch(AltDialog, {
+ init: function () {
+ this._super.apply(this, arguments);
+ defAltDialogInit = $.Deferred();
+ defAltDialogSave = $.Deferred();
+ this.opened(function () {
+ defAltDialogInit.resolve();
+ });
+ },
+ save: function () {
+ $.when(this._super.apply(this, arguments)).then(function () {
+ var def = defAltDialogSave;
+ defAltDialogInit = null;
+ defAltDialogSave = null;
+ def.resolve();
+ });
+ },
+ });
+ $('body').on('submit.WysiwygTests', function (ev) {
+ ev.preventDefault();
+ var $from = $(ev.target);
+ var iframe = $from.find('iframe[name="' + $from.attr('target') + '"]')[0];
+ if (iframe) {
+ iframe.contentWindow.attachments = [{
+ id: 1,
+ public: true,
+ name: 'image',
+ datas_fname: 'image.png',
+ mimetype: 'image/png',
+ checksum: false,
+ url: '/web_editor/static/src/img/transparent.png',
+ type: 'url',
+ res_id: 0,
+ res_model: false,
+ access_token: false
+ }];
+ $(iframe).trigger('load');
+ }
+ });
+
+ this.data = {
+ debug: false,
+ wysiwygOptions: {
+
+ },
+ mockRPC: function (route, args) {
+ if (args.model === 'ir.attachment' || !args.length) {
+ if (!args.length && route.indexOf('data:image/png;base64') === 0 ||
+ args.method === "search_read" &&
+ args.kwargs.domain[7][2].join(',') === "image/gif,image/jpe,image/jpeg,image/jpg,image/gif,image/png") {
+ return $.when(this.data.records);
+ }
+ }
+ if (route.indexOf('youtube') !== -1) {
+ return $.when();
+ }
+ return this._super(route, args);
+ },
+ };
+ },
+ afterEach: function () {
+ testUtils.mock.unpatch(MediaDialog);
+ testUtils.mock.unpatch(CropDialog);
+ testUtils.mock.unpatch(AltDialog);
+ $('body').off('submit.WysiwygTests');
+ },
+}, function () {
+
+
+var _clickMedia = function (wysiwyg, assert, callbackInit, test) {
+ wysiwyg.$('.note-insert .fa-file-image-o').mousedown().click();
+
+ defMediaDialogSave.then(function () {
+ if (test.check) {
+ test.check();
+ }
+ if (test.content) {
+ assert.deepEqual(wysiwyg.getValue(), test.content, testName);
+ }
+ if (test.start) {
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ assert.deepEqual(Wysiwyg.getRange($editable[0]), range, testName + carretTestSuffix);
+ }
+ });
+ return defMediaDialogInit.then(function () {
+ callbackInit();
+ var def = $.Deferred();
+ setTimeout(function () {
+ def.resolve();
+ }, 0);
+ return def;
+ });
+};
+var _uploadAndInsertImg = function (url) {
+ $('.modal-dialog input[name="url"]:first').val(url).trigger('input');
+ $('.modal-dialog .o_upload_media_url_button:first').mousedown().click();
+};
+var _insertVideo = function (wysiwyg, assert, url, checkOptions) {
+ $('.modal-dialog .nav-link:contains("Video")').mousedown().click();
+ $('.modal-dialog #o_video_text').val(url).trigger('change');
+ if (checkOptions) {
+ assert.strictEqual($('.modal-dialog .o_yt_option').parent().css('display'), 'block', testName + ' (options)');
+ }
+ $('.modal-dialog .modal-footer .btn.btn-primary').mousedown().click();
+};
+var _insertPictogram = function (className) {
+ $('.modal-dialog .nav-link:contains("Pictogram")').mousedown().click();
+ $('.modal-dialog .font-icons-icons .font-icons-icon.fa.' + className).mousedown().click();
+ $('.modal-dialog .modal-footer .btn.btn-primary').mousedown().click();
+};
+var _valueToRatio = function (value) {
+ return value < 0 ? 1 / (1 - value) : 1 + value;
+};
+
+
+QUnit.test('Image', function (assert) {
+ var done = assert.async();
+ assert.expect(13);
+
+ return weTestUtils.createWysiwyg(this.data).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var mediaTests = [{
+ name: "Click ADD AN IMAGE URL in empty p: p -> img in p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ content: '<p>\u200B<img class="img-fluid o_we_custom_image" data-src="/web_editor/static/src/img/transparent.png">\u200B</p>',
+ check: function () {
+ assert.strictEqual($('.note-image-popover').css('display'), 'block', testName + ' (popover)');
+ },
+ },
+ },
+ {
+ name: "add an image in a table",
+ async: true,
+ content: '<section><div class="container"><div class="row"><div class="col-lg-6">' +
+ '<table class="table table-bordered">' +
+ ' <tbody>' +
+ ' <tr>' +
+ ' <td>' +
+ ' aaa' +
+ ' </td>' +
+ ' <td>' +
+ ' bbb' +
+ ' </td>' +
+ ' <td>' +
+ ' ccc' +
+ ' </td>' +
+ ' </tr>' +
+ ' </tbody>' +
+ '</table>' +
+ '</div></div></div></section>',
+ start: "td:eq(1):contents()[0]->18",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ content: '<section><div class="container"><div class="row"><div class="col-lg-6">' +
+ '<table class="table table-bordered">' +
+ ' <tbody>' +
+ ' <tr>' +
+ ' <td>' +
+ ' aaa' +
+ ' </td>' +
+ ' <td>' +
+ ' bb<img class="img-fluid o_we_custom_image" data-src="/web_editor/static/src/img/transparent.png">b' +
+ ' </td>' +
+ ' <td>' +
+ ' ccc' +
+ ' </td>' +
+ ' </tr>' +
+ ' </tbody>' +
+ '</table>' +
+ '</div></div></div></section>',
+ check: function () {
+ assert.strictEqual($('.note-image-popover').css('display'), 'block', testName + ' (popover)');
+ },
+ },
+ },
+ {
+ name: "add an image in an empty table",
+ async: true,
+ content: '<section><div class="container"><div class="row"><div class="col-lg-6">' +
+ '<table class="table table-bordered">' +
+ ' <tbody>' +
+ ' <tr>' +
+ ' <td>' +
+ ' <br>' +
+ ' </td>' +
+ ' <td>' +
+ ' <br>' +
+ ' </td>' +
+ ' <td>' +
+ ' <br>' +
+ ' </td>' +
+ ' </tr>' +
+ ' </tbody>' +
+ '</table>' +
+ '</div></div></div></section>',
+ start: "br:eq(1)->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ content: '<section><div class="container"><div class="row"><div class="col-lg-6">' +
+ '<table class="table table-bordered">' +
+ ' <tbody>' +
+ ' <tr>' +
+ ' <td>' +
+ ' <br>' +
+ ' </td>' +
+ ' <td>' +
+ ' <img class="img-fluid o_we_custom_image" data-src="/web_editor/static/src/img/transparent.png"><br>' +
+ ' </td>' +
+ ' <td>' +
+ ' <br>' +
+ ' </td>' +
+ ' </tr>' +
+ ' </tbody>' +
+ '</table>' +
+ '</div></div></div></section>',
+ check: function () {
+ assert.strictEqual($('.note-image-popover').css('display'), 'block', testName + ' (popover)');
+ },
+ },
+ },
+ /* IMAGE POPOVER */
+ {
+ name: "Click PADDING XL in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ content: '<p>\u200B<img class="img-fluid o_we_custom_image padding-xl" data-src="/web_editor/static/src/img/transparent.png">\u200B</p>',
+ check: function () {
+ $('.note-image-popover .note-padding .dropdown-toggle').mousedown().click();
+ $('.note-image-popover .note-padding .dropdown-item:contains("Xl")').mousedown().click();
+ },
+ },
+ },
+ {
+ name: "Click IMAGE SIZE 25% in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ content: '<p>\u200B<img class="img-fluid o_we_custom_image" data-src="/web_editor/static/src/img/transparent.png" style="width: 25%;">\u200B</p>',
+ check: function () {
+ $('.note-image-popover .note-imagesize .note-btn:contains(25%)').mousedown().click();
+ },
+ },
+ },
+ {
+ name: "Click FLOAT RIGHT in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ content: '<p>\u200B<img class="img-fluid o_we_custom_image pull-right" data-src="/web_editor/static/src/img/transparent.png">\u200B</p>',
+ check: function () {
+ $('.note-image-popover .note-float .note-icon-align-right').mousedown().click();
+ },
+ },
+ },
+ {
+ name: "Click FLOAT CENTER then FLOAT LEFT in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ content: '<p>\u200B<img class="img-fluid o_we_custom_image pull-left" data-src="/web_editor/static/src/img/transparent.png">\u200B</p>',
+ check: function () {
+ $('.note-image-popover .note-float .note-icon-align-center').mousedown().click();
+ $('.note-image-popover .note-float .note-icon-align-left').mousedown().click();
+ },
+ },
+ },
+ {
+ name: "Click SHAPE ROUNDED in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ content: '<p>\u200B<img class="img-fluid o_we_custom_image rounded" data-src="/web_editor/static/src/img/transparent.png">\u200B</p>',
+ check: function () {
+ $('.note-image-popover .note-imageShape .note-btn:has(.fa-square)').mousedown().click();
+ },
+ },
+ },
+ // Remove picture
+ {
+ name: "Click REMOVE in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ content: '<p><br></p>',
+ check: function () {
+ $('.note-image-popover .note-btn .note-icon-trash').mousedown().click();
+ },
+ },
+ },
+ // Describe picture
+ {
+ name: "Click DESCRIPTION in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ check: function () {
+ $('.note-image-popover .note-btn:contains("Description")').mousedown().click();
+ defAltDialogSave.then(function () {
+ assert.deepEqual(wysiwyg.getValue(),
+ '<p>\u200B<img class="img-fluid o_we_custom_image" data-src="/web_editor/static/src/img/transparent.png" alt="Description" title="Title">\u200B</p>',
+ testName);
+ });
+ defAltDialogInit.then(function () {
+ $('.modal-dialog input#alt').val('Description');
+ $('.modal-dialog input#title').val('Title');
+ $('.modal-dialog .modal-footer .btn.btn-primary:contains("Save")').mousedown().click();
+ });
+ },
+ },
+ },
+ ];
+
+ var def = $.when();
+ _.each(mediaTests, function (test) {
+ def = def.then(function () {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ return _clickMedia(wysiwyg, assert, test.do, test.test);
+ });
+ });
+ def.then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+QUnit.test('Image crop', function (assert) {
+ var done = assert.async();
+ assert.expect(5);
+
+ return weTestUtils.createWysiwyg(this.data).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var mediaTests = [{
+ name: "Click CROP 16:9 + ZOOM IN in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ check: function () {
+ var zoomRatio;
+ $('.note-image-popover .note-btn:has(.fa-crop)').mousedown().click();
+ defCropDialogSave.then(function () {
+ var $img = $(wysiwyg.getValue()).find('img.o_cropped_img_to_save');
+ assert.strictEqual($img.data('aspect-ratio'), '16/9', testName + " (aspect-ratio)");
+ assert.strictEqual($img.data('width'), imgWidth / zoomRatio, testName + " (zoom)");
+ });
+ defCropDialogInit.then(function () {
+ $('.o_crop_image_dialog .o_crop_options .btn:contains("16:9")').mousedown().click();
+ var $zoomBtn = $('.o_crop_image_dialog .o_crop_options .btn:has(.fa-search-plus)');
+ zoomRatio = _valueToRatio(Number($zoomBtn.data('value')));
+ $zoomBtn.mousedown().click();
+ $('.modal-dialog .modal-footer .btn.btn-primary:contains("Save")').mousedown().click();
+ });
+ },
+ },
+ },
+ {
+ name: "Click CROP ROTATE LEFT + FLIP HORIZONTAL in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ check: function () {
+ $('.note-image-popover .note-btn:has(.fa-crop)').mousedown().click();
+ defCropDialogSave.then(function () {
+ var $img = $(wysiwyg.getValue()).find('img.o_cropped_img_to_save');
+ assert.strictEqual($img.data('rotate'), -45, testName + " (rotate)");
+ assert.strictEqual($img.data('scale-x'), -1, testName + " (flip)");
+ });
+ defCropDialogInit.then(function () {
+ $('.o_crop_image_dialog .o_crop_options .btn:contains("16:9")').mousedown().click();
+ $('.o_crop_image_dialog .o_crop_options .btn:has(.fa-rotate-left)').mousedown().click();
+ $('.o_crop_image_dialog .o_crop_options .btn:has(.fa-arrows-h)').mousedown().click();
+ $('.modal-dialog .modal-footer .btn.btn-primary:contains("Save")').mousedown().click();
+ });
+ },
+ },
+ },
+ {
+ name: "Click CROP FREE in popover after adding image in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _uploadAndInsertImg('https://www.odoo.com/logo.png');
+ },
+ test: {
+ check: function () {
+ var cropFactor = 10;
+ $('.note-image-popover .note-btn:has(.fa-crop)').mousedown().click();
+ defCropDialogSave.then(function () {
+ var $img = $(wysiwyg.getValue()).find('img.o_cropped_img_to_save');
+ assert.strictEqual(Math.round($img.data('width')), Math.round(imgWidth - (imgWidth / cropFactor)), testName + " (rotate)");
+ });
+ defCropDialogInit.then(function () {
+ var $cropperPoints = $('.modal-dialog .cropper-crop-box .cropper-point');
+ var $pointW = $cropperPoints.filter('.point-w');
+ var pos1 = $pointW.offset();
+ var cropperWidth = $cropperPoints.filter('.point-e').offset().left - pos1.left;
+ $pointW.trigger($.Event("pointerdown", {
+ pageX: pos1.left,
+ pageY: pos1.top,
+ }));
+ $pointW.trigger($.Event("pointermove", {
+ pageX: pos1.left + (cropperWidth / cropFactor),
+ pageY: pos1.top,
+ }));
+ $pointW.trigger('pointerup');
+ $('.modal-dialog .modal-footer .btn.btn-primary:contains("Save")').mousedown().click();
+ });
+ },
+ },
+ },
+ ];
+
+ var def = $.when();
+ _.each(mediaTests, function (test) {
+ def = def.then(function () {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ return _clickMedia(wysiwyg, assert, test.do, test.test);
+ });
+ });
+ def.then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+QUnit.test('Pictogram (fontawesome)', function (assert) {
+ var done = assert.async();
+ assert.expect(8);
+
+ return weTestUtils.createWysiwyg(this.data).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var mediaTests = [{
+ name: "Add PICTOGRAM in empty p: p -> span.fa in p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _insertPictogram('fa-glass');
+ },
+ test: {
+ content: '<p>\u200B<span class="fa fa-glass"></span>\u200B</p>',
+ check: function () {
+ assert.strictEqual($('.note-icon-popover').css('display'), 'block', testName + ' (popover)');
+ },
+ },
+ },
+ // Remove font
+ {
+ name: "Click REMOVE in popover after adding font in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _insertPictogram('fa-glass');
+ },
+ test: {
+ content: '<p><br></p>',
+ check: function () {
+ $('.note-image-popover .note-btn .note-icon-trash').mousedown().click();
+ },
+ },
+ },
+ // Icon size
+ {
+ name: "Click ICON SIZE then 5X in popover after adding pictogram in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _insertPictogram('fa-glass');
+ },
+ test: {
+ content: '<p>\u200B<span class="fa fa-glass fa-5x"></span>\u200B</p>',
+ check: function () {
+ $('.note-icon-popover .note-faSize .dropdown-toggle').mousedown().click();
+ $('.note-icon-popover .note-faSize .dropdown-item:contains("5x")').mousedown().click();
+ assert.ok($('.note-icon-popover .note-faSize .dropdown-item:contains("5x")').hasClass('active'), testName + ' (popover)');
+ },
+ },
+ },
+ // Spin
+ {
+ name: "Click SPIN in popover after adding pictogram in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _insertPictogram('fa-glass');
+ },
+ test: {
+ content: '<p>\u200B<span class="fa fa-glass fa-spin"></span>\u200B</p>',
+ check: function () {
+ $('.note-icon-popover .note-faSpin .note-btn').mousedown().click();
+ },
+ },
+ },
+ // Replace pictogram
+ {
+ name: "Add PICTOGRAM in empty p then replace it",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _insertPictogram('fa-glass');
+ },
+ test: {
+ check: function () {
+ _clickMedia(wysiwyg, assert, function () {
+ _insertPictogram('fa-heart');
+ }, {
+ content: '<p>\u200B<span class="fa fa-heart"></span>\u200B</p>',
+ });
+ },
+ },
+ },
+ {
+ name: "Replace PICTOGRAM (i.fa => span.fa)",
+ async: true,
+ content: '<div class="row">\n' +
+ ' <div class="col-lg-12">\n' +
+ ' <i class="fa fa-heart rounded-circle"></i>\n' +
+ ' <div>Other content</div>\n' +
+ ' </div>\n' +
+ '</div>',
+ start: "i->0",
+ do: function () {
+ _insertPictogram('fa-glass');
+ },
+ test: {
+ content: '<div class="row">\n' +
+ ' <div class="col-lg-12">\n' +
+ ' <span class="rounded-circle fa fa-glass"></span>\n' +
+ ' <div>Other content</div>\n' +
+ ' </div>\n' +
+ '</div>',
+ },
+ },
+ ];
+
+ var def = $.when();
+ _.each(mediaTests, function (test) {
+ def = def.then(function () {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ $(range.sc).mousedown().click();
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ return _clickMedia(wysiwyg, assert, test.do, test.test);
+ });
+ });
+ def.then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+QUnit.test('Video', function (assert) {
+ var done = assert.async();
+ assert.expect(10);
+
+ return weTestUtils.createWysiwyg(this.data).then(function (wysiwyg) {
+ var $editable = wysiwyg.$('.note-editable');
+
+ var mediaTests = [{
+ name: "Add VIDEO (youtube) in empty p: p -> div.media_iframe_video after p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _insertVideo(wysiwyg, assert, 'https://www.youtube.com/watch?v=xxxxxxxxxxx', true);
+ },
+ test: {
+ content: '<p><br></p><div class="media_iframe_video" data-oe-expression="about:blank"><div class="css_editable_mode_display">&nbsp;</div><div class="media_iframe_video_size">&nbsp;</div><iframe src="about:blank" frameborder="0"></iframe></div><p><br></p>',
+ check: function () {
+ assert.strictEqual($('.note-video-popover').css('display'), 'block', testName + ' (popover)');
+ },
+ },
+ },
+ {
+ name: "Add VIDEO (youtube) in p in breakable in unbreakable in breakable: p -> div.media_iframe_video after p",
+ async: true,
+ content: '<breakable><unbreakable><breakable><p>tata yoyo</p></breakable></unbreakable></breakable>',
+ start: "p:contents()[0]->4",
+ do: function () {
+ _insertVideo(wysiwyg, assert, 'https://www.youtube.com/watch?v=xxxxxxxxxxx');
+ },
+ test: {
+ content: '<breakable><unbreakable><breakable><p>tata</p></breakable>' +
+ '<div class="media_iframe_video" data-oe-expression="about:blank"><div class="css_editable_mode_display">&nbsp;</div><div class="media_iframe_video_size">&nbsp;</div><iframe src="about:blank" frameborder="0"></iframe></div>' +
+ '<breakable><p> yoyo</p></breakable></unbreakable></breakable>',
+ check: function () {
+ assert.strictEqual($('.note-video-popover').css('display'), 'block', testName + ' (popover)');
+ },
+ },
+ },
+ // Remove video
+ {
+ name: "Click REMOVE in popover after adding video in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _insertVideo(wysiwyg, assert, 'https://www.youtube.com/watch?v=xxxxxxxxxxx');
+ },
+ test: {
+ content: '<p><br></p>',
+ check: function () {
+ $('.note-image-popover .note-btn .note-icon-trash').mousedown().click();
+ },
+ },
+ },
+ /* VIDEO POPOVER */
+ // Multiple clicks
+ {
+ name: "Click FLOAT CENTER then FLOAT LEFT in popover after adding youtube video in empty p",
+ async: true,
+ content: '<p><br></p>',
+ start: "p->0",
+ do: function () {
+ _insertVideo(wysiwyg, assert, 'https://www.youtube.com/watch?v=xxxxxxxxxxx');
+ },
+ test: {
+ content: '<p><br></p><div class="media_iframe_video pull-left" data-oe-expression="about:blank"><div class="css_editable_mode_display">&nbsp;</div><div class="media_iframe_video_size">&nbsp;</div><iframe src="about:blank" frameborder="0"></iframe></div><p><br></p>',
+ check: function () {
+ $('.note-image-popover .note-float .note-icon-align-center').mousedown().click();
+ $('.note-image-popover .note-float .note-icon-align-left').mousedown().click();
+ },
+ },
+ },
+ // Replace picture
+ {
+ name: "replace picture with video",
+ async: true,
+ content: '<p><img src="https://www.odoo.com/logo.png"></p>',
+ start: "img->0",
+ do: function () {
+ _insertVideo(wysiwyg, assert, 'https://www.youtube.com/watch?v=xxxxxxxxxxx');
+ },
+ test: {
+ check: function () {
+ assert.deepEqual(wysiwyg.getValue(),
+ '<p><br></p><div class="media_iframe_video" data-oe-expression="about:blank"><div class="css_editable_mode_display">&nbsp;</div><div class="media_iframe_video_size">&nbsp;</div><iframe src="about:blank" frameborder="0"></iframe></div><p><br></p>',
+ testName);
+ },
+ },
+ },
+ // Replace video
+ {
+ name: "replace video by pictogram",
+ async: true,
+ content: '<p><br></p><div class="media_iframe_video" data-oe-expression="about:blank"><div class="css_editable_mode_display">&nbsp;</div><div class="media_iframe_video_size">&nbsp;</div><iframe src="about:blank" frameborder="0"></iframe></div><p><br></p>',
+ start: "div->0",
+ do: function () {
+ _insertPictogram('fa-glass');
+ },
+ test: {
+ check: function () {
+ assert.deepEqual(wysiwyg.getValue(),
+ '<p>\u200B<span class="fa fa-glass"></span>\u200B</p>',
+ testName);
+ },
+ },
+ },
+ {
+ name: "replace video by pictogram (2)",
+ async: true,
+ content: '<p>aaa</p><div class="media_iframe_video" data-oe-expression="about:blank"><div class="css_editable_mode_display">&nbsp;</div><div class="media_iframe_video_size">&nbsp;</div><iframe src="about:blank" frameborder="0"></iframe></div><p>bbb</p>',
+ start: "div->0",
+ do: function () {
+ _insertPictogram('fa-glass');
+ },
+ test: {
+ check: function () {
+ assert.deepEqual(wysiwyg.getValue(),
+ '<p>aaa<span class="fa fa-glass"></span>bbb</p>',
+ testName);
+ },
+ },
+ },
+ ];
+
+ var def = $.when();
+ _.each(mediaTests, function (test) {
+ def = def.then(function () {
+ testName = test.name;
+ wysiwyg.setValue(test.content);
+ var range = weTestUtils.select(test.start, test.end, $editable);
+ $(range.sc).mousedown().click();
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ return _clickMedia(wysiwyg, assert, test.do, test.test);
+ });
+ });
+ def.then(function () {
+ wysiwyg.destroy();
+ done();
+ });
+ });
+});
+
+
+});
+});
+});
+});