field_html_tests.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912
  1. odoo.define('web_editor.field_html_tests', function (require) {
  2. "use strict";
  3. var ajax = require('web.ajax');
  4. var FormController = require('web.FormController');
  5. var FormView = require('web.FormView');
  6. var testUtils = require('web.test_utils');
  7. var weTestUtils = require('web_editor.test_utils');
  8. var core = require('web.core');
  9. var Wysiwyg = require('web_editor.wysiwyg');
  10. const { MediaDialogWrapper } = require('@web_editor/components/media_dialog/media_dialog');
  11. var LinkDialog = require('wysiwyg.widgets.LinkDialog');
  12. const { legacyExtraNextTick, patchWithCleanup } = require("@web/../tests/helpers/utils");
  13. const { useEffect } = require("@odoo/owl");
  14. var _t = core._t;
  15. let _formResolveTestPromise;
  16. FormController.include({
  17. _setEditMode: async function () {
  18. await this._super.apply(this, arguments);
  19. if (_formResolveTestPromise) {
  20. _formResolveTestPromise();
  21. }
  22. }
  23. });
  24. QUnit.module('web_editor', {}, function () {
  25. QUnit.module('field html', {
  26. beforeEach: function () {
  27. this.linkDialogTestHtml = '<p><a href="https://www.external.com" target="_blank">External website</a></p>' +
  28. '<p><a href="' + window.location.href + '/test">This website</a></p>' +
  29. '<p>New external link</p><p>New internal link</p>';
  30. this.data = weTestUtils.wysiwygData({
  31. 'note.note': {
  32. fields: {
  33. display_name: {
  34. string: "Displayed name",
  35. type: "char"
  36. },
  37. header: {
  38. string: "Header",
  39. type: "html",
  40. required: true,
  41. },
  42. body: {
  43. string: "Message",
  44. type: "html"
  45. },
  46. },
  47. records: [{
  48. id: 1,
  49. display_name: "first record",
  50. header: "<p> &nbsp;&nbsp; <br> </p>",
  51. body: "<p>toto toto toto</p><p>tata</p>",
  52. }, {
  53. id: 2,
  54. display_name: "second record",
  55. header: "<p>Hello World</p>",
  56. body: '<p><a href="https://www.external.com" target="_blank">External website</a></p>',
  57. }, {
  58. id: 3,
  59. display_name: "third record",
  60. header: "<p>Hello World</p>",
  61. body: '<p><a href="' + window.location.href + '/test">This website</a></p>',
  62. }, {
  63. id: 4,
  64. display_name: "fourth record",
  65. header: "<p>Hello World</p>",
  66. body: '<p>New external link</p>',
  67. }, {
  68. id: 5,
  69. display_name: "fifth record",
  70. header: "<p>Hello World</p>",
  71. body: '<p>New internal link</p>',
  72. }, {
  73. id: 6,
  74. display_name: "sixth record",
  75. header: "<p>Hello World</p>",
  76. body: `
  77. <div class="o_form_sheet_bg">
  78. <div class="clearfix position-relative o_form_sheet" style="width: 1140px;">
  79. <div class="o_notebook">
  80. <div class="tab-content">
  81. <div class="tab-pane active" id="notebook_page_820">
  82. <div class="oe_form_field oe_form_field_html o_field_widget" name="description" style="margin-bottom: 5px;">
  83. hacky code to test
  84. </div>
  85. </div>
  86. </div>
  87. </div>
  88. </div>
  89. </div>`,
  90. }, {
  91. id: 7,
  92. display_name: "seventh record",
  93. header: "<p>Hello World</p>",
  94. body: `
  95. <p class="a">
  96. a
  97. </p>
  98. <p class="b o_not_editable">
  99. b
  100. </p>`,
  101. }],
  102. },
  103. 'mass.mailing': {
  104. fields: {
  105. display_name: {
  106. string: "Displayed name",
  107. type: "char"
  108. },
  109. body_html: {
  110. string: "Message Body inline (to send)",
  111. type: "html"
  112. },
  113. body_arch: {
  114. string: "Message Body for edition",
  115. type: "html"
  116. },
  117. },
  118. records: [{
  119. id: 1,
  120. display_name: "first record",
  121. body_html: "<div class='field_body' style='background-color: red;'>yep</div>",
  122. body_arch: "<div class='field_body'>yep</div>",
  123. }],
  124. },
  125. });
  126. testUtils.mock.patch(ajax, {
  127. loadAsset: function (xmlId) {
  128. if (xmlId === 'template.assets') {
  129. return Promise.resolve({
  130. cssLibs: [],
  131. cssContents: ['body {background-color: red;}']
  132. });
  133. }
  134. if (xmlId === 'template.assets_all_style') {
  135. return Promise.resolve({
  136. cssLibs: $('link[href]:not([type="image/x-icon"])').map(function () {
  137. return $(this).attr('href');
  138. }).get(),
  139. cssContents: ['body {background-color: red;}']
  140. });
  141. }
  142. throw 'Wrong template';
  143. },
  144. });
  145. },
  146. afterEach: function () {
  147. testUtils.mock.unpatch(ajax);
  148. },
  149. }, function () {
  150. QUnit.module('basic');
  151. QUnit.test('simple rendering', async function (assert) {
  152. assert.expect(3);
  153. var form = await testUtils.createView({
  154. View: FormView,
  155. model: 'note.note',
  156. data: this.data,
  157. arch: '<form>' +
  158. '<field name="body" widget="html" style="height: 100px"/>' +
  159. '</form>',
  160. res_id: 1,
  161. });
  162. var $field = form.$('.oe_form_field[name="body"]');
  163. assert.strictEqual($field.children('.o_readonly').html(),
  164. '<p>toto toto toto</p><p>tata</p>',
  165. "should have rendered a div with correct content in readonly");
  166. assert.strictEqual($field.attr('style'), 'height: 100px',
  167. "should have applied the style correctly");
  168. const promise = new Promise((resolve) => _formResolveTestPromise = resolve);
  169. await testUtils.form.clickEdit(form);
  170. await promise;
  171. $field = form.$('.oe_form_field[name="body"]');
  172. assert.strictEqual($field.find('.note-editable').html(),
  173. '<p>toto toto toto</p><p>tata</p>',
  174. "should have rendered the field correctly in edit");
  175. form.destroy();
  176. });
  177. QUnit.test('notebooks defined inside HTML field widgets are ignored when calling setLocalState', async function (assert) {
  178. assert.expect(1);
  179. var form = await testUtils.createView({
  180. View: FormView,
  181. model: 'note.note',
  182. data: this.data,
  183. arch: '<form>' +
  184. '<field name="body" widget="html" style="height: 100px"/>' +
  185. '</form>',
  186. res_id: 6,
  187. });
  188. // check that there is no error on clicking Edit
  189. const promise = new Promise((resolve) => _formResolveTestPromise = resolve);
  190. await testUtils.form.clickEdit(form);
  191. await promise;
  192. assert.containsOnce(form, '.o_form_editable');
  193. form.destroy();
  194. });
  195. QUnit.test('check if required field is set', async function (assert) {
  196. assert.expect(3);
  197. var form = await testUtils.createView({
  198. View: FormView,
  199. model: 'note.note',
  200. data: this.data,
  201. arch: '<form>' +
  202. '<field name="header" widget="html" style="height: 100px" />' +
  203. '</form>',
  204. res_id: 1,
  205. });
  206. testUtils.mock.intercept(form, 'call_service', function (ev) {
  207. if (ev.data.service === 'notification') {
  208. assert.strictEqual(ev.data.args[0].title, 'Invalid fields:');
  209. assert.strictEqual(ev.data.args[0].message.toString(), '<ul><li>Header</li></ul>');
  210. assert.strictEqual(ev.data.args[0].type, 'danger');
  211. }
  212. }, true);
  213. const promise = new Promise((resolve) => _formResolveTestPromise = resolve);
  214. await testUtils.form.clickEdit(form);
  215. await promise;
  216. await testUtils.dom.click(form.$('.o_form_button_save'));
  217. form.destroy();
  218. });
  219. QUnit.test('colorpicker', async function (assert) {
  220. assert.expect(7);
  221. var form = await testUtils.createView({
  222. View: FormView,
  223. model: 'note.note',
  224. data: this.data,
  225. arch: '<form>' +
  226. '<field name="body" widget="html" style="height: 100px"/>' +
  227. '</form>',
  228. res_id: 1,
  229. });
  230. await testUtils.form.clickEdit(form);
  231. await new Promise(resolve => setTimeout(resolve, 50));
  232. var $field = form.$('.oe_form_field[name="body"]');
  233. // select the text
  234. var pText = $field.find('.note-editable p').first().contents()[0];
  235. Wysiwyg.setRange(pText, 1, pText, 10);
  236. // text is selected
  237. var range = Wysiwyg.getRange();
  238. assert.strictEqual(range.sc, pText,
  239. "should select the text");
  240. async function openColorpicker(selector) {
  241. const $colorpicker = $(selector);
  242. const openingProm = new Promise(resolve => {
  243. $colorpicker.one('shown.bs.dropdown', () => resolve());
  244. });
  245. await testUtils.dom.click($colorpicker.find('button:first'));
  246. return openingProm;
  247. }
  248. await openColorpicker('#toolbar .note-back-color-preview');
  249. assert.ok($('.note-back-color-preview .dropdown-menu').hasClass('show'),
  250. "should display the color picker");
  251. await testUtils.dom.click($('#toolbar .note-back-color-preview .o_we_color_btn[style="background-color:#00FFFF;"]'));
  252. assert.ok(!$field.find('.note-back-color-preview').hasClass('show'),
  253. "should close the color picker");
  254. assert.strictEqual($field.find('.note-editable').html(),
  255. '<p>t<font style="background-color: rgb(0, 255, 255);">oto toto </font>toto</p><p>tata</p>',
  256. "should have rendered the field correctly in edit");
  257. var fontElement = $field.find('.note-editable font')[0];
  258. var rangeControl = {
  259. sc: fontElement,
  260. so: 0,
  261. ec: fontElement,
  262. eo: 1,
  263. };
  264. range = Wysiwyg.getRange();
  265. assert.deepEqual(_.pick(range, 'sc', 'so', 'ec', 'eo'), rangeControl,
  266. "should select the text after color change");
  267. // select the text
  268. pText = $field.find('.note-editable p').first().contents()[2];
  269. Wysiwyg.setRange(fontElement.firstChild, 5, pText, 2);
  270. // text is selected
  271. await openColorpicker('#toolbar .note-back-color-preview');
  272. await testUtils.dom.click($('#toolbar .note-back-color-preview [style="background-color: var(--we-cp-o-color-3);"]'));
  273. assert.strictEqual($field.find('.note-editable').html(),
  274. '<p>t<font style="background-color: rgb(0, 255, 255);">oto t</font><font style="" class="bg-o-color-3">oto to</font>to</p><p>tata</p>',
  275. "should have rendered the field correctly in edit");
  276. // Make sure the reset button works too
  277. await openColorpicker('#toolbar .note-back-color-preview');
  278. await testUtils.dom.click($('#toolbar .note-back-color-preview .o_colorpicker_reset'));
  279. // TODO right now the behavior is to force "inherit" as background
  280. // but it should remove the useless font element when possible.
  281. assert.strictEqual($field.find('.note-editable').html(),
  282. '<p>t<font style="background-color: rgb(0, 255, 255);">oto t</font>oto toto</p><p>tata</p>',
  283. "should have properly reset the background color");
  284. form.destroy();
  285. });
  286. QUnit.test('media dialog: image', async function (assert) {
  287. assert.expect(1);
  288. var form = await testUtils.createView({
  289. View: FormView,
  290. model: 'note.note',
  291. data: this.data,
  292. arch: '<form>' +
  293. '<field name="body" widget="html" style="height: 100px"/>' +
  294. '</form>',
  295. res_id: 1,
  296. mockRPC: function (route, args) {
  297. if (args.model === 'ir.attachment') {
  298. if (args.method === "generate_access_token") {
  299. return Promise.resolve();
  300. }
  301. }
  302. if (route.indexOf('/web/image/123/transparent.png') === 0) {
  303. return Promise.resolve();
  304. }
  305. if (route.indexOf('/web_unsplash/fetch_images') === 0) {
  306. return Promise.resolve();
  307. }
  308. if (route.indexOf('/web_editor/media_library_search') === 0) {
  309. return Promise.resolve();
  310. }
  311. return this._super(route, args);
  312. },
  313. });
  314. await testUtils.form.clickEdit(form);
  315. var $field = form.$('.oe_form_field[name="body"]');
  316. var pText = $field.find('.note-editable p').first().contents()[0];
  317. Wysiwyg.setRange(pText, 1, pText, 2);
  318. await new Promise((resolve) => setTimeout(resolve));
  319. const wysiwyg = $field.find('.note-editable').data('wysiwyg');
  320. // Mock the MediaDialogWrapper
  321. const defMediaDialog = testUtils.makeTestPromise();
  322. patchWithCleanup(MediaDialogWrapper.prototype, {
  323. setup() {
  324. useEffect(() => {
  325. this.save();
  326. }, () => []);
  327. },
  328. save() {
  329. const imageEl = document.createElement('img');
  330. imageEl.src = '/web/image/123/transparent.png';
  331. this.props.save(imageEl);
  332. defMediaDialog.resolve();
  333. },
  334. });
  335. wysiwyg.openMediaDialog();
  336. await defMediaDialog;
  337. var $editable = form.$('.oe_form_field[name="body"] .note-editable');
  338. assert.ok($editable.find('img')[0].dataset.src.includes('/web/image/123/transparent.png'),
  339. "should have the image in the dom");
  340. form.destroy();
  341. });
  342. QUnit.test('media dialog: icon', async function (assert) {
  343. assert.expect(1);
  344. var form = await testUtils.createView({
  345. View: FormView,
  346. model: 'note.note',
  347. data: this.data,
  348. arch: '<form>' +
  349. '<field name="body" widget="html" style="height: 100px"/>' +
  350. '</form>',
  351. res_id: 1,
  352. mockRPC: function (route, args) {
  353. if (args.model === 'ir.attachment') {
  354. return Promise.resolve([]);
  355. }
  356. if (route.indexOf('/web_unsplash/fetch_images') === 0) {
  357. return Promise.resolve();
  358. }
  359. return this._super(route, args);
  360. },
  361. });
  362. await testUtils.form.clickEdit(form);
  363. var $field = form.$('.oe_form_field[name="body"]');
  364. var pText = $field.find('.note-editable p').first().contents()[0];
  365. Wysiwyg.setRange(pText, 1, pText, 2);
  366. const wysiwyg = $field.find('.note-editable').data('wysiwyg');
  367. // Mock the MediaDialogWrapper
  368. const defMediaDialog = testUtils.makeTestPromise();
  369. patchWithCleanup(MediaDialogWrapper.prototype, {
  370. setup() {
  371. useEffect(() => {
  372. this.save();
  373. }, () => []);
  374. },
  375. save() {
  376. const iconEl = document.createElement('span');
  377. iconEl.classList.add('fa', 'fa-glass');
  378. this.props.save(iconEl);
  379. defMediaDialog.resolve();
  380. },
  381. });
  382. wysiwyg.openMediaDialog();
  383. await defMediaDialog;
  384. var $editable = form.$('.oe_form_field[name="body"] .note-editable');
  385. assert.strictEqual($editable.data('wysiwyg').getValue(),
  386. '<p>t<span class="fa fa-glass"></span>to toto toto</p><p>tata</p>',
  387. "should have the image in the dom");
  388. form.destroy();
  389. });
  390. QUnit.test('link dialog - external link - no edit', async function (assert) {
  391. assert.expect(2);
  392. const form = await testUtils.createView({
  393. View: FormView,
  394. model: 'note.note',
  395. data: this.data,
  396. arch: '<form>' +
  397. '<field name="body" widget="html" style="height: 100px"/>' +
  398. '</form>',
  399. res_id: 2,
  400. });
  401. let $field = form.$('.oe_form_field[name="body"]');
  402. assert.strictEqual($field.children('.o_readonly').html(),
  403. '<p><a href="https://www.external.com" target="_blank" rel="noreferrer">External website</a></p>',
  404. "should have rendered a div with correct content in readonly");
  405. const promise = new Promise((resolve) => _formResolveTestPromise = resolve);
  406. await testUtils.form.clickEdit(form);
  407. await promise;
  408. $field = form.$('.oe_form_field[name="body"]');
  409. // the dialog load some xml assets
  410. const defLinkDialog = testUtils.makeTestPromise();
  411. testUtils.mock.patch(LinkDialog, {
  412. init: function () {
  413. this._super.apply(this, arguments);
  414. this.opened(defLinkDialog.resolve.bind(defLinkDialog));
  415. }
  416. });
  417. let pText = $field.find('.note-editable p').first().contents()[0];
  418. Wysiwyg.setRange(pText.firstChild, 0, pText.firstChild, pText.firstChild.length);
  419. await testUtils.dom.triggerEvent($('#toolbar #create-link'), 'click');
  420. // load static xml file (dialog, link dialog)
  421. await defLinkDialog;
  422. $('.modal .tab-content .tab-pane').removeClass('fade'); // to be sync in test
  423. await testUtils.dom.click($('.modal .modal-footer button:contains(Save)'));
  424. await testUtils.form.clickSave(form);
  425. $field = form.$('.oe_form_field[name="body"]');
  426. assert.strictEqual($field.children('.o_readonly').html(),
  427. '<p><a href="https://www.external.com" target="_blank" rel="noreferrer">External website</a></p>',
  428. "the link shouldn't change");
  429. testUtils.mock.unpatch(LinkDialog);
  430. form.destroy();
  431. });
  432. QUnit.test('link dialog - internal link - no edit', async function (assert) {
  433. assert.expect(2);
  434. const form = await testUtils.createView({
  435. View: FormView,
  436. model: 'note.note',
  437. data: this.data,
  438. arch: '<form>' +
  439. '<field name="body" widget="html" style="height: 100px"/>' +
  440. '</form>',
  441. res_id: 3,
  442. });
  443. let $field = form.$('.oe_form_field[name="body"]');
  444. assert.strictEqual($field.children('.o_readonly').html(),
  445. '<p><a href="' + window.location.href.replace(/&/g, "&amp;") + '/test">This website</a></p>',
  446. "should have rendered a div with correct content in readonly");
  447. const promise = new Promise((resolve) => _formResolveTestPromise = resolve);
  448. await testUtils.form.clickEdit(form);
  449. await promise;
  450. $field = form.$('.oe_form_field[name="body"]');
  451. // the dialog load some xml assets
  452. const defLinkDialog = testUtils.makeTestPromise();
  453. testUtils.mock.patch(LinkDialog, {
  454. init: function () {
  455. this._super.apply(this, arguments);
  456. this.opened(defLinkDialog.resolve.bind(defLinkDialog));
  457. }
  458. });
  459. let pText = $field.find('.note-editable p').first().contents()[0];
  460. Wysiwyg.setRange(pText.firstChild, 0, pText.firstChild, pText.firstChild.length);
  461. await testUtils.dom.triggerEvent($('#toolbar #create-link'), 'click');
  462. // load static xml file (dialog, link dialog)
  463. await defLinkDialog;
  464. $('.modal .tab-content .tab-pane').removeClass('fade'); // to be sync in test
  465. await testUtils.dom.click($('.modal input#o_link_dialog_url_strip_domain'));
  466. await testUtils.dom.click($('.modal .modal-footer button:contains(Save)'));
  467. await testUtils.form.clickSave(form);
  468. $field = form.$('.oe_form_field[name="body"]');
  469. assert.strictEqual($field.children('.o_readonly').html(),
  470. '<p><a href="' + window.location.href.replace(/&/g, "&amp;") + '/test">This website</a></p>',
  471. "the link shouldn't change");
  472. testUtils.mock.unpatch(LinkDialog);
  473. form.destroy();
  474. });
  475. QUnit.test('link dialog - external link - new', async function (assert) {
  476. assert.expect(2);
  477. const form = await testUtils.createView({
  478. View: FormView,
  479. model: 'note.note',
  480. data: this.data,
  481. arch: '<form>' +
  482. '<field name="body" widget="html" style="height: 100px"/>' +
  483. '</form>',
  484. res_id: 4,
  485. });
  486. let $field = form.$('.oe_form_field[name="body"]');
  487. assert.strictEqual($field.children('.o_readonly').html(), '<p>New external link</p>',
  488. "should have rendered a div with correct content in readonly");
  489. const promise = new Promise((resolve) => _formResolveTestPromise = resolve);
  490. await testUtils.form.clickEdit(form);
  491. await promise;
  492. $field = form.$('.oe_form_field[name="body"]');
  493. // the dialog load some xml assets
  494. const defLinkDialog = testUtils.makeTestPromise();
  495. testUtils.mock.patch(LinkDialog, {
  496. init: function () {
  497. this._super.apply(this, arguments);
  498. this.opened(defLinkDialog.resolve.bind(defLinkDialog));
  499. }
  500. });
  501. let pText = $field.find('.note-editable p').first().contents()[0];
  502. Wysiwyg.setRange(pText, 0, pText, pText.length);
  503. await testUtils.dom.triggerEvent($('#toolbar #create-link'), 'click');
  504. // load static xml file (dialog, link dialog)
  505. await defLinkDialog;
  506. $('.modal .tab-content .tab-pane').removeClass('fade'); // to be sync in test
  507. $('input#o_link_dialog_url_input').val('www.test.com');
  508. await testUtils.dom.click($('.modal .modal-footer button:contains(Save)'));
  509. await testUtils.form.clickSave(form);
  510. $field = form.$('.oe_form_field[name="body"]');
  511. assert.strictEqual($field.children('.o_readonly').html(),
  512. '<p><a href="http://www.test.com" target="_blank" rel="noreferrer">New external link</a></p>',
  513. "the link should be created with the right format");
  514. testUtils.mock.unpatch(LinkDialog);
  515. form.destroy();
  516. });
  517. QUnit.test('link dialog - internal link - new', async function (assert) {
  518. assert.expect(3);
  519. const form = await testUtils.createView({
  520. View: FormView,
  521. model: 'note.note',
  522. data: this.data,
  523. arch: '<form>' +
  524. '<field name="body" widget="html" style="height: 100px"/>' +
  525. '</form>',
  526. res_id: 5,
  527. });
  528. let $field = form.$('.oe_form_field[name="body"]');
  529. assert.strictEqual($field.children('.o_readonly').html(), '<p>New internal link</p>',
  530. "should have rendered a div with correct content in readonly");
  531. let promise = new Promise((resolve) => _formResolveTestPromise = resolve);
  532. await testUtils.form.clickEdit(form);
  533. await promise;
  534. $field = form.$('.oe_form_field[name="body"]');
  535. // the dialog load some xml assets
  536. const defLinkDialog = testUtils.makeTestPromise();
  537. testUtils.mock.patch(LinkDialog, {
  538. init: function () {
  539. this._super.apply(this, arguments);
  540. this.opened(defLinkDialog.resolve.bind(defLinkDialog));
  541. }
  542. });
  543. let pText = $field.find('.note-editable p').first().contents()[0];
  544. Wysiwyg.setRange(pText, 0, pText, pText.length);
  545. await testUtils.dom.triggerEvent($('#toolbar #create-link'), 'click');
  546. // load static xml file (dialog, link dialog)
  547. await defLinkDialog;
  548. $('.modal .tab-content .tab-pane').removeClass('fade'); // to be sync in test
  549. const $input = $('input#o_link_dialog_url_input');
  550. await testUtils.fields.editAndTrigger($input, window.location.href + '/test', ["change"]);
  551. $('.modal input#o_link_dialog_url_strip_domain').click();
  552. await testUtils.dom.click($('.modal .modal-footer button:contains(Save)'));
  553. await testUtils.form.clickSave(form);
  554. $field = form.$('.oe_form_field[name="body"]');
  555. assert.strictEqual($field.children('.o_readonly').html(),
  556. '<p><a href="' + window.location.href.replace(/&/g, "&amp;") + '/test">New internal link</a></p>',
  557. "the link should be created with the right format");
  558. promise = new Promise((resolve) => _formResolveTestPromise = resolve);
  559. await testUtils.form.clickEdit(form);
  560. await promise;
  561. $field = form.$('.oe_form_field[name="body"]');
  562. pText = $field.find('.note-editable a').eq(0).contents()[0];
  563. Wysiwyg.setRange(pText, 0, pText, pText.length);
  564. await testUtils.dom.triggerEvent($('#toolbar #create-link'), 'click');
  565. // load static xml file (dialog, link dialog)
  566. await defLinkDialog;
  567. $('.modal .tab-content .tab-pane').removeClass('fade'); // to be sync in test
  568. await testUtils.dom.click($('.modal .modal-footer button:contains(Save)'));
  569. await testUtils.form.clickSave(form);
  570. $field = form.$('.oe_form_field[name="body"]');
  571. assert.strictEqual($field.children('.o_readonly').html(),
  572. '<p><a href="' + window.location.href.slice(window.location.origin.length).replace(/&/g, "&amp;") + '/test">New internal link</a></p>',
  573. "the link should be created with the right format");
  574. testUtils.mock.unpatch(LinkDialog);
  575. form.destroy();
  576. });
  577. QUnit.test('save', async function (assert) {
  578. assert.expect(0);
  579. var form = await testUtils.createView({
  580. View: FormView,
  581. model: 'note.note',
  582. data: this.data,
  583. arch: '<form>' +
  584. '<field name="body" widget="html" style="height: 100px"/>' +
  585. '</form>',
  586. res_id: 1,
  587. mockRPC: function (route, args) {
  588. if (args.method === "write") {
  589. assert.strictEqual(args.args[1].body,
  590. '<p>t<font class="bg-o-color-3">oto toto&nbsp;</font>toto</p><p>tata</p>',
  591. "should save the content");
  592. }
  593. return this._super.apply(this, arguments);
  594. },
  595. });
  596. await testUtils.form.clickEdit(form);
  597. await testUtils.form.clickSave(form);
  598. form.destroy();
  599. });
  600. QUnit.test('Quick Edition: click on link inside html field', async function (assert) {
  601. assert.expect(6);
  602. this.data['note.note'].records[0]['body'] = '<p><a href="#">hello</a> world</p>';
  603. const MULTI_CLICK_DELAY = 6498651354; // arbitrary large number to identify setTimeout calls
  604. let quickEditCB;
  605. let quickEditTimeoutId;
  606. let nextId = 1;
  607. const originalSetTimeout = window.setTimeout;
  608. const originalClearTimeout = window.clearTimeout;
  609. patchWithCleanup(window, {
  610. setTimeout(fn, delay) {
  611. if (delay === MULTI_CLICK_DELAY) {
  612. quickEditCB = fn;
  613. quickEditTimeoutId = `quick_edit_${nextId++}`;
  614. return quickEditTimeoutId;
  615. } else {
  616. return originalSetTimeout(...arguments);
  617. }
  618. },
  619. clearTimeout(id) {
  620. if (id === quickEditTimeoutId) {
  621. quickEditCB = undefined;
  622. } else {
  623. return originalClearTimeout(...arguments);
  624. }
  625. },
  626. });
  627. const form = await testUtils.createView({
  628. View: FormView,
  629. model: 'note.note',
  630. data: this.data,
  631. arch: '<form>' +
  632. '<field name="body" widget="html" style="height: 100px"/>' +
  633. '</form>',
  634. formMultiClickTime: MULTI_CLICK_DELAY,
  635. res_id: 1,
  636. });
  637. assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
  638. await testUtils.dom.click(form.$('.oe_form_field[name="body"] a'));
  639. await testUtils.nextTick();
  640. assert.strictEqual(quickEditCB, undefined, "no quickEdit callback should have been set");
  641. assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
  642. await testUtils.dom.click(form.$('.oe_form_field[name="body"] p'));
  643. await testUtils.nextTick();
  644. assert.containsOnce(form, '.o_legacy_form_view.o_form_readonly');
  645. assert.ok(quickEditCB, "quickEdit callback should have been set");
  646. quickEditCB();
  647. await testUtils.nextTick();
  648. await legacyExtraNextTick();
  649. assert.containsOnce(form, '.o_legacy_form_view.o_form_editable');
  650. form.destroy();
  651. });
  652. QUnit.test('.o_not_editable should be contenteditable=false', async function (assert) {
  653. assert.expect(8);
  654. const form = await testUtils.createView({
  655. View: FormView,
  656. model: 'note.note',
  657. data: this.data,
  658. arch: '<form>' +
  659. '<field name="body" widget="html" style="height: 100px"/>' +
  660. '</form>',
  661. res_id: 7,
  662. });
  663. const waitForMutation = (element) => {
  664. let currentResolve;
  665. const promise = new Promise((resolve)=>{
  666. currentResolve = resolve;
  667. });
  668. const observer = new MutationObserver((records) => {
  669. currentResolve();
  670. observer.disconnect();
  671. });
  672. observer.observe(element, {
  673. childList: true,
  674. subtree: true,
  675. attributes: true,
  676. });
  677. return promise;
  678. }
  679. assert.equal(form.$('.b').attr('contenteditable'), undefined);
  680. let promise = new Promise((resolve) => _formResolveTestPromise = resolve);
  681. await testUtils.form.clickEdit(form);
  682. await promise;
  683. assert.equal(form.$('.b').attr('contenteditable'), 'false');
  684. await testUtils.form.clickSave(form);
  685. assert.equal(form.$('.b').attr('contenteditable'), undefined);
  686. // edit a second time
  687. promise = new Promise((resolve) => _formResolveTestPromise = resolve);
  688. await testUtils.form.clickEdit(form);
  689. await promise;
  690. await testUtils.nextTick();
  691. // adding an element with o_not_editable
  692. form.$('.b').after($(`<div class="c o_not_editable">c</div>`));
  693. await waitForMutation(form.$('.c')[0]);
  694. assert.equal(form.$('.c').attr('contenteditable'), 'false');
  695. // changing the class o_not_editable back and forth
  696. form.$('.a').addClass('o_not_editable');
  697. await waitForMutation(form.$('.a')[0]);
  698. assert.equal(form.$('.a').attr('contenteditable'), 'false');
  699. form.$('.a').removeClass('o_not_editable');
  700. await waitForMutation(form.$('.a')[0]);
  701. assert.equal(form.$('.a').attr('contenteditable'), undefined);
  702. // changing the class o_not_editable back and forth again
  703. form.$('.a').addClass('o_not_editable');
  704. await waitForMutation(form.$('.a')[0]);
  705. assert.equal(form.$('.a').attr('contenteditable'), 'false');
  706. form.$('.a').removeClass('o_not_editable');
  707. await waitForMutation(form.$('.a')[0]);
  708. assert.equal(form.$('.a').attr('contenteditable'), undefined);
  709. form.destroy();
  710. });
  711. QUnit.module('cssReadonly');
  712. QUnit.test('rendering with iframe for readonly mode', async function (assert) {
  713. assert.expect(3);
  714. var form = await testUtils.createView({
  715. View: FormView,
  716. model: 'note.note',
  717. data: this.data,
  718. arch: '<form>' +
  719. '<field name="body" widget="html" style="height: 100px" options="{\'cssReadonly\': \'template.assets\'}"/>' +
  720. '</form>',
  721. res_id: 1,
  722. debug: 1,
  723. });
  724. var $field = form.$('.oe_form_field[name="body"]');
  725. var $iframe = $field.find('iframe.o_readonly');
  726. await $iframe.data('loadDef');
  727. var doc = $iframe.contents()[0];
  728. assert.strictEqual($(doc).find('#iframe_target').html(),
  729. '<p>toto toto toto</p><p>tata</p>',
  730. "should have rendered a div with correct content in readonly");
  731. assert.strictEqual(doc.defaultView.getComputedStyle(doc.body).backgroundColor,
  732. 'rgb(255, 0, 0)',
  733. "should load the asset css");
  734. await testUtils.form.clickEdit(form);
  735. $field = form.$('.oe_form_field[name="body"]');
  736. assert.strictEqual($field.find('#iframe_target').length, 0);
  737. form.destroy();
  738. });
  739. QUnit.module('translation');
  740. QUnit.test('field html translatable', async function (assert) {
  741. assert.expect(4);
  742. var multiLang = _t.database.multi_lang;
  743. _t.database.multi_lang = true;
  744. this.data['note.note'].fields.body.translate = true;
  745. var form = await testUtils.createView({
  746. View: FormView,
  747. model: 'note.note',
  748. data: this.data,
  749. arch: '<form string="Partners">' +
  750. '<field name="body" widget="html"/>' +
  751. '</form>',
  752. res_id: 1,
  753. mockRPC: function (route, args) {
  754. if (route === "/web/dataset/call_kw/note.note/get_field_translations") {
  755. assert.deepEqual(args.args, [[1],"body"], "should translate the body field of the record");
  756. return Promise.resolve([
  757. [{lang: "en_US", source: "first paragraph", value: "first paragraph"},
  758. {lang: "en_US", source: "second paragraph", value: "second paragraph"},
  759. {lang: "fr_BE", source: "first paragraph", value: "premier paragraphe"},
  760. {lang: "fr_BE", source: "second paragraph", value: "deuxième paragraphe"}],
  761. {translation_type: "text", translation_show_source: true},
  762. ]);
  763. }
  764. if (route === "/web/dataset/call_kw/res.lang/get_installed") {
  765. return Promise.resolve([["en_US", "English"], ["fr_BE", "French (Belgium)"]]);
  766. }
  767. return this._super.apply(this, arguments);
  768. },
  769. });
  770. assert.strictEqual(form.$('.oe_form_field_html .o_field_translate').length, 0,
  771. "should not have a translate button in readonly mode");
  772. await testUtils.form.clickEdit(form);
  773. var $button = form.$('.oe_form_field_html .o_field_translate');
  774. assert.strictEqual($button.length, 1, "should have a translate button");
  775. await testUtils.dom.click($button);
  776. assert.containsOnce($(document), '.o_translation_dialog', 'should have a modal to translate');
  777. form.destroy();
  778. _t.database.multi_lang = multiLang;
  779. });
  780. });
  781. });
  782. });