summaryrefslogtreecommitdiff
path: root/addons/web_editor/static/tests
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/web_editor/static/tests
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/web_editor/static/tests')
-rw-r--r--addons/web_editor/static/tests/field_html_tests.js528
-rw-r--r--addons/web_editor/static/tests/test_utils.js722
2 files changed, 1250 insertions, 0 deletions
diff --git a/addons/web_editor/static/tests/field_html_tests.js b/addons/web_editor/static/tests/field_html_tests.js
new file mode 100644
index 00000000..89dda4cc
--- /dev/null
+++ b/addons/web_editor/static/tests/field_html_tests.js
@@ -0,0 +1,528 @@
+odoo.define('web_editor.field_html_tests', function (require) {
+"use strict";
+
+var ajax = require('web.ajax');
+var FormView = require('web.FormView');
+var testUtils = require('web.test_utils');
+var weTestUtils = require('web_editor.test_utils');
+var core = require('web.core');
+var Wysiwyg = require('web_editor.wysiwyg');
+var MediaDialog = require('wysiwyg.widgets.MediaDialog');
+
+var _t = core._t;
+
+QUnit.module('web_editor', {}, function () {
+
+ QUnit.module('field html', {
+ beforeEach: function () {
+ this.data = weTestUtils.wysiwygData({
+ 'note.note': {
+ fields: {
+ display_name: {
+ string: "Displayed name",
+ type: "char"
+ },
+ header: {
+ string: "Header",
+ type: "html",
+ required: true,
+ },
+ body: {
+ string: "Message",
+ type: "html"
+ },
+ },
+ records: [{
+ id: 1,
+ display_name: "first record",
+ header: "<p> &nbsp;&nbsp; <br> </p>",
+ body: "<p>toto toto toto</p><p>tata</p>",
+ }, {
+ id: 2,
+ display_name: "second record",
+ header: "<p> &nbsp;&nbsp; <br> </p>",
+ body: `
+<div class="o_form_sheet_bg">
+ <div class="clearfix position-relative o_form_sheet" style="width: 1140px;">
+ <div class="o_notebook">
+ <div class="tab-content">
+ <div class="tab-pane active" id="notebook_page_820">
+ <div class="oe_form_field oe_form_field_html o_field_widget" name="description" style="margin-bottom: 5px;">
+ hacky code to test
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>`,
+ }],
+ },
+ 'mass.mailing': {
+ fields: {
+ display_name: {
+ string: "Displayed name",
+ type: "char"
+ },
+ body_html: {
+ string: "Message Body inline (to send)",
+ type: "html"
+ },
+ body_arch: {
+ string: "Message Body for edition",
+ type: "html"
+ },
+ },
+ records: [{
+ id: 1,
+ display_name: "first record",
+ body_html: "<div class='field_body' style='background-color: red;'>yep</div>",
+ body_arch: "<div class='field_body'>yep</div>",
+ }],
+ },
+ "ir.translation": {
+ fields: {
+ lang_code: {type: "char"},
+ value: {type: "char"},
+ res_id: {type: "integer"}
+ },
+ records: [{
+ id: 99,
+ res_id: 12,
+ value: '',
+ lang_code: 'en_US'
+ }]
+ },
+ });
+
+ testUtils.mock.patch(ajax, {
+ loadAsset: function (xmlId) {
+ if (xmlId === 'template.assets') {
+ return Promise.resolve({
+ cssLibs: [],
+ cssContents: ['body {background-color: red;}']
+ });
+ }
+ if (xmlId === 'template.assets_all_style') {
+ return Promise.resolve({
+ cssLibs: $('link[href]:not([type="image/x-icon"])').map(function () {
+ return $(this).attr('href');
+ }).get(),
+ cssContents: ['body {background-color: red;}']
+ });
+ }
+ throw 'Wrong template';
+ },
+ });
+ },
+ afterEach: function () {
+ testUtils.mock.unpatch(ajax);
+ },
+ }, function () {
+
+ QUnit.module('basic');
+
+ QUnit.test('simple rendering', async function (assert) {
+ assert.expect(3);
+
+ var form = await testUtils.createView({
+ View: FormView,
+ model: 'note.note',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px"/>' +
+ '</form>',
+ res_id: 1,
+ });
+ var $field = form.$('.oe_form_field[name="body"]');
+ assert.strictEqual($field.children('.o_readonly').html(),
+ '<p>toto toto toto</p><p>tata</p>',
+ "should have rendered a div with correct content in readonly");
+ assert.strictEqual($field.attr('style'), 'height: 100px',
+ "should have applied the style correctly");
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.nextTick();
+ $field = form.$('.oe_form_field[name="body"]');
+ assert.strictEqual($field.find('.note-editable').html(),
+ '<p>toto toto toto</p><p>tata</p>',
+ "should have rendered the field correctly in edit");
+
+ form.destroy();
+ });
+
+ QUnit.test('notebooks defined inside HTML field widgets are ignored when calling setLocalState', async function (assert) {
+ assert.expect(1);
+
+ var form = await testUtils.createView({
+ View: FormView,
+ model: 'note.note',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px"/>' +
+ '</form>',
+ res_id: 2,
+ });
+ // check that there is no error on clicking Edit
+ await testUtils.form.clickEdit(form);
+ await testUtils.nextTick();
+ assert.containsOnce(form, '.o_form_editable');
+
+ form.destroy();
+ });
+
+ QUnit.test('check if required field is set', async function (assert) {
+ assert.expect(1);
+
+ var form = await testUtils.createView({
+ View: FormView,
+ model: 'note.note',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="header" widget="html" style="height: 100px" />' +
+ '</form>',
+ res_id: 1,
+ });
+
+ testUtils.mock.intercept(form, 'call_service', function (ev) {
+ if (ev.data.service === 'notification') {
+ assert.deepEqual(ev.data.args[0], {
+ "className": undefined,
+ "message": "<ul><li>Header</li></ul>",
+ "sticky": undefined,
+ "title": "Invalid fields:",
+ "type": "danger"
+ });
+ }
+ }, true);
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.nextTick();
+ await testUtils.dom.click(form.$('.o_form_button_save'));
+
+ form.destroy();
+ });
+
+ QUnit.test('colorpicker', async function (assert) {
+ assert.expect(6);
+
+ var form = await testUtils.createView({
+ View: FormView,
+ model: 'note.note',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // Summernote needs a RootWidget to set as parent of the ColorPaletteWidget. In the
+ // tests, there is no RootWidget, so we set it here to the parent of the form view, which
+ // can act as RootWidget, as it will honor rpc requests correctly (to the MockServer).
+ const rootWidget = odoo.__DEBUG__.services['root.widget'];
+ odoo.__DEBUG__.services['root.widget'] = form.getParent();
+
+ await testUtils.form.clickEdit(form);
+ 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");
+
+ async function openColorpicker(selector) {
+ const $colorpicker = $field.find(selector);
+ const openingProm = new Promise(resolve => {
+ $colorpicker.one('shown.bs.dropdown', () => resolve());
+ });
+ await testUtils.dom.click($colorpicker.find('button:first'));
+ return openingProm;
+ }
+
+ await openColorpicker('.note-toolbar .note-back-color-preview');
+ assert.ok($field.find('.note-back-color-preview').hasClass('show'),
+ "should display the color picker");
+
+ await testUtils.dom.click($field.find('.note-toolbar .note-back-color-preview .o_we_color_btn[style="background-color:#00FFFF;"]'));
+
+ assert.ok(!$field.find('.note-back-color-preview').hasClass('show'),
+ "should close the color picker");
+
+ assert.strictEqual($field.find('.note-editable').html(),
+ '<p>t<font style="background-color: rgb(0, 255, 255);">oto toto&nbsp;</font>toto</p><p>tata</p>',
+ "should have rendered the field correctly in edit");
+
+ var fontContent = $field.find('.note-editable font').contents()[0];
+ var rangeControl = {
+ sc: fontContent,
+ so: 0,
+ ec: fontContent,
+ eo: fontContent.length,
+ };
+ range = Wysiwyg.getRange($field[0]);
+ assert.deepEqual(_.pick(range, 'sc', 'so', 'ec', 'eo'), rangeControl,
+ "should select the text after color change");
+
+ // select the text
+ pText = $field.find('.note-editable p').first().contents()[2];
+ Wysiwyg.setRange(fontContent, 5, pText, 2);
+ // text is selected
+
+ await openColorpicker('.note-toolbar .note-back-color-preview');
+ await testUtils.dom.click($field.find('.note-toolbar .note-back-color-preview .o_we_color_btn.bg-o-color-3'));
+
+ assert.strictEqual($field.find('.note-editable').html(),
+ '<p>t<font style="background-color: rgb(0, 255, 255);">oto t</font><font style="" class="bg-o-color-3">oto&nbsp;</font><font class="bg-o-color-3" style="">to</font>to</p><p>tata</p>',
+ "should have rendered the field correctly in edit");
+
+ odoo.__DEBUG__.services['root.widget'] = rootWidget;
+ form.destroy();
+ });
+
+ QUnit.test('media dialog: image', async function (assert) {
+ assert.expect(1);
+
+ var form = await testUtils.createView({
+ View: FormView,
+ model: 'note.note',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.model === 'ir.attachment') {
+ if (args.method === "generate_access_token") {
+ return Promise.resolve();
+ }
+ }
+ if (route.indexOf('/web/image/123/transparent.png') === 0) {
+ return Promise.resolve();
+ }
+ if (route.indexOf('/web_unsplash/fetch_images') === 0) {
+ return Promise.resolve();
+ }
+ if (route.indexOf('/web_editor/media_library_search') === 0) {
+ return Promise.resolve();
+ }
+ return this._super(route, args);
+ },
+ });
+ await testUtils.form.clickEdit(form);
+ var $field = form.$('.oe_form_field[name="body"]');
+
+ // the dialog load some xml assets
+ var defMediaDialog = testUtils.makeTestPromise();
+ 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);
+
+ await testUtils.dom.click($field.find('.note-toolbar .note-insert button:has(.fa-file-image-o)'));
+
+ // load static xml file (dialog, media dialog, unsplash image widget)
+ await defMediaDialog;
+
+ await testUtils.dom.click($('.modal #editor-media-image .o_existing_attachment_cell:first').removeClass('d-none'));
+
+ var $editable = form.$('.oe_form_field[name="body"] .note-editable');
+ assert.ok($editable.find('img')[0].dataset.src.includes('/web/image/123/transparent.png'),
+ "should have the image in the dom");
+
+ testUtils.mock.unpatch(MediaDialog);
+
+ form.destroy();
+ });
+
+ QUnit.test('media dialog: icon', async function (assert) {
+ assert.expect(1);
+
+ var form = await testUtils.createView({
+ View: FormView,
+ model: 'note.note',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.model === 'ir.attachment') {
+ return Promise.resolve([]);
+ }
+ if (route.indexOf('/web_unsplash/fetch_images') === 0) {
+ return Promise.resolve();
+ }
+ return this._super(route, args);
+ },
+ });
+ await testUtils.form.clickEdit(form);
+ var $field = form.$('.oe_form_field[name="body"]');
+
+ // the dialog load some xml assets
+ var defMediaDialog = testUtils.makeTestPromise();
+ 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);
+
+ await testUtils.dom.click($field.find('.note-toolbar .note-insert button:has(.fa-file-image-o)'));
+
+ // load static xml file (dialog, media dialog, unsplash image widget)
+ await defMediaDialog;
+ $('.modal .tab-content .tab-pane').removeClass('fade'); // to be sync in test
+ await testUtils.dom.click($('.modal a[aria-controls="editor-media-icon"]'));
+ await testUtils.dom.click($('.modal #editor-media-icon .font-icons-icon.fa-glass'));
+
+ var $editable = form.$('.oe_form_field[name="body"] .note-editable');
+
+ assert.strictEqual($editable.data('wysiwyg').getValue(),
+ '<p>t<span class="fa fa-glass"></span>oto toto toto</p><p>tata</p>',
+ "should have the image in the dom");
+
+ testUtils.mock.unpatch(MediaDialog);
+
+ form.destroy();
+ });
+
+ QUnit.test('save', async function (assert) {
+ assert.expect(1);
+
+ var form = await testUtils.createView({
+ View: FormView,
+ model: 'note.note',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === "write") {
+ assert.strictEqual(args.args[1].body,
+ '<p>t<font class="bg-o-color-3">oto toto&nbsp;</font>toto</p><p>tata</p>',
+ "should save the content");
+
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ await testUtils.form.clickEdit(form);
+ 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
+
+ async function openColorpicker(selector) {
+ const $colorpicker = $field.find(selector);
+ const openingProm = new Promise(resolve => {
+ $colorpicker.one('shown.bs.dropdown', () => resolve());
+ });
+ await testUtils.dom.click($colorpicker.find('button:first'));
+ return openingProm;
+ }
+
+ await openColorpicker('.note-toolbar .note-back-color-preview');
+ await testUtils.dom.click($field.find('.note-toolbar .note-back-color-preview .o_we_color_btn.bg-o-color-3'));
+
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.module('cssReadonly');
+
+ QUnit.test('rendering with iframe for readonly mode', async function (assert) {
+ assert.expect(3);
+
+ var form = await testUtils.createView({
+ View: FormView,
+ model: 'note.note',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="body" widget="html" style="height: 100px" options="{\'cssReadonly\': \'template.assets\'}"/>' +
+ '</form>',
+ res_id: 1,
+ });
+ var $field = form.$('.oe_form_field[name="body"]');
+ var $iframe = $field.find('iframe.o_readonly');
+ await $iframe.data('loadDef');
+ var doc = $iframe.contents()[0];
+ assert.strictEqual($(doc).find('#iframe_target').html(),
+ '<p>toto toto toto</p><p>tata</p>',
+ "should have rendered a div with correct content in readonly");
+
+ assert.strictEqual(doc.defaultView.getComputedStyle(doc.body).backgroundColor,
+ 'rgb(255, 0, 0)',
+ "should load the asset css");
+
+ await testUtils.form.clickEdit(form);
+
+ $field = form.$('.oe_form_field[name="body"]');
+ assert.strictEqual($field.find('.note-editable').html(),
+ '<p>toto toto toto</p><p>tata</p>',
+ "should have rendered the field correctly in edit");
+
+ form.destroy();
+ });
+
+ QUnit.module('translation');
+
+ QUnit.test('field html translatable', async function (assert) {
+ assert.expect(4);
+
+ var multiLang = _t.database.multi_lang;
+ _t.database.multi_lang = true;
+
+ this.data['note.note'].fields.body.translate = true;
+
+ var form = await testUtils.createView({
+ View: FormView,
+ model: 'note.note',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="body" widget="html"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_button' && args.method === 'translate_fields') {
+ assert.deepEqual(args.args, ['note.note', 1, 'body'], "should call 'call_button' route");
+ return Promise.resolve({
+ domain: [],
+ context: {search_default_name: 'partnes,foo'},
+ });
+ }
+ if (route === "/web/dataset/call_kw/res.lang/get_installed") {
+ return Promise.resolve([["en_US"], ["fr_BE"]]);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ assert.strictEqual(form.$('.oe_form_field_html .o_field_translate').length, 0,
+ "should not have a translate button in readonly mode");
+
+ await testUtils.form.clickEdit(form);
+ var $button = form.$('.oe_form_field_html .o_field_translate');
+ assert.strictEqual($button.length, 1, "should have a translate button");
+ await testUtils.dom.click($button);
+ assert.containsOnce($(document), '.o_translation_dialog', 'should have a modal to translate');
+
+ form.destroy();
+ _t.database.multi_lang = multiLang;
+ });
+ });
+});
+});
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 00000000..1aa4a79c
--- /dev/null
+++ b/addons/web_editor/static/tests/test_utils.js
@@ -0,0 +1,722 @@
+odoo.define('web_editor.test_utils', function (require) {
+"use strict";
+
+var ajax = require('web.ajax');
+var MockServer = require('web.MockServer');
+var testUtils = require('web.test_utils');
+var Widget = require('web.Widget');
+var Wysiwyg = require('web_editor.wysiwyg');
+var options = require('web_editor.snippets.options');
+
+const COLOR_PICKER_TEMPLATE = `
+ <t t-name="web_editor.colorpicker">
+ <colorpicker>
+ <div class="o_colorpicker_section" data-name="theme" data-display="Theme Colors" data-icon-class="fa fa-flask">
+ <button data-color="o-color-1"/>
+ <button data-color="o-color-2"/>
+ <button data-color="o-color-3"/>
+ <button data-color="o-color-4"/>
+ <button data-color="o-color-5"/>
+ </div>
+ <div class="o_colorpicker_section" data-name="transparent_grayscale" data-display="Transparent Colors" data-icon-class="fa fa-eye-slash">
+ <button class="o_btn_transparent"/>
+ <button data-color="black-25"/>
+ <button data-color="black-50"/>
+ <button data-color="black-75"/>
+ <button data-color="white-25"/>
+ <button data-color="white-50"/>
+ <button data-color="white-75"/>
+ </div>
+ <div class="o_colorpicker_section" data-name="common" data-display="Common Colors" data-icon-class="fa fa-paint-brush"/>
+ </colorpicker>
+ </t>`;
+const SNIPPETS_TEMPLATE = `
+ <h2 id="snippets_menu">Add blocks</h2>
+ <div id="o_scroll">
+ <div id="snippet_structure" class="o_panel">
+ <div class="o_panel_header">First Panel</div>
+ <div class="o_panel_body">
+ <div name="Separator" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_separator.png">
+ <div class="s_hr pt32 pb32">
+ <hr class="s_hr_1px s_hr_solid w-100 mx-auto"/>
+ </div>
+ </div>
+ <div name="Content" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_text_block.png">
+ <section name="Content+Options" class="test_option_all pt32 pb32" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_text_block.png">
+ <div class="container">
+ <div class="row">
+ <div class="col-lg-10 offset-lg-1 pt32 pb32">
+ <h2>Title</h2>
+ <p class="lead o_default_snippet_text">Content</p>
+ </div>
+ </div>
+ </div>
+ </section>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div id="snippet_options" class="d-none">
+ <div data-js="many2one" data-selector="[data-oe-many2one-model]:not([data-oe-readonly])" data-no-check="true"/>
+ <div data-js="content"
+ data-selector=".s_hr, .test_option_all"
+ data-drop-in=".note-editable"
+ data-drop-near="p, h1, h2, h3, blockquote, .s_hr"/>
+ <div data-js="sizing_y" data-selector=".s_hr, .test_option_all"/>
+ <div data-selector=".test_option_all">
+ <we-colorpicker string="Background Color" data-select-style="true" data-css-property="background-color" data-color-prefix="bg-"/>
+ </div>
+ <div data-js="BackgroundImage" data-selector=".test_option_all">
+ <we-button data-choose-image="true" data-no-preview="true">
+ <i class="fa fa-picture-o"/> Background Image
+ </we-button>
+ </div>
+ <div data-js="option_test" data-selector=".s_hr">
+ <we-select string="Alignment">
+ <we-button data-select-class="align-items-start">Top</we-button>
+ <we-button data-select-class="align-items-center">Middle</we-button>
+ <we-button data-select-class="align-items-end">Bottom</we-button>
+ <we-button data-select-class="align-items-stretch">Equal height</we-button>
+ </we-select>
+ </div>
+ </div>`;
+
+MockServer.include({
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ * @private
+ * @returns {Promise}
+ */
+ async _performRpc(route, args) {
+ if (args.model === "ir.ui.view") {
+ if (args.method === 'read_template' && args.args[0] === "web_editor.colorpicker") {
+ return COLOR_PICKER_TEMPLATE;
+ }
+ if (args.method === 'render_public_asset' && args.args[0] === "web_editor.snippets") {
+ return SNIPPETS_TEMPLATE;
+ }
+ }
+ return this._super(...arguments);
+ },
+});
+
+/**
+ * Options with animation and edition for test.
+ */
+options.registry.option_test = options.Class.extend({
+ cleanForSave: function () {
+ this.$target.addClass('cleanForSave');
+ },
+ onBuilt: function () {
+ this.$target.addClass('built');
+ },
+ onBlur: function () {
+ this.$target.removeClass('focus');
+ },
+ onClone: function () {
+ this.$target.addClass('clone');
+ this.$target.removeClass('focus');
+ },
+ onFocus: function () {
+ this.$target.addClass('focus');
+ },
+ onMove: function () {
+ this.$target.addClass('move');
+ },
+ onRemove: function () {
+ this.$target.closest('.note-editable').addClass('snippet_has_removed');
+ },
+});
+
+
+/**
+ * Constructor WysiwygTest why editable and unbreakable node used in test.
+ */
+var WysiwygTest = Wysiwyg.extend({
+ _parentToDestroyForTest: null,
+ /**
+ * Override 'destroy' of discuss so that it calls 'destroy' on the parent.
+ *
+ * @override
+ */
+ destroy: function () {
+ unpatch();
+ this._super();
+ this.$target.remove();
+ this._parentToDestroyForTest.destroy();
+ },
+});
+
+
+function patch() {
+ testUtils.mock.patch(ajax, {
+ loadAsset: function (xmlId) {
+ if (xmlId === 'template.assets') {
+ return Promise.resolve({
+ cssLibs: [],
+ cssContents: ['body {background-color: red;}']
+ });
+ }
+ if (xmlId === 'template.assets_all_style') {
+ return Promise.resolve({
+ cssLibs: $('link[href]:not([type="image/x-icon"])').map(function () {
+ return $(this).attr('href');
+ }).get(),
+ cssContents: ['body {background-color: red;}']
+ });
+ }
+ throw 'Wrong template';
+ },
+ });
+}
+
+function unpatch() {
+ testUtils.mock.unpatch(ajax);
+}
+
+/**
+ * @param {object} data
+ * @returns {object}
+ */
+function wysiwygData(data) {
+ return _.defaults({}, data, {
+ 'ir.ui.view': {
+ fields: {
+ display_name: {
+ string: "Displayed name",
+ type: "char",
+ },
+ },
+ records: [],
+ read_template(args) {
+ if (args[0] === 'web_editor.colorpicker') {
+ return COLOR_PICKER_TEMPLATE;
+ }
+ },
+ render_template(args) {
+ if (args[0] === 'web_editor.snippets') {
+ return SNIPPETS_TEMPLATE;
+ }
+ },
+ },
+ 'ir.attachment': {
+ fields: {
+ display_name: {
+ string: "display_name",
+ type: 'char',
+ },
+ description: {
+ string: "description",
+ type: 'char',
+ },
+ mimetype: {
+ string: "mimetype",
+ type: 'char',
+ },
+ checksum: {
+ string: "checksum",
+ type: 'char',
+ },
+ url: {
+ string: "url",
+ type: 'char',
+ },
+ type: {
+ string: "type",
+ type: 'char',
+ },
+ res_id: {
+ string: "res_id",
+ type: 'integer',
+ },
+ res_model: {
+ string: "res_model",
+ type: 'char',
+ },
+ public: {
+ string: "public",
+ type: 'boolean',
+ },
+ access_token: {
+ string: "access_token",
+ type: 'char',
+ },
+ image_src: {
+ string: "image_src",
+ type: 'char',
+ },
+ image_width: {
+ string: "image_width",
+ type: 'integer',
+ },
+ image_height: {
+ string: "image_height",
+ type: 'integer',
+ },
+ original_id: {
+ string: "original_id",
+ type: 'many2one',
+ relation: 'ir.attachment',
+ },
+ },
+ records: [{
+ id: 1,
+ name: 'image',
+ description: '',
+ mimetype: 'image/png',
+ checksum: false,
+ url: '/web/image/123/transparent.png',
+ type: 'url',
+ res_id: 0,
+ res_model: false,
+ public: true,
+ access_token: false,
+ image_src: '/web/image/123/transparent.png',
+ image_width: 256,
+ image_height: 256,
+ }],
+ generate_access_token: function () {
+ return;
+ },
+ },
+ });
+}
+
+/**
+ * Create the wysiwyg instance for test (contains patch, usefull ir.ui.view, snippets).
+ *
+ * @param {object} params
+ */
+async function createWysiwyg(params) {
+ patch();
+ params.data = wysiwygData(params.data);
+
+ var parent = new Widget();
+ await testUtils.mock.addMockEnvironment(parent, params);
+
+ var wysiwygOptions = _.extend({}, params.wysiwygOptions, {
+ recordInfo: {
+ context: {},
+ res_model: 'module.test',
+ res_id: 1,
+ },
+ useOnlyTestUnbreakable: params.useOnlyTestUnbreakable,
+ });
+
+ var wysiwyg = new WysiwygTest(parent, wysiwygOptions);
+ wysiwyg._parentToDestroyForTest = parent;
+
+ var $textarea = $('<textarea/>');
+ if (wysiwygOptions.value) {
+ $textarea.val(wysiwygOptions.value);
+ }
+ var selector = params.debug ? 'body' : '#qunit-fixture';
+ $textarea.prependTo($(selector));
+ if (params.debug) {
+ $('body').addClass('debug');
+ }
+ return wysiwyg.attachTo($textarea).then(function () {
+ if (wysiwygOptions.snippets) {
+ var defSnippets = testUtils.makeTestPromise();
+ testUtils.mock.intercept(wysiwyg, "snippets_loaded", function () {
+ defSnippets.resolve(wysiwyg);
+ });
+ return defSnippets;
+ }
+ return wysiwyg;
+ });
+}
+
+
+/**
+ * Char codes.
+ */
+var dom = $.summernote.dom;
+var keyboardMap = {
+ "8": "BACKSPACE",
+ "9": "TAB",
+ "13": "ENTER",
+ "16": "SHIFT",
+ "17": "CONTROL",
+ "18": "ALT",
+ "19": "PAUSE",
+ "20": "CAPS_LOCK",
+ "27": "ESCAPE",
+ "32": "SPACE",
+ "33": "PAGE_UP",
+ "34": "PAGE_DOWN",
+ "35": "END",
+ "36": "HOME",
+ "37": "LEFT",
+ "38": "UP",
+ "39": "RIGHT",
+ "40": "DOWN",
+ "45": "INSERT",
+ "46": "DELETE",
+ "91": "OS_KEY", // 'left command': Windows Key (Windows) or Command Key (Mac)
+ "93": "CONTEXT_MENU", // 'right command'
+};
+_.each(_.range(40, 127), function (keyCode) {
+ if (!keyboardMap[keyCode]) {
+ keyboardMap[keyCode] = String.fromCharCode(keyCode);
+ }
+});
+
+/**
+ * Perform a series of tests (`keyboardTests`) for using keyboard inputs.
+ *
+ * @see wysiwyg_keyboard_tests.js
+ * @see wysiwyg_tests.js
+ *
+ * @param {jQuery} $editable
+ * @param {object} assert
+ * @param {object[]} keyboardTests
+ * @param {string} keyboardTests.name
+ * @param {string} keyboardTests.content
+ * @param {object[]} keyboardTests.steps
+ * @param {string} keyboardTests.steps.start
+ * @param {string} [keyboardTests.steps.end] default: steps.start
+ * @param {string} keyboardTests.steps.key
+ * @param {object} keyboardTests.test
+ * @param {string} [keyboardTests.test.content]
+ * @param {string} [keyboardTests.test.start]
+ * @param {string} [keyboardTests.test.end] default: steps.start
+ * @param {function($editable, assert)} [keyboardTests.test.check]
+ * @param {Number} addTests
+ */
+var testKeyboard = function ($editable, assert, keyboardTests, addTests) {
+ var tests = _.compact(_.pluck(keyboardTests, 'test'));
+ var testNumber = _.compact(_.pluck(tests, 'start')).length +
+ _.compact(_.pluck(tests, 'content')).length +
+ _.compact(_.pluck(tests, 'check')).length +
+ (addTests | 0);
+ assert.expect(testNumber);
+
+ function keydown(target, keypress) {
+ var $target = $(target.tagName ? target : target.parentNode);
+ if (!keypress.keyCode) {
+ keypress.keyCode = +_.findKey(keyboardMap, function (key) {
+ return key === keypress.key;
+ });
+ } else {
+ keypress.key = keyboardMap[keypress.keyCode] || String.fromCharCode(keypress.keyCode);
+ }
+ keypress.keyCode = keypress.keyCode;
+ var event = $.Event("keydown", keypress);
+ $target.trigger(event);
+
+ if (!event.isDefaultPrevented()) {
+ if (keypress.key.length === 1) {
+ textInput($target[0], keypress.key);
+ } else {
+ console.warn('Native "' + keypress.key + '" is not supported in test');
+ }
+ }
+ $target.trigger($.Event("keyup", keypress));
+ return $target;
+ }
+
+ function _select(selector) {
+ // eg: ".class:contents()[0]->1" selects the first contents of the 'class' class, with an offset of 1
+ var reDOMSelection = /^(.+?)(:contents(\(\)\[|\()([0-9]+)[\]|\)])?(->([0-9]+))?$/;
+ var sel = selector.match(reDOMSelection);
+ var $node = $editable.find(sel[1]);
+ var point = {
+ node: sel[3] ? $node.contents()[+sel[4]] : $node[0],
+ offset: sel[5] ? +sel[6] : 0,
+ };
+ if (!point.node || point.offset > (point.node.tagName ? point.node.childNodes : point.node.textContent).length) {
+ assert.notOk("Node not found: '" + selector + "' " + (point.node ? "(container: '" + (point.node.outerHTML || point.node.textContent) + "')" : ""));
+ }
+ return point;
+ }
+
+ function selectText(start, end) {
+ start = _select(start);
+ var target = start.node;
+ $(target.tagName ? target : target.parentNode).trigger("mousedown");
+ if (end) {
+ end = _select(end);
+ Wysiwyg.setRange(start.node, start.offset, end.node, end.offset);
+ } else {
+ Wysiwyg.setRange(start.node, start.offset);
+ }
+ target = end ? end.node : start.node;
+ $(target.tagName ? target : target.parentNode).trigger('mouseup');
+ }
+
+ function endOfAreaBetweenTwoNodes(point) {
+ // move the position because some browser make the caret on the end of the previous area after normalize
+ if (
+ !point.node.tagName &&
+ point.offset === point.node.textContent.length &&
+ !/\S|\u00A0/.test(point.node.textContent)
+ ) {
+ point = dom.nextPoint(dom.nextPoint(point));
+ while (point.node.tagName && point.node.textContent.length) {
+ point = dom.nextPoint(point);
+ }
+ }
+ return point;
+ }
+
+ var defPollTest = Promise.resolve();
+
+ function pollTest(test) {
+ var def = Promise.resolve();
+ $editable.data('wysiwyg').setValue(test.content);
+
+ function poll(step) {
+ var def = testUtils.makeTestPromise();
+ if (step.start) {
+ selectText(step.start, step.end);
+ if (!Wysiwyg.getRange($editable[0])) {
+ throw 'Wrong range! \n' +
+ 'Test: ' + test.name + '\n' +
+ 'Selection: ' + step.start + '" to "' + step.end + '"\n' +
+ 'DOM: ' + $editable.html();
+ }
+ }
+ setTimeout(function () {
+ if (step.keyCode || step.key) {
+ var target = Wysiwyg.getRange($editable[0]).ec;
+ if (window.location.search.indexOf('notrycatch') !== -1) {
+ keydown(target, {
+ key: step.key,
+ keyCode: step.keyCode,
+ ctrlKey: !!step.ctrlKey,
+ shiftKey: !!step.shiftKey,
+ altKey: !!step.altKey,
+ metaKey: !!step.metaKey,
+ });
+ } else {
+ try {
+ keydown(target, {
+ key: step.key,
+ keyCode: step.keyCode,
+ ctrlKey: !!step.ctrlKey,
+ shiftKey: !!step.shiftKey,
+ altKey: !!step.altKey,
+ metaKey: !!step.metaKey,
+ });
+ } catch (e) {
+ assert.notOk(e.name + '\n\n' + e.stack, test.name);
+ }
+ }
+ }
+ setTimeout(function () {
+ if (step.keyCode || step.key) {
+ var $target = $(target.tagName ? target : target.parentNode);
+ $target.trigger($.Event('keyup', {
+ key: step.key,
+ keyCode: step.keyCode,
+ ctrlKey: !!step.ctrlKey,
+ shiftKey: !!step.shiftKey,
+ altKey: !!step.altKey,
+ metaKey: !!step.metaKey,
+ }));
+ }
+ setTimeout(def.resolve.bind(def));
+ });
+ });
+ return def;
+ }
+ while (test.steps.length) {
+ def = def.then(poll.bind(null, test.steps.shift()));
+ }
+
+ return def.then(function () {
+ if (!test.test) {
+ return;
+ }
+
+ if (test.test.check) {
+ test.test.check($editable, assert);
+ }
+
+ // test content
+ if (test.test.content) {
+ var value = $editable.data('wysiwyg').getValue({
+ keepPopover: true,
+ });
+ var allInvisible = /\u200B/g;
+ value = value.replace(allInvisible, '&#8203;');
+ var result = test.test.content.replace(allInvisible, '&#8203;');
+ assert.strictEqual(value, result, test.name);
+
+ if (test.test.start && value !== result) {
+ assert.notOk("Wrong DOM (see previous assert)", test.name + " (carret position)");
+ return;
+ }
+ }
+
+ $editable[0].normalize();
+
+ // test carret position
+ if (test.test.start) {
+ var start = _select(test.test.start);
+ var range = Wysiwyg.getRange($editable[0]);
+ if ((range.sc !== range.ec || range.so !== range.eo) && !test.test.end) {
+ assert.ok(false, test.name + ": the carret is not colapsed and the 'end' selector in test is missing");
+ return;
+ }
+ var end = test.test.end ? _select(test.test.end) : start;
+ if (start.node && end.node) {
+ range = Wysiwyg.getRange($editable[0]);
+ var startPoint = endOfAreaBetweenTwoNodes({
+ node: range.sc,
+ offset: range.so,
+ });
+ var endPoint = endOfAreaBetweenTwoNodes({
+ node: range.ec,
+ offset: range.eo,
+ });
+ var sameDOM = (startPoint.node.outerHTML || startPoint.node.textContent) === (start.node.outerHTML || start.node.textContent);
+ var stringify = function (obj) {
+ if (!sameDOM) {
+ delete obj.sameDOMsameNode;
+ }
+ return JSON.stringify(obj, null, 2)
+ .replace(/"([^"\s-]+)":/g, "\$1:")
+ .replace(/([^\\])"/g, "\$1'")
+ .replace(/\\"/g, '"');
+ };
+ assert.deepEqual(stringify({
+ startNode: startPoint.node.outerHTML || startPoint.node.textContent,
+ startOffset: startPoint.offset,
+ endPoint: endPoint.node.outerHTML || endPoint.node.textContent,
+ endOffset: endPoint.offset,
+ sameDOMsameNode: sameDOM && startPoint.node === start.node,
+ }),
+ stringify({
+ startNode: start.node.outerHTML || start.node.textContent,
+ startOffset: start.offset,
+ endPoint: end.node.outerHTML || end.node.textContent,
+ endOffset: end.offset,
+ sameDOMsameNode: true,
+ }),
+ test.name + " (carret position)");
+ }
+ }
+ });
+ }
+ while (keyboardTests.length) {
+ defPollTest = defPollTest.then(pollTest.bind(null, keyboardTests.shift()));
+ }
+
+ return defPollTest;
+};
+
+
+/**
+ * Select a node in the dom with is offset.
+ *
+ * @param {String} startSelector
+ * @param {String} endSelector
+ * @param {jQuery} $editable
+ * @returns {Object} {sc, so, ec, eo}
+ */
+var select = (function () {
+ var __select = function (selector, $editable) {
+ var sel = selector.match(/^(.+?)(:contents\(\)\[([0-9]+)\]|:contents\(([0-9]+)\))?(->([0-9]+))?$/);
+ var $node = $editable.find(sel[1]);
+ return {
+ node: sel[2] ? $node.contents()[sel[3] ? +sel[3] : +sel[4]] : $node[0],
+ offset: sel[5] ? +sel[6] : 0,
+ };
+ };
+ return function (startSelector, endSelector, $editable) {
+ var start = __select(startSelector, $editable);
+ var end = endSelector ? __select(endSelector, $editable) : start;
+ return {
+ sc: start.node,
+ so: start.offset,
+ ec: end.node,
+ eo: end.offset,
+ };
+ };
+})();
+
+/**
+ * Trigger a keydown event.
+ *
+ * @param {String or Number} key (name or code)
+ * @param {jQuery} $editable
+ * @param {Object} [options]
+ * @param {Boolean} [options.firstDeselect] (default: false) true to deselect before pressing
+ */
+var keydown = function (key, $editable, options) {
+ var keyPress = {};
+ if (typeof key === 'string') {
+ keyPress.key = key;
+ keyPress.keyCode = +_.findKey(keyboardMap, function (k) {
+ return k === key;
+ });
+ } else {
+ keyPress.key = keyboardMap[key] || String.fromCharCode(key);
+ keyPress.keyCode = key;
+ }
+ var range = Wysiwyg.getRange($editable[0]);
+ if (!range) {
+ console.error("Editor have not any range");
+ return;
+ }
+ if (options && options.firstDeselect) {
+ range.sc = range.ec;
+ range.so = range.eo;
+ Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
+ }
+ var target = range.ec;
+ var $target = $(target.tagName ? target : target.parentNode);
+ var event = $.Event("keydown", keyPress);
+ $target.trigger(event);
+
+ if (!event.isDefaultPrevented()) {
+ if (keyPress.key.length === 1) {
+ textInput($target[0], keyPress.key);
+ } else {
+ console.warn('Native "' + keyPress.key + '" is not supported in test');
+ }
+ }
+};
+
+var textInput = function (target, char) {
+ var ev = new CustomEvent('textInput', {
+ bubbles: true,
+ cancelBubble: false,
+ cancelable: true,
+ composed: true,
+ data: char,
+ defaultPrevented: false,
+ detail: 0,
+ eventPhase: 3,
+ isTrusted: true,
+ returnValue: true,
+ sourceCapabilities: null,
+ type: "textInput",
+ which: 0,
+ });
+ ev.data = char;
+ target.dispatchEvent(ev);
+
+ if (!ev.defaultPrevented) {
+ document.execCommand("insertText", 0, ev.data);
+ }
+};
+
+return {
+ wysiwygData: wysiwygData,
+ createWysiwyg: createWysiwyg,
+ testKeyboard: testKeyboard,
+ select: select,
+ keydown: keydown,
+ patch: patch,
+ unpatch: unpatch,
+};
+
+
+});