19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:39 +01:00
parent 5df8c07b59
commit daa394e8b0
2114 changed files with 564841 additions and 299642 deletions

View file

@ -1,71 +0,0 @@
/** @odoo-module **/
import { setupViewRegistries } from "@web/../tests/views/helpers";
import { patchWithCleanup } from "@web/../tests/helpers/utils";
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
import { MassMailingFullWidthViewController } from "@mass_mailing/js/mailing_mailing_view_form_full_width";
let serverData;
QUnit.module("mass_mailing", {}, function () {
QUnit.module("MassMailingFullWidthViewController", (hooks) => {
hooks.beforeEach(() => {
serverData = {
models: {
partner: {
fields: {
display_name: { string: "Displayed name", type: "char" },
},
},
},
actions: {
1: {
id: 1,
name: "test",
res_model: "partner",
type: "ir.actions.act_window",
views: [[false, "form"]],
},
2: {
id: 2,
name: "test",
res_model: "partner",
type: "ir.actions.act_window",
views: [[false, "list"]],
},
},
views: {
"partner,false,form": `<form js_class="mailing_mailing_view_form_full_width">
<field name="display_name"/>
</form>`,
"partner,false,list": `<tree><field name="display_name"/></tree>`,
"partner,false,search": `<search/>`,
},
};
setupViewRegistries();
});
QUnit.test("unregister ResizeObserver on unmount", async (assert) => {
patchWithCleanup(MassMailingFullWidthViewController.prototype, {
setup() {
this._super();
patchWithCleanup(this._resizeObserver, {
disconnect() {
assert.step("disconnect");
return this._super(...arguments);
},
});
},
});
const webClient = await createWebClient({ serverData });
await doAction(webClient, 1);
assert.verifySteps([]);
await doAction(webClient, 2);
assert.verifySteps(["disconnect"]);
});
});
});

View file

@ -0,0 +1,448 @@
import { expect, test, describe } from "@odoo/hoot";
import {
defineModels,
fields,
models,
mountView,
onRpc,
contains,
clickSave,
MockServer,
} from "@web/../tests/web_test_helpers";
import { queryFirst } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
class Mailing extends models.Model {
_name = "mailing.mailing";
display_name = fields.Char();
subject = fields.Char();
mailing_model_id = fields.Many2one({ relation: "ir.model", string: "Recipients" });
mailing_model_name = fields.Char({ string: "Recipients Model Name" });
mailing_filter_id = fields.Many2one({ relation: "mailing.filter", string: "Filters" });
mailing_domain = fields.Char({ string: "Domain" });
mailing_filter_domain = fields.Char({ related: "mailing_domain", string: "Domain" });
mailing_filter_count = fields.Integer({ string: "Filter Count" });
_records = [
{
id: 1,
display_name: "Belgian Event promotion",
subject: "Early bird discount for Belgian Events! Register Now!",
mailing_model_id: 1,
mailing_model_name: "event",
mailing_domain: '[["country","=","be"]]',
mailing_filter_id: 1,
mailing_filter_count: 1,
mailing_filter_domain: '[["country","=","be"]]',
},
{
id: 2,
display_name: "New Users Promotion",
subject: "Early bird discount for new users! Register Now!",
mailing_model_id: 1,
mailing_filter_count: 1,
mailing_model_name: "event",
mailing_domain: '[["new_user","=",True]]',
mailing_filter_domain: '[["new_user","=",True]]',
},
];
}
class IrModel extends models.Model {
_name = "ir.model";
name = fields.Char();
model = fields.Char();
_records = [
{
id: 1,
name: "Event",
model: "event",
},
{
id: 2,
name: "Partner",
model: "partner",
},
];
}
class MailingFilter extends models.Model {
_name = "mailing.filter";
name = fields.Char();
mailing_domain = fields.Char();
mailing_model_id = fields.Many2one({ relation: "ir.model", string: "Recipients Model" });
_records = [
{
id: 1,
name: "Belgian Events",
mailing_domain: '[["country","=","be"]]',
mailing_model_id: 1,
},
];
}
class Partner extends models.Model {
_name = "partner";
name = fields.Char();
_records = [
{ id: 1, name: "Azure Interior" },
{ id: 2, name: "Acme Corporation" },
{ id: 3, name: "Marc Demo" },
];
}
class Event extends models.Model {
_name = "event";
name = fields.Char();
country = fields.Char();
_records = [{ id: 1, name: "BE Event", country: "be" }];
}
defineMailModels();
defineModels([IrModel, Mailing, MailingFilter, Partner, Event]);
describe.current.tags("desktop");
test("create favorite filter", async () => {
expect.assertions(8);
onRpc("mailing.filter", "create", (params) => {
expect(params.args[0]).toEqual([
{
mailing_domain: '[["new_user","=",True]]',
mailing_model_id: 1,
name: "event promo - new users",
},
]);
});
await mountView({
type: "form",
resModel: "mailing.mailing",
resId: 2,
arch: `<form>
<field name="display_name"/>
<field name="subject"/>
<field name="mailing_domain"/>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id"/>
<field name="mailing_filter_count"/>
<field name="mailing_filter_domain" invisible="1"/>
<field name="mailing_filter_id"
widget="mailing_filter"
options="{'no_create': '1', 'no_open': '1', 'domain_field': 'mailing_domain', 'model': 'mailing_model_id'}"/>
</form>`,
});
queryFirst(".o_field_mailing_filter input").autocomplete = "widget";
expect(".o_mass_mailing_remove_filter").not.toBeVisible();
expect(".o_mass_mailing_save_filter_container").toBeVisible();
await contains(".o_field_mailing_filter input").click();
expect(".o_field_mailing_filter .dropdown li.ui-menu-item").toHaveCount(2);
await contains(".o_mass_mailing_add_filter").click();
await contains(".o_mass_mailing_filter_name").edit("event promo - new users", {
confirm: "Enter",
});
await contains(".o_content").click();
expect(".o_field_mailing_filter input").toHaveValue("event promo - new users");
expect(".o_mass_mailing_remove_filter").toBeVisible();
expect(".o_mass_mailing_save_filter_container").not.toBeVisible();
await contains(".o_field_mailing_filter input").click();
expect(".o_field_mailing_filter .dropdown li.ui-menu-item").toHaveCount(3);
await clickSave();
});
test("unlink favorite filter", async () => {
expect.assertions(10);
onRpc("mailing.filter", "unlink", (params) => {
expect(params.args[0]).toEqual([1]);
});
onRpc("mailing.mailing", "web_save", (params) => {
expect(params.args[1].mailing_filter_id).toBe(false);
expect(params.args[1].mailing_domain).toBe('[["country","=","be"]]');
});
await mountView({
type: "form",
resModel: "mailing.mailing",
resId: 1,
arch: `<form>
<field name="display_name"/>
<field name="subject"/>
<field name="mailing_domain"/>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id"/>
<field name="mailing_filter_count"/>
<field name="mailing_filter_domain" invisible="1"/>
<field name="mailing_filter_id"
widget="mailing_filter"
options="{'no_create': '1', 'no_open': '1', 'domain_field': 'mailing_domain', 'model': 'mailing_model_id'}"/>
</form>`,
});
expect(".o_field_mailing_filter input").toHaveValue("Belgian Events");
expect(".o_mass_mailing_remove_filter").toBeVisible();
expect(".o_mass_mailing_save_filter_container").not.toBeVisible();
await contains(".o_mass_mailing_remove_filter").click();
await animationFrame();
expect(".o_field_mailing_filter input").toHaveValue("");
expect(".o_mass_mailing_remove_filter").not.toBeVisible();
expect(".o_mass_mailing_save_filter_container").toBeVisible();
queryFirst(".o_field_mailing_filter input").autocomplete = "widget";
await contains(".o_field_mailing_filter input").click();
expect(".o_field_mailing_filter .dropdown li.ui-menu-item.o_m2o_no_result").toHaveCount(1);
await clickSave();
});
test("changing filter correctly applies the domain", async () => {
MailingFilter._records = [
{
id: 1,
name: "Azure Partner Only",
mailing_domain: "[['name','=', 'Azure Interior']]",
mailing_model_id: 2,
},
];
Mailing._records.push({
id: 3,
display_name: "Partner Event promotion",
subject: "Early bird discount for Partners!",
mailing_model_id: 2,
mailing_model_name: "partner",
mailing_filter_count: 1,
mailing_domain: "[['name','!=', 'Azure Interior']]",
});
Mailing._onChanges = {
mailing_filter_id(record) {
record.mailing_domain = MockServer.env["mailing.filter"].filter(
(r) => r.id === record.mailing_filter_id
)[0].mailing_domain;
},
};
await mountView({
type: "form",
resModel: "mailing.mailing",
resId: 3,
arch: `<form>
<field name="display_name"/>
<field name="subject"/>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id"/>
<field name="mailing_filter_count" />
<field name="mailing_filter_id" widget="mailing_filter" options="{'no_create': '1', 'no_open': '1', 'domain_field': 'mailing_domain', 'model': 'mailing_model_id'}"/>
<group>
<field name="mailing_domain" widget="domain" options="{'model': 'mailing_model_name'}"/>
</group>
</form>`,
});
expect(".o_domain_show_selection_button").toHaveText("2 record(s)");
await contains(".o_field_mailing_filter input").click();
queryFirst(".o_field_mailing_filter input").autocomplete = "widget";
await contains(".o_field_mailing_filter .dropdown li:first-of-type").click();
await animationFrame();
expect(".o_domain_show_selection_button").toHaveText("1 record(s)");
});
test("filter drop-down and filter icons visibility toggles properly based on filters available", async () => {
MailingFilter._records = [
{
id: 2,
name: "Azure partner",
mailing_domain: '[["name","=","Azure Interior"]]',
mailing_model_id: 2,
},
{
id: 3,
name: "Ready Mat partner",
mailing_domain: '[["name","=","Ready Mat"]]',
mailing_model_id: 2,
},
];
Mailing._records = [
{
id: 1,
display_name: "Belgian Event promotion",
subject: "Early bird discount for Belgian Events! Register Now!",
mailing_model_id: 1,
mailing_model_name: "event",
mailing_domain: '[["country","=","be"]]',
mailing_filter_id: false,
mailing_filter_count: 0,
},
];
Mailing._onChanges = {
mailing_model_id(record) {
record.mailing_filter_count = MockServer.env["mailing.filter"].filter(
(r) => r.mailing_model_id === record.mailing_model_id
).length;
},
mailing_filter_id(record) {
const filterDomain = MockServer.env["mailing.filter"].filter(
(r) => r.id === record.mailing_filter_id
)[0].mailing_domain;
record.mailing_domain = filterDomain;
record.mailing_filter_domain = filterDomain;
},
};
await mountView({
type: "form",
resModel: "mailing.mailing",
resId: 1,
arch: `<form>
<field name="display_name"/>
<field name="subject"/>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id"/>
<field name="mailing_filter_count" />
<field name="mailing_filter_id" widget="mailing_filter"
options="{'no_create': '1', 'no_open': '1', 'domain_field': 'mailing_domain', 'model': 'mailing_model_id'}"/>
<field name="mailing_filter_domain" invisible="1"/>
<group>
<field name="mailing_domain" widget="domain" options="{'model': 'mailing_model_name'}"/>
</group>
</form>`,
});
expect(".o_field_mailing_filter .o_input_dropdown").not.toBeVisible();
expect(".o_mass_mailing_no_filter").toBeVisible();
expect(".o_mass_mailing_save_filter_container").toBeVisible();
await contains(".o_tree_editor_node_control_panel > button:nth-child(2)").click();
expect(".o_field_mailing_filter .o_input_dropdown").not.toBeVisible();
expect(".o_mass_mailing_filter_container").not.toBeVisible();
await contains(".o_field_widget[name='mailing_model_id'] input").click();
await contains(".dropdown-item:contains('Partner')").click();
expect(".o_field_mailing_filter .o_input_dropdown").toBeVisible();
expect(".o_mass_mailing_filter_container").not.toBeVisible();
await contains(".o_field_mailing_filter input").click();
await contains(".dropdown-item:contains('Azure partner')").click();
await animationFrame();
expect(".o_mass_mailing_remove_filter").toBeVisible();
expect(".o_mass_mailing_save_filter_container").not.toBeVisible();
await contains(".o_tree_editor_node_control_panel > button:nth-child(1)").click();
await animationFrame();
expect(".o_mass_mailing_save_filter_container").toBeVisible();
expect(".o_mass_mailing_remove_filter").not.toBeVisible();
});
test("filter widget does not raise traceback when losing focus with unexpected domain format", async () => {
await mountView({
type: "form",
resModel: "mailing.mailing",
resId: 2,
arch: `<form>
<field name="display_name"/>
<field name="subject"/>
<field name="mailing_domain"/>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id"/>
<field name="mailing_filter_count"/>
<field name="mailing_filter_domain" invisible="1"/>
<field name="mailing_filter_id"
widget="mailing_filter"
options="{'no_create': '1', 'no_open': '1', 'domain_field': 'mailing_domain', 'model': 'mailing_model_id'}"/>
</form>`,
});
expect(".o_mass_mailing_save_filter_container").toBeVisible();
expect(".o_mass_mailing_remove_filter").not.toBeVisible();
await contains("div[name='mailing_domain'] input").edit("[");
await animationFrame();
expect(".o_mass_mailing_save_filter_container").not.toBeVisible();
expect(".o_mass_mailing_remove_filter").not.toBeVisible();
});
test("filter widget works in edit and readonly", async () => {
Partner._fields.name.searchable = true;
MailingFilter._records = [
{
id: 1,
name: "Azure Partner Only",
mailing_domain: "[['name','=', 'Azure Interior']]",
mailing_model_id: 2,
},
];
Mailing._fields.state = fields.Selection({
selection: [
["draft", "Draft"],
["running", "Running"],
],
});
Mailing._records.push({
id: 3,
display_name: "Partner Event promotion",
subject: "Early bird discount for Partners!",
mailing_model_id: 2,
mailing_model_name: "partner",
mailing_filter_count: 1,
mailing_filter_domain: "[['name','=', 'Azure Interior']]",
mailing_filter_id: 1,
mailing_domain: "[['name','=', 'Azure Interior']]",
state: "draft",
});
Mailing._onChanges = {
mailing_filter_id(record) {
record.mailing_domain = MockServer.env["mailing.filter"].filter(
(r) => r.id === record.mailing_filter_id
)[0].mailing_domain;
},
};
await mountView({
type: "form",
resModel: "mailing.mailing",
resId: 3,
arch: `<form>
<field name="display_name"/>
<field name="subject"/>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id" readonly="state != 'draft'"/>
<field name="mailing_filter_count" />
<field name="mailing_filter_id" widget="mailing_filter" options="{'no_create': '1', 'no_open': '1', 'domain_field': 'mailing_domain', 'model': 'mailing_model_id'}"/>
<field name="state" widget="statusbar" options="{'clickable' : '1'}"/>
<group>
<field name="mailing_domain" widget="domain" options="{'model': 'mailing_model_name'}"/>
</group>
</form>`,
});
expect("div[name='mailing_model_id']").not.toHaveClass("o_readonly_modifier");
expect(".o_mass_mailing_save_filter_container").not.toHaveClass("d-none");
await contains("button[data-value='running']").click();
await animationFrame();
expect("div[name='mailing_model_id']").toHaveClass("o_readonly_modifier");
expect(".o_mass_mailing_save_filter_container").not.toHaveClass("d-none");
});

View file

@ -1,521 +0,0 @@
/** @odoo-module alias=mass_mailing.FieldMassMailingFavoriteFilter.test */
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import * as testUtils from "@web/../tests/helpers/utils";
import weTestUtils from "web_editor.test_utils";
let fixture;
let serverData;
QUnit.module('mass_mailing_favourite_filter', {}, function () {
QUnit.module('favorite filter widget', (hooks) => {
hooks.beforeEach(() => {
fixture = testUtils.getFixture();
const models = weTestUtils.wysiwygData({
'mailing.mailing': {
fields: {
display_name: {
string: 'Display name',
type: 'char',
},
subject: {
string: 'subject',
type: 'char',
},
mailing_model_id: {
string: 'Recipients',
type: 'many2one',
relation: 'ir.model',
},
mailing_model_name: {
string: 'Recipients Model Name',
type: 'char',
},
mailing_filter_id: {
string: 'Filters',
type: 'many2one',
relation: 'mailing.filter',
},
mailing_domain: {
string: 'Domain',
type: 'char',
},
mailing_filter_domain: {
string: 'Domain',
type: 'char',
related: 'mailing_filter_id.mailing_domain',
},
mailing_filter_count: {
string: 'filter Count',
type: 'integer',
},
},
records: [{
id: 1,
display_name: 'Belgian Event promotion',
subject: 'Early bird discount for Belgian Events! Register Now!',
mailing_model_id: 1,
mailing_model_name: 'event',
mailing_domain: '[["country","=","be"]]',
mailing_filter_id: 1,
mailing_filter_count: 1,
mailing_filter_domain: '[["country","=","be"]]',
}, {
id: 2,
display_name: 'New Users Promotion',
subject: 'Early bird discount for new users! Register Now!',
mailing_model_id: 1,
mailing_filter_count: 1,
mailing_model_name: 'event',
mailing_domain: '[["new_user","=",True]]',
mailing_filter_domain: '[["new_user","=",True]]',
}],
},
'ir.model': {
fields: {
model: {string: 'Model', type: 'char'},
},
records: [{
id: 1, name: 'Event', model: 'event',
}, {
id: 2, name: 'Partner', model: 'partner',
}],
},
'mailing.filter': {
fields: {
name: {
string: 'Name',
type: 'char',
},
mailing_domain: {
string: 'Mailing Domain',
type: 'char',
},
mailing_model_id: {
string: 'Recipients Model',
type: 'many2one',
relation: 'ir.model'
},
},
records: [{
id: 1,
name: 'Belgian Events',
mailing_domain: '[["country","=","be"]]',
mailing_model_id: 1,
}],
},
});
serverData = { models };
setupViewRegistries();
});
QUnit.test('create favorite filter', async (assert) => {
assert.expect(8);
await makeView({
type: "form",
resModel: "mailing.mailing",
resId: 2,
serverData,
arch: `<form>
<field name="display_name"/>
<field name="subject"/>
<field name="mailing_domain"/>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id"/>
<field name="mailing_filter_count"/>
<field name="mailing_filter_domain" invisible="1"/>
<field name="mailing_filter_id"
widget="mailing_filter"
options="{'no_create': '1', 'no_open': '1', 'domain_field': 'mailing_domain', 'model': 'mailing_model_id'}"/>
</form>`,
mockRPC: function (route, args) {
if (args.method === 'create' && args.model === 'mailing.filter') {
assert.deepEqual(args.args,
[{mailing_domain: '[["new_user","=",True]]', mailing_model_id: 1, name: 'event promo - new users'}],
"should pass correct data in create");
}
},
});
fixture.querySelector('.o_field_mailing_filter input').autocomplete = 'widget';
const $dropdown = fixture.querySelector('.o_field_mailing_filter .dropdown');
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_remove_filter'),
"should hide the option to remove filter if no filter is set");
assert.isVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
"should have option to save filter if no filter is set");
await testUtils.click(fixture.querySelector('.o_field_mailing_filter input'));
assert.containsOnce($dropdown, 'li.ui-menu-item',
"there should be only one existing filter");
// create a new filter
await testUtils.click(fixture, '.o_mass_mailing_add_filter');
fixture.querySelector('.o_mass_mailing_filter_name').value = 'event promo - new users';
// Simulate 'Enter' key, which actually 'clicks' the 'o_mass_mailing_btn_save_filter' btn
await testUtils.triggerEvent(fixture, '.o_mass_mailing_filter_name', 'keydown', { key: 'Enter'});
// check if filter is set correctly
assert.strictEqual(
fixture.querySelector('.o_field_mailing_filter input').value,
'event promo - new users', "saved filter should be set automatically");
await testUtils.nextTick();
assert.isVisible(fixture.querySelector('.o_mass_mailing_remove_filter'),
"should have option to remove filter if filter is already set");
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
"should not have option to save filter if filter is already set");
// Ensures input is not focussed otherwise clicking on it will just close the dropdown instead of opening it
fixture.querySelector('.o_field_mailing_filter .o_input_dropdown input').blur();
await testUtils.click(fixture.querySelector('.o_field_mailing_filter input'));
assert.containsN($dropdown, 'li.ui-menu-item', 2,
"there should be two existing filters");
await testUtils.clickSave(fixture);
});
QUnit.test('unlink favorite filter', async (assert) => {
assert.expect(10);
await makeView({
type: "form",
resModel: "mailing.mailing",
resId: 1,
serverData,
arch: `<form>
<field name="display_name"/>
<field name="subject"/>
<field name="mailing_domain"/>
<field name="mailing_model_id"/>
<field name="mailing_filter_domain" invisible="1"/>
<field name="mailing_filter_count"/>
<field name="mailing_filter_id"
widget="mailing_filter"
options="{'no_create': '1', 'no_open': '1', 'domain_field': 'mailing_domain', 'model': 'mailing_model_id'}"/>
</form>`,
mockRPC: function (route, args) {
if (args.method === 'unlink' && args.model === 'mailing.filter') {
assert.deepEqual(args.args[0], [1], "should pass correct filter ID for deletion");
} else if (args.method === 'write' && args.model === 'mailing.mailing') {
assert.strictEqual(args.args[1].mailing_filter_id,
false, "filter id should be");
assert.strictEqual(args.args[1].mailing_domain,
'[["country","=","be"]]', "mailing domain should be retained while unlinking filter");
}
},
});
assert.strictEqual(
fixture.querySelector('.o_field_mailing_filter input').value,
'Belgian Events', "there should be filter set");
assert.isVisible(fixture.querySelector('.o_mass_mailing_remove_filter'),
"should have option to remove filter if filter is already set");
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
"should hide the option to save filter if filter is already set");
// unlink filter
await testUtils.click(fixture.querySelector('.o_mass_mailing_remove_filter'));
assert.strictEqual(
fixture.querySelector('.o_field_mailing_filter input').value,
'', "filter should be empty");
await testUtils.nextTick();
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_remove_filter'),
"should hide the option to remove filter if no filter is set");
assert.isVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
"should not hide the option to save filter if no filter is set");
// check drop-down after filter deletion
fixture.querySelector('.o_field_mailing_filter input').autocomplete = 'widget';
const $dropdown = fixture.querySelector('.o_field_mailing_filter .dropdown');
await testUtils.click(fixture.querySelector('.o_field_mailing_filter input'));
assert.containsOnce($dropdown, 'li.ui-menu-item.o_m2o_no_result',
"there should be no available filters");
await testUtils.clickSave(fixture);
});
QUnit.test('changing filter correctly applies the domain', async (assert) => {
assert.expect(2);
serverData.models.partner = {
fields: {
name: {string: 'Name', type: 'char', searchable: true},
},
records: [
{id: 1, name: 'Azure Interior'},
{id: 2, name: 'Deco Addict'},
{id: 3, name: 'Marc Demo'},
]
};
serverData.models['mailing.filter'].records = [{
id: 1,
name: 'Azure Partner Only',
mailing_domain: "[['name','=', 'Azure Interior']]",
mailing_model_id: 2,
}];
serverData.models['mailing.mailing'].records.push({
id: 3,
display_name: 'Partner Event promotion',
subject: 'Early bird discount for Partners!',
mailing_model_id: 2,
mailing_model_name: 'partner',
mailing_filter_count: 1,
mailing_domain: "[['name','!=', 'Azure Interior']]",
});
serverData.models['mailing.mailing'].onchanges = {
mailing_filter_id: obj => {
obj.mailing_domain = serverData.models['mailing.filter'].records.filter(r => r.id === obj.mailing_filter_id)[0].mailing_domain;
},
};
await makeView({
type: "form",
resModel: "mailing.mailing",
resId: 3,
serverData,
arch: `<form>
<field name="display_name"/>
<field name="subject"/>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id"/>
<field name="mailing_filter_count" />
<field name="mailing_filter_id" widget="mailing_filter" options="{'no_create': '1', 'no_open': '1', 'domain_field': 'mailing_domain', 'model': 'mailing_model_id'}"/>
<group>
<field name="mailing_domain" widget="domain" options="{'model': 'mailing_model_name'}"/>
</group>
</form>`,
});
assert.equal(fixture.querySelector('.o_domain_show_selection_button').textContent.trim(), '2 record(s)',
"default domain should filter 2 records (all but Azure)");
await testUtils.click(fixture.querySelector('.o_field_mailing_filter input'));
fixture.querySelector('.o_field_mailing_filter input').autocomplete = 'widget';
const $dropdown = fixture.querySelector('.o_field_mailing_filter .dropdown');
await testUtils.click($dropdown.lastElementChild, 'li');
assert.equal(fixture.querySelector('.o_domain_show_selection_button').textContent.trim(), '1 record(s)',
"applied filter should only display single record (only Azure)");
await testUtils.clickSave(fixture);
});
QUnit.test('filter drop-down and filter icons visibility toggles properly based on filters available', async (assert) => {
assert.expect(11);
serverData.models.partner = {
fields: {
name: {string: 'Name', type: 'char', searchable: true},
},
records: [
{id: 1, name: 'Azure Interior'},
]
};
serverData.models.event = {
fields: {
name: {string: 'Name', type: 'char', searchable: true},
country: {string: 'Country', type: 'char', searchable: true},
},
records: [
{id: 1, name: 'BE Event', country: 'be'},
]
};
serverData.models['mailing.filter'].records = [{
id: 2,
name: 'Azure partner',
mailing_domain: '[["name","=","Azure Interior"]]',
mailing_model_id: 2,
}, {
id: 3,
name: 'Ready Mat partner',
mailing_domain: '[["name","=","Ready Mat"]]',
mailing_model_id: 2,
}];
serverData.models['mailing.mailing'].records = [{
id: 1,
display_name: 'Belgian Event promotion',
subject: 'Early bird discount for Belgian Events! Register Now!',
mailing_model_id: 1,
mailing_model_name: 'event',
mailing_domain: '[["country","=","be"]]',
mailing_filter_id: false,
mailing_filter_count: 0,
}];
serverData.models['mailing.mailing'].onchanges = {
mailing_model_id: obj => {
obj.mailing_filter_count = serverData.models['mailing.filter'].records.filter(r => r.mailing_model_id === obj.mailing_model_id).length;
},
mailing_filter_id: obj => {
const filterDomain = serverData.models['mailing.filter'].records.filter(r => r.id === obj.mailing_filter_id)[0].mailing_domain;
obj.mailing_domain = filterDomain, obj.mailing_filter_domain = filterDomain;
},
};
await makeView({
type: "form",
resModel: "mailing.mailing",
resId: 1,
serverData,
arch: `<form>
<field name="display_name"/>
<field name="subject"/>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id"/>
<field name="mailing_filter_count" />
<field name="mailing_filter_id" widget="mailing_filter"
options="{'no_create': '1', 'no_open': '1', 'domain_field': 'mailing_domain', 'model': 'mailing_model_id'}"/>
<field name="mailing_filter_domain" invisible="1"/>
<group>
<field name="mailing_domain" widget="domain" options="{'model': 'mailing_model_name'}"/>
</group>
</form>`,
});
assert.isNotVisible(fixture.querySelector('.o_field_mailing_filter .o_input_dropdown'),
"should hide the drop-down to select a filter because there is no filter available for 'Event'");
assert.isVisible(fixture.querySelector('.o_mass_mailing_no_filter'),
"should show custom message because there is no filter available for 'Event'");
assert.isVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
"should show icon to save the filter because domain is set in the mailing");
// If domain is not set on mailing and no filter available, both drop-down and icon container are hidden
await testUtils.click(fixture.querySelector('.o_domain_delete_node_button'));
assert.isNotVisible(fixture.querySelector('.o_field_mailing_filter .o_input_dropdown'),
"should not display drop-down because there is still no filter available to select from");
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_filter_container'),
"should not show filter container because there is no filter available and no domain set in mailing");
// If domain is not set on mailing but filters available, display drop-down but hide the icon container
await testUtils.clickDropdown(fixture, 'mailing_model_id');
await testUtils.clickOpenedDropdownItem(fixture, 'mailing_model_id', 'Partner');
assert.isVisible(fixture.querySelector('.o_field_mailing_filter .o_input_dropdown'),
"should show the drop-down to select a filter because there are filters available for 'Partner'");
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_filter_container'),
"should not show filter container because there is no filter selected and no domain set in mailing");
// Save / Remove icons visibility
await testUtils.click(fixture.querySelector('.o_field_mailing_filter input'));
await testUtils.clickOpenedDropdownItem(fixture, 'mailing_filter_id', 'Azure partner');
await testUtils.nextTick();
assert.isVisible(fixture.querySelector('.o_mass_mailing_remove_filter'),
"should have option to remove filter if filter is selected");
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
"should not have option to save filter if filter is selected");
await testUtils.click(fixture.querySelector('.o_domain_add_node_button'));
await testUtils.nextTick();
assert.isVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
"should have option to save filter because mailing domain is changed");
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_remove_filter'),
"should not have option to remove filter because mailing domain is changed");
});
QUnit.test('filter widget does not raise traceback when losing focus with unexpected domain format', async (assert) => {
assert.expect(4);
await makeView({
type: "form",
resModel: "mailing.mailing",
resId: 2,
serverData,
arch: `<form>
<field name="display_name"/>
<field name="subject"/>
<field name="mailing_domain"/>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id"/>
<field name="mailing_filter_count"/>
<field name="mailing_filter_domain" invisible="1"/>
<field name="mailing_filter_id"
widget="mailing_filter"
options="{'no_create': '1', 'no_open': '1', 'domain_field': 'mailing_domain', 'model': 'mailing_model_id'}"/>
</form>`,
});
// Initial state of icons with no filter
assert.isVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
"should have option to save filter if no filter is set");
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_remove_filter'));
// Set incorrect domain format
await testUtils.editInput(fixture, "#mailing_domain", "[");
// Wait to lose the focus
await testUtils.nextTick();
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
"should not have option to save filter if domain format is incorrect");
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_remove_filter'),
"should still not be visible");
});
QUnit.test('filter widget works in edit and readonly', async (assert) => {
assert.expect(4);
serverData.models.partner = {
fields: {
name: { string: 'Name', type: 'char', searchable: true },
},
};
serverData.models['mailing.filter'].records = [{
id: 1,
name: 'Azure Partner Only',
mailing_domain: "[['name','=', 'Azure Interior']]",
mailing_model_id: 2,
}];
serverData.models['mailing.mailing'].records.push({
id: 3,
display_name: 'Partner Event promotion',
subject: 'Early bird discount for Partners!',
mailing_model_id: 2,
mailing_model_name: 'partner',
mailing_filter_count: 1,
mailing_filter_domain: "[['name','=', 'Azure Interior']]",
mailing_filter_id: 1,
mailing_domain: "[['name','=', 'Azure Interior']]",
state: 'draft',
});
serverData.models['mailing.mailing'].fields.state = {
string: 'Stage',
type: 'selection',
selection: [['draft', 'Draft'], ['running', 'Running']]
};
serverData.models['mailing.mailing'].onchanges = {
mailing_filter_id: obj => {
obj.mailing_domain = serverData.models['mailing.filter'].records.filter(r => r.id === obj.mailing_filter_id)[0].mailing_domain;
},
};
await makeView({
type: "form",
resModel: "mailing.mailing",
resId: 3,
serverData,
arch: `<form>
<field name="display_name"/>
<field name="subject"/>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="mailing_filter_count" />
<field name="mailing_filter_id" widget="mailing_filter" options="{'no_create': '1', 'no_open': '1', 'domain_field': 'mailing_domain', 'model': 'mailing_model_id'}"/>
<field name="state" widget="statusbar" options="{'clickable' : '1'}"/>
<group>
<field name="mailing_domain" widget="domain" options="{'model': 'mailing_model_name'}"/>
</group>
</form>`,
});
await testUtils.nextTick();
const selectField = fixture.querySelector("button[data-value='running']");
assert.containsOnce(fixture, "div[name='mailing_model_id']:not(.o_readonly_modifier)");
assert.ok(fixture.querySelector(".o_mass_mailing_save_filter_container").checkVisibility());
// set to 'running'
selectField.dispatchEvent(new Event('click'));
selectField.dispatchEvent(new Event('change'));
await testUtils.nextTick();
assert.containsOnce(fixture, "div[name='mailing_model_id'].o_readonly_modifier");
assert.ok(fixture.querySelector(".o_mass_mailing_save_filter_container").checkVisibility());
});
});
});

View file

@ -0,0 +1,521 @@
import { expect, test, describe, beforeEach, getFixture } from "@odoo/hoot";
import {
defineModels,
fields,
models,
mountView,
onRpc,
clickSave,
patchWithCleanup,
contains,
getPagerValue,
getPagerLimit,
} from "@web/../tests/web_test_helpers";
import { click, queryAny, queryOne, waitFor } from "@odoo/hoot-dom";
import { animationFrame, runAllTimers } from "@odoo/hoot-mock";
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { unmockedOrm } from "@web/../tests/_framework/module_set.hoot";
import { MassMailingIframe } from "../src/iframe/mass_mailing_iframe";
import { MassMailingHtmlField } from "../src/fields/html_field/mass_mailing_html_field";
import { FormController } from "@web/views/form/form_controller";
class Mailing extends models.Model {
_name = "mailing.mailing";
display_name = fields.Char();
subject = fields.Char();
body_arch = fields.Html();
body_html = fields.Html();
mailing_model_id = fields.Many2one({ relation: "ir.model", string: "Recipients" });
mailing_model_real = fields.Char({
string: "Recipients Model Name (real)",
compute: "compute_model_real",
});
mailing_model_name = fields.Char({ string: "Recipients Model Name" });
state = fields.Selection({
string: "Status",
default: "draft",
selection: [
["draft", "Draft"],
["in_queue", "In Queue"],
["sending", "Sending"],
["done", "Sent"],
],
});
compute_model_real() {
for (const record of this) {
record.mailing_model_real = this.env["ir.model"].browse([
record.mailing_model_id,
])[0].model;
}
}
action_fetch_favorites() {
return [];
}
_records = [
{
id: 1,
display_name: "Belgian Event promotion",
mailing_model_id: 1,
},
{
id: 2,
display_name: "Sent Belgian Event promotion",
mailing_model_id: 1,
body_arch: `
<div data_name="Mailing" class="o_layout oe_unremovable oe_unmovable o_empty_theme">
<div class="container o_mail_wrapper o_mail_regular oe_unremovable">
<div class="row">
<div class="col o_mail_no_options o_mail_wrapper_td bg-white oe_structure o_editable oe_empty" data-editor-message-default="true" data-editor-message="DRAG BUILDING BLOCKS HERE" contenteditable="true">
This element <t t-out="'should be inline'"/>
</div>
</div>
</div>
</div>
`,
state: "done",
},
{
id: 3,
display_name: "Readonly",
mailing_model_id: 1,
body_arch: `
<div data_name="Mailing" class="o_layout oe_unremovable oe_unmovable o_default_theme">
<div class="container o_mail_wrapper o_mail_regular oe_unremovable">
<div class="row mw-100 mx-0">
<div class="col o_mail_no_options o_mail_wrapper_td bg-white oe_structure">
<section class="s_text_block o_mail_snippet_general" data-snippet="s_text_block">
<div class="container">
<p>Readonly</p>
</div>
</section>
</div>
</div>
</div>
</div>
`,
state: "done",
},
{
id: 4,
display_name: "Basic",
mailing_model_id: 1,
body_arch: `
<div data_name="Mailing" class="o_layout oe_unremovable oe_unmovable o_basic_theme">
<div class="oe_structure">
<div class="o_mail_no_options">
<p>Basic</p>
</div>
</div>
</div>
`,
},
{
id: 5,
display_name: "Builder",
mailing_model_id: 1,
body_arch: `
<div data_name="Mailing" class="o_layout oe_unremovable oe_unmovable o_empty_theme">
<div class="container o_mail_wrapper o_mail_regular oe_unremovable">
<div class="row mw-100 mx-0">
<div class="col o_mail_no_options o_mail_wrapper_td bg-white oe_structure">
<section class="s_text_block o_mail_snippet_general" data-snippet="s_text_block">
<div class="container">
<p>Builder</p>
</div>
</section>
</div>
</div>
</div>
</div>
`,
},
];
}
class IrUiView extends models.Model {
async render_public_asset(template, values) {
return unmockedOrm("ir.ui.view", "render_public_asset", [template, values], {});
}
}
class IrModel extends models.Model {
_name = "ir.model";
name = fields.Char();
display_name = fields.Char();
model = fields.Char();
_records = [
{
id: 1,
name: "Event",
display_name: "Event",
model: "event",
},
];
}
class Event extends models.Model {
_name = "event";
name = fields.Char();
country = fields.Char();
_records = [{ id: 1, name: "BE Event", country: "be" }];
}
defineMailModels();
defineModels([IrModel, IrUiView, Mailing, Event]);
const mailViewArch = `
<form>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id" invisible="1"/>
<field name="mailing_model_real" invisible="1"/>
<field name="state" invisible="1"/>
<field name="body_html" class="o_mail_body_inline"/>
<field name="body_arch" class="o_mail_body_mailing" widget="mass_mailing_html"
options="{
'inline_field': 'body_html',
'dynamic_placeholder': true,
'dynamic_placeholder_model_reference_field': 'mailing_model_real'
}" readonly="state in ('sending', 'done')"/>
</form>
`;
/**
* @type {MassMailingHtmlField}
*/
let htmlField;
describe.current.tags("desktop");
beforeEach(() => {
patchWithCleanup(MassMailingHtmlField.prototype, {
setup() {
super.setup();
htmlField = this;
},
});
});
describe("field HTML", () => {
beforeEach(() => {
patchWithCleanup(MassMailingIframe.prototype, {
// Css assets are not needed for these tests.
loadIframeAssets() {
return {
"mass_mailing.assets_iframe_style": {
toggle: () => {},
},
"mass_mailing.assets_inside_basic_editor_iframe": {
toggle: () => {},
},
"mass_mailing.assets_inside_builder_iframe": {
toggle: () => {},
},
};
},
});
});
test("save arch and html", async () => {
onRpc("web_save", ({ args }) => {
expect(args[1].body_arch).toMatch(/^<div/);
expect(args[1].body_html).toMatch(/^<table/);
expect.step("web_save mail body");
});
await mountView({
type: "form",
resModel: "mailing.mailing",
resId: 1,
arch: mailViewArch,
});
expect(queryOne(".o_mass_mailing_iframe_wrapper iframe")).toHaveClass("d-none");
await click(
waitFor(
".o_mailing_template_preview_wrapper div[role='menuitem']:contains(Start From Scratch)"
)
);
await waitFor(".o_mass_mailing_iframe_wrapper iframe:not(.d-none)");
expect(await waitFor(":iframe .o_layout", { timeout: 3000 })).toHaveClass("o_empty_theme");
await clickSave();
await expect.waitForSteps(["web_save mail body"]);
});
test("t-out field in uneditable mode inline", async () => {
await mountView({
type: "form",
resModel: "mailing.mailing",
resId: 2,
arch: mailViewArch,
});
await waitFor(".o_mass_mailing_iframe_wrapper iframe:not(.d-none)");
const tElement = await waitFor(":iframe t", { timeout: 3000 });
// assert that we are in readonly mode (sanity check)
expect(":iframe .o_mass_mailing_value.o_readonly").toHaveCount(1);
// assert that tElement style has inline attibute
expect(tElement).toHaveAttribute("data-oe-t-inline", "true");
});
test("switch out from a notebook tab with html field should update the record", async () => {
const arch = `
<form>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id" invisible="1"/>
<field name="mailing_model_real" invisible="1"/>
<field name="state" invisible="1"/>
<notebook>
<page string="body_arch" name="body_arch">
<field name="body_arch" class="o_mail_body_mailing" widget="mass_mailing_html"
options="{
'inline_field': 'body_html',
'dynamic_placeholder': true,
'dynamic_placeholder_model_reference_field': 'mailing_model_real'
}" readonly="state in ('sending', 'done')"/>
</page>
<page string="body_html" name="body_html">
<field name="body_html" class="o_mail_body_inline" readonly="true"/>
</page>
</notebook>
</form>
`;
await mountView({
type: "form",
resModel: "mailing.mailing",
resId: 5,
arch,
});
await waitFor(":iframe .o_layout .container:contains(Builder)", { timeout: 3000 });
// ensure conversion of the existing html content
await htmlField.commitChanges();
const oldConvert = htmlField.converter.convertToEmailHtml;
const { promise: conversionDelay, resolve: resumeConversion } = Promise.withResolvers();
htmlField.converter.convertToEmailHtml = (fragment, config) =>
conversionDelay.then(() => oldConvert(fragment, config));
const p = htmlField.editor.editable.querySelector("p");
p.append(htmlField.editor.document.createTextNode("Updated"));
htmlField.editor.shared.history.addStep();
await contains(".o_notebook a[name='body_html']").click();
await animationFrame();
await waitFor(".o_notebook a[name='body_html']");
resumeConversion();
expect(
(
await waitFor(".o_field_widget[name='body_html']:contains(BuilderUpdated)", {
timeout: 3000,
})
).innerText.trim()
).toBe("BuilderUpdated");
});
test("beforeLeave a FormController with html field should save the record", async () => {
let formController;
patchWithCleanup(FormController.prototype, {
setup() {
formController = this;
super.setup();
},
});
const arch = `
<form>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id" invisible="1"/>
<field name="mailing_model_real" invisible="1"/>
<field name="state" invisible="1"/>
<notebook>
<page string="body_arch" name="body_arch">
<field name="body_arch" class="o_mail_body_mailing" widget="mass_mailing_html"
options="{
'inline_field': 'body_html',
'dynamic_placeholder': true,
'dynamic_placeholder_model_reference_field': 'mailing_model_real'
}" readonly="state in ('sending', 'done')"/>
<field name="body_html" class="o_mail_body_inline" readonly="true"/>
</page>
</notebook>
</form>
`;
await mountView({
type: "form",
resModel: "mailing.mailing",
resId: 5,
arch,
});
await waitFor(":iframe .o_layout .container:contains(Builder)", { timeout: 3000 });
// ensure conversion of the existing html content
await htmlField.commitChanges();
expect(
(
await waitFor(".o_field_widget[name='body_html']:contains(Builder)", {
timeout: 3000,
})
).innerText.trim()
).toBe("Builder");
const p = htmlField.editor.editable.querySelector("p");
p.append(htmlField.editor.document.createTextNode("Updated"));
htmlField.editor.shared.history.addStep();
await formController.beforeLeave();
expect(
(
await waitFor(".o_field_widget[name='body_html']:contains(BuilderUpdated)", {
timeout: 3000,
})
).innerText.trim()
).toBe("BuilderUpdated");
});
test("builder in modal -- owl reconciliation iframe unload", async () => {
// Related to ad-hoc fix where in modal, some editor's popovers
// get to be spawned before the modal in the DOM and in OWL
// When those popovers are killed, OWL tries to reconcile its element List
// in OverlayContainer, displaces the node that contains the iframe
// and the editor subsequently crashes
const base64Img =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=";
onRpc("/html_editor/get_image_info", () => ({
original: { image_src: base64Img },
}));
class SomeModel extends models.Model {
_name = "some.model";
mailing_ids = fields.One2many({ relation: "mailing.mailing" });
_records = [
{
id: 1,
mailing_ids: [1],
},
];
}
Mailing._views["form"] = mailViewArch;
defineModels([SomeModel]);
const arch = `<form><field name="mailing_ids"> <list><field name="display_name" /></list> </field></form>`;
await mountView({
type: "form",
resModel: "some.model",
arch,
resId: 1,
});
await contains(".o_data_cell").click();
await waitFor(".o_dialog");
await contains(".o_dialog [data-name='event']").click();
await waitFor(".o_dialog .o_mass_mailing-builder_sidebar", { timeout: 1000 });
await contains(".o_dialog :iframe p", { timeout: 1000 }).click();
await waitFor(
".o_dialog .o_mass_mailing-builder_sidebar .options-container-header:contains(Text)"
);
const overlayOptionsSelect =
".o-main-components-container .o-overlay-container .o_overlay_options";
await waitFor(overlayOptionsSelect + ":has(button[title='Move up'])");
await contains(".o_dialog :iframe img").click();
await waitFor(overlayOptionsSelect + ":not(:has(button[title='Move up']))");
});
test("preprocess some domain", async () => {
await mountView({
type: "form",
resModel: "mailing.mailing",
resId: 1,
arch: mailViewArch,
});
await click(waitFor(".o_mailing_template_preview_wrapper [data-name='default']"));
await waitFor(".o_mass_mailing_iframe_wrapper iframe:not(.d-none)");
expect(await waitFor(":iframe .o_layout", { timeout: 3000 })).toHaveClass(
"o_default_theme"
);
await runAllTimers();
const section = queryAny(":iframe section");
section.dataset.filterDomain = JSON.stringify([["id", "=", 1]]);
htmlField.editor.config.onChange({ isPreviewing: false });
await click(section);
await waitFor(".hb-row .hb-row-label span:contains(Domain)");
expect(queryOne(".hb-row span.fa-filter + span").textContent.toLowerCase()).toBe("id = 1");
await clickSave();
await waitFor("table[t-if]");
expect(queryOne("table[t-if]")).toHaveAttribute(
"t-if",
'object.filtered_domain([("id", "=", 1)])'
);
});
test(`Switching mailing records in the Form view properly switches between basic Editor, HtmlBuilder and readonly`, async () => {
const fixture = getFixture();
let htmlField;
patchWithCleanup(MassMailingHtmlField.prototype, {
setup() {
super.setup();
htmlField = this;
},
});
await mountView({
resModel: "mailing.mailing",
type: "form",
arch: mailViewArch,
resIds: [3, 4, 5],
resId: 3,
});
// readonly default
expect(await waitFor(":iframe .o_layout", { timeout: 3000 })).toHaveClass(
"o_default_theme"
);
expect(getPagerValue()).toEqual([1]);
expect(getPagerLimit()).toBe(3);
expect(htmlField.state.activeTheme).toBe("default");
expect(fixture.querySelectorAll(".o_mass_mailing-builder_sidebar")).toHaveCount(0);
// editable basic
await contains(`.o_pager_next`).click();
await waitFor(".o_mass_mailing_iframe_wrapper :iframe .o_layout.o_basic_theme:only-child", {
timeout: 3000,
});
expect(getPagerValue()).toEqual([2]);
expect(htmlField.state.activeTheme).toBe("basic");
expect(fixture.querySelectorAll(".o_mass_mailing-builder_sidebar")).toHaveCount(0);
// editable builder
await contains(`.o_pager_next`).click();
await waitFor(".o_mass_mailing_iframe_wrapper :iframe .o_layout.o_empty_theme:only-child", {
timeout: 3000,
});
expect(getPagerValue()).toEqual([3]);
expect(htmlField.state.activeTheme).toBe("empty");
expect(fixture.querySelectorAll(".o_mass_mailing-builder_sidebar")).toHaveCount(1);
// readonly default
await contains(`.o_pager_next`).click();
await waitFor(
".o_mass_mailing_iframe_wrapper :iframe .o_layout.o_default_theme:only-child",
{ timeout: 3000 }
);
expect(getPagerValue()).toEqual([1]);
expect(htmlField.state.activeTheme).toBe("default");
expect(fixture.querySelectorAll(".o_mass_mailing-builder_sidebar")).toHaveCount(0);
});
});
describe("field HTML: with loaded assets", () => {
test("Ensure style bundles loaded in the `MassMailingIframe` can be toggled On or Off", async () => {
await mountView({
type: "form",
resModel: "mailing.mailing",
resId: 1,
arch: mailViewArch,
});
await click(waitFor(".o_mailing_template_preview_wrapper [data-name='default']"));
await waitFor(".o_mass_mailing_iframe_wrapper iframe:not(.d-none)");
const { bundleControls } = await htmlField.ensureIframeLoaded();
expect(
htmlField.iframeRef.el.contentDocument.head.querySelectorAll(
'[href*="mass_mailing.assets_inside_builder_iframe"]'
)
).toHaveLength(1);
bundleControls["mass_mailing.assets_inside_builder_iframe"].toggle(false);
expect(
htmlField.iframeRef.el.contentDocument.head.querySelectorAll(
'[href*="mass_mailing.assets_inside_builder_iframe"]'
)
).toHaveLength(0);
bundleControls["mass_mailing.assets_inside_builder_iframe"].toggle(true);
expect(
htmlField.iframeRef.el.contentDocument.head.querySelectorAll(
'[href*="mass_mailing.assets_inside_builder_iframe"]'
)
).toHaveLength(1);
});
});

View file

@ -1,140 +0,0 @@
/** @odoo-module alias=mass_mailing.field_html_tests **/
import * as ajax from "web.ajax";
import weTestUtils from "web_editor.test_utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import {
editInput,
getFixture,
makeDeferred,
nextTick,
patchWithCleanup,
} from "@web/../tests/helpers/utils";
import * as legacyTestUtils from "web.test_utils";
import { assets } from "@web/core/assets";
let serverData;
let fixture;
QUnit.module('mass_mailing', {}, function () {
QUnit.module('field html', (hooks) => {
hooks.beforeEach(() => {
fixture = getFixture();
const models = weTestUtils.wysiwygData({
'mailing.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;'><p>code to edit</p></div>",
body_arch: "<div class='field_body'><p>code to edit</p></div>",
}],
},
});
serverData = { models };
setupViewRegistries();
legacyTestUtils.mock.patch(ajax, {
loadAsset: function (xmlId) {
if (xmlId === 'template.assets') {
return Promise.resolve({
cssLibs: [],
cssContents: ['.field_body {background-color: red;}'],
jsContents: ['window.odoo = {define: function(){}}; // inline asset'],
});
}
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: ['.field_body {background-color: red;}']
});
}
if (xmlId === 'web_editor.wysiwyg_iframe_editor_assets') {
return Promise.resolve({});
}
throw 'Wrong template';
},
});
});
hooks.afterEach(() => {
legacyTestUtils.mock.unpatch(ajax);
});
QUnit.test('save arch and html', async function (assert) {
assert.expect(2);
await makeView({
type: "form",
resModel: 'mailing.mailing',
resId: 1,
serverData,
arch: '<form>' +
' <field name="body_html" class="oe_read_only"'+
' options="{'+
' \'cssReadonly\': \'template.assets\','+
' }"'+
' />'+
' <field name="body_arch" class="oe_edit_only" widget="mass_mailing_html"'+
' options="{'+
' \'snippets\': \'web_editor.snippets\','+
' \'cssEdit\': \'template.assets\','+
' \'inline-field\': \'body_html\''+
' }"'+
' />'+
'</form>',
});
await nextTick();
let fieldReadonly = fixture.querySelector('.o_field_widget[name="body_html"]');
let fieldEdit = fixture.querySelector('.o_field_widget[name="body_arch"]');
assert.strictEqual($(fieldReadonly).css('display'), 'none', "should hide the readonly mode");
assert.strictEqual($(fieldEdit).css('display'), 'block', "should display the edit mode");
});
QUnit.test('component destroyed while loading', async function (assert) {
const def = makeDeferred();
patchWithCleanup(assets, {
loadBundle() {
assert.step("loadBundle");
return def;
}
})
await makeView({
type: "form",
resModel: 'mailing.mailing',
resId: 1,
serverData,
arch: `
<form>
<field name="display_name"/>
<field name="body_arch" widget="mass_mailing_html" attrs="{'invisible': [['display_name', '=', 'hide']]}"/>
</form>`,
});
assert.containsOnce(fixture, ".o_field_widget[name=body_arch]");
await editInput(fixture, ".o_field_widget[name=display_name] input", "hide");
assert.containsNone(fixture, ".o_field_widget[name=body_arch]");
def.resolve();
await nextTick();
assert.containsNone(fixture, ".o_field_widget[name=body_arch]");
assert.verifySteps(["loadBundle"]);
});
});
});

View file

@ -0,0 +1,65 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
registry.category('web_tour.tours').add('mailing_campaign', {
url: '/odoo',
steps: () => [
stepUtils.showAppsMenuItem(),
{
content: 'Select the "Email Marketing" app',
trigger: '.o_app[data-menu-xmlid="mass_mailing.mass_mailing_menu_root"]',
run: "click",
},
{
content: 'Select "Campaings" Navbar item',
trigger: '.o_nav_entry[data-menu-xmlid="mass_mailing.menu_email_campaigns"]',
run: "click",
},
{
content: 'Select "Newsletter" campaign',
trigger: '.o_kanban_record:contains("Newsletter")',
run: "click",
},
{
content: 'Add a line (create new mailing)',
trigger: '.o_field_x2many_list_row_add a',
run: "click",
},
{
content: 'Pick the basic theme',
trigger: ".o_mailing_template_preview_wrapper [data-name='basic']",
run: "click",
},
{
trigger: ":iframe .o_mass_mailing_value .o_layout",
},
{
content: 'Fill in Subject',
trigger: '#subject_0',
run: "edit TestFromTour",
},
{
content: 'Fill in Mailing list',
trigger: '#contact_list_ids_0',
run: "edit Newsletter",
},
{
content: 'Pick "Newsletter" option',
trigger: '.o_input_dropdown a:contains(Newsletter)',
run: "click",
},
{
content: 'Save form',
trigger: ".modal .o_form_button_save:contains(Save & Close)",
run: "click",
},
{
trigger: "body:not(:has(.modal))",
},
{
content: 'Check that newly created record is on the list',
trigger: '[name="mailing_mail_ids"] td[name="subject"]:contains("TestFromTour")',
},
...stepUtils.saveForm(),
],
});

View file

@ -0,0 +1,58 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
registry.category("web_tour.tours").add('mailing_editor', {
url: '/odoo',
steps: () => [stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="mass_mailing.mass_mailing_menu_root"]',
run: "click",
}, {
trigger: 'button.o_list_button_add',
run: "click",
}, {
trigger: 'div[name="contact_list_ids"] .o_input_dropdown input[type="text"]',
run: "edit Test",
}, {
trigger: 'div[name="contact_list_ids"] .ui-state-active',
run: "click",
}, {
content: 'choose the theme "empty" to edit the mailing with snippets',
trigger: '[name="body_arch"] .o_mailing_template_preview_wrapper [data-name="empty"]',
run: "click",
}, {
content: 'wait for the editor to be rendered',
trigger: '[name="body_arch"] :iframe .o_editable[data-editor-message="DRAG BUILDING BLOCKS HERE"]',
}, {
trigger: '.o_snippet[name="Text"] button',
content: 'Click the "Text" snippet category to drop a snippet in the editor',
run: "click",
}, {
trigger: ":iframe .o_snippet_preview_wrap:has(.s_title)",
content: "Select the Title Snippet",
run: "click",
}, {
content: 'wait for the snippet menu to finish the drop process',
trigger: 'body:not(:has(.o_we_ongoing_insertion))',
}, {
content: 'verify that the title was inserted properly in the editor',
trigger: '[name="body_arch"] :iframe .o_editable h1',
}, {
trigger: 'button.o_form_button_save',
run: "click",
}, {
content: 'verify that the save failed (since the field "subject" was not set and it is required)',
trigger: 'label.o_field_invalid',
}, {
content: 'verify that the edited mailing body was not lost during the failed save',
trigger: '[name="body_arch"] :iframe .o_editable h1',
}, {
trigger: 'input#subject_0',
run: "edit TestFromTour",
}, {
trigger: '.o_form_view', // blur previous input
run: "click",
},
...stepUtils.saveForm(),
{
trigger: ':iframe .o_editable',
}]});

View file

@ -0,0 +1,166 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
import { boundariesIn } from "@html_editor/utils/position";
import { setSelection } from "@html_editor/../tests/tours/helpers/editor";
registry.category("web_tour.tours").add('mailing_editor_theme', {
url: '/odoo',
steps: () => [
stepUtils.showAppsMenuItem(),
{
content: "Select the 'Email Marketing' app.",
trigger: '.o_app[data-menu-xmlid="mass_mailing.mass_mailing_menu_root"]',
run: "click",
},
{
content: "Click on the create button to create a new mailing.",
trigger: 'button.o_list_button_add',
run: "click",
},
{
content: "Fill in Subject",
trigger: '#subject_0',
run: "edit Test Basic Theme",
},
{
content: "Fill in Mailing list",
trigger: '#contact_list_ids_0',
run: "edit Newsletter",
},
{
content: "Pick 'Newsletter' option",
trigger: '.o_input_dropdown a:contains(Newsletter)',
run: "click",
},
{
trigger: ".o_mailing_template_preview_wrapper",
},
{
content: "Pick the basic theme",
trigger: '.o_mailing_template_preview_wrapper [data-name="basic"]',
run: "click",
},
{
trigger: "html:not(:has(.o_mailing_template_preview_wrapper))",
},
{
content: "Make sure the snippets menu is hidden",
trigger: "html:not(:has(.o-snippets-menu))",
},
...stepUtils.saveForm(),
{
content: "Click on the New button to create another mailing",
trigger: 'button.o_form_button_create',
run: "click",
},
{
trigger: ".o_mailing_template_preview_wrapper",
},
{
content: "Fill in Subject",
trigger: '#subject_0',
run: "edit Test Newsletter Theme",
},
{
content: "Fill in Mailing list",
trigger: '#contact_list_ids_0',
run: "edit Newsletter",
},
{
content: "Pick 'Newsletter' option",
trigger: '.o_input_dropdown a:contains(Newsletter)',
run: "click",
},
{
content: "Pick the newsletter theme",
trigger: '.o_mailing_template_preview_wrapper [data-name="newsletter"]',
run: "click",
},
{
content: "Make sure the snippets menu is displayed",
trigger: ".o-snippets-menu",
},
...stepUtils.discardForm(),
{
content: 'Go back to previous mailing',
trigger: 'td[name="subject"]:contains(Test Basic Theme)',
run: "click",
},
{
content: "Make sure the snippets menu is hidden",
trigger: "html:not(:has(.o-snippets-menu))",
},
{
content: "Add some content to be selected afterwards",
trigger: ':iframe p',
run: "editor content",
},
{
content: "Select text",
trigger: ':iframe p:contains(content)',
run() {
const [anchorNode, anchorOffset, focusNode, focusOffset] = boundariesIn(
this.anchor
);
setSelection({ anchorNode, anchorOffset, focusNode, focusOffset });
}
},
{
content: "Make sure the floating toolbar is visible",
trigger: '.overlay:has(.o-we-toolbar)[style*="visible"]',
},
{
content: "Expand Toolbar",
trigger: ".o-we-toolbar button[name='expand_toolbar']",
run: "click",
},
{
content: "Open the color picker",
trigger: ".o-select-color-foreground",
run: "click",
},
{
content: "Open Solid tab",
trigger: ".btn-tab.solid-tab",
run: "click",
},
{
content: "Pick a color",
trigger: '.o_font_color_selector button[data-color="o-color-1"]',
run: "click",
},
{
content: "Check that color was applied",
trigger: ':iframe p font.text-o-color-1',
},
...stepUtils.saveForm(),
{
content: "Go to 'Mailings' list view",
trigger: '.breadcrumb a:contains(Mailings)',
run: "click",
},
{
content: "Open newly created mailing",
trigger: 'td:contains("Test Basic Theme")',
run: "click",
},
{
content: "Make sure the snippets menu is hidden",
trigger: "html:not(:has(.o-snippets-menu))",
},
{
content: "Select content",
trigger: ':iframe p:contains(content)',
run() {
const [anchorNode, anchorOffset, focusNode, focusOffset] = boundariesIn(
this.anchor
);
setSelection({ anchorNode, anchorOffset, focusNode, focusOffset });
}
},
{
content: "Make sure the floating toolbar is visible",
trigger: '.overlay:has(.o-we-toolbar)[style*="visible"]',
},
],
});

View file

@ -0,0 +1,122 @@
import { registry } from "@web/core/registry";
/*
* Tour: unsubscribe from a mailing done on documents (aka not on contacts or
* mailing lists). We assume email is not member of any mailing list in this test.
*/
registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_document', {
steps: () => [
{
content: "Confirmation unsubscribe is done",
trigger: "div#o_mailing_subscription_info span:contains('You are no longer part of our services and will not be contacted again.')",
run: "click",
}, {
content: "No warning should be displayed",
trigger: "div#o_mailing_subscription_form_blocklisted:not(:has(p:contains('You will not receive any news from those mailing lists you are a member of')))",
run: "click",
}, {
content: "Warning will not receive anything anymore",
trigger: "div#o_mailing_subscription_form_blocklisted p:contains('You will not hear from us anymore.')",
run: "click",
}, {
content: "Feedback textarea not displayed (see data)",
trigger: "div#o_mailing_portal_subscription:not(textarea)",
run: "click",
}, {
content: "Choose 'Other' reason",
trigger: "fieldset label:contains('Other')",
run: "click",
}, {
content: "This should display the Feedback area",
trigger: "div#o_mailing_portal_subscription textarea",
}, {
content: "Write feedback reason",
trigger: "textarea[name='feedback']",
run: "edit My feedback",
}, {
content: "Hit Send",
trigger: "button#button_feedback",
run: "click",
}, {
content: "Confirmation feedback is sent",
trigger: "div#o_mailing_subscription_feedback_info span:contains('Sent. Thanks you for your feedback!')",
run: "click",
}, {
content: "Revert exclusion list",
trigger: "div#button_blocklist_remove",
run: "click",
}, {
content: "Confirmation exclusion list is removed",
trigger: "div#o_mailing_subscription_update_info span:contains('Email removed from our blocklist')",
run: "click",
}, {
content: "Now exclude me (again)",
trigger: "div#button_blocklist_add",
run: "click",
}, {
content: "Confirmation exclusion is done",
trigger: "div#o_mailing_subscription_update_info span:contains('Email added to our blocklist')",
},
],
});
/*
* Tour: unsubscribe from a mailing done on documents (aka not on contacts or
* mailing lists). We assume email is member of mailing lists in this test.
*/
registry.category("web_tour.tours").add('mailing_portal_unsubscribe_from_document_with_lists', {
steps: () => [
{
content: "Confirmation unsubscribe is done",
trigger: "div#o_mailing_subscription_info span:contains('You are no longer part of our services and will not be contacted again.')",
run: "click",
}, {
content: "Display warning about mailing lists",
trigger: "div#o_mailing_subscription_form_blocklisted p:contains('You will not receive any news from those mailing lists you are a member of')",
run: "click",
}, {
content: "Warning should contain reference to memberships",
trigger: "div#o_mailing_subscription_form_blocklisted li strong:contains('List1')",
run: "click",
}, {
content: "Feedback textarea not displayed (see data)",
trigger: "div#o_mailing_portal_subscription:not(textarea)",
run: "click",
}, {
content: "Choose 'Other' reason",
trigger: "fieldset label:contains('Other')",
run: "click",
}, {
content: "This should display the Feedback area",
trigger: "div#o_mailing_portal_subscription textarea",
}, {
content: "Write feedback reason",
trigger: "textarea[name='feedback']",
run: "edit My feedback",
}, {
content: "Hit Send",
trigger: "button#button_feedback",
run: "click",
}, {
content: "Confirmation feedback is sent",
trigger: "div#o_mailing_subscription_feedback_info span:contains('Sent. Thanks you for your feedback!')",
run: "click",
}, {
content: "Revert exclusion list",
trigger: "div#button_blocklist_remove",
run: "click",
}, {
content: "Confirmation exclusion list is removed",
trigger: "div#o_mailing_subscription_update_info span:contains('Email removed from our blocklist')",
run: "click",
}, {
content: "Now exclude me (again)",
trigger: "div#button_blocklist_add",
run: "click",
}, {
content: "Confirmation exclusion is done",
trigger: "div#o_mailing_subscription_update_info span:contains('Email added to our blocklist')",
},
],
});

View file

@ -0,0 +1,236 @@
import { registry } from "@web/core/registry";
/*
* Tour: unsubscribe from a mailing done on lists (aka playing with opt-out flag
* instead of directly blocking emails).
*/
registry.category("web_tour.tours").add("mailing_portal_unsubscribe_from_list", {
steps: () => [
{
content: "Confirmation unsubscribe is done",
trigger:
"div#o_mailing_subscription_info span:contains('You are no longer part of the List1, List2 mailing list')",
run: "click",
},
{
content: "Feedback textarea not displayed (see data)",
trigger: "div#o_mailing_portal_subscription:not(textarea)",
run: "click",
},
{
content: "Choose 'Other' reason",
trigger: "fieldset label:contains('Other')",
run: "click",
},
{
content: "Write feedback reason",
trigger: "textarea[name='feedback']",
run: "edit My feedback",
},
{
content: "Hit Send",
trigger: "button#button_feedback",
run: "click",
},
{
content: "Confirmation feedback is sent",
trigger:
"div#o_mailing_subscription_feedback_info span:contains('Sent. Thanks you for your feedback!')",
run: "click",
},
{
content: "Now exclude me",
trigger: "div#button_blocklist_add",
run: "click",
},
{
content: "Confirmation exclusion is done",
trigger:
"div#o_mailing_subscription_update_info span:contains('Email added to our blocklist')",
},
],
});
/*
* Tour: unsubscribe from a mailing done on lists (aka playing with opt-out flag
* instead of directly blocking emails), then play with list subscriptions and
* blocklist addition / removal. This is mainly an extended version of the tour
* hereabove, easing debug and splitting checks.
*/
registry.category("web_tour.tours").add("mailing_portal_unsubscribe_from_list_with_update", {
steps: () => [
{
content: "Confirmation unsubscribe is done",
trigger:
"div#o_mailing_subscription_info span:contains('You are no longer part of the List1, List2 mailing list')",
run: "click",
},
{
content: "List1 is present, just opt-outed",
trigger:
"ul#o_mailing_subscription_form_lists li.list-group-item:contains('List1') span:contains('Not subscribed')",
run: "click",
},
{
content: "List3 is present, opt-outed (test starting data)",
trigger:
"ul#o_mailing_subscription_form_lists li.list-group-item:contains('List3') span:contains('Not subscribed')",
run: "click",
},
{
content: "List2 is proposed (not member -> proposal to join)",
trigger:
"ul#o_mailing_subscription_form_lists_additional li.list-group-item:contains('List2')",
run: "click",
},
{
content: "List4 is not proposed (not member but not private)",
trigger:
"ul#o_mailing_subscription_form_lists_additional:not(:has(li.list-group-item:contains('List4')))",
run: "click",
},
{
content: "Feedback textarea not displayed (see data)",
trigger: "div#o_mailing_portal_subscription:not(textarea)",
run: "click",
},
{
content: "Choose 'Other' reason",
trigger: "fieldset label:contains('Other')",
run: "click",
},
{
content: "Write feedback reason",
trigger: "textarea[name='feedback']",
run: "edit My feedback",
},
{
content: "Hit Send",
trigger: "button#button_feedback",
run: "click",
},
{
content: "Confirmation feedback is sent",
trigger:
"div#o_mailing_subscription_feedback_info span:contains('Sent. Thanks you for your feedback!')",
run: "click",
},
{
content: "Now exclude me",
trigger: "div#button_blocklist_add",
run: "click",
},
{
content: "Confirmation exclusion is done",
trigger:
"div#o_mailing_subscription_update_info span:contains('Email added to our blocklist')",
run: "click",
},
{
content: "This should disable the 'Update my subscriptions' (Apply changes) button",
trigger: "div#o_mailing_subscription_blocklist:not(button#button_form_send)",
},
{
content: "Revert exclusion list",
trigger: "div#button_blocklist_remove",
run: "click",
},
{
content: "Confirmation exclusion list is removed",
trigger:
"div#o_mailing_subscription_update_info span:contains('Email removed from our blocklist')",
run: "click",
},
{
content: "'Update my subscriptions' button usable again",
trigger: "button#button_form_send:not([disabled])",
},
{
content: "Choose the mailing list 3 to come back",
trigger: "ul#o_mailing_subscription_form_lists input[title='List3']",
run: "click",
},
{
content: "Add list 2",
trigger: "ul#o_mailing_subscription_form_lists_additional input[title='List2']",
run: "click",
},
{
content: "Update subscription",
trigger: "button#button_form_send",
run: "click",
},
{
content: "Confirmation changes are done",
trigger: "div#o_mailing_subscription_update_info span:contains('Membership updated')",
run: "click",
},
{
content: "List 3 is noted as subscribed again",
trigger:
"ul#o_mailing_subscription_form_lists li.list-group-item:contains('List3') span:contains('Subscribed')",
run: "click",
},
{
content: "List 2 has joined the subscriptions",
trigger:
"ul#o_mailing_subscription_form_lists li.list-group-item:contains('List2') span:contains('Subscribed')",
run: "click",
},
{
content: "No list in proposals",
trigger:
"div#o_mailing_subscription_form_manage:not(:has(ul#o_mailing_subscription_form_lists_additional))",
run: "click",
},
{
trigger: "div#o_mailing_portal_subscription:not(fieldset)",
},
{
content:
"Feedback area is not displayed (nothing opt-out or no blocklist done, no feedback required)",
trigger: "div#o_mailing_portal_subscription:not(textarea)",
run: "click",
},
{
content: "Now exclude me (again)",
trigger: "div#button_blocklist_add",
run: "click",
},
{
content: "Confirmation exclusion is done",
trigger:
"div#o_mailing_subscription_update_info span:contains('Email added to our blocklist')",
run: "click",
},
{
content: "Should display warning about mailing lists",
trigger:
"div#o_mailing_subscription_form_blocklisted p:contains('You will not receive any news from those mailing lists you are a member of')",
run: "click",
},
{
trigger: "div#o_mailing_subscription_form_blocklisted li strong:contains('List3')",
},
{
content: "Warning should contain reference to memberships",
trigger: "div#o_mailing_subscription_form_blocklisted li strong:contains('List2')",
run: "click",
},
{
content: "Give a reason for blocklist (first one)",
trigger: "fieldset input.o_mailing_subscription_opt_out_reason:first",
run: "click",
},
{
content: "Hit Send",
trigger: "button#button_feedback",
run: "click",
},
{
content: "Confirmation feedback is sent",
trigger:
"div#o_mailing_subscription_feedback_info span:contains('Sent. Thanks you for your feedback!')",
},
],
});

View file

@ -0,0 +1,170 @@
import { registry } from "@web/core/registry";
/*
* Tour: use 'my' portal page of mailing to manage mailing lists subscription
* as well as manage blocklist (add / remove my own email from block list).
*/
registry.category("web_tour.tours").add("mailing_portal_unsubscribe_from_my", {
steps: () => [
{
content: "List1 is present, opt-in member",
trigger:
"ul#o_mailing_subscription_form_lists li.list-group-item:contains('List1') span:contains('Subscribed')",
run: "click",
},
{
content: "List3 is present, opt-outed (test starting data)",
trigger:
"ul#o_mailing_subscription_form_lists li.list-group-item:contains('List3') span:contains('Not subscribed')",
run: "click",
},
{
content: "List2 is proposed (not member -> proposal to join)",
trigger:
"ul#o_mailing_subscription_form_lists_additional li.list-group-item:contains('List2')",
run: "click",
},
{
content: "List4 is not proposed (not member but not private)",
trigger:
"ul#o_mailing_subscription_form_lists_additional:not(:has(li.list-group-item:contains('List4')))",
run: "click",
},
{
content: "List5 is not proposed (not member and not public)",
trigger: "body:not(:has(li.list-group-item:contains('List5')))",
},
{
trigger: "div#o_mailing_portal_subscription:not(fieldset)",
},
{
content: "Feedback area is not displayed (nothing done, no feedback required)",
trigger: "div#o_mailing_portal_subscription:not(textarea)",
run: "click",
},
{
content: "List3: come back (choose to opt-in instead of opt-out)",
trigger: "ul#o_mailing_subscription_form_lists input[title='List3']",
run: "click",
},
{
content: "List2: join (opt-in, not already member)",
trigger: "ul#o_mailing_subscription_form_lists_additional input[title='List2']",
run: "click",
},
{
content: "List1: opt-out",
trigger: "ul#o_mailing_subscription_form_lists input[title='List1']",
run: "click",
},
{
content: "Update subscription",
trigger: "button#button_form_send",
run: "click",
},
{
content: "Confirmation changes are done",
trigger: "div#o_mailing_subscription_update_info span:contains('Membership updated')",
run: "click",
},
{
trigger: "div#o_mailing_portal_subscription:not(textarea)",
},
{
content:
"Should make feedback reasons choice appear (feedback still not displayed, linked to reasons)",
trigger: "div#o_mailing_portal_subscription fieldset",
run: "click",
},
{
content: "Choose first reason, which should not display feedback (see data)",
trigger: "fieldset input.o_mailing_subscription_opt_out_reason:first",
run: "click",
},
{
content: "Feedback textarea not displayed (see data)",
trigger: "div#o_mailing_portal_subscription:not(textarea)",
run: "click",
},
{
content: "Choose 'Other' reason",
trigger: "fieldset label:contains('Other')",
run: "click",
},
{
content: "This should display the Feedback area",
trigger: "div#o_mailing_portal_subscription textarea",
},
{
content: "Write feedback reason",
trigger: "textarea[name='feedback']",
run: "edit My feedback",
},
{
content: "Hit Send",
trigger: "button#button_feedback",
run: "click",
},
{
content: "Confirmation feedback is sent",
trigger:
"div#o_mailing_subscription_feedback_info span:contains('Sent. Thanks you for your feedback!')",
run: "click",
},
{
trigger: "textarea[disabled]",
},
{
content: "Once sent feedback area is readonly",
trigger: "fieldset input.o_mailing_subscription_opt_out_reason[disabled]",
},
{
content: "Now exclude me",
trigger: "div#button_blocklist_add",
run: "click",
},
{
content: "Confirmation exclusion is done",
trigger:
"div#o_mailing_subscription_update_info span:contains('Email added to our blocklist')",
run: "click",
},
{
content: "This should disable the 'Update my subscriptions' (Apply changes) button",
trigger: "div#o_mailing_subscription_blocklist:not(button#button_form_send)",
},
{
content: "This should enabled Feedback again",
trigger: "div#o_mailing_portal_subscription textarea",
},
{
content: "Display warning about mailing lists",
trigger:
"div#o_mailing_subscription_form_blocklisted p:contains('You will not receive any news from those mailing lists you are a member of')",
run: "click",
},
{
trigger: "div#o_mailing_subscription_form_blocklisted li strong:contains('List3')",
},
{
content: "Warning should contain reference to memberships",
trigger: "div#o_mailing_subscription_form_blocklisted li strong:contains('List2')",
run: "click",
},
{
content: "Give a reason for blocklist (first one)",
trigger: "fieldset input.o_mailing_subscription_opt_out_reason:first",
run: "click",
},
{
content: "Hit Send",
trigger: "button#button_feedback",
run: "click",
},
{
content: "Confirmation feedback is sent",
trigger:
"div#o_mailing_subscription_feedback_info span:contains('Sent. Thanks you for your feedback!')",
},
],
});

View file

@ -0,0 +1,69 @@
import { markup } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
registry.category("web_tour.tours").add('mass_mailing_code_view_tour', {
url: '/odoo?debug=tests',
steps: () => [
stepUtils.showAppsMenuItem(),
{
trigger: '.o_app[data-menu-xmlid="mass_mailing.mass_mailing_menu_root"]',
run: "click",
}, {
trigger: 'button.o_list_button_add',
run: "click",
}, {
trigger: 'input#subject_0',
content: markup('Pick the <b>email subject</b>.'),
tooltipPosition: 'bottom',
run: "edit Test",
}, {
trigger: 'div[name="contact_list_ids"] .o_input_dropdown input[type="text"]',
content: 'Click on the dropdown to open it and then start typing to search.',
run: "edit Test"
}, {
trigger: 'div[name="contact_list_ids"] .ui-state-active',
content: 'Select item from dropdown',
run: 'click',
}, {
trigger: 'div[name="body_arch"] .o_mailing_template_preview_wrapper [data-name="default"]',
content: markup('Choose this <b>theme</b>.'),
run: 'click',
}, {
trigger: '.o_codeview_btn',
content: markup('Click here to switch to <b>code view</b>'),
run: 'click'
}, {
trigger: "textarea.o_codeview",
content: "Remove all content from codeview",
run: function () {
const element = document.querySelector(".o_codeview");
element.value = "";
},
}, {
trigger: '.o_codeview_btn',
content: markup('Click here to switch back from <b>code view</b>'),
run: 'click'
}, {
trigger: '[name="body_arch"] :iframe .o_mail_wrapper_td',
content: 'Verify that the dropable zone was not removed',
}, {
trigger: ".o_builder_sidebar_open",
content: "Wait for the html_builder to be visible",
}, {
trigger: '.o_snippet[name="Text"] button',
content: 'Click the "Text" snippet category to drop a snippet in the editor',
run: "click",
},
{
trigger: ".modal-body :iframe .o_snippet_preview_wrap:has(.s_title)",
content: "Select the Title Snippet",
run: "click",
},
{
trigger: '[name="body_arch"] :iframe .o_editable h1',
content: 'Verify that the title was inserted properly in the editor',
},
...stepUtils.discardForm(),
]
});

View file

@ -0,0 +1,90 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
registry.category("web_tour.tours").add('mass_mailing_dynamic_placeholder_tour', {
url: '/odoo',
steps: () => [
stepUtils.showAppsMenuItem(),
{
content: "Select the 'Email Marketing' app.",
trigger: '.o_app[data-menu-xmlid="mass_mailing.mass_mailing_menu_root"]',
run: "click",
},
{
content: "Click on the create button to create a new mailing.",
trigger: 'button.o_list_button_add',
run: "click",
},
{
content: "Fill in Subject",
trigger: '#subject_0',
run: "edit Test Dynamic Placeholder",
},
{
trigger: ".o_mailing_template_preview_wrapper",
},
{
content: "Pick the basic theme",
trigger: '.o_mailing_template_preview_wrapper [data-name="basic"]',
run: "click",
},
{
content: "Insert text inside editable",
trigger: ':iframe .odoo-editor-editable',
async run(actions) {
await actions.editor(`/`);
const iframe = document.querySelector("iframe");
iframe?.contentDocument?.querySelector(".note-editable").dispatchEvent(
new InputEvent("input", {
inputType: "insertText",
data: "/",
})
);
},
},
{
content: "Click on the the dynamic placeholder powerBox options",
trigger: '.o-we-command-name:contains("Dynamic Placeholder")',
run: "click",
},
{
content: "Check if the dynamic placeholder popover is opened",
trigger: "div.o_model_field_selector_popover",
run: "click",
},
{
content: "filter the dph result",
trigger: "div.o_model_field_selector_popover_search input[type='text']",
run: "edit name",
},
{
content: "Click on the first entry of the dynamic placeholder",
trigger: 'div.o_model_field_selector_popover button:contains("Company Name")',
run: "click",
},
{
content: "Enter a default value",
trigger:
'div.o_model_field_selector_popover .o_model_field_selector_default_value_input input[type="text"]',
run: "edit defValue",
},
{
content: "Click on the insert button",
trigger: "div.o_model_field_selector_popover button:first-child",
run: "click",
},
{
content: "Ensure the editable contain the dynamic placeholder t tag",
trigger: `:iframe .note-editable.odoo-editor-editable t[t-out="object.company_name"]:contains("defValue")`,
},
{
content: "Discard form changes",
trigger: "button.o_form_button_cancel",
run: "click",
},
{
content: "Wait for the form view to disappear",
trigger: "body:not(:has(.o_form_sheet))",
},
],
});

View file

@ -0,0 +1,46 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
registry.category("web_tour.tours").add('snippets_mailing_menu_tabs', {
url: '/odoo',
steps: () => [
stepUtils.showAppsMenuItem(), {
content: "Select the 'Email Marketing' app.",
trigger: '.o_app[data-menu-xmlid="mass_mailing.mass_mailing_menu_root"]',
run: "click",
},
{
content: "Click on the create button to create a new mailing.",
trigger: 'button.o_list_button_add',
run: "click",
},
{
content: "Click on the 'Start From Scratch' template.",
trigger: '.o_mailing_template_preview_wrapper [data-name="empty"]',
run: "click",
},
{
content: "Click on the 'Design' tab.",
trigger: 'button[data-name="theme"]',
run: "click",
},
{
content: "Verify that the customize panel is not empty.",
trigger: ".o_design_tab:not(:empty)",
},
{
content: "Click on the style tab.",
trigger: 'button[data-name="customize"]',
run: "click",
},
{
content: "Click on the 'Design' tab.",
trigger: 'button[data-name="theme"]',
run: "click",
},
{
content: "Verify that the customize panel is not empty.",
trigger: ".tab-content .o_design_tab:not(:empty)",
},
...stepUtils.discardForm(),
]});

View file

@ -0,0 +1,76 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
registry.category("web_tour.tours").add('snippets_mailing_menu_toolbar', {
url: '/odoo',
steps: () => [
stepUtils.showAppsMenuItem(), {
content: "Select the 'Email Marketing' app.",
trigger: '.o_app[data-menu-xmlid="mass_mailing.mass_mailing_menu_root"]',
run: "click",
},
{
content: "Click on the create button to create a new mailing.",
trigger: 'button.o_list_button_add',
run: "click",
},
{
content: "Wait for the theme selector to load.",
trigger: '.o_mailing_template_preview_wrapper',
run: "click",
},
{
content: "Make sure there does not exist a floating toolbar",
trigger: "iframe:not(:visible)",
run: function () {
const iframeDocument = this.anchor.contentDocument;
if (iframeDocument.querySelector('#toolbar.oe-floating')) {
console.error('There should not be a floating toolbar in the iframe');
}
},
},
{
content: "Make sure the empty template is an option on non-mobile devices.",
trigger: '.o_mailing_template_preview_wrapper [data-name="empty"]',
},
{
content: "Click on the default 'welcome' template.",
trigger: '.o_mailing_template_preview_wrapper [data-name="default"]',
run: "click",
},
{ // necessary to wait for the cursor to be placed in the first p
// and to avoid leaving the page before the selection is added
content: "Wait for template selection event to be over.",
trigger: ":iframe .odoo-editor-editable .o_editable",
run: "click",
},
{
content: "Make sure the snippets menu is not hidden",
trigger: ".o-snippets-menu",
},
{
content: "Wait for .s_text_block to be populated",
trigger: ':iframe .s_text_block p',
},
{
content: "Click and select p block inside the editor",
trigger: 'iframe',
run: function () {
const iframeWindow = this.anchor.contentWindow;
const iframeDocument = iframeWindow.document;
const p = iframeDocument.querySelector('.s_text_block p');
p.click();
const selection = iframeWindow.getSelection();
const range = iframeDocument.createRange();
range.selectNodeContents(p);
selection.removeAllRanges();
selection.addRange(range);
},
},
{
content: "Make sure the toolbar is there",
trigger: ".overlay .o-we-toolbar",
},
...stepUtils.discardForm(),
],
});

View file

@ -0,0 +1,69 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
registry.category("web_tour.tours").add('snippets_mailing_menu_toolbar_mobile', {
url: '/odoo',
steps: () => [
stepUtils.showAppsMenuItem(), {
content: "Select the 'Email Marketing' app.",
trigger: '.o_app[data-menu-xmlid="mass_mailing.mass_mailing_menu_root"]',
run: "click",
},
{
isActive: ["mobile"],
content: "Click on the create button to create a new mailing.",
trigger: 'button.o_list_button_add',
run: "click",
},
{
isActive: ["mobile"],
content: "Check templates available in theme selector",
trigger: '.o_mailing_template_preview_wrapper',
run: function () {
if (this.anchor.querySelector("#empty")) {
console.error('The empty template should not be visible on mobile.');
}
},
},
{
isActive: ["mobile"],
content: "Make sure the toolbar isn't floating",
trigger: ':iframe',
run: function () {
const iframeDocument = this.anchor.contentDocument;
if (iframeDocument.querySelector('#toolbar.oe-floating')) {
console.error('There should not be a floating toolbar in the iframe');
}
},
},
{
isActive: ["mobile"],
content: "Click on the 'Start From Scratch' template.",
trigger: '.o_mailing_template_preview_wrapper [data-name="default"]',
run: "click",
},
{
isActive: ["mobile"],
content: "Select an editable element",
trigger: ':iframe .s_text_block',
run: "click",
},
{
isActive: ["mobile"],
content: "Make sure the snippets menu is hidden",
trigger: ':iframe',
run: function () {
const iframeDocument = this.anchor.contentDocument;
if (iframeDocument.querySelector(".o-snippets-menu")) {
console.error('The snippet menu should be hidden');
}
},
},
{
isActive: ["mobile"],
content: "Make sure the toolbar is there",
trigger: ':iframe #toolbar.oe-floating',
},
...stepUtils.discardForm().map(command => ({...command, isActive: ["mobile"]})),
]
});