[FIX] mail: allow editing of mail mail
As it holds full HTML documents, mail.mail often encounters issues with the html editor. We piggy-back off the intoduction of the switching behavior between code-view and readonly-view behavior introduced in a5c0085ce91570c6c961e3293dfe50f4faba6043 The 'sandboxed preview' mode can now be enabled with an option and will ensure all content can only be viewed in a sandboxed iframe or in the code editor. This re-introduces editing, which was mistakenly fully disabled in the usability fix that introduced body_content here: 5055b374c3bac0126f441dad61afd7e0b68b1370 Some tests are added to ensure the key points of this commit remain the same. Tests are re-ordered alphabetically to match the order of execution, making debugging easier. task-3255777 closes odoo/odoo#121976 X-original-commit: 062569ef042f3b01044c5e4cdbd8b71e96bfd39d Signed-off-by: David Monjoie (dmo) <dmo@odoo.com>
This commit is contained in:
@@ -43,7 +43,9 @@
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Body" name="body">
|
||||
<field name="body_content" options="{'style-inline': true}"/>
|
||||
<field name="body_html" widget="html"
|
||||
options="{'sandboxedPreview': true}"
|
||||
attrs="{'readonly': [('state', 'not in', ['outgoing', 'exception'])]}"/>
|
||||
</page>
|
||||
<page string="Advanced" name="advanced" groups="base.group_no_one">
|
||||
<group>
|
||||
|
||||
@@ -76,11 +76,8 @@ export class HtmlFieldWysiwygAdapterComponent extends ComponentAdapter {
|
||||
|
||||
export class HtmlField extends Component {
|
||||
setup() {
|
||||
// readonly if nodes in <head> as the head will be removed on insertion in the DOM
|
||||
// this prevents the field from overwriting the value with invalid HTML when relevant
|
||||
const domParser = new DOMParser();
|
||||
const parsedOriginal = domParser.parseFromString(this.props.record.data[this.props.name] || '', 'text/html');
|
||||
this.containsComplexHTML = !!parsedOriginal.head.innerHTML.trim();
|
||||
this.containsComplexHTML = this.computeContainsComplexHTML();
|
||||
this.sandboxedPreview = this.props.sandboxedPreview || this.containsComplexHTML;
|
||||
|
||||
this.readonlyElementRef = useRef("readonlyElement");
|
||||
this.codeViewRef = useRef("codeView");
|
||||
@@ -126,7 +123,7 @@ export class HtmlField extends Component {
|
||||
res_id: this.props.record.resId,
|
||||
};
|
||||
onWillUpdateProps((newProps) => {
|
||||
if (!newProps.readonly && this.state.iframeVisible) {
|
||||
if (!newProps.readonly && !this.sandboxedPreview && this.state.iframeVisible) {
|
||||
this.state.iframeVisible = false;
|
||||
}
|
||||
|
||||
@@ -144,7 +141,7 @@ export class HtmlField extends Component {
|
||||
if (this._qwebPlugin) {
|
||||
this._qwebPlugin.destroy();
|
||||
}
|
||||
if (this.props.readonly) {
|
||||
if (this.props.readonly || (!this.state.showCodeView && this.sandboxedPreview)) {
|
||||
if (this.showIframe) {
|
||||
await this._setupReadonlyIframe();
|
||||
} else if (this.readonlyElementRef.el) {
|
||||
@@ -182,6 +179,19 @@ export class HtmlField extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the current value contains nodes that would break
|
||||
* on insertion inside an existing body.
|
||||
*
|
||||
* @returns {boolean} true if 'this.props.value' contains a node
|
||||
* that can only exist once per document.
|
||||
*/
|
||||
computeContainsComplexHTML() {
|
||||
const domParser = new DOMParser();
|
||||
const parsedOriginal = domParser.parseFromString(this.props.record.data[this.props.name] || '', 'text/html');
|
||||
return !!parsedOriginal.head.innerHTML.trim();
|
||||
}
|
||||
|
||||
get isTranslatable() {
|
||||
return this.props.record.fields[this.props.name].translate;
|
||||
}
|
||||
@@ -189,7 +199,7 @@ export class HtmlField extends Component {
|
||||
return this.props.record.data[this.props.name];
|
||||
}
|
||||
get showIframe () {
|
||||
return this.props.readonly && this.props.cssReadonlyAssetId;
|
||||
return (this.sandboxedPreview && !this.state.showCodeView) || (this.props.readonly && this.props.cssReadonlyAssetId);
|
||||
}
|
||||
get wysiwygOptions() {
|
||||
let dynamicPlaceholderOptions = {};
|
||||
@@ -411,7 +421,10 @@ export class HtmlField extends Component {
|
||||
return this.state.showCodeView && this.codeViewRef.el;
|
||||
}
|
||||
async _setupReadonlyIframe() {
|
||||
const iframeTarget = this.iframeRef.el.contentDocument.querySelector('#iframe_target');
|
||||
const iframeTarget = this.sandboxedPreview
|
||||
? this.iframeRef.el.contentDocument.documentElement
|
||||
: this.iframeRef.el.contentDocument.querySelector('#iframe_target');
|
||||
|
||||
if (this.iframePromise && iframeTarget) {
|
||||
if (iframeTarget.innerHTML !== this.props.record.data[this.props.name]) {
|
||||
iframeTarget.innerHTML = this.props.record.data[this.props.name];
|
||||
@@ -440,7 +453,6 @@ export class HtmlField extends Component {
|
||||
|
||||
this.iframeRef.el.addEventListener('load', async () => {
|
||||
const _avoidDoubleLoad = ++avoidDoubleLoad;
|
||||
const asset = await ajax.loadAsset(this.props.cssReadonlyAssetId);
|
||||
|
||||
if (_avoidDoubleLoad !== avoidDoubleLoad) {
|
||||
console.warn('Wysiwyg immediate iframe double load detected');
|
||||
@@ -452,52 +464,65 @@ export class HtmlField extends Component {
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
cwindow.document
|
||||
.open("text/html", "replace")
|
||||
.write(
|
||||
'<!DOCTYPE html><html>' +
|
||||
'<head>' +
|
||||
'<meta charset="utf-8"/>' +
|
||||
'<meta http-equiv="X-UA-Compatible" content="IE=edge"/>\n' +
|
||||
'<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>\n' +
|
||||
'</head>\n' +
|
||||
'<body class="o_in_iframe o_readonly" style="overflow: hidden;">\n' +
|
||||
'<div id="iframe_target"></div>\n' +
|
||||
'</body>' +
|
||||
'</html>');
|
||||
|
||||
for (const cssLib of asset.cssLibs) {
|
||||
const link = cwindow.document.createElement('link');
|
||||
link.setAttribute('type', 'text/css');
|
||||
link.setAttribute('rel', 'stylesheet');
|
||||
link.setAttribute('href', cssLib);
|
||||
cwindow.document.head.append(link);
|
||||
if (!this.sandboxedPreview) {
|
||||
cwindow.document
|
||||
.open("text/html", "replace")
|
||||
.write(
|
||||
'<!DOCTYPE html><html>' +
|
||||
'<head>' +
|
||||
'<meta charset="utf-8"/>' +
|
||||
'<meta http-equiv="X-UA-Compatible" content="IE=edge"/>\n' +
|
||||
'<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>\n' +
|
||||
'</head>\n' +
|
||||
'<body class="o_in_iframe o_readonly" style="overflow: hidden;">\n' +
|
||||
'<div id="iframe_target"></div>\n' +
|
||||
'</body>' +
|
||||
'</html>');
|
||||
}
|
||||
for (const cssContent of asset.cssContents) {
|
||||
const style = cwindow.document.createElement('style');
|
||||
style.setAttribute('type', 'text/css');
|
||||
const textNode = cwindow.document.createTextNode(cssContent);
|
||||
style.append(textNode);
|
||||
cwindow.document.head.append(style);
|
||||
if (this.props.cssReadonlyAssetId) {
|
||||
const asset = await ajax.loadAsset(this.props.cssReadonlyAssetId);
|
||||
for (const cssLib of asset.cssLibs) {
|
||||
const link = cwindow.document.createElement('link');
|
||||
link.setAttribute('type', 'text/css');
|
||||
link.setAttribute('rel', 'stylesheet');
|
||||
link.setAttribute('href', cssLib);
|
||||
cwindow.document.head.append(link);
|
||||
}
|
||||
for (const cssContent of asset.cssContents) {
|
||||
const style = cwindow.document.createElement('style');
|
||||
style.setAttribute('type', 'text/css');
|
||||
const textNode = cwindow.document.createTextNode(cssContent);
|
||||
style.append(textNode);
|
||||
cwindow.document.head.append(style);
|
||||
}
|
||||
}
|
||||
|
||||
const iframeTarget = cwindow.document.querySelector('#iframe_target');
|
||||
iframeTarget.innerHTML = value;
|
||||
if (!this.sandboxedPreview) {
|
||||
const iframeTarget = cwindow.document.querySelector('#iframe_target');
|
||||
iframeTarget.innerHTML = value;
|
||||
|
||||
const script = cwindow.document.createElement('script');
|
||||
script.setAttribute('type', 'text/javascript');
|
||||
const scriptTextNode = document.createTextNode(
|
||||
`if (window.top.${this._onUpdateIframeId}) {` +
|
||||
`window.top.${this._onUpdateIframeId}(${_avoidDoubleLoad})` +
|
||||
`}`
|
||||
);
|
||||
script.append(scriptTextNode);
|
||||
cwindow.document.body.append(script);
|
||||
const script = cwindow.document.createElement('script');
|
||||
script.setAttribute('type', 'text/javascript');
|
||||
const scriptTextNode = document.createTextNode(
|
||||
`if (window.top.${this._onUpdateIframeId}) {` +
|
||||
`window.top.${this._onUpdateIframeId}(${_avoidDoubleLoad})` +
|
||||
`}`
|
||||
);
|
||||
script.append(scriptTextNode);
|
||||
cwindow.document.body.append(script);
|
||||
} else {
|
||||
cwindow.document.documentElement.innerHTML = value;
|
||||
}
|
||||
|
||||
const height = cwindow.document.body.scrollHeight;
|
||||
this.iframeRef.el.style.height = Math.max(30, Math.min(height, 500)) + 'px';
|
||||
|
||||
retargetLinks(cwindow.document.body);
|
||||
if (this.sandboxedPreview) {
|
||||
this.state.iframeVisible = true;
|
||||
this.onIframeUpdated();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
// Force the iframe to call the `load` event. Without this line, the
|
||||
// event 'load' might never trigger.
|
||||
@@ -648,6 +673,7 @@ HtmlField.props = {
|
||||
cssReadonlyAssetId: { type: String, optional: true },
|
||||
cssEditAssetId: { type: String, optional: true },
|
||||
isInlineStyle: { type: Boolean, optional: true },
|
||||
sandboxedPreview: {type: Boolean, optional: true},
|
||||
wrapper: { type: String, optional: true },
|
||||
wysiwygOptions: { type: Object },
|
||||
hasReadonlyModifiers: { type: Boolean, optional: true },
|
||||
@@ -739,6 +765,7 @@ export const htmlField = {
|
||||
return {
|
||||
codeview: Boolean(odoo.debug && options.codeview),
|
||||
placeholder: attrs.placeholder,
|
||||
sandboxedPreview: Boolean(options.sandboxedPreview),
|
||||
|
||||
isCollaborative: options.collaborative,
|
||||
cssReadonlyAssetId: options.cssReadonly,
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="web_editor.HtmlField" owl="1">
|
||||
<t t-if="props.readonly || props.notEditable || (containsComplexHTML and !state.showCodeView)">
|
||||
<t t-if="props.readonly || props.notEditable || (sandboxedPreview and !state.showCodeView)">
|
||||
<t t-if="this.showIframe">
|
||||
<iframe t-ref="iframe" t-att-class="{'d-none': !this.state.iframeVisible, 'o_readonly': true}"></iframe>
|
||||
<iframe t-ref="iframe" t-att-class="{'d-none': !this.state.iframeVisible, 'o_readonly': true}" t-att-sandbox="sandboxedPreview ? 'allow-same-origin' : false"></iframe>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div t-ref="readonlyElement" class="o_readonly" t-out="markupValue" />
|
||||
@@ -29,7 +29,7 @@
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="state.showCodeView || (containsComplexHTML and !props.readonly and !props.notEditable)" t-ref="codeViewButton" id="codeview-btn-group" class="btn-group" t-on-click="toggleCodeView">
|
||||
<div t-if="state.showCodeView || (sandboxedPreview and !props.readonly and !props.notEditable)" t-ref="codeViewButton" id="codeview-btn-group" class="btn-group" t-on-click="toggleCodeView">
|
||||
<button class="o_codeview_btn btn btn-primary">
|
||||
<i class="fa fa-code" />
|
||||
</button>
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { click, editInput, getFixture, makeDeferred, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { click, editInput, getFixture, makeDeferred, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { HtmlField } from "@web_editor/js/backend/html_field";
|
||||
import { onRendered } from "@odoo/owl";
|
||||
|
||||
async function iframeReady(iframe) {
|
||||
const iframeLoadPromise = makeDeferred();
|
||||
iframe.addEventListener("load", function () {
|
||||
iframeLoadPromise.resolve();
|
||||
});
|
||||
if (!iframe.contentDocument.body) {
|
||||
await iframeLoadPromise;
|
||||
}
|
||||
await nextTick(); // ensure document is loaded
|
||||
}
|
||||
|
||||
QUnit.module("WebEditor.HtmlField", ({ beforeEach }) => {
|
||||
let serverData;
|
||||
let target;
|
||||
@@ -25,110 +36,46 @@ QUnit.module("WebEditor.HtmlField", ({ beforeEach }) => {
|
||||
setupViewRegistries();
|
||||
});
|
||||
|
||||
/**
|
||||
* Check that documents with data in a <head> node are set to readonly
|
||||
* with a codeview option.
|
||||
*/
|
||||
QUnit.test("html fields with complete HTML document", async (assert) => {
|
||||
assert.timeout(2000);
|
||||
assert.expect(12);
|
||||
let codeViewState = false;
|
||||
let togglePromiseId = 0;
|
||||
const togglePromises = [makeDeferred(), makeDeferred()];
|
||||
patchWithCleanup(HtmlField.prototype, {
|
||||
setup: function () {
|
||||
this._super(...arguments);
|
||||
onRendered(() => {
|
||||
if (codeViewState !== this.state.showCodeView) {
|
||||
togglePromises[togglePromiseId].resolve();
|
||||
}
|
||||
codeViewState = this.state.showCodeView;
|
||||
});
|
||||
},
|
||||
});
|
||||
const htmlDocumentTextTemplate = (text, color) => `
|
||||
<!DOCTYPE HTML>
|
||||
<html xml:lang="en" lang="en">
|
||||
<head>
|
||||
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<meta name="format-detection" content="telephone=no"/>
|
||||
<style type="text/css">
|
||||
body {
|
||||
color: ${color};
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${text}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
QUnit.module('Sandboxed Preview');
|
||||
|
||||
QUnit.test("complex html is automatically in sandboxed preview mode", async (assert) => {
|
||||
serverData.models.partner.records = [{
|
||||
id: 1,
|
||||
txt: htmlDocumentTextTemplate('Hello', 'red'),
|
||||
txt: `
|
||||
<!DOCTYPE HTML>
|
||||
<html xml:lang="en" lang="en">
|
||||
<head>
|
||||
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<meta name="format-detection" content="telephone=no"/>
|
||||
<style type="text/css">
|
||||
body {
|
||||
color: blue;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
Hello
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
}];
|
||||
const writePromise = makeDeferred();
|
||||
await makeView({
|
||||
type: "form",
|
||||
resId: 1,
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form string="Partner">
|
||||
<sheet>
|
||||
<notebook>
|
||||
<page string="Body" name="body">
|
||||
<field name="txt" widget="html"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<form>
|
||||
<field name="txt" widget="html"/>
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
if (args.method === "write" && args.model === 'partner') {
|
||||
assert.equal(args.args[1].txt, htmlDocumentTextTemplate('Hi', 'black'));
|
||||
writePromise.resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const fieldHtml = target.querySelector('.o_field_html');
|
||||
let readonlyNode = fieldHtml.querySelector('.o_readonly');
|
||||
assert.ok(readonlyNode);
|
||||
assert.equal(readonlyNode.innerText, 'Hello');
|
||||
assert.equal(window.getComputedStyle(readonlyNode).color, 'rgb(255, 0, 0)');
|
||||
|
||||
const codeViewButton = fieldHtml.querySelector('.o_codeview_btn');
|
||||
assert.ok(codeViewButton);
|
||||
|
||||
await click(codeViewButton);
|
||||
await togglePromises[togglePromiseId];
|
||||
const codeView = fieldHtml.querySelector('textarea.o_codeview');
|
||||
assert.ok(codeView);
|
||||
assert.equal(codeView.value, htmlDocumentTextTemplate('Hello', 'red'));
|
||||
|
||||
await editInput(codeView, null, htmlDocumentTextTemplate('Hi', 'black'));
|
||||
|
||||
assert.ok(codeViewButton);
|
||||
togglePromiseId++;
|
||||
await click(codeViewButton);
|
||||
await togglePromises[togglePromiseId];
|
||||
readonlyNode = fieldHtml.querySelector('.o_readonly');
|
||||
assert.ok(readonlyNode);
|
||||
assert.equal(readonlyNode.innerText, 'Hi');
|
||||
assert.equal(window.getComputedStyle(readonlyNode).color, 'rgb(0, 0, 0)');
|
||||
|
||||
const saveButton = target.querySelector('.o_form_button_save');
|
||||
assert.ok(saveButton);
|
||||
await click(saveButton);
|
||||
await writePromise;
|
||||
assert.containsOnce(target, '.o_field_html[name="txt"] iframe[sandbox="allow-same-origin"]');
|
||||
});
|
||||
|
||||
/**
|
||||
* Check that documents with data in a <head> node and with the readonly prop
|
||||
* do not display the codeview button
|
||||
*/
|
||||
QUnit.test("html fields with complete HTML document in readonly mode", async (assert) => {
|
||||
QUnit.test("readonly sandboxed preview", async (assert) => {
|
||||
serverData.models.partner.records = [{
|
||||
id: 1,
|
||||
txt: `
|
||||
@@ -156,15 +103,153 @@ QUnit.module("WebEditor.HtmlField", ({ beforeEach }) => {
|
||||
serverData,
|
||||
arch: `
|
||||
<form string="Partner">
|
||||
<field name="txt" widget="html" readonly="1"/>
|
||||
<field name="txt" widget="html" readonly="1" options="{'sandboxedPreview': true}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
const readonlyElement = target.querySelector('.o_field_html .o_readonly');
|
||||
assert.ok(readonlyElement);
|
||||
assert.strictEqual(readonlyElement.innerText, 'Hello');
|
||||
assert.strictEqual(window.getComputedStyle(readonlyElement).color, 'rgb(0, 0, 255)');
|
||||
const readonlyIframe = target.querySelector('.o_field_html[name="txt"] iframe[sandbox="allow-same-origin"]');
|
||||
assert.ok(readonlyIframe);
|
||||
await iframeReady(readonlyIframe);
|
||||
assert.strictEqual(readonlyIframe.contentDocument.body.innerText, 'Hello');
|
||||
assert.strictEqual(readonlyIframe.contentWindow.getComputedStyle(readonlyIframe.contentDocument.body).color, 'rgb(0, 0, 255)');
|
||||
|
||||
assert.containsN(target, '.o_codeview_btn', 0, 'Codeview toggle should not be possible in readonly mode.');
|
||||
assert.containsN(target, '#codeview-btn-group > button', 0, 'Codeview toggle should not be possible in readonly mode.');
|
||||
});
|
||||
|
||||
QUnit.test("sandboxed preview display and editing", async (assert) => {
|
||||
let codeViewState = false;
|
||||
const togglePromises = [makeDeferred(), makeDeferred()];
|
||||
let togglePromiseId = 0;
|
||||
const writePromise = makeDeferred();
|
||||
patchWithCleanup(HtmlField.prototype, {
|
||||
setup: function () {
|
||||
this._super(...arguments);
|
||||
onRendered(() => {
|
||||
if (codeViewState !== this.state.showCodeView) {
|
||||
togglePromises[togglePromiseId].resolve();
|
||||
}
|
||||
codeViewState = this.state.showCodeView;
|
||||
});
|
||||
},
|
||||
});
|
||||
const htmlDocumentTextTemplate = (text, color) => `
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
color: ${color};
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${text}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
serverData.models.partner.records = [{
|
||||
id: 1,
|
||||
txt: htmlDocumentTextTemplate('Hello', 'red'),
|
||||
}];
|
||||
await makeView({
|
||||
type: "form",
|
||||
resId: 1,
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<sheet>
|
||||
<notebook>
|
||||
<page string="Body" name="body">
|
||||
<field name="txt" widget="html" options="{'sandboxedPreview': true}"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>`,
|
||||
mockRPC(route, args) {
|
||||
if (args.method === "write" && args.model === 'partner') {
|
||||
assert.equal(args.args[1].txt, htmlDocumentTextTemplate('Hi', 'blue'));
|
||||
writePromise.resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// check original displayed content
|
||||
let iframe = target.querySelector('.o_field_html[name="txt"] iframe[sandbox="allow-same-origin"]');
|
||||
assert.ok(iframe, 'Should use a sanboxed iframe');
|
||||
await iframeReady(iframe);
|
||||
assert.strictEqual(iframe.contentDocument.body.textContent.trim(), 'Hello');
|
||||
assert.strictEqual(iframe.contentDocument.head.querySelector('style').textContent.trim().replace(/\s/g, ''),
|
||||
'body{color:red;}', 'Head nodes should remain unaltered in the head');
|
||||
assert.equal(iframe.contentWindow.getComputedStyle(iframe.contentDocument.body).color, 'rgb(255, 0, 0)');
|
||||
// check button is there
|
||||
assert.containsOnce(target, '#codeview-btn-group > button');
|
||||
// edit in xml editor
|
||||
await click(target, '#codeview-btn-group > button');
|
||||
await togglePromises[togglePromiseId];
|
||||
togglePromiseId++;
|
||||
assert.containsOnce(target, '.o_field_html[name="txt"] textarea');
|
||||
await editInput(target, '.o_field_html[name="txt"] textarea', htmlDocumentTextTemplate('Hi', 'blue'));
|
||||
await click(target, '#codeview-btn-group > button');
|
||||
await togglePromises[togglePromiseId];
|
||||
// check dispayed content after edit
|
||||
iframe = target.querySelector('.o_field_html[name="txt"] iframe[sandbox="allow-same-origin"]');
|
||||
await iframeReady(iframe);
|
||||
assert.strictEqual(iframe.contentDocument.body.textContent.trim(), 'Hi');
|
||||
assert.strictEqual(iframe.contentDocument.head.querySelector('style').textContent.trim().replace(/\s/g, ''),
|
||||
'body{color:blue;}', 'Head nodes should remain unaltered in the head');
|
||||
assert.equal(iframe.contentWindow.getComputedStyle(iframe.contentDocument.body).color, 'rgb(0, 0, 255)',
|
||||
'Style should be applied inside the iframe.');
|
||||
|
||||
const saveButton = target.querySelector('.o_form_button_save');
|
||||
assert.ok(saveButton);
|
||||
await click(saveButton);
|
||||
await writePromise;
|
||||
});
|
||||
|
||||
|
||||
QUnit.test("sanboxed preview mode not automatically enabled for regular values", async (assert) => {
|
||||
serverData.models.partner.records = [{
|
||||
id: 1,
|
||||
txt: `
|
||||
<body>
|
||||
<p>Hello</p>
|
||||
</body>
|
||||
`,
|
||||
}];
|
||||
await makeView({
|
||||
type: "form",
|
||||
resId: 1,
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="txt" widget="html"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsN(target, '.o_field_html[name="txt"] iframe[sandbox]', 0);
|
||||
assert.containsN(target, '.o_field_html[name="txt"] textarea', 0);
|
||||
});
|
||||
|
||||
QUnit.test("sandboxed preview option applies even for simple text", async (assert) => {
|
||||
serverData.models.partner.records = [{
|
||||
id: 1,
|
||||
txt: `
|
||||
Hello
|
||||
`,
|
||||
}];
|
||||
await makeView({
|
||||
type: "form",
|
||||
resId: 1,
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `
|
||||
<form>
|
||||
<field name="txt" widget="html" options="{'sandboxedPreview': true}"/>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(target, '.o_field_html[name="txt"] iframe[sandbox="allow-same-origin"]');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -80,17 +80,5 @@ registry.category("web_tour.tours").add('shop_mail', {
|
||||
{
|
||||
content: "wait mail to be sent, and go see it",
|
||||
trigger: '.o-mail-Message-body:contains("Your"):contains("order")',
|
||||
run: function () {
|
||||
window.location.href = "/web#action=mail.action_view_mail_mail&view_type=list";
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "click on the first email",
|
||||
trigger: '.o_data_cell:contains("(Ref S")',
|
||||
},
|
||||
{
|
||||
content: "check it's the correct email, and the URL is correct too",
|
||||
trigger: 'div.o_field_html[name="body_content"] p:contains("Your"):contains("order")',
|
||||
extra_trigger: 'div.o_field_html[name="body_content"] a[href^="https://my-test-domain.com"]',
|
||||
},
|
||||
]});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import odoo
|
||||
from odoo import fields
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import HttpCase
|
||||
|
||||
@@ -29,4 +30,11 @@ class TestWebsiteSaleMail(HttpCase):
|
||||
self.env['ir.config_parameter'].sudo().set_param('mail_mobile.disable_redirect_firebase_dynamic_link', True)
|
||||
|
||||
with patch.object(MailMail, 'unlink', lambda self: None):
|
||||
start_time = fields.Datetime.now()
|
||||
self.start_tour("/", 'shop_mail', login="admin")
|
||||
new_mail = self.env['mail.mail'].search([('create_date', '>=', start_time),
|
||||
('body_html', 'ilike', 'https://my-test-domain.com')],
|
||||
order='create_date DESC', limit=1)
|
||||
self.assertTrue(new_mail)
|
||||
self.assertIn('Your', new_mail.body_html)
|
||||
self.assertIn('Order', new_mail.body_html)
|
||||
|
||||
Reference in New Issue
Block a user