diff --git a/addons/mass_mailing/static/src/js/mass_mailing_widget.js b/addons/mass_mailing/static/src/js/mass_mailing_widget.js index 1e2bf350fec..e633ad2b4a1 100644 --- a/addons/mass_mailing/static/src/js/mass_mailing_widget.js +++ b/addons/mass_mailing/static/src/js/mass_mailing_widget.js @@ -56,8 +56,8 @@ var MassMailingFieldHtml = FieldHtml.extend({ convertInline.classToStyle($editable); self.trigger_up('field_changed', { - dataPointID: this.dataPointID, - changes: _.object([fieldName], [this._unWrap($editable.html())]) + dataPointID: self.dataPointID, + changes: _.object([fieldName], [self._unWrap($editable.html())]) }); if (self._isDirty && self.mode === 'edit') { diff --git a/addons/mass_mailing/static/tests/mass_mailing_html_tests.js b/addons/mass_mailing/static/tests/mass_mailing_html_tests.js new file mode 100644 index 00000000000..36bea2db624 --- /dev/null +++ b/addons/mass_mailing/static/tests/mass_mailing_html_tests.js @@ -0,0 +1,138 @@ +odoo.define('mass_mailing.field_html_tests', function (require) { +"use strict"; + +var ajax = require('web.ajax'); +var FormView = require('web.FormView'); +var testUtils = require('web.test_utils'); +var weTestUtils = require('web_editor.test_utils'); +var Wysiwyg = require('web_editor.wysiwyg'); + + +QUnit.module('mass_mailing', {}, function () { +QUnit.module('field html', { + beforeEach: function () { + this.data = weTestUtils.wysiwygData({ + 'mail.mass_mailing': { + fields: { + display_name: { + string: "Displayed name", + type: "char" + }, + body_html: { + string: "Message Body inline (to send)", + type: "html" + }, + body_arch: { + string: "Message Body for edition", + type: "html" + }, + }, + records: [{ + id: 1, + display_name: "first record", + body_html: "
code to edit
code to edit
code to edit
code to edit
toto toto toto
tata
", }], - onchanges: {}, }, + '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: "toto toto toto
tata
', + "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(), + 'toto toto toto
tata
', + "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: '', + 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(), + 'toto toto toto
tata
', + "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(), + 'toto toto toto
tata
', + "should have rendered the field correctly in edit"); + + form.destroy(); + done(); + }); }); -QUnit.test('field html widget', function (assert) { +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: '', + 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(), + 't
oto toto toto
tata
', + "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: '', + 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(), + 'toto toto toto
tata
', + "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: '', + res_id: 1, + mockRPC: function (route, args) { + if (args.method === "write") { + assert.strictEqual(args.args[1].body, + 'toto toto toto
tata
', + "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 = 'toto ' +
+ '' +
+ 'toto toto
tata
'; + + testUtils.createAsyncView({ + View: FormView, + model: 'note.note', + data: this.data, + arch: '', + res_id: 1, + mockRPC: function (route, args) { + if (args.method === "write") { + assert.strictEqual(args.args[1].body, + '' +
+ 'toto ' +
+ '' +
+ 'toto toto' +
+ '
' + + 'tata' + + '
', + "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(), + '' +
+ 'toto ' +
+ '' +
+ 'toto toto' +
+ '
tata
', + "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(), + '' + + 'toto ' + + '' + + 'toto toto' + + '
' + + 'tata
', + "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); - var form = testUtils.createView({ + testUtils.createAsyncView({ View: FormView, - model: 'mass.mailing', + model: 'note.note', data: this.data, - arch: '', 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(), + 'toto toto toto
tata
', + "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(), + 'toto toto toto
tata
', + "should have rendered the field correctly in edit"); + + form.destroy(); + done(); + }); }); - - 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(), 'toto toto toto
tata
', + "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(), + 'toto toto toto
tata
', + "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(); + }); }); - - 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(), 'toto toto toto
tata
', + "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: '', + 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: '', + 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, ' '), + 'toto toto toto
tata
', + "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: '', + 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: '', + 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, ' '), + 'toto toto toto
tata
', + "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: '', + 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, ' '), + 'toto toto toto
tata
', + "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, ' '), + 'toto toto toto
tata
', + "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['mass.mailing'].fields.body.translate = true; + this.data['note.note'].fields.body.translate = true; - var form = testUtils.createView({ + testUtils.createAsyncView({ View: FormView, - model: 'mass.mailing', + model: 'note.note', data: this.data, arch: '', 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"); + 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"); - assert.containsNone(form, '.oe_form_field_html_text .o_field_translate', - "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(); - 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: '', - 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: '', - 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: '', - 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: '', - 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: '', - 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(); + _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: '', + 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 'Content
' + + 'dom to edit
", + steps: [{ + start: "p:contents()[0]->1", + end: "p:contents()[0]->1", + key: 'ENTER', + }], + test: { + content: 'd
om to edit
dom to edit
", + steps: [{ + start: "p:contents()[0]->3", + keyCode: 66, + }], + test: { + content: "domB to edit
", + start: "p:contents()[0]->4", + end: null, + check: function ($editable, assert) { + assert.ok(true); + }, + }, + }, + { + name: "visible char in a link tag entirely selected", + content: '', + steps: [{ + start: "a:contents()[0]->0", + end: "a:contents()[0]->11", + key: 'a', + }], + test: { + content: '', + start: "a:contents()[0]->2", + }, + }, + { + name: "'a' on a selection of all the contents of a complex dom", + content: "dom
to
completelyremove
a
", + start: "b:contents()[0]->1", + }, + }, + { + name: "'a' on a selection of all the contents of a complex dom (2)", + content: "xxx
yyy
xxx a
yyy
xxx
yyy
xxx a
yyy
xxx |
| yyy |
xxx | a | yyy |
xxx |
| yyy |
xxx | a | yyy |
xxx |
| yyy |
xxx | a | yyy |
xxx |
| yyy |
xxx | a | yyy |
| \n' +
+ ' xxx \n' + + ' | \n' +
+ ' \n' +
+ '
| \n' +
+ ' \n' +
+ ' yyy \n' + + ' | \n' +
+ '
| \n' +
+ ' xxx \n' + + ' | \n' +
+ ' \n' +
+ ' a | \n' +
+ ' \n' +
+ ' yyy \n' + + ' | \n' +
+ '
| \n' +
+ ' xxx \n' + + ' | \n' +
+ ' \n' +
+ '
| \n' +
+ ' \n' +
+ ' yyy \n' + + ' | \n' +
+ '
| \n' +
+ ' xxx \n' + + ' | \n' +
+ ' \n' +
+ ' a | \n' +
+ ' \n' +
+ ' yyy \n' + + ' | \n' +
+ '
| \n' +
+ ' xxx \n' + + ' | \n' +
+ ' \n' +
+ '
| \n' +
+ ' \n' +
+ ' yyy \n' + + ' | \n' +
+ '
| \n' +
+ ' xxx \n' + + ' | \n' +
+ ' \n' +
+ ' a | \n' +
+ ' \n' +
+ ' yyy \n' + + ' | \n' +
+ '
bbb
', + steps: [{ + start: "span->0", + end: 'p:contents(1)->3', + key: 'a', + }], + test: { + content: 'a
', + start: "p:contents()[0]->1", + }, + }, + { + name: "' ' at start of p", + content: 'dom to edit
', + steps: [{ + start: "p:contents()[0]->0", + key: ' ', + }], + test: { + content: 'dom to edit
', + start: "p:contents()[0]->1", + }, + }, + { + name: "' ' at end of p", + content: 'dom to edit
', + steps: [{ + start: "p:contents()[0]->11", + key: ' ', + }], + test: { + content: 'dom to edit
', + start: "p:contents()[0]->12", + }, + }, + { + name: "' ' within p", + content: 'dom to edit
', + steps: [{ + start: "p:contents()[0]->2", + key: ' ', + }], + test: { + content: 'do m to edit
', + start: "p:contents()[0]->3", + }, + }, + { + name: "' ' before space within p", + content: 'dom to edit
', + steps: [{ + start: "p:contents()[0]->3", + key: ' ', + }], + test: { + content: 'dom to edit
', + start: "p:contents()[0]->4", + }, + }, + { + name: "' ' after space within p", + content: 'dom to edit
', + steps: [{ + start: "p:contents()[0]->4", + key: ' ', + }], + test: { + content: 'dom to edit
', + start: "p:contents()[0]->5", + }, + }, + { + name: "3x ' ' at start of p", + content: 'dom to edit
', + steps: [{ + start: "p:contents()[0]->0", + key: ' ', + }, { + key: ' ', + }, { + key: ' ', + }], + test: { + content: 'dom to edit
', + start: "p:contents()[0]->3", + }, + }, + { + name: "3x ' ' at end of p", + content: 'dom to edit
', + steps: [{ + start: "p:contents()[0]->11", + key: ' ', + }, { + key: ' ', + }, { + key: ' ', + }], + test: { + content: 'dom to edit
', + start: "p:contents()[0]->14", + }, + }, + { + name: "3x ' ' within p", + content: 'dom to edit
', + steps: [{ + start: "p:contents()[0]->2", + key: ' ', + }, { + key: ' ', + }, { + key: ' ', + }], + test: { + content: 'do m to edit
', + start: "p:contents()[0]->5", + }, + }, + { + name: "3x ' ' before space in p", + content: 'dom to edit
', + steps: [{ + start: "p:contents()[0]->3", + key: ' ', + }, { + key: ' ', + }, { + key: ' ', + }], + test: { + content: 'dom to edit
', + start: "p:contents()[0]->6", + }, + }, + { + name: "3x ' ' after space in p", + content: 'dom to edit
', + steps: [{ + start: "p:contents()[0]->4", + key: ' ', + }, { + key: ' ', + }, { + key: ' ', + }], + test: { + content: 'dom to edit
', + start: "p:contents()[0]->7", + }, + }, + { + name: "'a' in unbreakable with font", + content: 'dom to edit
", + steps: [{ + start: "p:contents()[0]->1", + key: 'ENTER', + }], + test: { + content: "d
om to edit
", + start: "p:contents()[1]->0", + }, + }, + { + name: "in p: 2x ENTER", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->1", + key: 'ENTER', + }, { + key: 'ENTER', + }], + test: { + content: "d
om to edit
", + start: "p:contents()[2]->0", + }, + }, + { + name: "in p: SHIFT+ENTER", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->1", + key: 'ENTER', + shiftKey: true, + }], + test: { + content: "d
om to edit
dom to edit
", + steps: [{ + start: "p:contents()[0]->1", + key: 'ENTER', + shiftKey: true, + }, { + key: 'ENTER', + shiftKey: true, + }], + test: { + content: "d
om to edit
dom to edit
", + steps: [{ + start: "p:contents()[0]->1", + key: 'ENTER', + }, { + key: 'ENTER', + shiftKey: true, + }], + test: { + content: "d
om to edit
dom to edit
", + steps: [{ + start: "p:contents()[0]->1", + end: "p:contents()[0]->7", + key: 'ENTER', + }], + test: { + content: "d
edit
", + start: "p:contents()[1]->0", + end: "p:contents()[1]->0", + }, + }, + { + name: "in p: SHIFT+ENTER on selection", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->1", + end: "p:contents()[0]->7", + key: 'ENTER', + shiftKey: true, + }], + test: { + content: "d
edit
a
", + start: "p:contents()[0]->1", + }, + }, + { + name: "across 2 li: ENTER on partial selection", + content: "dom to edit
dom to edit
dom to edit
dom to edit
dom to edit
dom to edit
aaa
dom to edit
bbb
aaa
dom to edit
bbb
aaa
dom to edit
bbb
aaa
dom to edit
bbb
dom to edit
", + steps: [{ + start: "p:contents()[1]->0", + key: 'ENTER', + shiftKey: true, + }], + test: { + content: "dom
to edit
dom to edit
", + steps: [{ + start: "b:contents()[0]->2", + key: 'ENTER', + }, { + key: 'a', + }], + test: { + content: "do
am to edit
", + start: "b:eq(1):contents()[0]->1", + }, + }, + { + name: "after p > b: ENTER -> 'a'", + content: "dom to edit
", + steps: [{ + start: "p:contents()[1]->0", + key: 'ENTER', + }, { + key: 'a', + }], + test: { + content: "dom
a to edit
", + start: "p:eq(1):contents()[0]->1", + }, + }, + { + name: "after p > b: SHIFT+ENTER -> 'a'", + content: "dom to edit
", + steps: [{ + start: "p:contents()[1]->0", + key: 'ENTER', + shiftKey: true, + }, { + key: 'a', + }], + test: { + content: "dom
a to edit
dom to
edit
", + steps: [{ + start: "p:eq(1):contents()[0]->0", + key: 'ENTER', + }], + test: { + content: "dom to
edit
", + start: "span:eq(2):contents()[0]->0", + }, + }, + { + name: "in p (other-p > span.a before - p > span.b after): ENTER -> 'a' at beginning", + content: "dom to
edit
", + steps: [{ + start: "p:eq(1):contents()[0]->0", + key: 'ENTER', + }, { + key: 'a', + }], + test: { + content: "dom to
aedit
", + start: "span:eq(2):contents()[0]->1", + }, + }, + { + name: "in p (other-p > span.a before - p > span.b after): SHIFT+ENTER at beginning", + content: "dom to
edit
", + steps: [{ + start: "p:eq(1):contents()[0]->0", + key: 'ENTER', + shiftKey: true, + }], + test: { + content: "dom to
edit
dom to
edit
", + steps: [{ + start: "p:eq(1):contents()[0]->0", + key: 'ENTER', + shiftKey: true, + }, { + key: 'a', + }], + test: { + content: "dom to
aedit
dom to edit
", + steps: [{ + start: "p:contents()[0]->0", + end: "p:contents()[0]->11", + key: 'ENTER', + }], + test: { + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->0", + end: "p:contents()[0]->11", + key: 'ENTER', + }, { + key: 'a', + }], + test: { + content: "a
", + start: "p:eq(1):contents()[0]->1", + }, + }, + { + name: "in p: SHIFT+ENTER on selection of all contents", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->0", + end: "p:contents()[0]->11", + key: 'ENTER', + shiftKey: true, + }], + test: { + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->0", + end: "p:contents()[0]->11", + key: 'ENTER', + shiftKey: true, + }, { + key: 'a', + }], + test: { + content: "
a
dom to edit
", + steps: [{ + start: "p:contents()[0]->3", + key: 'ENTER', + }, { + key: 'ENTER', + }, { + key: 'a', + }], + test: { + content: "dom
a to edit
", + start: "p:eq(2):contents()[0]->1", + }, + }, + { + name: "in p > b: ENTER at start", + content: "dom to edit
", + steps: [{ + start: "b:contents()[0]->0", + key: 'ENTER', + }], + test: { + content: "dom to edit
", + start: "b:eq(1):contents()[0]->0", + }, + }, + { + name: "in p > b: ENTER", + content: "dom to edit
", + steps: [{ + start: "b:contents()[0]->3", + key: 'ENTER', + }], + test: { + content: "dom
to edit
", + start: "b:eq(1):contents()[0]->0", + }, + }, + { + name: "in p > b: SHIFT+ENTER -> ENTER", + content: "dom to edit
", + steps: [{ + start: "b:contents()[0]->3", + key: 'ENTER', + shiftKey: true, + }, { + key: 'ENTER', + }], + test: { + content: "dom
to edit
", + start: "b:eq(1):contents()[0]->0", + }, + }, + { + name: "in p > b: ENTER -> a'", + content: "dom to edit
", + steps: [{ + start: "b:contents()[0]->3", + key: 'ENTER', + }, { + key: 'a', + }], + test: { + content: "dom
a to edit
", + start: "b:eq(1):contents()[0]->1", + }, + }, + { + name: "in p > b: SHIFT+ENTER", + content: "dom to edit
", + steps: [{ + start: "b:contents()[0]->3", + key: 'ENTER', + shiftKey: true, + }], + test: { + content: "dom
to edit
dom to edit
", + steps: [{ + start: "b:contents()[0]->3", + key: 'ENTER', + shiftKey: true, + }, { + key: 'a', + }], + test: { + content: "dom
a to edit
dom to edit
", + steps: [{ + start: "b:contents()[0]->3", + key: 'ENTER', + shiftKey: true, + }, { + key: 'ENTER', + }, { + key: 'a', + }], + test: { + content: "dom
a to edit
", + start: "b:eq(1):contents()[0]->1", + }, + }, + { + name: "in span > b: ENTER", + content: "dom to edit", + steps: [{ + start: "b:contents()[0]->3", + key: 'ENTER', + }], + test: { + content: "domdom to edit
", + steps: [{ + start: "p:contents()[0]->3", + key: 'ENTER', + shiftKey: true, + }, { + key: 'ENTER', + shiftKey: true, + }, { + key: 'a', + }], + test: { + content: "dom
a to edit
dom to edit
", + steps: [{ + start: "p:contents()[0]->3", + key: 'ENTER', + }, { + key: 'ENTER', + shiftKey: true, + }, { + key: 'a', + }], + test: { + content: "dom
a to edit
dom
to edit
", + steps: [{ + start: "p:eq(1):contents()[0]->0", + key: 'ENTER', + }, { + key: 'a', + }], + test: { + content: "dom
a
to edit
", + start: "p:eq(2):contents()[0]->1", + }, + }, + { + name: "in p: SHIFT+ENTER at end", + content: "dom
to edit
", + steps: [{ + start: "p:first:contents()[0]->4", + key: 'ENTER', + shiftKey: true, + }], + test: { + content: "dom
to edit
", + start: "p:first:contents()[2]->0", + }, + }, + { + name: "in p: SHIFT+ENTER at end -> '寺'", + content: "dom
to edit
", + steps: [{ + start: "p:first:contents()[0]->4", + key: 'ENTER', + shiftKey: true, + }, { + keyCode: 23546, /*temple in chinese*/ + }], + test: { + content: "dom
寺
to edit
", + start: "p:first:contents()[2]->1", + }, + }, + { + name: "in empty-p (div > a after): 3x SHIFT+ENTER -> 'a'", + content: "
a
dom to edit
", + steps: [{ + start: "b:contents()[0]->3", + key: 'ENTER', + shiftKey: true, + }, { + key: 'ENTER', + shiftKey: true, + }, { + key: 'BACKSPACE', + }], + test: { + content: "dom
to edit
dom to edit
", + steps: [{ + start: "b:contents()[0]->3", + key: 'ENTER', + shiftKey: false + }, { + key: 'ENTER', + shiftKey: false + }, { + key: 'BACKSPACE', + }, { + key: 'BACKSPACE', + }], + test: { + content: "dom to edit
", + start: "b:contents()[0]->3", + }, + }, + { + name: "in empty-p: 2x ENTER -> 2x BACKSPACE", + content: "dom not to edit
dom not to edit
", + start: "p:first:contents()[0]->15", + }, + }, + { + name: "in p > b: 2x SHIFT+ENTER -> BACKSPACE -> 'a'", + content: "dom to edit
", + steps: [{ + start: "b:contents()[0]->3", + key: 'ENTER', + shiftKey: true, + }, { + key: 'ENTER', + shiftKey: true, + }, { + key: 'BACKSPACE', + }, { + key: 'a', + }], + test: { + content: "dom
a to edit
dom
to edit
dom
a
to edit
dom
to edit
dom
a to edit
dom not to edit
dom to edit
dom not to edit
", + 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: "dom not to edit
dom to edit
dom not to edit
", + start: "p:eq(1):contents()[0]->5", + }, + }, + { + name: "in h1.a > font: 'a' on selection of all contents", + content: "pif
paf
pouf
pif
', + start: "p:contents()[0]->2", + }, + }, + { + name: "in complex-dom: DELETE on partial selection (requires merging non-similar blocks)", + content: "pif
paf
pouf
pif
', + start: "p:contents()[0]->2", + }, + }, + + // paragraph outdent + + { + name: "in p (h1 before): TAB -> BACKSPACE at start (must indent then outdent)", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->0", + key: 'TAB', + }, { + key: 'BACKSPACE', + }], + test: { + content: "dom to edit
", + start: "p:contents()[0]->0", + }, + }, + { + name: "in p (h1 before): TAB -> 2x BACKSPACE at start (must indent then outdent, then merge blocks)", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->0", + key: 'TAB', + }, { + key: 'BACKSPACE', + }, { + key: 'BACKSPACE', + }], + test: { + content: "dom to edit | node |
dom to edit | node |
dom to edit
", + steps: [{ + start: "p:contents()[0]->0", + key: 'TAB', + }], + test: { + content: "dom to edit
", + start: "p:contents()[0]->0", + }, + }, + { + name: "in indented-p: SHIFT+TAB at start", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->0", + key: 'TAB', + shiftKey: true, + }], + test: { + content: "dom to edit
", + start: "p:contents()[0]->0", + }, + }, + { + name: "in p: TAB -> BACKSPACE at start", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->0", + key: 'TAB', + }, { + key: 'BACKSPACE', + }], + test: { + content: "dom to edit
", + start: "p:contents()[0]->0", + }, + }, + { + name: "in p > b: TAB at start", + content: "dom to edit
", + steps: [{ + start: "b:contents()[0]->0", + key: 'TAB', + }], + test: { + content: "dom to edit
", + start: "b:contents()[0]->4", + }, + }, + { + name: "in p > b: TAB -> BACKSPACE at start", + content: "dom to edit
", + steps: [{ + start: "b:contents()[0]->0", + key: 'TAB', + }, { + key: 'BACKSPACE', + }], + test: { + content: "dom to edit
", + 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: 'aaabbb
', + steps: [{ + start: "p:contents()[2]->0", + key: 'BACKSPACE', + }], + test: { + content: 'aaabbb
', + start: "p:contents()[0]->3", + }, + }, + { + name: "in empty-p (no br): BACKSPACE (must insert br)", + content: "", + steps: [{ + start: "p->0", + key: 'BACKSPACE', + }], + test: { + content: "dom to edit
", + steps: [{ + start: "p:eq(1):contents()[0]->0", + key: 'BACKSPACE', + }], + test: { + content: "dom to edit
", + start: "p:contents()[0]->0", + }, + }, + { + name: "in p (empty-p.a before): BACKSPACE", + content: "dom to edit
", + steps: [{ + start: "p:eq(1):contents()[0]->0", + key: 'BACKSPACE', + }], + test: { + content: "dom to edit
", + start: "p:contents()[0]->0", + }, + }, + { + name: "in p: BACKSPACE within text", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->5", + key: 'BACKSPACE', + }], + test: { + content: "dom o edit
", // 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: "dom to edit
", + steps: [{ + start: "p:contents()[0]->5", + key: 'BACKSPACE', + }, { + key: 'BACKSPACE', + }], + test: { + content: "domo edit
", // 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: "dom to
edit
", + steps: [{ + start: "p:eq(1):contents()[0]->0", + key: 'BACKSPACE', + }], + test: { + content: "dom toedit
", + start: "span:eq(0):contents()[0]->6", + }, + }, + { + name: "in p (p > span.a before - span.a after): BACKSPACE (must merge them)", + content: "dom to
edit
", + steps: [{ + start: "p:eq(1):contents()[0]->0", + key: 'BACKSPACE', + }], + test: { + content: "dom to edit
", + start: "span:contents()[0]->7", + }, + }, + { + name: "in p (b before): BACKSPACE", + content: "dom to edit
", + steps: [{ + start: "p:contents()[1]->0", + key: 'BACKSPACE', + }], + test: { + content: "do to edit
", + start: "b:contents()[0]->2", + }, + }, + { + name: "in p (div > span.a before - span.a after): BACKSPACE at beginning (must do nothing)", + content: "edit
", + steps: [{ + start: "p:contents()[0]->0", + key: 'BACKSPACE', + }], + test: { + content: "edit
", + start: "span:eq(1):contents()[0]->0", + }, + }, + { + name: "in p: BACKSPACE on partial selection", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->1", + end: "p:contents()[0]->7", + key: 'BACKSPACE', + }], + test: { + content: "dedit
", + start: "p:contents()[0]->1", + }, + }, + { + name: "across 2 p's: BACKSPACE on partial selection", + content: "dom
to edit
", + steps: [{ + start: "p:contents()[0]->1", + end: "p:eq(1):contents()[0]->3", + key: 'BACKSPACE', + }], + test: { + content: "dedit
", + start: "p:contents()[0]->1", + }, + }, + { + name: "in p: BACKSPACE within text, with space at beginning", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->10", + key: 'BACKSPACE', + }], + test: { + content: "dom o edit
", + start: "p:contents()[0]->4", + }, + }, + { + name: "in p: BACKSPACE within text, with one space at beginning", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->6", + key: 'BACKSPACE', + }], + test: { + content: "dom o edit
", + start: "p:contents()[0]->5", + }, + }, + { + name: "in p: BACKSPACE within text, with space at end", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->5", + key: 'BACKSPACE', + }], + test: { + content: "dom o edit
", + start: "p:contents()[0]->4", + }, + }, + { + name: "in p: BACKSPACE within text, with one space at end", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->5", + key: 'BACKSPACE', + }], + test: { + content: "dom o edit
", + start: "p:contents()[0]->4", + }, + }, + { + name: "in p: BACKSPACE within text, with space at beginning and end", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->10", + key: 'BACKSPACE', + }], + test: { + content: "dom o edit
", + start: "p:contents()[0]->4", + }, + }, + { + name: "in p: BACKSPACE within text, with one space at beginning and one at end", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->6", + key: 'BACKSPACE', + }], + test: { + content: "dom o edit
", + start: "p:contents()[0]->5", + }, + }, + { + name: "in p: BACKSPACE after \\wdom to edi
t
dom to edi
dom t
", + steps: [{ + start: "p:contents()[0]->5", + key: 'BACKSPACE', + }, { + key: 'a', + }], + test: { + content: "dom a
", + start: "p:contents()[0]->5", + }, + }, + { + name: "in pre: BACKSPACE within text, with space at beginning", + content: "dom to edit", + steps: [{ + start: "pre:contents()[0]->10", + key: 'BACKSPACE', + }], + test: { + content: "
dom o edit", + start: "pre:contents()[0]->9", + }, + }, + { + name: "in pre: BACKSPACE within text, with one space at beginning", + content: "
dom to edit", + steps: [{ + start: "pre:contents()[0]->6", + key: 'BACKSPACE', + }], + test: { + content: "
dom o edit", + start: "pre:contents()[0]->5", + }, + }, + { + name: "in pre: BACKSPACE within text, with space at end", + content: "
dom to edit", + steps: [{ + start: "pre:contents()[0]->5", + key: 'BACKSPACE', + }], + test: { + content: "
dom o edit", + start: "pre:contents()[0]->4", + }, + }, + { + name: "in pre: BACKSPACE within text, with one space at end", + content: "
dom to edit", + steps: [{ + start: "pre:contents()[0]->5", + key: 'BACKSPACE', + }], + test: { + content: "
dom o edit", + start: "pre:contents()[0]->4", + }, + }, + { + name: "in pre: BACKSPACE within text, with space at beginning and end", + content: "
dom to edit", + steps: [{ + start: "pre:contents()[0]->10", + key: 'BACKSPACE', + }], + test: { + content: "
dom o edit", + start: "pre:contents()[0]->9", + }, + }, + { + name: "in pre: BACKSPACE within text, with one space at beginning and one at end", + content: "
dom to edit", + steps: [{ + start: "pre:contents()[0]->6", + key: 'BACKSPACE', + }], + test: { + content: "
dom o edit", + start: "pre:contents()[0]->5", + }, + }, + + // list UL / OL + + { + name: "from p to ul > li: BACKSPACE on whole list", + content: "
dom not to edit
dom to edit
dom not to edit
", + start: "p:contents()[0]->15", + }, + }, + { + name: "in ul > second-li > p: BACKSPACE within text", + content: "dom to
edit
dom to
edi
dom to
dom to
dom to
dom to
a
a
", + start: "p:eq(1):contents()[0]->1", + }, + }, + { + name: "in empty-li (no other li - p before): BACKSPACE", + content: "toto
toto
toto

toto

toto
tutu
toto
tutu
toto
tutu
toto
tutu
toto
xxx
yyy
tutu
toto
xxx
yyy
tutu
toto
xxx
yyy
tutu
toto
xxxyyy
tutu
dom to edit
dom to edit
dom not to edit
dom to edit dom to edit
dom not to edit
a
a
a
dom to edit
", + steps: [{ + start: "p:contents()[0]->0", + end: "p:contents()[0]->11", + key: 'BACKSPACE', + }], + test: { + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->0", + end: "p:contents()[0]->11", + key: 'BACKSPACE', + }, { + key: 'a', + }], + test: { + content: "a
", + start: "p:contents()[0]->1", + }, + }, + { + name: "in complex-dom: BACKSPACE on selection of most contents", + content: "dom
to
partiallyremove
doove
", + start: "b:contents()[0]->2", + }, + }, + { + name: "in complex-dom: BACKSPACE on selection of all contents", + content: "dom
to
completelyremove
dom
to edit
dom to edit
", + start: "p:contents()[0]->4", + }, + }, + { + name: "in complex-dom (span > b -> ENTER in contents)", + content: "domaaa
bbb
', + steps: [{ + start: "p:eq(1):contents()[0]->0", + key: 'BACKSPACE', + }], + test: { + content: 'aaabbb
', + start: "p:contents()[0]->3", + }, + }, + { + name: "in p with multi br[id] (p before and after) (1)", + content: 'dom not to edit
dom not to edit
', + steps: [{ + start: "p:eq(1)->2", + key: 'BACKSPACE', + }], + test: { + content: 'dom not to edit
dom not to edit
', + start: "p:eq(1)->1", + }, + }, + { + name: "in p with multi br[id] (p before and after) (2)", + content: 'dom not to edit
dom not to edit
', + steps: [{ + start: "br:eq(2)->0", + key: 'BACKSPACE', + }], + test: { + content: 'dom not to edit
dom not to edit
', + start: "p:eq(1)->1", + }, + }, + { + name: "in p with multi br[id] (p before and after): 2x BACKSPACE", + content: 'dom not to edit
dom not to edit
', + steps: [{ + start: "p:eq(1)->2", + key: 'BACKSPACE', + }, { + key: 'BACKSPACE', + }], + test: { + content: 'dom not to edit
dom not to edit
', + start: "p:eq(1)->0", + }, + }, + { + name: "in p with multi br (p before and after) (1)", + content: 'dom not to edit
dom not to edit
', + steps: [{ + start: "p:eq(1)->2", + key: 'BACKSPACE', + }], + test: { + content: 'dom not to edit
dom not to edit
', + start: "p:eq(1)->1", + }, + }, + { + name: "in p with multi br (p before and after) (2)", + content: 'dom not to edit
dom not to edit
', + steps: [{ + start: "br:eq(2)->0", + key: 'BACKSPACE', + }], + test: { + content: 'dom not to edit
dom not to edit
', + start: "p:eq(1)->1", + }, + }, + { + name: "in p with multi br (p before and after): 2x BACKSPACE", + content: 'dom not to edit
dom not to edit
', + steps: [{ + start: "p:eq(1)->2", + key: 'BACKSPACE', + }, { + key: 'BACKSPACE', + }], + test: { + content: 'dom not to edit
dom not to edit
', + start: "p:eq(1)->0", + }, + }, + { + name: "in p: BACKSPACE within text after \w+dom to
edit
dom to
edt
a |
dom not to edit | dom to edit |
dom not to edit | aom to edit |
dom to edit |
dom o edit |
dom not to edit |
dom not to edit |
dom not to edit |
dom not to edit | a |
node to merge
', + steps: [{ + start: 'p:contents()[0]->0', + key: 'BACKSPACE', + }], + test: { + content: 'node to merge with
node to merge
', + steps: [{ + start: 'p:eq(1):contents()[0]->0', + key: 'BACKSPACE', + }], + test: { + content: 'node to merge withnode to merge
node to merge with
node to merge
', + steps: [{ + start: 'b:contents()[0]->0', + key: 'BACKSPACE', + }], + test: { + content: 'node to merge withnode to merge
node to merge with
node to merge
', + steps: [{ + start: 'b:contents()[0]->0', + key: 'BACKSPACE', + }], + test: { + content: 'node to merge withnode to merge
dom to
edit
", + steps: [{ + start: "p:eq(1):contents()[0]->0", + key: 'BACKSPACE', + }], + test: { + content: "dom to edit
", + start: "span:eq(0):contents()[0]->7", + }, + }, + { + name: "from h1 to p: BACKSPACE", + content: 'node to merge
', + steps: [{ + start: 'h1:contents()[0]->5', + end: 'p:contents()[0]->2', + key: 'BACKSPACE', + }], + test: { + content: 'node to merge
', + steps: [{ + start: 'b:contents()[0]->0', + end: 'p:contents()[0]->2', + key: 'BACKSPACE', + }], + test: { + content: 'de to merge
', + 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: "", + steps: [{ + start: "p->0", + key: 'DELETE', + }], + test: { + content: "\n
\n
\n
\n
dom to edit
", + steps: [{ + start: "p->0", + key: 'DELETE', + }], + test: { + content: "dom to edit
", + start: "p:contents()[0]->0", + }, + }, + { + name: "in empty-p (p after): DELETE (2)", + content: "\n
\n
\n dom to edit
", + steps: [{ + start: "p:contents()[1]->0", + key: 'DELETE', + }], + test: { + content: "dom to edit
", + start: "p:contents()[0]->0", + }, + }, + { + name: "in empty-p.a (p after): DELETE", + content: "dom to edit
", + steps: [{ + start: "p->0", + key: 'DELETE', + }], + test: { + content: "dom to edit
", + start: "p:contents()[0]->0", + }, + }, + { + name: "in empty-p.a (p after): DELETE (2)", + content: "dom to edit
", + steps: [{ + start: "p->1", + key: 'DELETE', + }], + test: { + content: "dom to edit
", + start: "p:contents()[0]->0", + }, + }, + { + name: "in p: DELETE at start", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->0", + key: 'DELETE', + }], + test: { + content: "om to edit
", // 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: "dom to edit
", + steps: [{ + start: "p:contents()[0]->0", + key: 'DELETE', + }, { + key: 'DELETE', + }], + test: { + content: "m to edit
", // 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: "dom to edit
", + steps: [{ + start: "p:contents()[0]->5", + key: 'DELETE', + }], + test: { + content: "dom t edit
", + start: "p:contents()[0]->5", + }, + }, + { + name: "in p: DELETE within text, with space at beginning", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->10", + key: 'DELETE', + }], + test: { + content: "dom t edit
", + start: "p:contents()[0]->5", + }, + }, + { + name: "in p: DELETE within text, with one space at beginning", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->6", + key: 'DELETE', + }], + test: { + content: "dom t edit
", + start: "p:contents()[0]->6", + }, + }, + { + name: "in p: DELETE within text, with space at end", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->5", + key: 'DELETE', + }], + test: { + content: "dom t edit
", + start: "p:contents()[0]->5", + }, + }, + { + name: "in p: DELETE within text, with one space at end", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->5", + key: 'DELETE', + }], + test: { + content: "dom t edit
", + start: "p:contents()[0]->5", + }, + }, + { + name: "in p: DELETE within text, with space at beginning and end", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->10", + key: 'DELETE', + }], + test: { + content: "dom t edit
", + start: "p:contents()[0]->5", + }, + }, + { + name: "in p: DELETE within text, with one space at beginning and one at end", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->6", + key: 'DELETE', + }], + test: { + content: "dom t edit
", + start: "p:contents()[0]->6", + }, + }, + { + name: "in p: DELETE -> 'a' within text, before \\w\\s", + content: "dom t
", + steps: [{ + start: "p:contents()[0]->2", + key: 'DELETE', + }, { + key: 'a', + }], + test: { + content: "doa t
", + start: "p:contents()[0]->3", + }, + }, + { + name: "in pre: DELETE within text, with space at beginning", + content: "dom to edit", + steps: [{ + start: "pre:contents()[0]->10", + key: 'DELETE', + }], + test: { + content: "
dom t edit", + start: "pre:contents()[0]->10", + }, + }, + { + name: "in pre: DELETE within text, with one space at beginning", + content: "
dom to edit", + steps: [{ + start: "pre:contents()[0]->6", + key: 'DELETE', + }], + test: { + content: "
dom t edit", + start: "pre:contents()[0]->6", + }, + }, + { + name: "in pre: DELETE within text, with space at end", + content: "
dom to edit", + steps: [{ + start: "pre:contents()[0]->5", + key: 'DELETE', + }], + test: { + content: "
dom t edit", + start: "pre:contents()[0]->5", + }, + }, + { + name: "in pre: DELETE within text, with one space at end", + content: "
dom to edit", + steps: [{ + start: "pre:contents()[0]->5", + key: 'DELETE', + }], + test: { + content: "
dom t edit", + start: "pre:contents()[0]->5", + }, + }, + { + name: "in pre: DELETE within text, with space at beginning and end", + content: "
dom to edit", + steps: [{ + start: "pre:contents()[0]->10", + key: 'DELETE', + }], + test: { + content: "
dom t edit", + start: "pre:contents()[0]->10", + }, + }, + { + name: "in pre: DELETE within text, with one space at beginning and one at end", + content: "
dom to edit", + steps: [{ + start: "pre:contents()[0]->6", + key: 'DELETE', + }], + test: { + content: "
dom t edit", + start: "pre:contents()[0]->6", + }, + }, + + // list UL / OL + + { + name: "in empty-li (no other li): DELETE (must do nothing)", + content: "
dom not to edit
dom to edit
dom not to edit
", + start: "p:contents()[0]->15", + }, + }, + { + name: "in empty-li (no other li): DELETE -> 'a' (must write into it)", + content: "a
a
a
a
toto
xxx
yyy
tutu
toto
xxxyyy
tutu
toto
xxx
yyy
tutu
toto
xxxyyy
tutu
toto
xxx
yyy
tutu
toto
xxxyyy
tutu
a
domto edit
", + steps: [{ + start: "b:contents()[0]->3", + key: 'DELETE', + }], + test: { + content: "domo edit
", + start: "p:contents(1)->0", + }, + }, + { + name: "in p > span.a (div > span.a after): DELETE at end (must do nothing)", + content: "dom to
dom to
dom to edit
", + steps: [{ + start: "p:contents()[0]->1", + end: "p:contents()[0]->7", + key: 'DELETE', + }], + test: { + content: "dedit
", + start: "p:contents()[0]->1", + }, + }, + { + name: "across 2 p: DELETE on partial selection", + content: "dom
to edit
", + steps: [{ + start: "p:contents()[0]->1", + end: "p:eq(1):contents()[0]->3", + key: 'DELETE', + }], + test: { + content: "dedit
", + start: "p:contents()[0]->1", + }, + }, + { + name: "in p: DELETE on selection of all contents", + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->0", + end: "p:contents()[0]->11", + key: 'DELETE', + }], + test: { + content: "dom to edit
", + steps: [{ + start: "p:contents()[0]->0", + end: "p:contents()[0]->11", + key: 'DELETE', + }, { + key: 'a', + }], + test: { + content: "a
", + start: "p:contents()[0]->1", + }, + }, + { + name: "in p: DELETE before br", + content: "dom
to edit
dom to edit
", + start: "p:contents()[0]->4", + }, + }, + { + name: "in complex-dom: DELETE on selection of most contents", + content: "dom
to
partiallyremove
doove
", + start: "b:contents()[0]->2", + }, + }, + { + name: "in complex-dom: DELETE on selection of all contents", + content: "dom
to
completelyremove
aaabbb
', + steps: [{ + start: "p:contents()[0]->3", + key: 'DELETE', + }], + test: { + content: 'aaabbb
', + start: "p:contents()[0]->3", + }, + }, + { + name: "in p (hr after): DELETE", + content: 'aaa
bbb
', + steps: [{ + start: "p:eq(0):contents()[0]->3", + key: 'DELETE', + }], + test: { + content: 'aaabbb
', + start: "p:contents()[0]->3", + }, + }, + + // table + + { + name: "in empty-td (td after): DELETE -> 'a' at start", + content: 'a |
dom to edit | dom not to edit |
da to edit | dom not to edit |
dom to edit |
dom o edit |
dom to edit
dom to edit
dom not to edit
dom to editdom to edit
dom not to edit
node to merge
', + steps: [{ + start: 'h1:contents()[0]->18', + key: 'DELETE', + }], + test: { + content: 'node to merge with
node to merge
', + steps: [{ + start: 'p:contents()[0]->18', + key: 'DELETE', + }], + test: { + content: 'node to merge withnode to merge
node to merge with
node to merge
', + steps: [{ + start: 'p:contents()[1]->5', + key: 'DELETE', + }], + test: { + content: 'node to merge withnode to merge
node to merge with
node to merge
', + steps: [{ + start: 'i:contents()[0]->10', + key: 'DELETE', + }], + test: { + content: 'node to merge withnode to merge
dom to
edit
", + steps: [{ + start: "span:contents()[0]->7", + key: 'DELETE', + }], + test: { + content: "dom to edit
", + start: "span:contents()[0]->7", + }, + }, +]; + +var keyboardTestsDeleteDOM = [ + '',
+ ' Dear Ready Mat',
+ '
',
+ ' Here is',
+ ' the quotation SO003',
+ ' amounting in $ 1,127.50',
+ ' from YourCompany.',
+ '
',
+ ' Do not hesitate to contact us if you have any question.',
+ '
',
+ '
| ',
+ ' Virtual Interior Design On Site ', + ' | ',
+ ' ', + ' 10 Hour(s)', + ' | ', + '
toto toto toto
tata
', + }, + }).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, ' '), + 'toto toto toto
tata
', + "should drop the snippet"); + + testUtils.mock.intercept(wysiwyg, "snippet_focused", function () { + assert.strictEqual($editable.html().replace(/\s+/g, ' '), + 'toto toto toto
tata
', + "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: + 'test
', + }, + }).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: + 'test
', + }, + }).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: 'toto toto toto
tata
', + }, + }).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, ' '), + 'toto toto toto
tata
', + "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: 'test
', + }, + }).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, ' '), + 'test
', + "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: 'test
', + }, + }).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, ' '), + 'test
', + "should duplicate the snippet"); + + wysiwyg.save().then(function (isDirty, html) { + assert.strictEqual(html.replace(/\s+/g, ' '), + 'test
', + "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: 'test
', + }, + }).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, ' '), + 'test
', + "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, ' '), + 'test
', + "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: + 'Content
' + + 'dom not to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + do: function () { + $dropdownStyle.mousedown().click(); + $btnsStyle.find('h1').mousedown().click(); + }, + test: { + content: 'dom not to edit
dom not to edit
dom not to edit
dom to edit', + start: 'pre:contents()[0]->0', + end: 'pre:contents()[0]->11', + }, + }, + { + name: "Click NORMAL: pre -> p", + content: '
dom not to edit
dom to edit', + start: 'pre:contents()[0]->1', + do: function () { + $dropdownStyle.mousedown().click(); + $btnsStyle.find('p').mousedown().click(); + }, + test: { + content: '
dom not to edit
dom to edit
', + 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: 'dom not to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(1):contents()[0]->5', + do: function () { + $btnBold.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
', + start: 'b:contents()[0]->0', + end: 'b:contents()[0]->4', + }, + }, + { + name: "Click BOLD then 'a': normal -> bold (empty p)", + content: 'a
', + start: 'b:contents()[0]->1', + }, + }, + { + name: "Click BOLD: normal -> bold (across paragraphs)", + content: 'dom to edit
dom to edit
', + start: 'p:contents()[0]->1', + end: 'p:eq(1):contents()[0]->5', + do: function () { + $btnBold.mousedown().click(); + }, + test: { + content: 'dom to edit
dom to edit
', + start: 'b:contents()[0]->0', + end: 'b:eq(1):contents()[0]->5', + }, + }, + { + name: "Click BOLD then 'a': normal -> bold (no selection)", + content: 'dom not to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->4', + do: function () { + $btnBold.mousedown().click(); + weTestUtils.keydown('a', $editable); + }, + test: { + content: 'dom not to edit
dom ato edit
', + start: 'b:contents()[0]->1', + }, + }, + { + name: "Click BOLD: bold -> normal", + content: 'dom not to edit
dom to edit
', + start: 'b:contents()[0]->0', + end: 'b:contents()[0]->11', + do: function () { + $btnBold.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->0', + end: 'p:eq(1):contents()[0]->11', + }, + }, + { + name: "Click BOLD: bold -> normal (partial selection)", + content: 'dom not to edit
dom to edit
', + start: 'b:contents()[0]->4', + end: 'b:contents()[0]->6', + do: function () { + $btnBold.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
', + start: 'p:eq(1):contents()[1]->0', + end: 'p:eq(1):contents()[1]->2', + }, + }, + { + name: "Click BOLD: bold -> normal (no selection)", + content: 'dom not to edit
dom to edit
', + start: 'b:contents()[0]->4', + do: function () { + $btnBold.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom \u200Bto edit
', + start: 'p:eq(1):contents()[1]->1', + }, + }, + { + name: "Click BOLD: bold + normal -> normal", + content: 'dom to edit
', + start: 'b:contents()[0]->1', + end: 'p:contents()[1]->4', + do: function () { + $btnBold.mousedown().click(); + }, + test: { + content: 'dom to edit
', + start: 'b:contents()[0]->1', + end: 'b:contents()[0]->8', + }, + }, + { + name: "Click BOLD: normal -> bold (with fontawesome)", + content: 'aaabbb
', + start: 'p:contents()[0]->1', + end: 'p:contents()[2]->2', + do: function () { + $btnBold.mousedown().click(); + }, + test: { + content: 'aaabbb
', + start: 'b:contents()[0]->0', + end: 'b:contents()[2]->2', + }, + }, + { + name: "Click BOLD + click BOLD: normal -> bold -> normal (with fontawesome)", + content: 'aaabbb
', + start: 'p:contents()[0]->1', + end: 'p:contents()[2]->2', + do: function () { + $btnBold.mousedown().click(); + $btnBold.mousedown().click(); + }, + test: { + content: 'aaabbb
', + start: 'p:contents()[0]->1', + end: 'p:contents()[2]->2', + }, + }, + { + name: "Click BOLD: bold -> normal (with fontawesome)", + content: 'aaabbb
', + start: 'b:contents()[0]->1', + end: 'b:contents()[2]->2', + do: function () { + $btnBold.mousedown().click(); + }, + test: { + content: 'aaabbb
', + start: 'p:contents()[1]->0', + end: 'p:contents()[3]->2', + }, + }, + /* ITALIC */ + { + name: "Click ITALIC: bold -> bold + italic", + content: 'dom not to edit
dom to edit
', + start: 'b:contents()[0]->1', + end: 'b:contents()[0]->5', + do: function () { + $btnItalic.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
', + start: 'i:contents()[0]->0', + end: 'i:contents()[0]->4', + }, + }, + { + name: "Click ITALIC: bold & normal -> italic & bold + italic (across paragraphs)", + content: 'dom to edit
dom to edit
', + start: 'p:contents()[0]->1', + end: 'b:eq(1):contents()[0]->5', + do: function () { + $btnItalic.mousedown().click(); + }, + test: { + content: 'dom to edit
dom to edit
', + start: 'i:contents()[0]->0', + end: 'i:eq(1):contents()[0]->5', + }, + }, + /* UNDERLINE */ + { + name: "Click UNDERLINE: bold -> bold + underlined", + content: 'dom not to edit
dom to edit
', + start: 'b:contents()[0]->1', + end: 'b:contents()[0]->5', + do: function () { + $btnUnderline.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
', + start: 'u:contents()[0]->0', + end: 'u:contents()[0]->4', + }, + }, + { + name: "Click UNDERLINE: bold & normal -> underlined & bold + underlined (across paragraphs)", + content: 'dom to edit
dom to edit
', + start: 'p:contents()[0]->1', + end: 'b:eq(1):contents()[0]->5', + do: function () { + $btnUnderline.mousedown().click(); + }, + test: { + content: 'dom to edit
dom to edit
', + start: 'u:contents()[0]->0', + end: 'u:eq(1):contents()[0]->5', + }, + }, + /* strikethrough */ + { + name: "Click strikethrough: bold -> bold + strikethrough", + content: 'dom not to edit
dom to edit
', + start: 'b:contents()[0]->1', + end: 'b:contents()[0]->5', + do: function () { + $strikethrough.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
dom to edit
dom to edit
', + start: 'p:contents()[0]->1', + end: 'b:eq(1):contents()[0]->5', + do: function () { + $strikethrough.mousedown().click(); + }, + test: { + content: 'dom to edit
dom to edit
dom not to edit
dom to edit
', + start: 'b:contents()[0]->1', + end: 'b:contents()[0]->5', + do: function () { + $superscript.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
', + start: 'sup:contents()[0]->0', + end: 'sup:contents()[0]->4', + }, + }, + { + name: "Click superscript: bold & normal -> superscript & bold + superscript (across paragraphs)", + content: 'dom to edit
dom to edit
', + start: 'p:contents()[0]->1', + end: 'b:eq(1):contents()[0]->5', + do: function () { + $superscript.mousedown().click(); + }, + test: { + content: 'dom to edit
dom to edit
', + start: 'sup:contents()[0]->0', + end: 'sup:eq(1):contents()[0]->5', + }, + }, + /* subscript */ + { + name: "Click subscript: bold -> bold + subscript", + content: 'dom not to edit
dom to edit
', + start: 'b:contents()[0]->1', + end: 'b:contents()[0]->5', + do: function () { + $subscript.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
', + start: 'sub:contents()[0]->0', + end: 'sub:contents()[0]->4', + }, + }, + { + name: "Click subscript: bold & normal -> subscript & bold + subscript (across paragraphs)", + content: 'dom to edit
dom to edit
', + start: 'p:contents()[0]->1', + end: 'b:eq(1):contents()[0]->5', + do: function () { + $subscript.mousedown().click(); + }, + test: { + content: 'dom to edit
dom to edit
', + start: 'sub:contents()[0]->0', + end: 'sub:eq(1):contents()[0]->5', + }, + }, + /* REMOVE FONT STYLE */ + { + name: "Click REMOVE FONT STYLE: bold -> normal", + content: 'dom not to edit
dom to edit
', + start: 'b:contents()[0]->1', + end: 'b:contents()[0]->5', + do: function () { + $btnRemoveStyles.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
', + 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: 'dom to edit
dom to edit
', + start: 'p:contents()[0]->1', + end: 'u:eq(1):contents()[0]->3', + do: function () { + $btnRemoveStyles.mousedown().click(); + }, + test: { + content: 'dom to edit
dom to edit
', + start: 'p:contents()[0]->1', + end: 'p:eq(1):contents()[0]->3', + }, + }, + { + name: "Click REMOVE FONT STYLE: complex -> normal", + content: 'aaabbb
ccc
', + start: 'p:contents()[0]->1', + end: 'font:eq(1):contents()[0]->1', + do: function () { + $btnRemoveStyles.mousedown().click(); + }, + test: { + content: 'aaabbb
ccc
', + start: 'p:contents()[0]->1', + end: 'p:eq(1):contents()[0]->1', + }, + }, + { + name: "Click REMOVE FONT STYLE: complex -> normal (with icon)", + content: 'aaabbb
', + start: 'p:contents()[0]->0', + end: 'i:contents()[0]->1', + do: function () { + $btnRemoveStyles.mousedown().click(); + }, + test: { + content: 'aaabbb
', + start: 'p:contents()[0]->0', + end: 'p:contents()[2]->2', + }, + }, + /* COMPLEX */ + { + name: "COMPLEX Click BOLD: italic -> italic bold (partial selection)", + content: 'dom not to edit
dom to edit
', + start: 'i:contents()[0]->1', + end: 'i:contents()[0]->5', + do: function () { + $btnBold.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
', + start: 'b:contents()[0]->0', + end: 'b:contents()[0]->4', + }, + }, + { + name: "COMPLEX Click BOLD then 'a': italic bold -> italic (across paragraphs)", + content: 'dom to edit
dom to edit
', + 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: 'dom to edit
dom tao edit
', + }, + }, + { + name: "COMPLEX Click BOLD then 'a': bold italic -> italic (no selection)", + content: 'dom to edit
', + start: 'i:contents()[0]->4', + do: function () { + $btnBold.mousedown().click(); + weTestUtils.keydown('a', $editable, { + firstDeselect: true, + }); + }, + test: { + content: 'dom ato edit
', + }, + }, + { + name: "COMPLEX Click BOLD then 'a': underlined italic -> underlined italic bold (across paragraphs)", + content: 'dom to edit
dom to edit
', + 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: 'dom to edit
dom tao edit
', + }, + }, + { + name: "COMPLEX Click BOLD then 'a': underlined italic -> underlined italic bold (no selection)", + content: 'dom to edit
', + start: 'i:contents()[0]->1', + do: function () { + $btnBold.mousedown().click(); + weTestUtils.keydown('a', $editable, { + firstDeselect: true, + }); + }, + test: { + content: 'daom to edit
', + }, + }, + ]; + + _.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: 'dom not to edit
dom to edit
', + 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: 'dom not to edit
dom to edit
', + start: 'font:contents()[0]->0', + end: 'font:contents()[0]->4', + }, + }, + { + name: "Click DEFAULT: 18px -> default", + content: 'dom not to edit
dom to edit
', + start: 'font:contents()[0]->1', + end: 'font:contents()[0]->5', + do: function () { + $dropdownFontSize.mousedown().click(); + $linksFontSize.filter(':contains("Default")').mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
', + start: 'p:eq(1):contents()[1]->0', + end: 'p:eq(1):contents()[1]->4', + }, + }, + { + name: "Click 18: 26px -> 18px", + content: 'dom to edit
', + start: 'font:contents()[0]->1', + end: 'font:contents()[0]->5', + do: function () { + $dropdownFontSize.mousedown().click(); + $linksFontSize.filter(':contains("18")').mousedown().click(); + }, + test: { + content: '' + + 'd' + + 'om t' + + 'o edit
', + start: 'font:eq(1):contents()[0]->0', + end: 'font:eq(1):contents()[0]->4', + }, + }, + { + name: "Click 18 then 'a' (no selection): default -> 18px", + content: 'dom not to edit
dom to edit
', + 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: 'dom not to edit
daom to edit
', + start: 'font:contents()[0]->1', + }, + }, + { + name: "Click 18 then 'a' (no selection): 26px -> 18px", + content: 'dom to edit
', + start: 'font:contents()[0]->1', + do: function () { + $dropdownFontSize.mousedown().click(); + $linksFontSize.filter(':contains("18")').mousedown().click(); + weTestUtils.keydown('a', $editable, { + firstDeselect: true, + }); + }, + test: { + content: '' + + 'd' + + 'a' + + 'om to edit
', + 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: 'dom not to edit
dom to edit
', + 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: 'dom not to edit
dom to edit
', + start: 'font:contents()[0]->0', + end: 'font:contents()[0]->4', + }, + }, + { + name: "Click THEME COLORS - BLACK 25: alpha theme color & default -> black 25", + content: 'dom not to edit
dom to edit
', + 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: 'dom not to edit
dom to edit
', + start: 'font:contents()[0]->0', + end: 'font:contents()[0]->4', + }, + }, + { + name: "Click COMMON COLORS - BLUE #0000FF: black 25 & default -> blue #0000FF", + content: 'dom not to edit
dom to edit
', + 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: 'dom not to edit
dom to edit
', + start: 'font:contents()[0]->0', + end: 'font:contents()[0]->4', + }, + }, + { + name: "Click RESET TO DEFAULT: black 25 & default -> default", + content: 'dom not to edit
dom to edit
', + 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: 'dom not to edit
dom to edit
', + 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: 'dom not to edit
dom to edit
', + 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(), + 'dom not to edit
dom to edit
', + 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: 'dom to edit
', + 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: 'dom to edit
', + start: 'font:contents()[0]->0', + end: 'font:contents()[0]->5', + }, + }, + { + name: "CUSTOM COLOR: change hue, saturation and lightness inputs", + content: 'dom to edit
', + 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: 'dom to edit
', + start: 'font:contents()[0]->0', + end: 'font:contents()[0]->5', + }, + }, + { + name: "CUSTOM COLOR: mousedown on area", + content: 'dom to edit
', + 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: 'dom to edit
', + start: 'font:contents()[0]->0', + end: 'font:contents()[0]->5', + }, + }, + { + name: "CUSTOM COLOR: mousedow on sliders", + content: 'dom to edit
', + 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: 'dom to edit
', + start: 'font:contents()[0]->0', + end: 'font:contents()[0]->5', + }, + }, + { + name: "Apply a color on a fontawesome", + content: 'dom not to edit
', + start: 'i->0', + do: function () { + $dropdownForeColor.mousedown().click(); + $btnsForeColor.filter('[style="background-color:#0000FF"]').mousedown().click(); + }, + test: { + content: 'dom not to edit
', + start: 'p->2', + }, + }, + { + name: "Apply a color on a font with text", + content: 'dom not to edit
', + 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: 'dom not to edit
', + start: 'font:eq(0):contents()[0]->0', + end: 'font:eq(1):contents()[0]->6', + }, + }, + { + name: "Apply color, then 'a' (no selection)", + content: 'dom not to edit
', + 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: 'daom not to edit
', + start: 'font:contents()[0]->1', + }, + }, + { + name: "Apply color on two ranges with the same color", + content: 'do
m not to edit
do
m not to edit
dom not to edit
dom to edit
', + 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: 'dom not to edit
dom to edit
', + start: 'font:contents()[0]->0', + end: 'font:contents()[0]->4', + }, + }, + { + name: "Click THEME COLORS - BLACK 25: alpha theme color & default -> black 25", + content: 'dom not to edit
dom to edit
', + 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: 'dom not to edit
dom to edit
', + start: 'font:contents()[0]->0', + end: 'font:contents()[0]->4', + }, + }, + { + name: "Click COMMON COLORS - BLUE #0000FF: black 25 & default -> blue #0000FF", + content: 'dom not to edit
dom to edit
', + 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: 'dom not to edit
dom to edit
', + start: 'font:contents()[0]->0', + end: 'font:contents()[0]->4', + }, + }, + { + name: "Click RESET TO DEFAULT: black 25 & default -> default", + content: 'dom not to edit
dom to edit
', + 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: 'dom not to edit
dom to edit
', + 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: 'dom not to edit
dom to edit
', + 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(), + 'dom not to edit
dom to edit
', + 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: 'dom not to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(1):contents()[0]->5', + do: function () { + $btnUL.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
dom not to edit
dom to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + do: function () { + $btnUL.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
dom to edit
dom not to edit
dom to edit
dom not to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(1):contents()[0]->5', + }, + }, + { + name: "Click UL: p -> ul (across li's)", + content: 'dom not to edit
dom to edit
dom to edit
dom not to edit
dom to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + }, + }, + { + name: "Click UL: ul -> p (from second li)", + content: 'dom not to edit
xxx
dom to edit
dom not to edit
xxx
dom to edit
', + 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: 'dom not to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(1):contents()[0]->5', + do: function () { + $btnOL.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
dom not to edit
dom to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + do: function () { + $btnOL.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
dom to edit
dom not to edit
dom to edit
dom not to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(1):contents()[0]->5', + }, + }, + { + name: "Click OL: p -> ol (across li's)", + content: 'dom not to edit
dom to edit
dom to edit
dom not to edit
dom to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + }, + }, + { + name: "Click OL: ol -> p (from second li) (2)", + content: 'dom not to edit
dom to edit
dom to edit
dom not to edit
dom to edit
dom to edit
dom not to edit
dom to edit
dom to edit
dom not to edit
dom to edit
dom to edit
a
' + + 'b
c
d
e
a
' + + 'b
c
d
e
a
' + + 'a
' + + 'a
' + + 'a
' + + 'a
' + + 'a
' + + 'a
' + + '| aaa | ' + + '||
a
' + + '
| ' +
+ '||
a
' + + '![]() | ' +
+ '||
a
' + + 'x
' + + 'aaa
bbb
ccc
ddd
y
', + start: 'p:eq(2):contents(0)->1', + end: 'p:eq(3):contents(0)->1', + do: function () { + $btnOL.mousedown().click(); + }, + test: { + content: 'x
' + + 'aaa
bbb
' + + 'ccc
' + + 'ddd
y
', + start: 'p:eq(2):contents(0)->1', + end: 'p:eq(3):contents(0)->1', + }, + }, + { + name: "Click OL with selected LI in UL", + content: 'x
' + + 'aaa
bbb
ccc
ddd
y
', + start: 'p:eq(2):contents(0)->1', + end: 'p:eq(3):contents(0)->1', + do: function () { + $btnOL.mousedown().click(); + }, + test: { + content: 'x
' + + 'aaa
bbb
ccc
ddd
y
', + start: 'p:eq(2):contents(0)->1', + end: 'p:eq(3):contents(0)->1', + }, + }, + { + name: "Click OL after ENTER in p > b", + content: "dom to edit
", + start: 'b:contents(0)->11', + do: function () { + weTestUtils.keydown(13, $editable); // ENTER + $btnOL.mousedown().click(); + weTestUtils.keydown('a', $editable); // ENTER + }, + test: { + content: "dom to edit
a
dom not to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + do: function () { + $dropdownPara.mousedown().click(); + $btnAlignLeft.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + }, + }, + { + name: "Click ALIGN LEFT: p (parent align right) -> p align left (does nothing)", + content: 'dom not to edit
dom to edit
dom not to edit
dom to edit
dom not to edit
dom to edit
dom not to edit
dom to edit
dom not to edit
dom to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + do: function () { + $dropdownPara.mousedown().click(); + $btnAlignRight.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
dom to edit
', + 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: 'dom not to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + do: function () { + $dropdownPara.mousedown().click(); + $btnAlignCenter.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + }, + }, + { + name: "Click ALIGN CENTER: p align left & default -> p align center (across paragraphs)", + content: 'dom not to edit
dom to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + do: function () { + $dropdownPara.mousedown().click(); + $btnAlignCenter.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
dom to edit
', + 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: 'dom not to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + do: function () { + $dropdownPara.mousedown().click(); + $btnAlignRight.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + }, + }, + { + name: "Click ALIGN RIGHT: p align center & default -> p align right (across paragraphs)", + content: 'dom not to edit
dom to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + do: function () { + $dropdownPara.mousedown().click(); + $btnAlignRight.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
dom to edit
', + 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: 'dom not to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + do: function () { + $dropdownPara.mousedown().click(); + $btnAlignJustify.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + }, + }, + { + name: "Click ALIGN JUSTIFY: p align right & default -> p align justify (across paragraphs)", + content: 'dom not to edit
dom to edit
dom to edit
', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + do: function () { + $dropdownPara.mousedown().click(); + $btnAlignJustify.mousedown().click(); + }, + test: { + content: 'dom not to edit
dom to edit
dom to edit
', + 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: 'dom to edit
', + start: 'p:contents()[0]->1', + do: function () { + $dropdownPara.mousedown().click(); + $btnIndent.mousedown().click(); + }, + test: { + content: 'dom to edit
', + start: 'p:contents()[0]->1', + }, + }, + { + name: "Click INDENT: li -> indented li", + content: 'dom
to edit
dom
to edit
dom to edit
', + start: 'p:contents()[0]->1', + do: function () { + $dropdownPara.mousedown().click(); + $btnOutdent.mousedown().click(); + }, + test: { + content: 'dom to edit
', + start: 'p:contents()[0]->1', + }, + }, + { + name: "Click OUTDENT: indented li -> li", + content: 'dom
to edit
dom
to edit
dom
to edit
dom
to edit
x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + start: 'ol ol p:eq(1):contents(0)->1', + do: function () { + $dropdownPara.mousedown().click(); + $btnOutdent.mousedown().click(); + }, + test: { + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + start: 'ol:first > li:eq(2) p:contents(0)->1', + }, + }, + { + name: "Click OUTDENT on LI in OL in OL (2)", + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + start: 'ol ol p:eq(1):contents(0)->1', + do: function () { + $dropdownPara.mousedown().click(); + $btnOutdent.mousedown().click(); + }, + test: { + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + start: 'li:eq(2) p:contents(0)->1', + }, + }, + { + name: "Click OUTDENT on LI in OL", + content: + 'aaa
bbb
ccc
aaa
bbb
' + + 'ccc
dom
' + + 'dom
' + + 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + do: function () { + var $li = $editable.find('li:first'); + var pos = $li.offset(); + $li.trigger($.Event('mousedown', { + offsetX: -10, + })); + }, + test: { + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + }, + }, + { + name: "check checkbox in checklist with children (2)", + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + do: function () { + var $li = $editable.find('li:first'); + var pos = $li.offset(); + $li.trigger($.Event('mousedown', { + offsetX: -10, + })); + }, + test: { + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + }, + }, + { + name: "uncheck checkbox in checklist with children", + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + do: function () { + var $li = $editable.find('li:first'); + var pos = $li.offset(); + $li.trigger($.Event('mousedown', { + offsetX: -10, + })); + }, + test: { + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + }, + }, + { + name: "uncheck checkbox in checklist with children (2)", + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + do: function () { + var $li = $editable.find('li:first'); + var pos = $li.offset(); + $li.trigger($.Event('mousedown', { + offsetX: -10, + })); + }, + test: { + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + }, + }, + { + name: "uncheck checkbox in checklist in checklist", + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + do: function () { + var $li = $editable.find('ul ul li:eq(1)'); + var pos = $li.offset(); + $li.trigger($.Event('mousedown', { + offsetX: -10, + })); + }, + test: { + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + }, + }, + { + name: "uncheck checkbox in checklist in checklist (2)", + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + do: function () { + var $li = $editable.find('ul ul li:eq(1)'); + var pos = $li.offset(); + $li.trigger($.Event('mousedown', { + offsetX: -10, + })); + }, + test: { + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + }, + }, + { + name: "check checkbox in checklist in checklist", + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + do: function () { + var $li = $editable.find('ul ul li:eq(1)'); + var pos = $li.offset(); + $li.trigger($.Event('mousedown', { + offsetX: -10, + })); + }, + test: { + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + }, + }, + { + name: "check checkbox in checklist in checklist (2)", + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + do: function () { + var $li = $editable.find('ul ul li:eq(1)'); + var pos = $li.offset(); + $li.trigger($.Event('mousedown', { + offsetX: -10, + })); + }, + test: { + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + }, + }, + { + name: "check checkbox in checklist in checklist (full)", + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + do: function () { + var $li = $editable.find('ul ul li:eq(1)'); + var pos = $li.offset(); + $li.trigger($.Event('mousedown', { + offsetX: -10, + })); + }, + test: { + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + }, + }, + { + name: "check checkbox in checklist in checklist (full) (2)", + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + do: function () { + var $li = $editable.find('ul ul li:eq(1)'); + var pos = $li.offset(); + $li.trigger($.Event('mousedown', { + offsetX: -10, + })); + }, + test: { + content: 'x
' + + 'aaa
bbb
ccc
ddd
eee
y
', + }, + }, + ]; + + _.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: 'dom to edit
', + 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: 'dom to edit
', + start: 'a:contents()[0]->0', + end: 'a:contents()[0]->4', + }, + }, + { + name: "Click LINK: p -> a in p (w/ URL) (no selection)", + async: true, + content: 'do edit
', + 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: 'dom to edit
', + 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: '', + 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: '', + start: 'a->0', + end: 'a->1', + }, + }, + { + name: "Click LINK: p -> a in p (w/ Email)", + async: true, + content: 'dom to edit
', + 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: 'dom to edit
', + start: 'a:contents()[0]->0', + end: 'a:contents()[0]->4', + }, + }, + { + name: "Click LINK: p -> a in p (w/ URL & Size Large)", + async: true, + content: 'dom to edit
', + 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: 'dom to edit
', + 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: '', + 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: '', + start: 'a->0', + end: 'a->1', + }, + }, + /* POPOVER */ + { + name: "Click LINK in popover after adding link in p", + async: true, + content: 'dom to edit
', + 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(), 'dom to edit
', testName); + }); + }, + }, + }, + { + name: "Click UNLINK in popover after adding link in p", + async: true, + content: 'dom to edit
', + start: "p:contents()[0]->1", + end: "p:contents()[0]->5", + do: function () { + $('.modal-dialog #o_link_dialog_url_input').val('/link'); + }, + test: { + content: 'dom to edit
', + 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('dom to edit
'); + 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, ' '), + 'dom to edit
', + "should create a table at start of single p with content"); + + // create a table at end of p with content + + wysiwyg.setValue('dom to edit
'); + 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, ' '), + 'dom to edit
' + + 'dom to edit
'); + 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, ' '), + 'dom t
' + + 'o edit
', + "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, ' '), + 'dom to edit
', + "should remove the table"); + + // re create a table and table in table + + createTable(wysiwyg); + createTable(wysiwyg); + + assert.strictEqual($editable.html().replace(/\s+/g, ' '), + 'dom t
' + + 'o edit
', + "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, ' '), + 'dom t
' + + 'o edit
', + "should remove the inner table"); + + // use popover to change row and line + + wysiwyg.setValue('dom t
' + + '0-0 | 0-1 | 0-2 |
1-0 | 1-1 | 1-2 |
2-0 | 2-1 | 2-2 |
o edit
'); + + // 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, ' '), + 'dom t
' + + '0-0 | 0-2 |
1-0 | 1-2 |
2-0 | 2-2 |
o edit
', + "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, ' '), + 'dom t
' + + '0-0 | 0-2 |
2-0 | 2-2 |
o edit
', + "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, ' '), + 'dom t
' + + '0-0 | 0-2 | |
2-0 | 2-2 |
o edit
', + "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, ' '), + 'dom t
' + + '0-0 | 0-2 | ||
2-0 | 2-2 |
o edit
', + "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, ' '), + 'dom t
' + + '0-0 | 0-2 | ||
2-0 | 2-2 | ||
o edit
', + "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, ' '), + 'dom t
' + + '0-0 | 0-2 | ||
2-0 | 2-2 | ||
o edit
', + "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('dom to edit 
dom to edit a 
\u200B
\u200B
| ' + + ' aaa' + + ' | ' + + '' + + ' bbb' + + ' | ' + + '' + + ' ccc' + + ' | ' + + '
| ' + + ' aaa' + + ' | ' + + '' +
+ ' bb b' +
+ ' | ' +
+ ' ' + + ' ccc' + + ' | ' + + '
| ' +
+ ' ' + + ' | ' +
+ ' ' +
+ ' ' + + ' | ' +
+ ' ' +
+ ' ' + + ' | ' +
+ '
| ' +
+ ' ' + + ' | ' +
+ ' ' +
+ ' ![]() ' + + ' | ' +
+ ' ' +
+ ' ' + + ' | ' +
+ '
\u200B
\u200B
\u200B
\u200B
\u200B
\u200B
\u200B
\u200B
\u200B
\u200B
\u200B
\u200B
\u200B\u200B
', + 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: '\u200B\u200B
', + 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: '\u200B\u200B
', + 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: '\u200B\u200B
', + }); + }, + }, + }, + { + name: "Replace PICTOGRAM (i.fa => span.fa)", + async: true, + content: 'tata yoyo
tata
yoyo

\u200B\u200B
', + testName); + }, + }, + }, + { + name: "replace video by pictogram (2)", + async: true, + content: 'aaa
bbb
', + start: "div->0", + do: function () { + _insertPictogram('fa-glass'); + }, + test: { + check: function () { + assert.deepEqual(wysiwyg.getValue(), + 'aaabbb
', + testName); + }, + }, + }, + ]; + + var def = $.when(); + _.each(mediaTests, function (test) { + def = def.then(function () { + testName = test.name; + wysiwyg.setValue(test.content); + var range = weTestUtils.select(test.start, test.end, $editable); + $(range.sc).mousedown().click(); + Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo); + return _clickMedia(wysiwyg, assert, test.do, test.test); + }); + }); + def.then(function () { + wysiwyg.destroy(); + done(); + }); + }); +}); + + +}); +}); +}); +}); diff --git a/addons/web_editor/tests/__init__.py b/addons/web_editor/tests/__init__.py index b531e18c918..8b77588a13c 100644 --- a/addons/web_editor/tests/__init__.py +++ b/addons/web_editor/tests/__init__.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import test_ui from . import test_converter diff --git a/addons/web_editor/tests/test_ui.py b/addons/web_editor/tests/test_ui.py deleted file mode 100644 index 9e9527e2d14..00000000000 --- a/addons/web_editor/tests/test_ui.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import odoo.tests - -@odoo.tests.tagged('post_install', '-at_install') -class TestUi(odoo.tests.HttpCase): - - def test_01_admin_rte(self): - self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('rte')", "odoo.__DEBUG__.services['web_tour.tour'].tours.rte.ready", login='admin') - - def test_02_admin_rte_inline(self): - self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('rte_inline')", "odoo.__DEBUG__.services['web_tour.tour'].tours.rte_inline.ready", login='admin') diff --git a/addons/web_tour/static/src/js/running_tour_action_helper.js b/addons/web_tour/static/src/js/running_tour_action_helper.js index 1c4df6c27db..479f3a2ea78 100644 --- a/addons/web_tour/static/src/js/running_tour_action_helper.js +++ b/addons/web_tour/static/src/js/running_tour_action_helper.js @@ -92,7 +92,11 @@ var RunningTourActionHelper = core.Class.extend({ $selectedOption.prop("selected", true); this._click(values); } else { + values.$element.focusIn(); + values.$element.trigger($.Event( "keydown", {key: '_', keyCode: 95})); values.$element.text(text); + values.$element.focusInEnd(); + values.$element.trigger($.Event( "keyup", {key: '_', keyCode: 95})); } values.$element.trigger("change"); }, diff --git a/addons/website/static/src/js/tours/rte.js b/addons/website/static/src/js/tours/rte.js index 46fbbff3fce..bdc676f4b33 100644 --- a/addons/website/static/src/js/tours/rte.js +++ b/addons/website/static/src/js/tours/rte.js @@ -1,12 +1,18 @@ odoo.define('website.tour.rte', function (require) { 'use strict'; +var ajax = require('web.ajax'); +var session = require('web.session'); var tour = require('web_tour.tour'); -var base = require('web_editor.base'); +var Wysiwyg = require('web_editor.wysiwyg'); + +var domReady = $.Deferred(); +$(domReady.resolve.bind(domReady)); +var ready = $.when(domReady, session.is_bound, ajax.loadXML()); tour.register('rte_translator', { test: true, - wait_for: base.ready(), + wait_for: ready, }, [{ content: "click on Add a language", trigger: '.js_language_selector a:has(i.fa)', @@ -59,10 +65,10 @@ tour.register('rte_translator', { content: "save", trigger: 'button[data-action=save]', extra_trigger: '#wrap p:first b', - }, { content : "click language dropdown", trigger : '.js_language_selector .dropdown-toggle', + extra_trigger: 'body:not(.o_wait_reload):not(:has(.note-editor))', }, { content: "click on french version", trigger: '.js_language_selector a[data-lang="fr_BE"]', @@ -81,14 +87,17 @@ tour.register('rte_translator', { trigger: '#wrap p font:first', run: function (action_helper) { action_helper.text('translated french text'); - this.$anchor.trigger('keydown'); + Wysiwyg.setRange(this.$anchor.contents()[0], 22); + this.$anchor.trigger($.Event( "keyup", {key: '_', keyCode: 95})); }, }, { content: "translate text with special char", trigger: '#wrap input + p span:first', run: function (action_helper) { action_helper.click(); - this.$anchor.prepend('<{translated}>').trigger('keydown'); + this.$anchor.prepend('<{translated}>'); + Wysiwyg.setRange(this.$anchor.contents()[0], 0); + this.$anchor.trigger($.Event( "keyup", {key: '_', keyCode: 95})); }, }, { content: "click on input", @@ -124,10 +133,13 @@ tour.register('rte_translator', { content : "click language dropdown", trigger : '.js_language_selector .dropdown-toggle', +}, { + content: "open language selector", + trigger: '.js_language_selector button:first', + extra_trigger: 'html[lang*="fr"]:not(:has(#wrap p span))', }, { content: "return to english version", trigger: '.js_language_selector a[data-lang="en_US"]', - extra_trigger: 'html[lang*="fr"]:not(:has(#wrap p span))', }, { content: "edit english version", trigger: 'a[data-action=edit]', @@ -139,27 +151,25 @@ tour.register('rte_translator', { run: function (action_helper) { action_helper.click(); var el = this.$anchor[0]; - var evt = document.createEvent("MouseEvents"); - $.summernote.core.range.create(el.childNodes[2], 6, el.childNodes[2], 13).select(); - evt.initMouseEvent('mouseup', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el); - el.dispatchEvent(evt); + this.$anchor.trigger('mousedown'); + Wysiwyg.setRange(el.childNodes[2], 6, el.childNodes[2], 13); + this.$anchor.trigger('mouseup'); }, }, { content: "underline", - trigger: '.note-air-popover button[data-event="underline"]', + trigger: 'button.note-btn-underline', }, { content: "save new change", trigger: 'button[data-action=save]', - extra_trigger: '#wrap p u', + extra_trigger: '#wrap.o_dirty p u', }, { content : "click language dropdown", trigger : '.js_language_selector .dropdown-toggle', - + extra_trigger: '#wrap p u', }, { content: "return in french", - trigger: 'html[lang="en-US"] .js_language_selector a[data-lang="fr_BE"]', - extra_trigger: 'html[lang*="en"]:not(:has(button[data-action=save]))', + trigger : 'html[lang="en-US"] .js_language_selector .js_change_lang[data-lang="fr_BE"]', }, { content: "check bis: content is translated", trigger: '#wrap p font:first:contains(translated french text)', diff --git a/addons/website_blog/static/src/js/website.tour.blog.js b/addons/website_blog/static/src/js/website.tour.blog.js index d1afa46f217..a004f360e33 100644 --- a/addons/website_blog/static/src/js/website.tour.blog.js +++ b/addons/website_blog/static/src/js/website.tour.blog.js @@ -16,7 +16,7 @@ odoo.define("website_blog.tour", function (require) { position: "bottom", }, { trigger: "h1[data-oe-expression=\"blog_post.name\"]", - extra_trigger: "body.editor_has_snippets", + extra_trigger: ".o_snippets_loaded", content: _t("Write a title, the subtitle is optional."), position: "top", run: "text", @@ -26,7 +26,7 @@ odoo.define("website_blog.tour", function (require) { content: _t("Set a blog post cover."), position: "bottom", }, { - trigger: "a:containsExact(" + _t("Change Cover")+ "):eq(1)", + trigger: "a:containsExact(" + _t("Change Cover")+ "):visible", content: _t("Click here to change your post cover."), position: "right", }, { diff --git a/addons/website_sale/static/src/js/website_sale_tour_shop.js b/addons/website_sale/static/src/js/website_sale_tour_shop.js index 6847bdd3bd2..deafdd83369 100644 --- a/addons/website_sale/static/src/js/website_sale_tour_shop.js +++ b/addons/website_sale/static/src/js/website_sale_tour_shop.js @@ -2,15 +2,11 @@ odoo.define("website_sale.tour_shop", function (require) { "use strict"; var core = require("web.core"); - var tour = require("web_tour.tour"); - var base = require("web_editor.base"); - var _t = core._t; - tour.register("shop", { - url: "/shop", - wait_for: base.ready(), - }, [{ + // return the steps, used for backend and frontend (because web_editor.base is only in the website) + + return [{ trigger: "#new-content-menu > a", content: _t("Let's create your first product."), extra_trigger: ".js_sale", @@ -28,13 +24,14 @@ odoo.define("website_sale.tour_shop", function (require) { content: _t("Click on Continue to create the product."), position: "right", }, { - trigger: ".product_price .o_is_inline_editable .oe_currency_value", + trigger: ".product_price .oe_currency_value:visible", + extra_trigger: ".note-editable", content: _t("Edit the price of this product by clicking on the amount."), position: "bottom", run: "text 1.99", }, { trigger: "#wrap img.product_detail_img", - extra_trigger: ".product_price .o_is_inline_editable .oe_currency_value:not(:containsExact(1.00))", + extra_trigger: ".product_price .o_dirty .oe_currency_value:not(:containsExact(1.00))", content: _t("Double click here to set an image describing your product."), position: "top", run: function (actions) { @@ -66,10 +63,10 @@ odoo.define("website_sale.tour_shop", function (require) { trigger: ".o_main_navbar .o_menu_toggle, #oe_applications .dropdown-toggle", content: _t("Let's now take a look at your administration dashboard to get your eCommerce website ready in no time."), position: "bottom", - }, { + }, { // backend trigger: '.o_apps > a[data-menu-xmlid="website.menu_website_configuration"], #oe_main_menu_navbar a[data-menu-xmlid="website.menu_website_configuration"]', content: _t("Open your website app here."), extra_trigger: ".o_apps,#oe_applications", position: "bottom", - }]); + }]; }); diff --git a/addons/website_sale/static/src/js/website_sale_tour_shop_backend.js b/addons/website_sale/static/src/js/website_sale_tour_shop_backend.js new file mode 100644 index 00000000000..ad31f8d4ab3 --- /dev/null +++ b/addons/website_sale/static/src/js/website_sale_tour_shop_backend.js @@ -0,0 +1,8 @@ +odoo.define("website_sale.tour_shop_backend", function (require) { +"use strict"; + +var tour = require("web_tour.tour"); +var steps = require("website_sale.tour_shop"); +tour.register("shop", steps); + +}); diff --git a/addons/website_sale/static/src/js/website_sale_tour_shop_frontend.js b/addons/website_sale/static/src/js/website_sale_tour_shop_frontend.js new file mode 100644 index 00000000000..f2aa39af42b --- /dev/null +++ b/addons/website_sale/static/src/js/website_sale_tour_shop_frontend.js @@ -0,0 +1,12 @@ +odoo.define("website_sale.tour_shop_frontend", function (require) { +"use strict"; + +var tour = require("web_tour.tour"); +var base = require("web_editor.base"); +var steps = require("website_sale.tour_shop"); +tour.register("shop", { + url: "/shop", + wait_for: base.ready(), +}, steps); + +});