合并企业版代码(未测试,先提交到测试分支)
This commit is contained in:
502
web_studio/static/tests/app_creator_tests.js
Normal file
502
web_studio/static/tests/app_creator_tests.js
Normal file
@@ -0,0 +1,502 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
import {
|
||||
click,
|
||||
findChildren,
|
||||
getFixture,
|
||||
nextTick,
|
||||
triggerEvent,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { AppCreatorWrapper } from "@web_studio/client_action/app_creator/app_creator";
|
||||
import { IconCreator } from "@web_studio/client_action/icon_creator/icon_creator";
|
||||
import testUtils from "web.test_utils";
|
||||
|
||||
const { Component } = owl;
|
||||
const sampleIconUrl = "/web_enterprise/Parent.src/img/default_icon_app.png";
|
||||
|
||||
const createAppCreator = async ({ env, rpc, state, onNewAppCreated }) => {
|
||||
onNewAppCreated = onNewAppCreated || (() => {});
|
||||
const cleanUp = await testUtils.mock.addMockEnvironmentOwl(Component, {
|
||||
debug: QUnit.config.debug,
|
||||
env,
|
||||
mockRPC: rpc,
|
||||
});
|
||||
const target = getFixture();
|
||||
const wrapper = new AppCreatorWrapper(null, { onNewAppCreated });
|
||||
await wrapper.prependTo(target);
|
||||
const { component } = findChildren(wrapper.appCreatorComponent);
|
||||
if (state) {
|
||||
Object.assign(component.state, state);
|
||||
await nextTick();
|
||||
}
|
||||
registerCleanup(() => {
|
||||
wrapper.destroy();
|
||||
cleanUp();
|
||||
});
|
||||
return { state: component.state, target };
|
||||
};
|
||||
|
||||
const editInput = async (el, selector, value) => {
|
||||
const target = el.querySelector(selector);
|
||||
target.value = value;
|
||||
await triggerEvent(target, null, "input");
|
||||
};
|
||||
|
||||
QUnit.module("Studio", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
IconCreator.enableTransitions = false;
|
||||
registerCleanup(() => {
|
||||
IconCreator.enableTransitions = true;
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("AppCreator");
|
||||
|
||||
QUnit.test("app creator: standard flow with model creation", async (assert) => {
|
||||
assert.expect(39);
|
||||
|
||||
const { state, target } = await createAppCreator({
|
||||
env: {
|
||||
services: {
|
||||
ui: {
|
||||
block: () => assert.step("UI blocked"),
|
||||
unblock: () => assert.step("UI unblocked"),
|
||||
},
|
||||
async httpRequest(route) {
|
||||
if (route === "/web/binary/upload_attachment") {
|
||||
assert.step(route);
|
||||
return `[{ "id": 666 }]`;
|
||||
}
|
||||
},
|
||||
http: {
|
||||
async post(route) {
|
||||
if (route === "/web/binary/upload_attachment") {
|
||||
assert.step(route);
|
||||
return `[{ "id": 666 }]`;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
onNewAppCreated: () => assert.step("new-app-created"),
|
||||
async rpc(route, params) {
|
||||
if (route === "/web_studio/create_new_app") {
|
||||
const { app_name, menu_name, model_choice, model_id, model_options } = params;
|
||||
assert.strictEqual(app_name, "Kikou", "App name should be correct");
|
||||
assert.strictEqual(menu_name, "Petite Perruche", "Menu name should be correct");
|
||||
assert.notOk(model_id, "Should not have a model id");
|
||||
assert.strictEqual(model_choice, "new", "Model choice should be 'new'");
|
||||
assert.deepEqual(
|
||||
model_options,
|
||||
["use_partner", "use_sequence", "use_mail", "use_active"],
|
||||
"Model options should include the defaults and 'use_partner'"
|
||||
);
|
||||
}
|
||||
if (route === "/web/dataset/call_kw/ir.attachment/read") {
|
||||
assert.strictEqual(params.model, "ir.attachment");
|
||||
return [{ datas: sampleIconUrl }];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// step: 'welcome'
|
||||
assert.strictEqual(state.step, "welcome", "Current step should be welcome");
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_web_studio_app_creator_previous",
|
||||
"Previous button should not be rendered at step welcome"
|
||||
);
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_web_studio_app_creator_next"),
|
||||
"is_ready",
|
||||
"Next button should be ready at step welcome"
|
||||
);
|
||||
|
||||
// go to step: 'app'
|
||||
await click(target, ".o_web_studio_app_creator_next");
|
||||
|
||||
assert.strictEqual(state.step, "app", "Current step should be app");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_web_studio_icon_creator .o_web_studio_selectors",
|
||||
"Icon creator should be rendered in edit mode"
|
||||
);
|
||||
|
||||
// Icon creator interactions
|
||||
const icon = target.querySelector(".o_app_icon i");
|
||||
|
||||
// Initial state: take default values
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_app_icon").style.backgroundColor,
|
||||
"rgb(52, 73, 94)",
|
||||
"default background color: #34495e"
|
||||
);
|
||||
assert.strictEqual(icon.style.color, "rgb(241, 196, 15)", "default color: #f1c40f");
|
||||
assert.hasClass(icon, "fa fa-diamond", "default icon class: diamond");
|
||||
|
||||
await click(target.getElementsByClassName("o_web_studio_selector")[0]);
|
||||
|
||||
assert.containsOnce(target, ".o_web_studio_palette", "the first palette should be open");
|
||||
|
||||
await triggerEvent(target, ".o_web_studio_palette", "mouseleave");
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_web_studio_palette",
|
||||
"leaving palette with mouse should close it"
|
||||
);
|
||||
|
||||
await click(target.querySelectorAll(".o_web_studio_selectors > .o_web_studio_selector")[0]);
|
||||
await click(target.querySelectorAll(".o_web_studio_selectors > .o_web_studio_selector")[1]);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_web_studio_palette",
|
||||
"opening another palette should close the first"
|
||||
);
|
||||
|
||||
await click(target.querySelectorAll(".o_web_studio_palette div")[2]);
|
||||
await click(target.querySelectorAll(".o_web_studio_selectors > .o_web_studio_selector")[2]);
|
||||
await click(target.querySelectorAll(".o_web_studio_icons_library div")[43]);
|
||||
|
||||
await triggerEvent(target, ".o_web_studio_icons_library", "mouseleave");
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_web_studio_palette",
|
||||
"no palette should be visible anymore"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_web_studio_selector")[1].style.backgroundColor,
|
||||
"rgb(0, 222, 201)", // translation of #00dec9
|
||||
"color selector should have changed"
|
||||
);
|
||||
assert.strictEqual(
|
||||
icon.style.color,
|
||||
"rgb(0, 222, 201)",
|
||||
"icon color should also have changed"
|
||||
);
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_web_studio_selector i"),
|
||||
"fa fa-heart",
|
||||
"class selector should have changed"
|
||||
);
|
||||
assert.hasClass(icon, "fa fa-heart", "icon class should also have changed");
|
||||
|
||||
// Click and upload on first link: upload a file
|
||||
// mimic the event triggered by the upload (jquery)
|
||||
// we do not use the triggerEvent helper as it requires the element to be visible,
|
||||
// which isn't the case here (and this is valid)
|
||||
target.querySelector(".o_web_studio_upload input").dispatchEvent(new Event("change"));
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(
|
||||
state.iconData.uploaded_attachment_id,
|
||||
666,
|
||||
"attachment id should have been given by the RPC"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_web_studio_uploaded_image").style.backgroundImage,
|
||||
`url("data:image/png;base64,${sampleIconUrl}")`,
|
||||
"icon should take the updated attachment data"
|
||||
);
|
||||
|
||||
// try to go to step 'model'
|
||||
await click(target, ".o_web_studio_app_creator_next");
|
||||
|
||||
const appNameInput = target.querySelector('input[name="appName"]').parentNode;
|
||||
|
||||
assert.strictEqual(
|
||||
state.step,
|
||||
"app",
|
||||
"Current step should not be update because the input is not filled"
|
||||
);
|
||||
assert.hasClass(
|
||||
appNameInput,
|
||||
"o_web_studio_app_creator_field_warning",
|
||||
"Input should be in warning mode"
|
||||
);
|
||||
|
||||
await editInput(target, 'input[name="appName"]', "Kikou");
|
||||
assert.doesNotHaveClass(
|
||||
appNameInput,
|
||||
"o_web_studio_app_creator_field_warning",
|
||||
"Input shouldn't be in warning mode anymore"
|
||||
);
|
||||
|
||||
// step: 'model'
|
||||
await click(target, ".o_web_studio_app_creator_next");
|
||||
|
||||
assert.strictEqual(state.step, "model", "Current step should be model");
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_web_studio_selectors",
|
||||
"Icon creator should be rendered in readonly mode"
|
||||
);
|
||||
|
||||
// try to go to next step
|
||||
await click(target, ".o_web_studio_app_creator_next");
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector('input[name="menuName"]').parentNode,
|
||||
"o_web_studio_app_creator_field_warning",
|
||||
"Input should be in warning mode"
|
||||
);
|
||||
|
||||
await editInput(target, 'input[name="menuName"]', "Petite Perruche");
|
||||
|
||||
// go to next step (model configuration)
|
||||
await click(target, ".o_web_studio_app_creator_next");
|
||||
assert.strictEqual(
|
||||
state.step,
|
||||
"model_configuration",
|
||||
"Current step should be model_configuration"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'input[name="use_active"]',
|
||||
"Debug options should be visible without debug mode"
|
||||
);
|
||||
// check an option
|
||||
await click(target, 'input[name="use_partner"]');
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'input[name="use_partner"]:checked',
|
||||
"Option should have been checked"
|
||||
);
|
||||
|
||||
// go back then go forward again
|
||||
await click(target, ".o_web_studio_model_configurator_previous");
|
||||
await click(target, ".o_web_studio_app_creator_next");
|
||||
// options should have been reset
|
||||
assert.containsNone(
|
||||
target,
|
||||
'input[name="use_partner"]:checked',
|
||||
"Options should have been reset by going back then forward"
|
||||
);
|
||||
|
||||
// check the option again, we want to test it in the RPC
|
||||
await click(target, 'input[name="use_partner"]');
|
||||
|
||||
await click(target, ".o_web_studio_model_configurator_next");
|
||||
|
||||
assert.verifySteps([
|
||||
"/web/binary/upload_attachment",
|
||||
"UI blocked",
|
||||
"new-app-created",
|
||||
"UI unblocked",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("app creator: has 'lines' options to auto-create a one2many", async (assert) => {
|
||||
assert.expect(7);
|
||||
|
||||
const { target } = await createAppCreator({
|
||||
env: {
|
||||
services: {
|
||||
ui: { block: () => {}, unblock: () => {} },
|
||||
},
|
||||
},
|
||||
rpc: async (route, params) => {
|
||||
if (route === "/web_studio/create_new_app") {
|
||||
const { app_name, menu_name, model_choice, model_id, model_options } = params;
|
||||
assert.strictEqual(app_name, "testApp", "App name should be correct");
|
||||
assert.strictEqual(menu_name, "testMenu", "Menu name should be correct");
|
||||
assert.notOk(model_id, "Should not have a model id");
|
||||
assert.strictEqual(model_choice, "new", "Model choice should be 'new'");
|
||||
assert.deepEqual(
|
||||
model_options,
|
||||
["lines", "use_sequence", "use_mail", "use_active"],
|
||||
"Model options should include the defaults and 'lines'"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, ".o_web_studio_app_creator_next");
|
||||
await editInput(target, "input[id='appName']", "testApp");
|
||||
await click(target, ".o_web_studio_app_creator_next");
|
||||
await editInput(target, "input[id='menuName']", "testMenu");
|
||||
await click(target, ".o_web_studio_app_creator_next");
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_web_studio_model_configurator_option input[type='checkbox'][name='lines'][id='lines']"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector("label[for='lines']").textContent,
|
||||
"LinesAdd details to your records with an embedded list view"
|
||||
);
|
||||
|
||||
await click(
|
||||
target,
|
||||
".o_web_studio_model_configurator_option input[type='checkbox'][name='lines']"
|
||||
);
|
||||
await click(target, ".o_web_studio_model_configurator_next");
|
||||
});
|
||||
|
||||
QUnit.test("app creator: debug flow with existing model", async (assert) => {
|
||||
assert.expect(16);
|
||||
|
||||
const { state, target } = await createAppCreator({
|
||||
env: {
|
||||
isDebug: () => true,
|
||||
services: {
|
||||
ui: { block: () => {}, unblock: () => {} },
|
||||
},
|
||||
},
|
||||
async rpc(route, params) {
|
||||
assert.step(route);
|
||||
switch (route) {
|
||||
case "/web/dataset/call_kw/ir.model/name_search": {
|
||||
assert.strictEqual(
|
||||
params.model,
|
||||
"ir.model",
|
||||
"request should target the right model"
|
||||
);
|
||||
return [[69, "The Value"]];
|
||||
}
|
||||
case "/web_studio/create_new_app": {
|
||||
assert.strictEqual(
|
||||
params.model_id,
|
||||
69,
|
||||
"model id should be the one provided"
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
state: {
|
||||
menuName: "Kikou",
|
||||
step: "model",
|
||||
},
|
||||
});
|
||||
|
||||
let buttonNext = target.querySelector("button.o_web_studio_app_creator_next");
|
||||
|
||||
assert.hasClass(buttonNext, "is_ready");
|
||||
|
||||
await editInput(target, 'input[name="menuName"]', "Petite Perruche");
|
||||
// check the 'new model' radio
|
||||
await click(target, 'input[name="model_choice"][value="new"]');
|
||||
|
||||
// go to next step (model configuration)
|
||||
await click(target, ".o_web_studio_app_creator_next");
|
||||
assert.strictEqual(
|
||||
state.step,
|
||||
"model_configuration",
|
||||
"Current step should be model_configuration"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
'input[name="use_active"]',
|
||||
"Debug options should be visible in debug mode"
|
||||
);
|
||||
// go back, we want the 'existing model flow'
|
||||
await click(target, ".o_web_studio_model_configurator_previous");
|
||||
|
||||
// since we came back, we need to update our buttonNext ref - the querySelector is not live
|
||||
buttonNext = target.querySelector("button.o_web_studio_app_creator_next");
|
||||
|
||||
// check the 'existing model' radio
|
||||
await click(target, 'input[name="model_choice"][value="existing"]');
|
||||
|
||||
assert.doesNotHaveClass(
|
||||
target.querySelector(".o_web_studio_app_creator_model"),
|
||||
"o_web_studio_app_creator_field_warning"
|
||||
);
|
||||
assert.doesNotHaveClass(buttonNext, "is_ready");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_many2one",
|
||||
"There should be a many2one to select a model"
|
||||
);
|
||||
|
||||
await click(buttonNext);
|
||||
|
||||
assert.hasClass(
|
||||
target.querySelector(".o_web_studio_app_creator_model"),
|
||||
"o_web_studio_app_creator_field_warning"
|
||||
);
|
||||
assert.doesNotHaveClass(buttonNext, "is_ready");
|
||||
|
||||
await click(target, ".o_field_many2one input");
|
||||
await click(document.querySelector(".ui-menu-item-wrapper"));
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_many2one input").value, "The Value");
|
||||
|
||||
assert.doesNotHaveClass(
|
||||
target.querySelector(".o_web_studio_app_creator_model"),
|
||||
"o_web_studio_app_creator_field_warning"
|
||||
);
|
||||
assert.hasClass(buttonNext, "is_ready");
|
||||
|
||||
await click(buttonNext);
|
||||
|
||||
assert.verifySteps([
|
||||
"/web/dataset/call_kw/ir.model/name_search",
|
||||
"/web_studio/create_new_app",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('app creator: navigate through steps using "ENTER"', async (assert) => {
|
||||
assert.expect(12);
|
||||
|
||||
const { state, target } = await createAppCreator({
|
||||
env: {
|
||||
services: {
|
||||
ui: {
|
||||
block: () => assert.step("UI blocked"),
|
||||
unblock: () => assert.step("UI unblocked"),
|
||||
},
|
||||
},
|
||||
},
|
||||
onNewAppCreated: () => assert.step("new-app-created"),
|
||||
async rpc(route, { app_name, menu_name, model_id }) {
|
||||
if (route === "/web_studio/create_new_app") {
|
||||
assert.strictEqual(app_name, "Kikou", "App name should be correct");
|
||||
assert.strictEqual(menu_name, "Petite Perruche", "Menu name should be correct");
|
||||
assert.notOk(model_id, "Should not have a model id");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// step: 'welcome'
|
||||
assert.strictEqual(state.step, "welcome", "Current step should be set to 1");
|
||||
|
||||
// go to step 'app'
|
||||
await triggerEvent(document, null, "keydown", { key: "Enter" });
|
||||
assert.strictEqual(state.step, "app", "Current step should be set to app");
|
||||
|
||||
// try to go to step 'model'
|
||||
await triggerEvent(document, null, "keydown", { key: "Enter" });
|
||||
assert.strictEqual(
|
||||
state.step,
|
||||
"app",
|
||||
"Current step should not be update because the input is not filled"
|
||||
);
|
||||
|
||||
await editInput(target, 'input[name="appName"]', "Kikou");
|
||||
|
||||
// go to step 'model'
|
||||
await triggerEvent(document, null, "keydown", { key: "Enter" });
|
||||
assert.strictEqual(state.step, "model", "Current step should be model");
|
||||
|
||||
// try to create app
|
||||
await triggerEvent(document, null, "keydown", { key: "Enter" });
|
||||
assert.hasClass(
|
||||
target.querySelector('input[name="menuName"]').parentNode,
|
||||
"o_web_studio_app_creator_field_warning",
|
||||
"a warning should be displayed on the input"
|
||||
);
|
||||
|
||||
await editInput(target, 'input[name="menuName"]', "Petite Perruche");
|
||||
await triggerEvent(document, null, "keydown", { key: "Enter" });
|
||||
await triggerEvent(document, null, "keydown", { key: "Enter" });
|
||||
|
||||
assert.verifySteps(["UI blocked", "new-app-created", "UI unblocked"]);
|
||||
});
|
||||
});
|
||||
94
web_studio/static/tests/helpers.js
Normal file
94
web_studio/static/tests/helpers.js
Normal file
@@ -0,0 +1,94 @@
|
||||
/** @odoo-module */
|
||||
import { legacyExtraNextTick, click } from "@web/../tests/helpers/utils";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { systrayItem } from "@web_studio/systray_item/systray_item";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { enterpriseSubscriptionService } from "@web_enterprise/webclient/home_menu/enterprise_subscription_service";
|
||||
import { homeMenuService } from "@web_enterprise/webclient/home_menu/home_menu_service";
|
||||
import { studioService } from "@web_studio/studio_service";
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
import { resetViewCompilerCache } from "@web/views/view_compiler";
|
||||
|
||||
export function registerStudioDependencies() {
|
||||
const serviceRegistry = registry.category("services");
|
||||
registry.category("systray").add("StudioSystrayItem", systrayItem);
|
||||
serviceRegistry.add("orm", ormService);
|
||||
serviceRegistry.add("enterprise_subscription", enterpriseSubscriptionService);
|
||||
serviceRegistry.add("home_menu", homeMenuService);
|
||||
serviceRegistry.add("studio", studioService);
|
||||
registerCleanup(() => resetViewCompilerCache());
|
||||
}
|
||||
|
||||
export async function openStudio(target, params = {}) {
|
||||
await click(target.querySelector(".o_main_navbar .o_web_studio_navbar_item a"));
|
||||
await legacyExtraNextTick();
|
||||
if (params.noEdit) {
|
||||
const studioTabViews = target.querySelector(".o_web_studio_menu_item a");
|
||||
await click(studioTabViews);
|
||||
const controlElm = target.querySelector(
|
||||
".o_action_manager .o_web_studio_editor .o_web_studio_views"
|
||||
);
|
||||
if (!controlElm) {
|
||||
throw new Error("We should be in the Tab 'Views' but we are not");
|
||||
}
|
||||
}
|
||||
if (params.report) {
|
||||
const studioTabReport = target.querySelectorAll(".o_web_studio_menu_item a")[1];
|
||||
await click(studioTabReport);
|
||||
await legacyExtraNextTick();
|
||||
let controlElm = target.querySelector(
|
||||
".o_action_manager .o_web_studio_editor .o_web_studio_report_kanban"
|
||||
);
|
||||
if (!controlElm) {
|
||||
throw new Error("We should be in the Tab 'Report' but we are not");
|
||||
}
|
||||
await click(controlElm.querySelector(`.o_kanban_record[data-id="${params.report}"`));
|
||||
await legacyExtraNextTick();
|
||||
controlElm = target.querySelector(
|
||||
".o_action_manager .o_web_studio_editor .o_web_studio_report_editor_manager"
|
||||
);
|
||||
if (!controlElm) {
|
||||
throw new Error("We should be editing the first report that showed up");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function leaveStudio(target) {
|
||||
await click(target.querySelector(".o_studio_navbar .o_web_studio_leave a"));
|
||||
return legacyExtraNextTick();
|
||||
}
|
||||
|
||||
export function getReportServerData() {
|
||||
const models = {
|
||||
"ir.actions.report": {
|
||||
fields: {
|
||||
model: { type: "char" },
|
||||
report_name: { type: "char" },
|
||||
report_type: { type: "char" },
|
||||
},
|
||||
records: [{ id: 11, model: "foo", report_name: "foo_report", report_type: "pdf" }],
|
||||
},
|
||||
};
|
||||
|
||||
const views = {
|
||||
"ir.actions.report,false,kanban": `
|
||||
<kanban class="o_web_studio_report_kanban" js_class="studio_report_kanban">
|
||||
<field name="report_name"/>
|
||||
<field name="report_type"/>
|
||||
<field name="id"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click" t-att-data-id="record.id.value">
|
||||
<div class="oe_kanban_details">
|
||||
<field name="report_name" groups="base.group_no_one"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
"ir.actions.report,false,search": `<search />`,
|
||||
};
|
||||
|
||||
return { models, views };
|
||||
}
|
||||
84
web_studio/static/tests/icon_creator_tests.js
Normal file
84
web_studio/static/tests/icon_creator_tests.js
Normal file
@@ -0,0 +1,84 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
import { click, getFixture, mount } from "@web/../tests/helpers/utils";
|
||||
import { IconCreator } from "@web_studio/client_action/icon_creator/icon_creator";
|
||||
import makeTestEnvironment from "web.test_env";
|
||||
|
||||
const sampleIconUrl = "/web_enterprise/Parent.src/img/default_icon_app.png";
|
||||
|
||||
QUnit.module("Studio", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
IconCreator.enableTransitions = false;
|
||||
registerCleanup(() => {
|
||||
IconCreator.enableTransitions = true;
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("IconCreator");
|
||||
|
||||
QUnit.test("icon creator: with initial web icon data", async (assert) => {
|
||||
assert.expect(5);
|
||||
|
||||
const target = getFixture();
|
||||
await mount(IconCreator, target, {
|
||||
props: {
|
||||
editable: true,
|
||||
type: "base64",
|
||||
webIconData: sampleIconUrl,
|
||||
onIconChange(icon) {
|
||||
// default values
|
||||
assert.step("icon-changed");
|
||||
assert.deepEqual(icon, {
|
||||
backgroundColor: "#34495e",
|
||||
color: "#f1c40f",
|
||||
iconClass: "fa fa-diamond",
|
||||
type: "custom_icon",
|
||||
});
|
||||
},
|
||||
},
|
||||
env: makeTestEnvironment(),
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_web_studio_uploaded_image").style.backgroundImage,
|
||||
`url("${sampleIconUrl}")`,
|
||||
"displayed image should prioritize web icon data"
|
||||
);
|
||||
|
||||
// click on first link: "Design icon"
|
||||
await click(target.querySelector(".o_web_studio_upload a"));
|
||||
|
||||
assert.verifySteps(["icon-changed"]);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_web_studio_upload input").accept,
|
||||
"image/png",
|
||||
"Input should now only accept pngs"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("icon creator: without initial web icon data", async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
const target = getFixture();
|
||||
await mount(IconCreator, target, {
|
||||
props: {
|
||||
backgroundColor: "rgb(255, 0, 128)",
|
||||
color: "rgb(0, 255, 0)",
|
||||
editable: false,
|
||||
iconClass: "fa fa-heart",
|
||||
type: "custom_icon",
|
||||
onIconChange: () => {},
|
||||
},
|
||||
env: makeTestEnvironment(),
|
||||
});
|
||||
|
||||
// Attributes should be correctly set
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_app_icon").style.backgroundColor,
|
||||
"rgb(255, 0, 128)"
|
||||
);
|
||||
assert.strictEqual(target.querySelector(".o_app_icon i").style.color, "rgb(0, 255, 0)");
|
||||
assert.hasClass(target.querySelector(".o_app_icon i"), "fa fa-heart");
|
||||
});
|
||||
});
|
||||
186
web_studio/static/tests/legacy/action_editor_action_tests.js
Normal file
186
web_studio/static/tests/legacy/action_editor_action_tests.js
Normal file
@@ -0,0 +1,186 @@
|
||||
odoo.define('web_studio.ActionEditorActionTests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
const { openStudio, registerStudioDependencies } = require("@web_studio/../tests/helpers");
|
||||
const { doAction } = require("@web/../tests/webclient/helpers");
|
||||
const { getFixture, legacyExtraNextTick } = require("@web/../tests/helpers/utils");
|
||||
const { createEnterpriseWebClient } = require("@web_enterprise/../tests/helpers");
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
QUnit.module('Studio', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
kikou: {
|
||||
fields: {
|
||||
display_name: { type: "char", string: "Display Name" },
|
||||
start: { type: 'datetime', store: 'true', string: "start date" },
|
||||
},
|
||||
},
|
||||
'res.groups': {
|
||||
fields: {
|
||||
display_name: { string: "Display Name", type: "char" },
|
||||
},
|
||||
records: [{
|
||||
id: 4,
|
||||
display_name: "Admin",
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
const views = {
|
||||
"kikou,1,list": `<tree><field name="display_name" /></tree>`,
|
||||
"kikou,2,form": `<form><field name="display_name" /></form>`,
|
||||
"kikou,false,search": `<search />`,
|
||||
};
|
||||
serverData = {models: this.data, views};
|
||||
target = getFixture();
|
||||
registerStudioDependencies();
|
||||
}
|
||||
}, function () {
|
||||
|
||||
QUnit.module('ActionEditorAction');
|
||||
|
||||
QUnit.test('add a gantt view', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (route === '/web_studio/add_view_type') {
|
||||
assert.strictEqual(args.view_type, 'gantt',
|
||||
"should add the correct view");
|
||||
return Promise.resolve(false);
|
||||
} else if (args.method === 'fields_get') {
|
||||
assert.strictEqual(args.model, 'kikou',
|
||||
"should read fields on the correct model");
|
||||
}
|
||||
};
|
||||
|
||||
const webClient = await createEnterpriseWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, {
|
||||
xml_id: "some.xml_id",
|
||||
type: "ir.actions.act_window",
|
||||
res_model: 'kikou',
|
||||
view_mode: 'list',
|
||||
views: [[1, 'list'], [2, 'form']],
|
||||
}, {clearBreadcrumbs: true});
|
||||
await openStudio(target, {noEdit: true});
|
||||
|
||||
await testUtils.dom.click($(target).find('.o_web_studio_view_type[data-type="gantt"] .o_web_studio_thumbnail'));
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.containsOnce($, '.o_web_studio_new_view_dialog',
|
||||
"there should be an opened dialog to select gantt attributes");
|
||||
assert.strictEqual($('.o_web_studio_new_view_dialog select[name="date_start"]').val(), 'start',
|
||||
"date start should be prefilled (mandatory)");
|
||||
assert.strictEqual($('.o_web_studio_new_view_dialog select[name="date_stop"]').val(), 'start',
|
||||
"date stop should be prefilled (mandatory)");
|
||||
});
|
||||
|
||||
QUnit.test('disable the view from studio', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const actions = {
|
||||
1: {
|
||||
id: 1,
|
||||
xml_id: "kikou.action",
|
||||
name: 'Kikou Action',
|
||||
res_model: 'kikou',
|
||||
type: 'ir.actions.act_window',
|
||||
view_mode: 'list,form',
|
||||
views: [[1, 'list'], [2, 'form']],
|
||||
}
|
||||
};
|
||||
|
||||
const views = {
|
||||
'kikou,1,list': `<tree><field name="display_name"/></tree>`,
|
||||
'kikou,1,search': `<search></search>`,
|
||||
'kikou,2,form': `<form><field name="display_name"/></form>`,
|
||||
};
|
||||
Object.assign(serverData, {actions, views});
|
||||
|
||||
let loadActionStep = 0;
|
||||
const mockRPC = (route, args) => {
|
||||
if (route === '/web_studio/edit_action') {
|
||||
return true;
|
||||
} else if (route === '/web/action/load') {
|
||||
loadActionStep++;
|
||||
/**
|
||||
* step 1: initial action/load
|
||||
* step 2: on disabling list view
|
||||
*/
|
||||
if (loadActionStep === 2) {
|
||||
return {
|
||||
name: 'Kikou Action',
|
||||
res_model: 'kikou',
|
||||
view_mode: 'form',
|
||||
type: 'ir.actions.act_window',
|
||||
views: [[2, 'form']],
|
||||
id: 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const webClient = await createEnterpriseWebClient({ serverData, mockRPC });
|
||||
|
||||
await doAction(webClient, 1);
|
||||
await openStudio(target);
|
||||
|
||||
await testUtils.dom.click(target.querySelector('.o_web_studio_menu_item a'));
|
||||
|
||||
// make list view disable and form view only will be there in studio view
|
||||
await testUtils.dom.click($(target).find('div[data-type="list"] .o_web_studio_more'));
|
||||
await testUtils.dom.click($(target).find('div[data-type="list"] a[data-action="disable_view"]'));
|
||||
// reloadAction = false;
|
||||
assert.hasClass(
|
||||
$(target).find('div[data-type="list"]'),
|
||||
'o_web_studio_inactive',
|
||||
"list view should have become inactive");
|
||||
|
||||
// make form view disable and it should prompt the alert dialog
|
||||
await testUtils.dom.click($(target).find('div[data-type="form"] .o_web_studio_more'));
|
||||
await testUtils.dom.click($(target).find('div[data-type="form"] a[data-action="disable_view"]'));
|
||||
assert.containsOnce(
|
||||
$,
|
||||
'.o_technical_modal',
|
||||
"should display a modal when attempting to disable last view");
|
||||
assert.strictEqual(
|
||||
$('.o_technical_modal .modal-body').text().trim(),
|
||||
"You cannot deactivate this view as it is the last one active.",
|
||||
"modal should tell that last view cannot be disabled");
|
||||
});
|
||||
|
||||
QUnit.test('add groups on action', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
const actions = {
|
||||
1: {
|
||||
id: 1,
|
||||
xml_id: "some.xml_id",
|
||||
type: "ir.actions.act_window",
|
||||
res_model: 'kikou',
|
||||
view_mode: 'list',
|
||||
views: [[1, 'list'], [2, 'form']],
|
||||
},
|
||||
};
|
||||
Object.assign(serverData, {actions});
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (route === '/web_studio/edit_action') {
|
||||
assert.strictEqual(args.args.groups_id[0], 4,
|
||||
"group admin should be applied on action");
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
const webClient = await createEnterpriseWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 1, {clearBreadcrumbs: true});
|
||||
await openStudio(target, {noEdit: true});
|
||||
|
||||
await testUtils.fields.many2one.clickOpenDropdown('groups_id');
|
||||
await testUtils.fields.many2one.clickHighlightedItem('groups_id');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
204
web_studio/static/tests/legacy/edit_menu_tests.js
Normal file
204
web_studio/static/tests/legacy/edit_menu_tests.js
Normal file
@@ -0,0 +1,204 @@
|
||||
odoo.define('web_studio.EditMenu_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
const { prepareWowlFormViewDialogs } = require("@web/../tests/views/helpers");
|
||||
|
||||
var EditMenu = require('web_studio.EditMenu');
|
||||
|
||||
QUnit.module('Studio', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
'ir.ui.menu': {
|
||||
fields: {},
|
||||
records: [{
|
||||
id: 1,
|
||||
name: 'Menu 1',
|
||||
}, {
|
||||
id: 2,
|
||||
name: 'Menu 2',
|
||||
}, {
|
||||
id: 21,
|
||||
name: 'Submenu 1',
|
||||
}, {
|
||||
id: 22,
|
||||
name: 'Submenu 2',
|
||||
}]
|
||||
}
|
||||
};
|
||||
this.menu_data = {
|
||||
childrenTree: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Menu 1',
|
||||
parent_id: false,
|
||||
childrenTree: [],
|
||||
}, {
|
||||
id: 2,
|
||||
name: 'Menu 2',
|
||||
parent_id: false,
|
||||
childrenTree: [
|
||||
{
|
||||
childrenTree: [],
|
||||
id: 21,
|
||||
name: 'Submenu 1',
|
||||
parent_id: 2,
|
||||
|
||||
}, {
|
||||
childrenTree: [],
|
||||
id: 21,
|
||||
name: 'Submenu 2',
|
||||
parent_id: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
this.archs = {
|
||||
'ir.ui.menu,false,form':
|
||||
'<form>'+
|
||||
'<sheet>' +
|
||||
'<field name="name"/>' +
|
||||
'</sheet>' +
|
||||
'</form>'
|
||||
};
|
||||
}
|
||||
}, function () {
|
||||
|
||||
QUnit.module('EditMenu');
|
||||
|
||||
QUnit.test('edit menu behavior', async function(assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var $target = $('#qunit-fixture');
|
||||
|
||||
var edit_menu = new EditMenu.MenuItem(null, this.menu_data, 2);
|
||||
await edit_menu.appendTo($target);
|
||||
|
||||
await testUtils.mock.addMockEnvironment(edit_menu, {
|
||||
data: this.data,
|
||||
archs: this.archs,
|
||||
});
|
||||
assert.strictEqual($('.o_web_studio_edit_menu_modal').length, 0,
|
||||
"there should not be any modal in the dom");
|
||||
assert.containsOnce(edit_menu, '.o_web_edit_menu',
|
||||
"there should be an edit menu link");
|
||||
|
||||
// open the dialog to edit the menu
|
||||
await testUtils.dom.click(edit_menu.$('.o_web_edit_menu'));
|
||||
assert.strictEqual($('.o_web_studio_edit_menu_modal').length, 1,
|
||||
"there should be a modal in the dom");
|
||||
|
||||
edit_menu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('edit menu dialog', async function(assert) {
|
||||
assert.expect(23);
|
||||
|
||||
const dialog = new EditMenu.Dialog(null, this.menu_data, 2);
|
||||
dialog.open();
|
||||
await dialog.opened();
|
||||
|
||||
let customizeCalls = 0;
|
||||
|
||||
await testUtils.mock.addMockEnvironment(dialog, {
|
||||
data: this.data,
|
||||
archs: this.archs,
|
||||
mockRPC: function (route, args) {
|
||||
if (route === "/web/dataset/call_kw/ir.ui.menu/customize") {
|
||||
if (customizeCalls === 0) {
|
||||
assert.deepEqual(args.kwargs, {
|
||||
to_delete: [],
|
||||
to_move: {
|
||||
2: { sequence: 1 },
|
||||
21: { parent_menu_id: 2, sequence: 0 },
|
||||
},
|
||||
});
|
||||
}
|
||||
customizeCalls++;
|
||||
return Promise.reject();
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
await prepareWowlFormViewDialogs({ models: this.data, views: this.archs });
|
||||
|
||||
const $modal = $('.modal');
|
||||
assert.containsOnce(dialog, 'ul.oe_menu_editor',
|
||||
"there should be the list of menus");
|
||||
assert.containsOnce(dialog, 'ul.oe_menu_editor > li',
|
||||
"there should be only one main menu");
|
||||
assert.strictEqual(dialog.$('ul.oe_menu_editor > li').data('menu-id'), 2,
|
||||
"the main menu should have the menu-id 2");
|
||||
assert.containsOnce(dialog, 'ul.oe_menu_editor > li > div button.js_edit_menu',
|
||||
"there should be a button to edit the menu");
|
||||
assert.containsOnce(dialog, 'ul.oe_menu_editor > li > div button.js_delete_menu',
|
||||
"there should be a button to remove the menu");
|
||||
assert.containsN(dialog, 'ul.oe_menu_editor > li > ul > li', 2,
|
||||
"there should be two submenus");
|
||||
assert.containsOnce($modal, '.js_add_menu',
|
||||
"there should be a link to add new menu");
|
||||
|
||||
// open the dialog to create a new menu
|
||||
await testUtils.dom.click($modal.find('.js_add_menu'));
|
||||
await testUtils.nextTick();
|
||||
assert.strictEqual($('.o_web_studio_add_menu_modal').length, 1,
|
||||
"there should be a modal in the dom");
|
||||
assert.strictEqual($('.o_web_studio_add_menu_modal input[name="name"]').length, 1,
|
||||
"there should be an input for the name in the dialog");
|
||||
await testUtils.dom.click($('.o_web_studio_add_menu_modal .o_field_widget[name="model_choice"] [data-value="new"]'));
|
||||
assert.isNotVisible($('.o_web_studio_add_menu_modal .o_field_many2one'),
|
||||
"there should be no visible many2one for the model in the dialog");
|
||||
// Define a name for new model
|
||||
$('input[name="name"]').val("new_model");
|
||||
await testUtils.dom.click($('.o_web_studio_add_menu_modal .btn-primary'));
|
||||
assert.containsOnce($, '.o_web_studio_model_configurator input[name="use_partner"]',
|
||||
"the ModelConfigurator should show the available model options");
|
||||
await testUtils.dom.click($('.o_web_studio_model_configurator .o_web_studio_model_configurator_previous'));
|
||||
assert.containsNone($, '.o_web_studio_model_configurator',
|
||||
"the ModelConfigurator should be gone");
|
||||
await testUtils.dom.click($('.o_web_studio_add_menu_modal .o_field_widget[name="model_choice"] [data-value="existing"]'));
|
||||
assert.strictEqual($('.o_web_studio_add_menu_modal .o_field_many2one').filter(':visible').length, 1,
|
||||
"there should be a visible many2one for the model in the dialog");
|
||||
|
||||
// add menu and close the modal
|
||||
await testUtils.fields.editInput($('.o_web_studio_add_menu_modal input[name="name"]'), "AA");
|
||||
await testUtils.dom.click($('.o_web_studio_add_menu_modal .btn-primary'));
|
||||
await testUtils.dom.click($('.o_web_studio_add_menu_modal .btn-secondary'));
|
||||
|
||||
// move submenu above root menu
|
||||
await testUtils.dom.dragAndDrop(dialog.$('li li .input-group:first'), dialog.$('.input-group:first'));
|
||||
assert.strictEqual(dialog.to_move[2].sequence, dialog.to_move[21].sequence + 1,
|
||||
"Root menu is after moved submenu");
|
||||
|
||||
// open the dialog to edit the menu
|
||||
await testUtils.dom.click(dialog.$('.js_edit_menu:nth(1)'));
|
||||
assert.strictEqual($('.o_dialog .o_form_view').length, 1,
|
||||
"there should be a form view dialog in the dom");
|
||||
assert.strictEqual($('.o_dialog .o_form_view .o_field_widget[name="name"] input').val(), "Menu 2",
|
||||
"the edited menu should be menu 2");
|
||||
// confirm the edition
|
||||
assert.strictEqual(customizeCalls, 0, "current changes have not been saved");
|
||||
await testUtils.dom.click($('.o_dialog .o_form_button_save'));
|
||||
assert.strictEqual(customizeCalls, 1, "current changes are saved after editing a menu");
|
||||
|
||||
// delete the last menu
|
||||
await testUtils.dom.click(dialog.$('.js_delete_menu:nth(2)'));
|
||||
assert.containsNone(dialog, 'ul.oe_menu_editor > li > ul > li',
|
||||
"there should be no submenu after deletion");
|
||||
assert.strictEqual(dialog.to_delete.length, 1,
|
||||
"there should be one menu to delete");
|
||||
|
||||
await testUtils.dom.click(dialog.$('.js_delete_menu:first'));
|
||||
await testUtils.dom.click(dialog.$('.js_delete_menu:first'));
|
||||
await testUtils.dom.click($modal.find('footer .btn-primary'));
|
||||
assert.containsN($(document), '.modal', 2,
|
||||
"should have 2 dialogs");
|
||||
assert.strictEqual($('.modal:not(.o_inactive_modal) .modal-title').text(), "Alert",
|
||||
"should have alert dialog if we delete all menus");
|
||||
|
||||
dialog.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
51
web_studio/static/tests/legacy/mock_server.js
Normal file
51
web_studio/static/tests/legacy/mock_server.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import MockServer from 'web.MockServer';
|
||||
|
||||
MockServer.include({
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
MockServer.currentMockServer = this;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_performRpc: function (route) {
|
||||
if (route === '/web_studio/get_default_value') {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
if (route === '/web_studio/activity_allowed') {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* Mocks method "_return_view" that generates the return value of a call
|
||||
* to edit_view. It's basically an object similar to the result of a call
|
||||
* to get_views. It is used in mockRPC functions that mock edit_view calls.
|
||||
*
|
||||
* @param {string} arch
|
||||
* @param {string} model
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mockReturnView(arch, model) {
|
||||
const view = this.getView({ arch, model });
|
||||
const models = {};
|
||||
for (const modelName of view.models) {
|
||||
models[modelName] = this.fieldsGet(modelName);
|
||||
}
|
||||
return Promise.resolve({
|
||||
models,
|
||||
views: { [view.type]: view },
|
||||
});
|
||||
},
|
||||
});
|
||||
54
web_studio/static/tests/legacy/new_model_tests.js
Normal file
54
web_studio/static/tests/legacy/new_model_tests.js
Normal file
@@ -0,0 +1,54 @@
|
||||
odoo.define('web_studio.NewModeltests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const NewModel = require('web_studio.NewModel');
|
||||
const testUtils = require('web.test_utils');
|
||||
|
||||
QUnit.module('Studio', function () {
|
||||
|
||||
QUnit.module('NewModel');
|
||||
|
||||
QUnit.test('Add New Model', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
const $target = $('#qunit-fixture');
|
||||
|
||||
const newModel = new NewModel.NewModelItem();
|
||||
await newModel.appendTo($target);
|
||||
|
||||
testUtils.mock.addMockEnvironment(newModel, {
|
||||
mockRPC: function (route, args) {
|
||||
if (route === "/web_studio/create_new_menu") {
|
||||
assert.strictEqual(args.menu_name, "ABCD", "Model name should be ABCD.")
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsNone($, '.o_web_studio_new_model_modal',
|
||||
"there should not be any modal in the dom");
|
||||
assert.containsOnce(newModel, '.o_web_create_new_model',
|
||||
"there should be an add new model link");
|
||||
|
||||
await testUtils.dom.click($('.o_web_create_new_model'));
|
||||
assert.containsOnce($, '.o_web_studio_new_model_modal',
|
||||
"there should be a modal in the dom");
|
||||
const $modal = $('.modal');
|
||||
assert.containsOnce($modal, 'input[name="name"]',
|
||||
"there should be an input for the name in the dialog");
|
||||
|
||||
await testUtils.fields.editInput($modal.find('input[name="name"]'), "ABCD");
|
||||
await testUtils.dom.click($modal.find('.btn-primary'));
|
||||
const $configuratorModal = $('.o_web_studio_model_configurator');
|
||||
assert.containsOnce($configuratorModal, 'input[name="use_partner"]',
|
||||
"the ModelConfigurator should show the available model options");
|
||||
|
||||
await testUtils.dom.click($configuratorModal.find('.o_web_studio_model_configurator_next'));
|
||||
assert.containsNone($, '.o_web_studio_model_configurator',
|
||||
"the ModelConfigurator should be gone");
|
||||
|
||||
newModel.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
odoo.define('web_studio.ReportEditorAction_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { controlPanel } = require('web.test_utils');
|
||||
const { getPagerValue, pagerNext } = controlPanel;
|
||||
|
||||
const { getFixture } = require("@web/../tests/helpers/utils");
|
||||
const { doAction } = require("@web/../tests/webclient/helpers");
|
||||
const { openStudio, registerStudioDependencies,getReportServerData } = require("@web_studio/../tests/helpers");
|
||||
const { createEnterpriseWebClient } = require("@web_enterprise/../tests/helpers");
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
QUnit.module('Studio', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
foo: {
|
||||
fields: {},
|
||||
records: [{ id: 22 }, { id: 23 }],
|
||||
},
|
||||
"ir.actions.report": {
|
||||
fields: { model: { type: "char" }, report_name: { type: "char" }, report_type:{ type: "char" }},
|
||||
records: [{ id: 11, model: "foo", report_name: "foo_report", report_type: "pdf" }],
|
||||
},
|
||||
"ir.model": {
|
||||
fields: {},
|
||||
},
|
||||
};
|
||||
const reportServerData = getReportServerData();
|
||||
const actions = {
|
||||
1: {
|
||||
id: 1,
|
||||
xml_id: "kikou.action",
|
||||
name: 'Kikou Action',
|
||||
res_model: 'foo',
|
||||
type: 'ir.actions.act_window',
|
||||
view_mode: 'list,form',
|
||||
views: [[1, 'form']],
|
||||
}
|
||||
};
|
||||
const views = Object.assign({
|
||||
"foo,2,form": `<form><field name="display_name" /></form>`,
|
||||
"foo,false,search": `<search />`,
|
||||
}, reportServerData.views);
|
||||
serverData = {actions, models: this.data, views};
|
||||
Object.assign(serverData.models, reportServerData.models);
|
||||
registerStudioDependencies();
|
||||
target = getFixture();
|
||||
},
|
||||
}, function () {
|
||||
QUnit.module('ReportEditorAction');
|
||||
|
||||
QUnit.test('use pager', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const reportHTML = `
|
||||
<html>
|
||||
<head/>
|
||||
<body>
|
||||
<div id="wrapwrap">
|
||||
<main>
|
||||
<div class="page"/>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
switch (route) {
|
||||
case "/web_studio/get_report_views":
|
||||
return { report_html: reportHTML };
|
||||
case "/web_studio/get_widgets_available_options":
|
||||
case "/web_studio/read_paperformat":
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const webClient = await createEnterpriseWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, 1);
|
||||
await openStudio(target, {report: 11});
|
||||
|
||||
assert.strictEqual(getPagerValue(target), "1");
|
||||
await pagerNext(target);
|
||||
assert.strictEqual(getPagerValue(target), "2");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,800 @@
|
||||
odoo.define('web_studio.ReportEditorComponents_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
var studioTestUtils = require('web_studio.testUtils');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var studioTestUtils = require('web_studio.testUtils');
|
||||
var editComponentsRegistry = require('web_studio.reportEditComponentsRegistry');
|
||||
var reportNewComponentsRegistry = require('web_studio.reportNewComponentsRegistry');
|
||||
|
||||
|
||||
QUnit.module('Studio', {}, function () {
|
||||
|
||||
QUnit.module('ReportComponents', {
|
||||
before: function() {
|
||||
return new Promise(function (resolve, reject) {
|
||||
studioTestUtils.createSidebar({}).then(function (sidebar) {
|
||||
sidebar.destroy();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
beforeEach: function () {
|
||||
this.widgetsOptions = {
|
||||
monetary: {
|
||||
company_id: {
|
||||
type: "model",
|
||||
string: "Company",
|
||||
description: "Company used for the original currency (only used for t-esc)",
|
||||
default_value: "Company used to render the template",
|
||||
params: "res.company"
|
||||
},
|
||||
date: {
|
||||
type: "date",
|
||||
string: "Date",
|
||||
description: "Date used for the original currency (only used for t-esc)",
|
||||
default_value: "Current date"
|
||||
},
|
||||
from_currency: {
|
||||
type: "model",
|
||||
string: "Original currency",
|
||||
params: "res.currency"
|
||||
},
|
||||
display_currency: {
|
||||
type: "model",
|
||||
string: "Display currency",
|
||||
required: "value_to_html",
|
||||
params: "res.currency"
|
||||
}
|
||||
},
|
||||
relative: {
|
||||
now: {
|
||||
type: "datetime",
|
||||
string: "Reference date",
|
||||
description: "Date to compare with the field value.",
|
||||
default_value: "Current date"
|
||||
}
|
||||
},
|
||||
image: {},
|
||||
text: {},
|
||||
html: {},
|
||||
many2many: {},
|
||||
date: {
|
||||
format: {
|
||||
type: "string",
|
||||
string: "Date format"
|
||||
}
|
||||
},
|
||||
datetime: {
|
||||
time_only: {
|
||||
type: "boolean",
|
||||
string: "Display only the time"
|
||||
},
|
||||
hide_seconds: {
|
||||
type: "boolean",
|
||||
string: "Hide seconds"
|
||||
},
|
||||
format: {
|
||||
type: "string",
|
||||
string: "Pattern to format"
|
||||
}
|
||||
},
|
||||
qweb: {},
|
||||
many2one: {},
|
||||
integer: {},
|
||||
float_time: {},
|
||||
contact: {
|
||||
separator: {
|
||||
type: "selection",
|
||||
params : {
|
||||
type: "selection",
|
||||
selection: [
|
||||
[" ", "Space"],
|
||||
[",", "Comma"],
|
||||
["-", "Dash"],
|
||||
["|", "Vertical bar"],
|
||||
["/", "Slash"]
|
||||
],
|
||||
placeholder: 'Linebreak',
|
||||
},
|
||||
string: "Address separator",
|
||||
description: "Separator use to split the addresse from the display_name.",
|
||||
default_value: false,
|
||||
},
|
||||
no_marker: {
|
||||
type: "boolean",
|
||||
string: "Hide marker",
|
||||
description: "Don't display the font awsome marker"
|
||||
},
|
||||
country_image: {
|
||||
type: "boolean",
|
||||
string: "Displayed contry image",
|
||||
description: "Display the country image if the field is present on the record"
|
||||
},
|
||||
fields: {
|
||||
type: "array",
|
||||
string: "Displayed fields",
|
||||
description: "List of contact fields to display in the widget",
|
||||
default_value: [
|
||||
"name",
|
||||
"address",
|
||||
"phone",
|
||||
"mobile",
|
||||
"email"
|
||||
],
|
||||
params: {
|
||||
type: "selection",
|
||||
params: [
|
||||
{'field_name': 'name', 'label': 'Name'},
|
||||
{'field_name': 'address', 'label': 'Address'},
|
||||
{'field_name': 'phone', 'label': 'Phone'},
|
||||
{'field_name': 'mobile', 'label': 'Mobile'},
|
||||
{'field_name': 'email', 'label': 'Email'},
|
||||
{'field_name': 'vat', 'label': 'VAT'},
|
||||
]
|
||||
}
|
||||
},
|
||||
no_tag_br: {
|
||||
type: "boolean",
|
||||
string: "Use comma",
|
||||
description: "Use comma instead of the <br> tag to display the address"
|
||||
},
|
||||
phone_icons: {
|
||||
type: "boolean",
|
||||
string: "Displayed phone icons",
|
||||
description: "Display the phone icons even if no_marker is True"
|
||||
}
|
||||
},
|
||||
duration: {
|
||||
unit: {
|
||||
type: "select",
|
||||
string: "Date unit",
|
||||
description: "Date unit used for comparison and formatting",
|
||||
default_value: "hour",
|
||||
params: [
|
||||
[ "year", "year" ],
|
||||
[ "month", "month" ],
|
||||
[ "week", "week" ],
|
||||
[ "day", "day" ],
|
||||
[ "hour", "hour" ],
|
||||
[ "minute", "minute" ],
|
||||
[ "second", "second" ]
|
||||
]
|
||||
},
|
||||
round: {
|
||||
type: "select",
|
||||
string: "Rounding unit",
|
||||
description: "Date unit used for the rounding. If the value is given, this must be smaller than the unit",
|
||||
default_value: "Same unit as \"unit\" option",
|
||||
params: [
|
||||
[ "year", "year" ],
|
||||
[ "month", "month" ],
|
||||
[ "week", "week" ],
|
||||
[ "day", "day" ],
|
||||
[ "hour", "hour" ],
|
||||
[ "minute", "minute" ],
|
||||
[ "second", "second" ]
|
||||
]
|
||||
}
|
||||
},
|
||||
selection: {
|
||||
selection: {
|
||||
type: "selection",
|
||||
string: "Selection",
|
||||
default_value: "Use the field information",
|
||||
required: true
|
||||
}
|
||||
},
|
||||
barcode: {
|
||||
type: {
|
||||
type: "string",
|
||||
string: "Barcode type",
|
||||
description: "Barcode type, eg: UPCA, EAN13, Code128",
|
||||
default_value: "Code128"
|
||||
},
|
||||
width: {
|
||||
type: "integer",
|
||||
string: "Width",
|
||||
default_value: 600
|
||||
},
|
||||
height: {
|
||||
type: "integer",
|
||||
string: "Height",
|
||||
default_value: 100
|
||||
},
|
||||
humanreadable: {
|
||||
type: "integer",
|
||||
string: "Human Readable",
|
||||
default_value: 0
|
||||
}
|
||||
},
|
||||
float: {
|
||||
precision: {
|
||||
type: "integer",
|
||||
string: "Rounding precision"
|
||||
}
|
||||
}
|
||||
};
|
||||
this.data = studioTestUtils.getData({
|
||||
'model.test': {
|
||||
fields: {
|
||||
name: {string: "Name", type: "char"},
|
||||
image: {string: "Image", type: "binary"},
|
||||
child: {string: "Child", type: 'many2one', relation: 'model.test.child', searchable: true},
|
||||
child_bis: {string: "Child Bis", type: 'many2one', relation: 'model.test.child', searchable: true},
|
||||
children: {string: "Children", type: 'many2many', relation: 'model.test.child', searchable: true},
|
||||
},
|
||||
records: [],
|
||||
},
|
||||
'model.test.child': {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
company_id: { string: "Company", type: "many2one", relation: 'res.company', searchable: true },
|
||||
currency_id: { string: "Currency", type: "many2one", relation: 'res.currency', searchable: true },
|
||||
date: { string: "Date", type: "datetime", searchable: true },
|
||||
},
|
||||
records: [],
|
||||
},
|
||||
'res.company': {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
},
|
||||
records: [],
|
||||
},
|
||||
'res.currency': {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
},
|
||||
records: [],
|
||||
},
|
||||
});
|
||||
studioTestUtils.patch();
|
||||
},
|
||||
afterEach: function () {
|
||||
studioTestUtils.unpatch();
|
||||
},
|
||||
}, function () {
|
||||
QUnit.module('New');
|
||||
|
||||
QUnit.test('field', async function (assert) {
|
||||
assert.expect(2);
|
||||
var parent = new Widget();
|
||||
await testUtils.mock.addMockEnvironment(parent, {
|
||||
data: this.data,
|
||||
});
|
||||
await parent.appendTo($('#qunit-fixture'));
|
||||
var InlineField = reportNewComponentsRegistry.get('Inline')[1];
|
||||
|
||||
var tOptions = new InlineField(parent, {
|
||||
models: {
|
||||
'model.test': 'Toto',
|
||||
},
|
||||
});
|
||||
|
||||
tOptions.add({
|
||||
targets: [{
|
||||
data: {},
|
||||
node: {
|
||||
attrs: {
|
||||
'data-oe-id': 99,
|
||||
'data-oe-xpath': '/my/node/path/',
|
||||
},
|
||||
contextOrder: ['toto'],
|
||||
context: {
|
||||
toto: 'model.test',
|
||||
},
|
||||
parent: {
|
||||
children: [],
|
||||
attrs: {},
|
||||
}
|
||||
},
|
||||
}]
|
||||
}).then(function (res) {
|
||||
assert.deepEqual(res.inheritance,
|
||||
[{content: '<span t-field="toto.child"></span>', xpath: '/my/node/path/', view_id: 99, position: undefined}],
|
||||
"Should send the operation");
|
||||
});
|
||||
await testUtils.nextTick();
|
||||
|
||||
await testUtils.dom.triggerEvents($('.o_web_studio_field_modal .o_field_selector'), ['focus']);
|
||||
await testUtils.dom.click($('.o_web_studio_field_modal .o_field_selector_close'));
|
||||
await testUtils.dom.click($('.o_web_studio_field_modal .btn-primary'));
|
||||
|
||||
assert.strictEqual($('.modal main[role="alert"]').length, 1,
|
||||
"Should display an alert because the field name of the record is wrong");
|
||||
await testUtils.dom.click($('.modal:has(main[role="alert"]) .btn-primary'));
|
||||
|
||||
await testUtils.dom.triggerEvents($('.o_web_studio_field_modal .o_field_selector'), ['focus']);
|
||||
await testUtils.dom.click($('.o_web_studio_field_modal .o_field_selector_item[data-name="child"]'));
|
||||
await testUtils.dom.click($('.o_web_studio_field_modal .btn-primary'));
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('add a binary field', async function (assert) {
|
||||
assert.expect(1);
|
||||
var parent = new Widget();
|
||||
await testUtils.mock.addMockEnvironment(parent, {
|
||||
data: this.data,
|
||||
});
|
||||
await parent.appendTo($('#qunit-fixture'));
|
||||
var InlineField = reportNewComponentsRegistry.get('Inline')[1];
|
||||
var tOptions = new InlineField(parent, {
|
||||
models: {
|
||||
'model.test': 'Kikou',
|
||||
},
|
||||
});
|
||||
|
||||
tOptions.add({
|
||||
targets: [{
|
||||
data: {},
|
||||
node: {
|
||||
attrs: {
|
||||
'data-oe-id': 99,
|
||||
'data-oe-xpath': '/my/node/path/',
|
||||
},
|
||||
contextOrder: ['toto'],
|
||||
context: {
|
||||
toto: 'model.test',
|
||||
},
|
||||
parent: {
|
||||
children: [],
|
||||
attrs: {},
|
||||
}
|
||||
},
|
||||
}]
|
||||
}).then(function (res) {
|
||||
assert.deepEqual(res.inheritance,
|
||||
[{content: '<span t-field="toto.image" t-options-widget=""image""></span>', xpath: '/my/node/path/', view_id: 99, position: undefined}],
|
||||
"image widget should be set");
|
||||
});
|
||||
await testUtils.nextTick();
|
||||
|
||||
await testUtils.dom.triggerEvents($('.o_web_studio_field_modal .o_field_selector'), ['focus']);
|
||||
await testUtils.dom.click($('.o_web_studio_field_modal .o_field_selector_item[data-name="image"]'));
|
||||
await testUtils.dom.click($('.o_web_studio_field_modal .btn-primary'));
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('Edit');
|
||||
|
||||
QUnit.test('column component with valid classes', async function (assert) {
|
||||
assert.expect(2);
|
||||
var parent = new Widget();
|
||||
await parent.appendTo($('#qunit-fixture'));
|
||||
var column = new (editComponentsRegistry.get('column'))(parent, {
|
||||
node: {
|
||||
attrs: {
|
||||
class: 'col-5 offset-3',
|
||||
},
|
||||
},
|
||||
});
|
||||
await column.appendTo(parent.$el);
|
||||
|
||||
assert.strictEqual(column.$('input[name="size"]').val(), "5",
|
||||
"the size should be correctly set");
|
||||
assert.strictEqual(column.$('input[name="offset"]').val(), "3",
|
||||
"the offset should be correctly set");
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('column component with invalid classes', async function (assert) {
|
||||
assert.expect(2);
|
||||
var parent = new Widget();
|
||||
await parent.appendTo($('#qunit-fixture'));
|
||||
var column = new (editComponentsRegistry.get('column'))(parent, {
|
||||
node: {
|
||||
attrs: {
|
||||
class: 'col- offset-kikou',
|
||||
},
|
||||
},
|
||||
});
|
||||
await column.appendTo(parent.$el);
|
||||
|
||||
assert.strictEqual(column.$('input[name="size"]').val(), "",
|
||||
"the size should be unkown");
|
||||
assert.strictEqual(column.$('input[name="offset"]').val(), "",
|
||||
"the offset should be unkown");
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('hidden "width" for layout component with col nodes', async function (assert) {
|
||||
assert.expect(1);
|
||||
var parent = new Widget();
|
||||
await testUtils.mock.addMockEnvironment(parent, this);
|
||||
await parent.appendTo($('#qunit-fixture'));
|
||||
var layout = new (editComponentsRegistry.get('layout'))(parent, {
|
||||
node: {
|
||||
tag: 'div',
|
||||
attrs: {
|
||||
class: 'col- offset-kikou',
|
||||
},
|
||||
$nodes: $(),
|
||||
},
|
||||
});
|
||||
await layout.appendTo(parent.$el);
|
||||
|
||||
assert.containsNone(layout.$('.o_web_studio_width'),
|
||||
"the width attribute shouldn't be displayed for div.col nodes");
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('tOptions component', async function (assert) {
|
||||
assert.expect(3);
|
||||
var parent = new Widget();
|
||||
await parent.appendTo($('#qunit-fixture'));
|
||||
var tOptions = new (editComponentsRegistry.get('tOptions'))(parent, {
|
||||
widgetsOptions: this.widgetsOptions,
|
||||
node: {
|
||||
attrs: {
|
||||
't-options': '{"widget": "text"}',
|
||||
't-options-widget': '"image"',
|
||||
't-options-other-options': 'True',
|
||||
'data-oe-id': 99,
|
||||
'data-oe-xpath': '/my/node/path/',
|
||||
},
|
||||
},
|
||||
context: {},
|
||||
state: null,
|
||||
models: null,
|
||||
});
|
||||
await tOptions.appendTo(parent.$el);
|
||||
assert.strictEqual(tOptions.$('select').val(), 'image',
|
||||
"Should select the image widget");
|
||||
assert.containsNone(tOptions, '.o_web_studio_toption_option',
|
||||
"there should be no available option");
|
||||
|
||||
// unset the `widget`
|
||||
await testUtils.mock.addMockEnvironment(parent, {
|
||||
intercepts: {
|
||||
view_change: function (ev) {
|
||||
assert.deepEqual(ev.data.operation.new_attrs, {'t-options-widget': '""'},
|
||||
"should correctly delete the group");
|
||||
},
|
||||
},
|
||||
});
|
||||
tOptions.$('select').val('').trigger('change');
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('tOptions component parse expression', async function (assert) {
|
||||
assert.expect(5);
|
||||
var parent = new Widget();
|
||||
await parent.appendTo($('#qunit-fixture'));
|
||||
|
||||
var fields = this.data['model.test.child'].fields;
|
||||
fields.company_id = {string: "Company", type: "many2one", relation: 'res.company', searchable: true};
|
||||
fields.currency_id = {string: "Currency", type: "many2one", relation: 'res.currency', searchable: true};
|
||||
fields.date = {string: "Date", type: "datetime", searchable: true};
|
||||
await testUtils.mock.addMockEnvironment(parent, {
|
||||
data: this.data,
|
||||
});
|
||||
|
||||
var tOptions = new (editComponentsRegistry.get('tOptions'))(parent, {
|
||||
widgetsOptions: this.widgetsOptions,
|
||||
node: {
|
||||
attrs: {
|
||||
't-options': 'dict(from_currency=o.child.currency_id, date=o.child.date)',
|
||||
't-options-widget': '"monetary"',
|
||||
't-options-company_id': 'o.child.company_id',
|
||||
'data-oe-id': 99,
|
||||
'data-oe-xpath': '/my/node/path/',
|
||||
},
|
||||
context: {"o": "model.test"},
|
||||
},
|
||||
context: {},
|
||||
state: null,
|
||||
models: {"model.test": "Model Test"},
|
||||
});
|
||||
|
||||
await tOptions.appendTo(parent.$el);
|
||||
assert.strictEqual(tOptions.$('select').val(), 'monetary',
|
||||
"Should select the image widget");
|
||||
assert.containsN(tOptions, '.o_web_studio_toption_option', 4,
|
||||
"there should be 4 available options for the monetary widget");
|
||||
assert.strictEqual(tOptions.$('.o_web_studio_toption_option_monetary_from_currency .o_field_selector_value').text().replace(/\s+/g, ''),
|
||||
"o(ModelTest)ChildCurrency",
|
||||
"Should display the currency field");
|
||||
assert.strictEqual(tOptions.$('.o_web_studio_toption_option_monetary_date .o_field_selector_value').text().replace(/\s+/g, ''),
|
||||
"o(ModelTest)ChildDate",
|
||||
"Should display the data field");
|
||||
assert.strictEqual(tOptions.$('.o_web_studio_toption_option_monetary_company_id .o_field_selector_value').text().replace(/\s+/g, ''),
|
||||
"o(ModelTest)ChildCompany",
|
||||
"Should display the company field");
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('tEsc component with parsable expression', async function (assert) {
|
||||
assert.expect(1);
|
||||
var parent = new Widget();
|
||||
await parent.appendTo($('#qunit-fixture'));
|
||||
|
||||
await testUtils.mock.addMockEnvironment(parent, {
|
||||
data: this.data,
|
||||
});
|
||||
|
||||
var tOptions = new (editComponentsRegistry.get('tEsc'))(parent, {
|
||||
node: {
|
||||
attrs: {
|
||||
't-esc': 'o.child.company_id',
|
||||
'data-oe-id': 99,
|
||||
'data-oe-xpath': '/my/node/path/',
|
||||
},
|
||||
context: {"o": "model.test"},
|
||||
},
|
||||
context: {},
|
||||
state: null,
|
||||
models: {"model.test": "Model Test"},
|
||||
});
|
||||
await tOptions.appendTo(parent.$el);
|
||||
await testUtils.nextTick();
|
||||
// the component value is parsable so we display it with ModelFieldSelector
|
||||
assert.strictEqual(tOptions.$('.o_field_selector_value').text().replace(/\s+/g, ''),
|
||||
"o(ModelTest)ChildCompany",
|
||||
"Should display the company field");
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('tEsc component with non-parsable expression', async function (assert) {
|
||||
assert.expect(1);
|
||||
var parent = new Widget();
|
||||
await parent.appendTo($('#qunit-fixture'));
|
||||
|
||||
await testUtils.mock.addMockEnvironment(parent, {
|
||||
data: this.data,
|
||||
});
|
||||
|
||||
var tOptions = new (editComponentsRegistry.get('tEsc'))(parent, {
|
||||
node: {
|
||||
attrs: {
|
||||
't-esc': 'o.child.getCompany()',
|
||||
'data-oe-id': 99,
|
||||
'data-oe-xpath': '/my/node/path/',
|
||||
},
|
||||
context: {"o": "model.test"},
|
||||
},
|
||||
context: {},
|
||||
state: null,
|
||||
models: {"model.test": "Model Test"},
|
||||
});
|
||||
await tOptions.appendTo(parent.$el);
|
||||
await testUtils.nextTick();
|
||||
// the component can not parse the value so we display a simple input
|
||||
assert.strictEqual(tOptions.$('input[name="t-esc"]').val(),
|
||||
"o.child.getCompany()",
|
||||
"Should display the company field");
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('contact: many2many_select', async function (assert) {
|
||||
assert.expect(11);
|
||||
var parent = new Widget();
|
||||
|
||||
$('ul.ui-autocomplete').remove(); // clean the body to avoid errors due to another test
|
||||
|
||||
var optionsFields;
|
||||
await testUtils.mock.addMockEnvironment(parent, {
|
||||
intercepts: {
|
||||
view_change: function (ev) {
|
||||
assert.deepEqual(ev.data.operation.new_attrs['t-options-fields'], optionsFields,
|
||||
'Should save the contact options');
|
||||
|
||||
params.node.attrs['t-options-fields'] = JSON.stringify(ev.data.operation.new_attrs['t-options-fields']);
|
||||
},
|
||||
},
|
||||
});
|
||||
await parent.appendTo($('#qunit-fixture'));
|
||||
|
||||
var params = {
|
||||
widgetsOptions: this.widgetsOptions,
|
||||
node: {
|
||||
attrs: {
|
||||
't-options': '{"widget": "contact"}',
|
||||
't-options-no_marker': 'True',
|
||||
'data-oe-id': 99,
|
||||
'data-oe-xpath': '/my/node/path/',
|
||||
},
|
||||
},
|
||||
context: {},
|
||||
state: null,
|
||||
models: null,
|
||||
};
|
||||
|
||||
var tOptions = new (editComponentsRegistry.get('tOptions'))(parent, params);
|
||||
await tOptions.appendTo(parent.$el);
|
||||
assert.containsN(tOptions, '.o_web_studio_toption_option', 3,
|
||||
"there should be 3 available options for the contact widget (they are filtered)");
|
||||
assert.strictEqual(tOptions.$('.o_badge_text').text(), 'NameAddressPhoneMobileEmail', 'Should display default value');
|
||||
await testUtils.dom.click(tOptions.$('.o_input_dropdown input'));
|
||||
assert.strictEqual($('ul.ui-autocomplete .ui-menu-item').length, 1, 'Should not display the unselected items');
|
||||
assert.strictEqual($('ul.ui-autocomplete .o_m2o_dropdown_option').length, 0, 'Should not display create button');
|
||||
|
||||
optionsFields = ["name", "address", "phone", "mobile", "email", "vat"];
|
||||
await testUtils.dom.click($('ul.ui-autocomplete .ui-menu-item:contains(VAT)'));
|
||||
tOptions.destroy();
|
||||
|
||||
tOptions = new (editComponentsRegistry.get('tOptions'))(parent, params);
|
||||
await tOptions.appendTo(parent.$el);
|
||||
assert.strictEqual(tOptions.$('.o_badge_text').text(), 'NameAddressPhoneMobileEmailVAT', 'Should display the new value');
|
||||
await testUtils.dom.click(tOptions.$('.o_input_dropdown input'));
|
||||
assert.strictEqual($('ul.ui-autocomplete .ui-menu-item').length, 0, 'Should not display the unselected items');
|
||||
await testUtils.dom.click(tOptions.$('.o_input_dropdown input'));
|
||||
|
||||
optionsFields = ["address", "phone", "mobile", "email", "vat"];
|
||||
await testUtils.dom.click(tOptions.$('.o_field_many2manytags .o_delete:first'));
|
||||
assert.strictEqual(tOptions.$('.o_badge_text').text(), 'AddressPhoneMobileEmailVAT', 'Should display the new value without "name"');
|
||||
|
||||
optionsFields = ["phone", "mobile", "email", "vat"];
|
||||
await testUtils.dom.click(tOptions.$('.o_field_many2manytags .o_delete:first'));
|
||||
assert.strictEqual(tOptions.$('.o_badge_text').text(), 'PhoneMobileEmailVAT', 'Should display the new value without "address"');
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('contact: address separator', async function (assert) {
|
||||
assert.expect(3);
|
||||
var parent = new Widget();
|
||||
|
||||
var addressSeparator;
|
||||
await testUtils.mock.addMockEnvironment(parent, {
|
||||
intercepts: {
|
||||
view_change: function (ev) {
|
||||
assert.strictEqual(ev.data.operation.new_attrs['t-options-separator'], addressSeparator,
|
||||
'Should save the selected address separator');
|
||||
},
|
||||
},
|
||||
});
|
||||
await parent.appendTo($('#qunit-fixture'));
|
||||
|
||||
var params = {
|
||||
widgetsOptions: this.widgetsOptions,
|
||||
node: {
|
||||
attrs: {
|
||||
't-options': '{"widget": "contact"}',
|
||||
't-options-no_marker': 'True',
|
||||
'data-oe-id': 99,
|
||||
'data-oe-xpath': '/my/node/path/',
|
||||
},
|
||||
},
|
||||
context: {},
|
||||
state: null,
|
||||
models: null,
|
||||
};
|
||||
|
||||
var tOptions = new (editComponentsRegistry.get('tOptions'))(parent, params);
|
||||
await tOptions.appendTo(parent.$el);
|
||||
var separators = _.map(tOptions.$('.o_web_studio_toption_option_contact_separator .o_field_widget option'), function (option) {
|
||||
return JSON.parse($(option).val());
|
||||
});
|
||||
assert.deepEqual(separators, [false, " ", ",", "-", "|", "/"], 'There should be a selection field with proper values');
|
||||
assert.strictEqual(tOptions.$('.o_web_studio_toption_option_contact_separator .o_field_widget option:selected').text(), 'Linebreak',
|
||||
'Default value should be "LineBrak"');
|
||||
addressSeparator = '","';
|
||||
await testUtils.fields.editSelect(tOptions.$('.o_web_studio_toption_option_contact_separator .o_field_widget'), addressSeparator);
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('contact: no_marker boolean field', async function (assert) {
|
||||
assert.expect(2);
|
||||
var parent = new Widget();
|
||||
|
||||
await testUtils.mock.addMockEnvironment(parent, {
|
||||
intercepts: {
|
||||
view_change: function (ev) {
|
||||
assert.strictEqual(ev.data.operation.new_attrs["t-options-no_marker"], false,
|
||||
'Toggling no_marker checkbox should change the option value');
|
||||
},
|
||||
},
|
||||
});
|
||||
await parent.appendTo($('#qunit-fixture'));
|
||||
|
||||
var params = {
|
||||
widgetsOptions: this.widgetsOptions,
|
||||
node: {
|
||||
attrs: {
|
||||
't-options': '{"widget": "contact"}',
|
||||
't-options-no_marker': 'True',
|
||||
'data-oe-id': 99,
|
||||
'data-oe-xpath': '/my/node/path/',
|
||||
},
|
||||
},
|
||||
context: {},
|
||||
state: null,
|
||||
models: null,
|
||||
};
|
||||
var tOptions = new (editComponentsRegistry.get('tOptions'))(parent, params);
|
||||
await tOptions.appendTo(parent.$el);
|
||||
|
||||
assert.containsOnce(tOptions, '.o_web_studio_toption_option_contact_no_marker input:checked',
|
||||
"no_marker checkbox is checked initially");
|
||||
await testUtils.dom.click(tOptions.$('.o_web_studio_toption_option_contact_no_marker input'));
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('no search more in many2many_select', async function (assert) {
|
||||
assert.expect(3);
|
||||
var parent = new Widget();
|
||||
|
||||
$('ul.ui-autocomplete').remove(); // clean the body to avoid errors due to another test
|
||||
|
||||
await parent.appendTo($('#qunit-fixture'));
|
||||
|
||||
// to display more options in the many2many_select
|
||||
this.widgetsOptions.contact.fields.default_value = [];
|
||||
|
||||
var tOptions = new (editComponentsRegistry.get('tOptions'))(parent, {
|
||||
widgetsOptions: this.widgetsOptions,
|
||||
node: {
|
||||
attrs: {
|
||||
't-options': '{"widget": "contact"}',
|
||||
't-options-no_marker': 'True',
|
||||
'data-oe-id': 99,
|
||||
'data-oe-xpath': '/my/node/path/',
|
||||
},
|
||||
},
|
||||
context: {},
|
||||
state: null,
|
||||
models: null,
|
||||
});
|
||||
await tOptions.appendTo(parent.$el);
|
||||
|
||||
assert.strictEqual(tOptions.$('.o_badge_text').text(), '', 'Should display default value');
|
||||
await testUtils.dom.click(tOptions.$('.o_input_dropdown input'));
|
||||
assert.strictEqual($('ul.ui-autocomplete .ui-menu-item').length, 6 , 'Should not display the unselected items');
|
||||
assert.strictEqual($('ul.ui-autocomplete .o_m2o_dropdown_option').length, 0, 'Should not display create button nor the search more');
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('groups component', async function (assert) {
|
||||
assert.expect(3);
|
||||
var parent = new Widget();
|
||||
await parent.appendTo($('#qunit-fixture'));
|
||||
var groups = new (editComponentsRegistry.get('groups'))(parent, {
|
||||
widgets: this.widgets,
|
||||
node: {
|
||||
tag: 'span',
|
||||
attrs: {
|
||||
studio_groups: "[" +
|
||||
"{\"name\": \"group_A\", \"display_name\": \"My Awesome Group\", \"id\": 42}," +
|
||||
"{\"name\": \"group_13\", \"display_name\": \"Kikou\", \"id\": 13}" +
|
||||
"]",
|
||||
},
|
||||
},
|
||||
});
|
||||
await groups.appendTo(parent.$el);
|
||||
|
||||
assert.containsN(groups, '.o_field_many2manytags .o_badge_text', 2,
|
||||
"there should be displayed two groups");
|
||||
assert.strictEqual(groups.$('.o_field_many2manytags').text().replace(/\s/g, ''), "MyAwesomeGroupKikou",
|
||||
"the groups should be correctly set");
|
||||
|
||||
// delete a group
|
||||
await testUtils.mock.addMockEnvironment(parent, {
|
||||
intercepts: {
|
||||
view_change: function (ev) {
|
||||
assert.deepEqual(ev.data.operation.new_attrs, {groups: [13]},
|
||||
"should correctly delete the group");
|
||||
},
|
||||
},
|
||||
});
|
||||
await testUtils.dom.click(groups.$('.o_field_many2manytags .o_delete:first'));
|
||||
|
||||
parent.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,914 @@
|
||||
odoo.define('web_studio.ReportEditorSidebar_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { nextTick } = require("@web/../tests/helpers/utils");
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
var studioTestUtils = require('web_studio.testUtils');
|
||||
|
||||
QUnit.module('Studio', {}, function () {
|
||||
|
||||
QUnit.module('ReportEditorSidebar', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
'report.paperformat': {
|
||||
fields: {
|
||||
display_name: {string: "Name", type: "char"},
|
||||
},
|
||||
records: [{
|
||||
id: 42,
|
||||
display_name: 'My Awesome Format',
|
||||
}],
|
||||
},
|
||||
'res.groups': {
|
||||
fields: {
|
||||
display_name: {string: "Name", type: "char"},
|
||||
},
|
||||
records: [{
|
||||
id: 6,
|
||||
display_name: 'Group6',
|
||||
}, {
|
||||
id: 7,
|
||||
display_name: 'Group7',
|
||||
}],
|
||||
},
|
||||
'x_mymodel': {
|
||||
fields: {
|
||||
display_name: {string: "Name", type: "char"},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
this.widgetsOptions = {
|
||||
image: {},
|
||||
integer: {},
|
||||
text: {},
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
QUnit.test("basic rendering", async function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(5);
|
||||
|
||||
studioTestUtils.createSidebar({
|
||||
state: { mode: 'report' },
|
||||
report: {},
|
||||
}).then(async function (sidebar) {
|
||||
|
||||
assert.hasClass(sidebar.$('.o_web_studio_sidebar_header [name="report"]'),'active',
|
||||
"the report tab should be active");
|
||||
assert.hasClass(sidebar.$('.o_web_studio_sidebar_header [name="options"]'),'inactive',
|
||||
"the options tab should be inactive");
|
||||
|
||||
testUtils.mock.intercept(sidebar, 'sidebar_tab_changed', function (ev) {
|
||||
assert.step(ev.data.mode);
|
||||
});
|
||||
testUtils.dom.click(sidebar.$('.o_web_studio_sidebar_header [name="new"]'));
|
||||
assert.verifySteps(['new'], "the sidebar should be updated");
|
||||
|
||||
await testUtils.dom.click(sidebar.$('.o_web_studio_sidebar_header [name="options"]'));
|
||||
assert.verifySteps([], "one should not be able to select options");
|
||||
|
||||
sidebar.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("'Report' tab behaviour", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
return studioTestUtils.createSidebar({
|
||||
data: this.data,
|
||||
state: { mode: 'report' },
|
||||
report: {
|
||||
name: 'Kikou',
|
||||
},
|
||||
}).then(async function (sidebar) {
|
||||
|
||||
assert.hasAttrValue(sidebar.$('.o_web_studio_sidebar_header > .active'), 'name', "report",
|
||||
"the 'Report' tab should be active");
|
||||
assert.strictEqual(sidebar.$('input[name="name"]').val(), "Kikou",
|
||||
"the report name should be displayed");
|
||||
|
||||
testUtils.mock.intercept(sidebar, 'studio_edit_report', function (ev) {
|
||||
if (ev.data.name) {
|
||||
assert.deepEqual(ev.data, { name: "wow_report" });
|
||||
} else if ('paperformat_id' in ev.data) {
|
||||
paperformatValues.push(ev.data);
|
||||
} else if (ev.data.groups_id) {
|
||||
assert.deepEqual(ev.data, { groups_id: [7] });
|
||||
}
|
||||
});
|
||||
// edit report name
|
||||
sidebar.$('input[name="name"]').val("wow_report").trigger('change');
|
||||
|
||||
// edit the report paperformat
|
||||
var paperformatValues = [];
|
||||
await testUtils.fields.many2one.clickOpenDropdown('paperformat_id');
|
||||
await testUtils.fields.many2one.clickHighlightedItem('paperformat_id');
|
||||
assert.deepEqual(paperformatValues, [{ paperformat_id: 42 }]);
|
||||
|
||||
// remove the report paperformat
|
||||
sidebar.$('[name="paperformat_id"] input').val('').trigger('keyup').trigger('focusout');
|
||||
await testUtils.nextTick();
|
||||
assert.deepEqual(paperformatValues, [{ paperformat_id: 42 }, { paperformat_id: false }]);
|
||||
|
||||
// edit groups
|
||||
await testUtils.fields.many2one.clickOpenDropdown('groups_id');
|
||||
await testUtils.fields.many2one.clickItem('groups_id', 'Group7');
|
||||
|
||||
sidebar.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("'Add' tab behaviour", function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(2);
|
||||
|
||||
studioTestUtils.createSidebar({
|
||||
state: { mode: 'new' },
|
||||
}).then(function (sidebar) {
|
||||
|
||||
assert.hasAttrValue(sidebar.$('.o_web_studio_sidebar_header > .active'), 'name', "new",
|
||||
"the 'Add' tab should be active");
|
||||
assert.ok(sidebar.$('.ui-draggable').length,
|
||||
"there should be draggable components");
|
||||
|
||||
sidebar.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("basic 'Options' tab behaviour", function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(4);
|
||||
|
||||
var node = {
|
||||
node: {
|
||||
attrs: {
|
||||
'data-oe-id': '42',
|
||||
'data-oe-xpath': '/t/t/div',
|
||||
},
|
||||
tag: 'span',
|
||||
$nodes: $(),
|
||||
},
|
||||
};
|
||||
studioTestUtils.createSidebar({
|
||||
state: {
|
||||
mode: 'properties',
|
||||
nodes: [node],
|
||||
},
|
||||
}).then(function (sidebar) {
|
||||
|
||||
assert.hasAttrValue(sidebar.$('.o_web_studio_sidebar_header > .active'), 'name', "options",
|
||||
"the 'Options' tab should be active");
|
||||
assert.containsOnce(sidebar, '.o_web_studio_sidebar_content .collapse',
|
||||
"there should be one node in the accordion");
|
||||
assert.hasClass(sidebar.$('.o_web_studio_sidebar_content .collapse'),'show',
|
||||
"the node should be expanded by default");
|
||||
|
||||
// remove the element
|
||||
testUtils.mock.intercept(sidebar, 'element_removed', function (ev) {
|
||||
assert.deepEqual(ev.data.node, node.node);
|
||||
});
|
||||
testUtils.dom.click(sidebar.$('.o_web_studio_sidebar_content .collapse .o_web_studio_remove'));
|
||||
|
||||
sidebar.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("'Options' tab with multiple nodes", async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
var node1 = {
|
||||
node: {
|
||||
attrs: {
|
||||
'data-oe-id': '42',
|
||||
'data-oe-xpath': '/t/t/div',
|
||||
},
|
||||
tag: 'span',
|
||||
$nodes: $(),
|
||||
},
|
||||
};
|
||||
|
||||
var node2 = {
|
||||
node: {
|
||||
attrs: {
|
||||
'data-oe-id': '40',
|
||||
'data-oe-xpath': '/t/t',
|
||||
},
|
||||
tag: 'div',
|
||||
$nodes: $(),
|
||||
},
|
||||
};
|
||||
const sidebar = await studioTestUtils.createSidebar({
|
||||
state: {
|
||||
mode: 'properties',
|
||||
nodes: [node1, node2],
|
||||
},
|
||||
});
|
||||
|
||||
assert.hasAttrValue(sidebar.$('.o_web_studio_sidebar_header > .active'), 'name', "options",
|
||||
"the 'Options' tab should be active");
|
||||
assert.containsN(sidebar, '.o_web_studio_sidebar_content .card', 2,
|
||||
"there should be one node in the accordion");
|
||||
assert.hasClass(sidebar.$('.o_web_studio_sidebar_content .card:has(.o_text:contains(span)) .collapse'),'show',
|
||||
"the 'span' node should be expanded by default");
|
||||
assert.doesNotHaveClass(sidebar.$('.o_web_studio_sidebar_content .card:has(.o_text:contains(div)) .collapse'), 'show',
|
||||
"the 'div' node shouldn't be expanded");
|
||||
assert.strictEqual(sidebar.$('.o_web_studio_sidebar_content .o_web_studio_accordion > .card:last .card-header:first').text().trim(), "span",
|
||||
"the last node should be the span");
|
||||
|
||||
// expand the first node
|
||||
// BS4 collapsing is asynchronous
|
||||
await new Promise((resolve) => {
|
||||
$(document.body).one("hidden.bs.collapse", () => {
|
||||
resolve();
|
||||
});
|
||||
testUtils.dom.click(sidebar.$('.o_web_studio_sidebar_content .o_web_studio_accordion > .card:first [data-bs-toggle="collapse"]:first'));
|
||||
})
|
||||
// await end of transitions: https://getbootstrap.com/docs/5.0/components/collapse/#example
|
||||
await nextTick()
|
||||
assert.doesNotHaveClass(sidebar.$('.o_web_studio_sidebar_content .card:has(.o_text:contains(span)) .collapse:first'), 'show',
|
||||
"the 'span' node should have been closed");
|
||||
assert.hasClass(sidebar.$('.o_web_studio_sidebar_content .card:has(.o_text:contains(div)) .collapse:first'),'show',
|
||||
"the 'div' node should be expanded");
|
||||
|
||||
// reexpand the second node
|
||||
await new Promise((resolve) => {
|
||||
$(document.body).one("shown.bs.collapse", () => {
|
||||
resolve();
|
||||
});
|
||||
testUtils.dom.click(sidebar.$('.o_web_studio_sidebar_content .o_web_studio_accordion > .card:last [data-bs-toggle="collapse"]:first'));
|
||||
})
|
||||
await nextTick();
|
||||
assert.hasClass(sidebar.$('.o_web_studio_sidebar_content .card:has(.o_text:contains(span)) .collapse:first'),'show',
|
||||
"the 'span' node should be expanded again");
|
||||
assert.doesNotHaveClass(sidebar.$('.o_web_studio_sidebar_content .card:has(.o_text:contains(div)) .collapse:first'), 'show',
|
||||
"the 'div' node shouldn't be expanded anymore");
|
||||
|
||||
sidebar.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("'Options' tab with layout component can be expanded", function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(3);
|
||||
|
||||
var node = {
|
||||
node: {
|
||||
attrs: {
|
||||
'data-oe-id': '42',
|
||||
'data-oe-xpath': '/t/t/div',
|
||||
},
|
||||
tag: 'span',
|
||||
$nodes: $(),
|
||||
},
|
||||
};
|
||||
studioTestUtils.createSidebar({
|
||||
state: {
|
||||
mode: 'properties',
|
||||
nodes: [node],
|
||||
},
|
||||
}).then(function (sidebar) {
|
||||
|
||||
assert.containsOnce(sidebar, '.o_web_studio_sidebar_content .collapse',
|
||||
"there should be one node in the accordion");
|
||||
assert.containsOnce(sidebar, '.o_web_studio_sidebar_content .o_web_studio_layout',
|
||||
"there should be a layout component");
|
||||
assert.containsOnce(sidebar, '.o_web_studio_sidebar_content .o_web_studio_layout .o_web_studio_margin',
|
||||
"there should be a margin section in the layout component");
|
||||
|
||||
sidebar.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("'Options' tab with layout component can be expanded on open ", function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(1);
|
||||
|
||||
var node = {
|
||||
node: {
|
||||
attrs: {
|
||||
'data-oe-id': '42',
|
||||
'data-oe-xpath': '/t/t/div',
|
||||
},
|
||||
tag: 'span',
|
||||
$nodes: $(),
|
||||
},
|
||||
};
|
||||
studioTestUtils.createSidebar({
|
||||
state: {
|
||||
mode: 'properties',
|
||||
nodes: [node],
|
||||
},
|
||||
previousState: {
|
||||
"42/t/t/div": { 'layout': { showAll: true } }, // opens the layout expanded
|
||||
},
|
||||
}).then(function (sidebar) {
|
||||
|
||||
assert.equal(sidebar.$('.o_web_studio_width:visible').length, 1);
|
||||
|
||||
sidebar.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("'Options' tab with layout component can be expanded on open with hierarchy", function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(2);
|
||||
|
||||
var nodes = [
|
||||
{
|
||||
context: {'rec': 'int'},
|
||||
node: {
|
||||
attrs: {
|
||||
'data-oe-id': '42',
|
||||
'data-oe-xpath': '/t/t/t/t/div',
|
||||
't-esc': 'rec',
|
||||
},
|
||||
tag: 'span',
|
||||
$nodes: $(),
|
||||
}
|
||||
},
|
||||
{
|
||||
context: {'rec': 'int'},
|
||||
node: {
|
||||
attrs: {
|
||||
'data-oe-id': '42',
|
||||
'data-oe-xpath': '/t/t/t/t',
|
||||
't-if': 'o.is_ok',
|
||||
},
|
||||
tag: 'div',
|
||||
$nodes: $(),
|
||||
},
|
||||
},
|
||||
{
|
||||
context: {'rec': 'int'},
|
||||
node: {
|
||||
attrs: {
|
||||
'data-oe-id': '42',
|
||||
'data-oe-xpath': '/t/t/t',
|
||||
't-else': '',
|
||||
},
|
||||
tag: 't',
|
||||
$nodes: $(),
|
||||
},
|
||||
},
|
||||
{
|
||||
context: {},
|
||||
node: {
|
||||
attrs: {
|
||||
'data-oe-id': '42',
|
||||
'data-oe-xpath': '/t/t',
|
||||
't-foreach': '5',
|
||||
't-as': 'rec',
|
||||
},
|
||||
tag: 't',
|
||||
$nodes: $(),
|
||||
},
|
||||
},
|
||||
{
|
||||
context: {},
|
||||
node: {
|
||||
attrs: {
|
||||
'data-oe-id': '42',
|
||||
'data-oe-xpath': '/t',
|
||||
't-name': 'my.template',
|
||||
},
|
||||
tag: 't',
|
||||
$nodes: $(),
|
||||
},
|
||||
},
|
||||
];
|
||||
nodes[0].node.parent = nodes[1].node;
|
||||
nodes[1].node.children = [nodes[0].node]
|
||||
nodes[1].node.parent = nodes[2].node;
|
||||
nodes[2].node.children = [nodes[1].node]
|
||||
nodes[2].node.parent = nodes[3].node;
|
||||
nodes[3].node.children = [{tag: 'span', attrs: {'t-if': 'false'}, $nodes: $(), parent: nodes[3].node}, nodes[2].node]
|
||||
nodes[3].node.parent = nodes[4].node;
|
||||
nodes[4].node.children = [nodes[3].node]
|
||||
nodes[4].node.parent = null;
|
||||
|
||||
studioTestUtils.createSidebar({
|
||||
state: {
|
||||
mode: 'properties',
|
||||
nodes: nodes,
|
||||
},
|
||||
previousState: {
|
||||
"42/t/t/t/t/div": { 'layout': { showAll: true } }, // opens the layout expanded
|
||||
},
|
||||
}).then(function (sidebar) {
|
||||
|
||||
assert.equal(sidebar.$('.card').length, 5, 'Should have 5 item');
|
||||
assert.equal(sidebar.$('.card h5 > button').get().map(el => el.textContent.replace(/[\s\n]+/g, ' ').trim()).join(' > '), 't > t [foreach="5"] > t > div > span [rec]', 'Should have all ordered parents');
|
||||
|
||||
sidebar.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("'Options' tab with widget selection (tOptions) component", function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(4);
|
||||
|
||||
var node = {
|
||||
context: {
|
||||
'doc': 'x_mymodel',
|
||||
},
|
||||
node: {
|
||||
attrs: {
|
||||
'data-oe-id': '42',
|
||||
'data-oe-xpath': '/t/t/div',
|
||||
't-field': 'doc.id',
|
||||
't-options-widget': '"text"',
|
||||
},
|
||||
tag: 'span',
|
||||
$nodes: $(),
|
||||
},
|
||||
};
|
||||
studioTestUtils.createSidebar({
|
||||
state: {
|
||||
mode: 'properties',
|
||||
nodes: [node],
|
||||
},
|
||||
widgetsOptions: this.widgetsOptions,
|
||||
}).then(function (sidebar) {
|
||||
|
||||
assert.containsOnce(sidebar, '.o_web_studio_tfield_fieldexpression',
|
||||
"the t-field component should be displayed");
|
||||
assert.containsOnce(sidebar, '.o_web_studio_toption_widget',
|
||||
"the t-options component should be displayed");
|
||||
assert.strictEqual(sidebar.$('.o_web_studio_toption_widget select').text().replace(/\s/g, ''), "imageintegertext",
|
||||
"all widgets should be selectable");
|
||||
assert.strictEqual(sidebar.$('.o_web_studio_toption_widget select').val(), "text",
|
||||
"the correct widget should be selected");
|
||||
|
||||
sidebar.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("'Options' tab with FieldSelector does not flicker", async function (assert) {
|
||||
assert.expect(3);
|
||||
var def = testUtils.makeTestPromise();
|
||||
|
||||
var node = {
|
||||
context: {
|
||||
'doc': 'x_mymodel',
|
||||
},
|
||||
node: {
|
||||
attrs: {
|
||||
'data-oe-id': '42',
|
||||
'data-oe-xpath': '/t/t/div',
|
||||
't-field': 'doc.id',
|
||||
't-options-widget': '"text"',
|
||||
},
|
||||
context: {
|
||||
'doc': 'x_mymodel',
|
||||
},
|
||||
tag: 'span',
|
||||
$nodes: $(),
|
||||
},
|
||||
};
|
||||
var sidebarDef = studioTestUtils.createSidebar({
|
||||
data: this.data,
|
||||
models: {
|
||||
'x_mymodel': 'My Model',
|
||||
},
|
||||
state: {
|
||||
mode: 'properties',
|
||||
nodes: [node],
|
||||
},
|
||||
widgetsOptions: this.widgetsOptions,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.model === 'x_mymodel' && args.method === 'fields_get') {
|
||||
// Block the 'read' call
|
||||
var result = this._super.apply(this, arguments);
|
||||
return Promise.resolve(def).then(_.constant(result));
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
await testUtils.nextTick();
|
||||
assert.strictEqual($('.o_web_studio_tfield_fieldexpression').length, 0,
|
||||
"the sidebar should wait its components to be rendered before its insertion");
|
||||
|
||||
// release the fields_get
|
||||
def.resolve();
|
||||
var sidebar = await sidebarDef;
|
||||
await testUtils.nextTick();
|
||||
assert.strictEqual($('.o_web_studio_tfield_fieldexpression').length, 1,
|
||||
"the t-field component should be displayed");
|
||||
assert.strictEqual(sidebar.$('.o_web_studio_tfield_fieldexpression .o_field_selector_value').text().replace(/\s/g, ''),
|
||||
"doc(MyModel)ID",
|
||||
"the field chain should be correctly displayed");
|
||||
|
||||
sidebar.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Various layout changes', function (assert) {
|
||||
var done = assert.async();
|
||||
// this test is a combinaison of multiple tests, to avoid copy
|
||||
// pasting multiple times de sidebar create/intercept/destroy
|
||||
|
||||
var layoutChangeNode = {
|
||||
attrs: {
|
||||
'data-oe-id': '99',
|
||||
'data-oe-xpath': '/t/t/div',
|
||||
},
|
||||
tag: 'div',
|
||||
$nodes: $(),
|
||||
};
|
||||
var layoutChangeTextNode = {
|
||||
attrs: {
|
||||
'data-oe-id': '99',
|
||||
'data-oe-xpath': '/t/t/span',
|
||||
},
|
||||
tag: 'span',
|
||||
$nodes: $(),
|
||||
};
|
||||
var nodeWithAllLayoutPropertiesSet = {
|
||||
tag: "div",
|
||||
attrs: {
|
||||
//width: "1",
|
||||
style: "margin-top:2px;width:1px;margin-right:3px;margin-bottom:4px;margin-left:5px;",
|
||||
class: "o_bold o_italic h3 bg-o-color-3 text-o-color-2 o_underline",
|
||||
'data-oe-id': '99',
|
||||
'data-oe-xpath': '/t/t/div',
|
||||
},
|
||||
$nodes: $(),
|
||||
};
|
||||
|
||||
var nodeWithAllLayoutPropertiesFontAndBackgroundSet = {
|
||||
tag: "div",
|
||||
attrs: {
|
||||
//width: "1",
|
||||
style: "margin-top:2px;margin-right:3px;width:1px;margin-bottom:4px;margin-left:5px;background-color:#00FF00;color:#00FF00",
|
||||
class: "o_bold o_italic h3 o_underline",
|
||||
'data-oe-id': '99',
|
||||
'data-oe-xpath': '/t/t/div',
|
||||
},
|
||||
$nodes: $(),
|
||||
};
|
||||
var layoutChangesOperations = [
|
||||
{
|
||||
testName: "add a margin top in pixels",
|
||||
nodeToUse: layoutChangeNode,
|
||||
eventToTrigger: "change",
|
||||
sidebarOperationInputSelector: '.o_web_studio_margin [data-margin="margin-top"]',
|
||||
valueToPut: "42",
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"style\" separator=\";\" add=\"margin-top:42px\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
testName: "add a margin bottom in pixels",
|
||||
nodeToUse: layoutChangeNode,
|
||||
eventToTrigger: "change",
|
||||
sidebarOperationInputSelector: '.o_web_studio_margin [data-margin="margin-bottom"]',
|
||||
valueToPut: "42",
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"style\" separator=\";\" add=\"margin-bottom:42px\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
testName: "add a margin left in pixels",
|
||||
nodeToUse: layoutChangeNode,
|
||||
eventToTrigger: "change",
|
||||
sidebarOperationInputSelector: '.o_web_studio_margin [data-margin="margin-left"]',
|
||||
valueToPut: "42",
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"style\" separator=\";\" add=\"margin-left:42px\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
testName: "add a margin right in pixels",
|
||||
nodeToUse: layoutChangeNode,
|
||||
eventToTrigger: "change",
|
||||
sidebarOperationInputSelector: '.o_web_studio_margin [data-margin="margin-right"]',
|
||||
valueToPut: "42",
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"style\" separator=\";\" add=\"margin-right:42px\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
testName: "add a width",
|
||||
nodeToUse: layoutChangeNode,
|
||||
eventToTrigger: "change",
|
||||
sidebarOperationInputSelector: '.o_web_studio_width input',
|
||||
valueToPut: "42",
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"style\" separator=\";\" add=\"width:42px\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
testName: "add a width on a text",
|
||||
nodeToUse: layoutChangeTextNode,
|
||||
eventToTrigger: "change",
|
||||
sidebarOperationInputSelector: '.o_web_studio_width input',
|
||||
valueToPut: "42",
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"style\" separator=\";\" add=\"width:42px;display:inline-block\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/span"
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
testName: "add a class",
|
||||
nodeToUse: layoutChangeNode,
|
||||
eventToTrigger: "change",
|
||||
sidebarOperationInputSelector: '.o_web_studio_classes input',
|
||||
valueToPut: "new_class",
|
||||
expectedRPC: {
|
||||
new_attrs: {
|
||||
class: "new_class"
|
||||
},
|
||||
type: "attributes",
|
||||
},
|
||||
}, {
|
||||
testName: "set the heading level",
|
||||
nodeToUse: layoutChangeNode,
|
||||
eventToTrigger: "click",
|
||||
sidebarOperationInputSelector: '.o_web_studio_font_size .dropdown-item-text[data-value="h3"]',
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"class\" separator=\" \" add=\"h3\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
},
|
||||
}, {
|
||||
testName: "set the background color to a theme color",
|
||||
nodeToUse: layoutChangeNode,
|
||||
eventToTrigger: "mousedown",
|
||||
sidebarOperationInputSelector: '.o_web_studio_colors .o_web_studio_background_colorpicker button[data-color="o-color-3"]',
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"class\" separator=\" \" add=\"bg-o-color-3\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
},
|
||||
}, {
|
||||
testName: "set the background color to a standard color",
|
||||
nodeToUse: layoutChangeNode,
|
||||
eventToTrigger: "mousedown",
|
||||
sidebarOperationInputSelector: '.o_web_studio_colors .o_web_studio_background_colorpicker button[data-value="#00FF00"]',
|
||||
valueToPut: "h3",
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"style\" separator=\";\" add=\"background-color:#00FF00\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
},
|
||||
}, {
|
||||
testName: "set the font color to a theme color",
|
||||
nodeToUse: layoutChangeNode,
|
||||
eventToTrigger: "mousedown",
|
||||
sidebarOperationInputSelector: '.o_web_studio_colors .o_web_studio_font_colorpicker button[data-color="o-color-3"]',
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"class\" separator=\" \" add=\"text-o-color-3\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
},
|
||||
}, {
|
||||
testName: "set the font color to a standard color",
|
||||
nodeToUse: layoutChangeNode,
|
||||
eventToTrigger: "mousedown",
|
||||
sidebarOperationInputSelector: '.o_web_studio_colors .o_web_studio_font_colorpicker button[data-value="#00FF00"]',
|
||||
valueToPut: "h3",
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"style\" separator=\";\" add=\"color:#00FF00\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
},
|
||||
}, {
|
||||
testName: "set the alignment",
|
||||
nodeToUse: layoutChangeNode,
|
||||
eventToTrigger: "click",
|
||||
sidebarOperationInputSelector: '.o_web_studio_text_alignment button[title="end"]',
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"class\" separator=\" \" add=\"text-end\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
},
|
||||
}, {
|
||||
testName: "remove margin top in pixels",
|
||||
nodeToUse: nodeWithAllLayoutPropertiesSet,
|
||||
eventToTrigger: "change",
|
||||
sidebarOperationInputSelector: '.o_web_studio_margin [data-margin="margin-top"]',
|
||||
valueToPut: "",
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"style\" separator=\";\" remove=\"margin-top:2px\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
testName: "remove a margin bottom in pixels",
|
||||
nodeToUse: nodeWithAllLayoutPropertiesSet,
|
||||
eventToTrigger: "change",
|
||||
sidebarOperationInputSelector: '.o_web_studio_margin [data-margin="margin-bottom"]',
|
||||
valueToPut: "",
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"style\" separator=\";\" remove=\"margin-bottom:4px\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
testName: "remove a margin left in pixels",
|
||||
nodeToUse: nodeWithAllLayoutPropertiesSet,
|
||||
eventToTrigger: "change",
|
||||
sidebarOperationInputSelector: '.o_web_studio_margin [data-margin="margin-left"]',
|
||||
valueToPut: "",
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"style\" separator=\";\" remove=\"margin-left:5px\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
testName: "remove a margin right in pixels",
|
||||
nodeToUse: nodeWithAllLayoutPropertiesSet,
|
||||
eventToTrigger: "change",
|
||||
sidebarOperationInputSelector: '.o_web_studio_margin [data-margin="margin-right"]',
|
||||
valueToPut: "",
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"style\" separator=\";\" remove=\"margin-right:3px\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
testName: "remove the width",
|
||||
nodeToUse: nodeWithAllLayoutPropertiesSet,
|
||||
eventToTrigger: "change",
|
||||
sidebarOperationInputSelector: '.o_web_studio_width input',
|
||||
valueToPut: "",
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"style\" separator=\";\" remove=\"width:1px\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
testName: "remove a class",
|
||||
nodeToUse: nodeWithAllLayoutPropertiesSet,
|
||||
eventToTrigger: "change",
|
||||
sidebarOperationInputSelector: '.o_web_studio_classes input',
|
||||
valueToPut: "o_bold o_italic bg-o-color-3 text-o-color-2 o_underline",
|
||||
expectedRPC: {
|
||||
new_attrs: {
|
||||
class: "o_bold o_italic bg-o-color-3 text-o-color-2 o_underline"
|
||||
},
|
||||
type: "attributes",
|
||||
},
|
||||
}, {
|
||||
testName: "unset the background color to a theme color",
|
||||
nodeToUse: nodeWithAllLayoutPropertiesSet,
|
||||
eventToTrigger: "click",
|
||||
sidebarOperationInputSelector: '.o_web_studio_colors .o_web_studio_background_colorpicker .o_web_studio_reset_color',
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"class\" separator=\" \" remove=\"bg-o-color-3\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
},
|
||||
},{
|
||||
testName: "unset the background color to a standard color",
|
||||
nodeToUse: nodeWithAllLayoutPropertiesFontAndBackgroundSet,
|
||||
eventToTrigger: "click",
|
||||
sidebarOperationInputSelector: '.o_web_studio_colors .o_web_studio_background_colorpicker .o_web_studio_reset_color',
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"style\" separator=\";\" remove=\"background-color:#00FF00\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
},
|
||||
}, {
|
||||
testName: "unset the font color to a theme color",
|
||||
nodeToUse: nodeWithAllLayoutPropertiesSet,
|
||||
eventToTrigger: "click",
|
||||
sidebarOperationInputSelector: '.o_web_studio_colors .o_web_studio_font_colorpicker .o_web_studio_reset_color',
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"class\" separator=\" \" remove=\"text-o-color-2\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
},
|
||||
}, {
|
||||
testName: "unset the font color to a standard color",
|
||||
nodeToUse: nodeWithAllLayoutPropertiesFontAndBackgroundSet,
|
||||
eventToTrigger: "click",
|
||||
sidebarOperationInputSelector: '.o_web_studio_colors .o_web_studio_font_colorpicker button.o_web_studio_reset_color',
|
||||
expectedRPC: {
|
||||
inheritance: [{
|
||||
content: "<attribute name=\"style\" separator=\";\" remove=\"color:#00FF00\"/>",
|
||||
position: "attributes",
|
||||
view_id: 99,
|
||||
xpath: "/t/t/div"
|
||||
}]
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// there is one assert by operation
|
||||
assert.expect(layoutChangesOperations.length);
|
||||
|
||||
var initialDebugMode = odoo.debug;
|
||||
// show 'class' in the sidebar
|
||||
odoo.debug = true;
|
||||
|
||||
function poll (changeOperation) {
|
||||
var node = {
|
||||
node: changeOperation.nodeToUse,
|
||||
};
|
||||
studioTestUtils.createSidebar({
|
||||
state: {
|
||||
mode: 'properties',
|
||||
nodes: [node],
|
||||
},
|
||||
previousState: {
|
||||
"99/t/t/div": { 'layout': { showAll: true } }, // opens the layout expanded
|
||||
},
|
||||
}).then(function (sidebar) {
|
||||
testUtils.mock.intercept(sidebar, 'view_change', function (ev) {
|
||||
assert.deepEqual(ev.data.operation, changeOperation.expectedRPC, changeOperation.testName);
|
||||
});
|
||||
sidebar.$(changeOperation.sidebarOperationInputSelector)
|
||||
.val(changeOperation.valueToPut)
|
||||
.trigger(changeOperation.eventToTrigger);
|
||||
sidebar.destroy();
|
||||
}).then(function () {
|
||||
if (layoutChangesOperations.length) {
|
||||
poll(layoutChangesOperations.shift());
|
||||
} else {
|
||||
odoo.debug = initialDebugMode;
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
poll(layoutChangesOperations.shift());
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
odoo.define('web_studio.ReportEditor_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var studioTestUtils = require('web_studio.testUtils');
|
||||
|
||||
QUnit.module('Studio', {}, function () {
|
||||
|
||||
QUnit.module('ReportEditor', {
|
||||
beforeEach: function () {
|
||||
},
|
||||
}, function () {
|
||||
QUnit.test('basic report rendering', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var nodesArchs = {
|
||||
42: {
|
||||
attrs: {
|
||||
'data-oe-id': '42',
|
||||
'data-oe-xpath': '/t',
|
||||
name: "Layout",
|
||||
't-name': '42',
|
||||
},
|
||||
},
|
||||
id: 42,
|
||||
key: 'report.layout',
|
||||
parent: null,
|
||||
tag: 't',
|
||||
};
|
||||
var reportHTML = "<html><body><t/></body></html>";
|
||||
var editor = await studioTestUtils.createReportEditor({
|
||||
nodesArchs: nodesArchs,
|
||||
reportHTML: reportHTML,
|
||||
});
|
||||
|
||||
assert.containsOnce(editor, 'iframe',
|
||||
"an iframe should be rendered");
|
||||
assert.hasAttrValue(editor.$('iframe'), 'src', "about:blank",
|
||||
"the source should be correctly set");
|
||||
|
||||
editor.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
354
web_studio/static/tests/legacy/test_utils.js
Normal file
354
web_studio/static/tests/legacy/test_utils.js
Normal file
@@ -0,0 +1,354 @@
|
||||
odoo.define('web_studio.testUtils', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { start } = require('@mail/../tests/helpers/test_utils');
|
||||
|
||||
var dom = require('web.dom');
|
||||
const MockServer = require('web.MockServer');
|
||||
const { ComponentAdapter } = require('web.OwlCompatibility');
|
||||
var QWeb = require('web.QWeb');
|
||||
const { registry } = require('@web/core/registry');
|
||||
var testUtils = require('web.test_utils');
|
||||
var utils = require('web.utils');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var ReportEditor = require('web_studio.ReportEditor');
|
||||
var ReportEditorManager = require('web_studio.ReportEditorManager');
|
||||
var ReportEditorSidebar = require('web_studio.ReportEditorSidebar');
|
||||
var ViewEditorManager = require('web_studio.ViewEditorManager');
|
||||
|
||||
var weTestUtils = require('web_editor.test_utils');
|
||||
|
||||
const { registerCleanup } = require("@web/../tests/helpers/cleanup");
|
||||
|
||||
/**
|
||||
* Create a ReportEditorManager widget.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @return {ReportEditorManager}
|
||||
*/
|
||||
async function createReportEditor(params) {
|
||||
var Parent = Widget.extend({
|
||||
start: function () {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments).then(function () {
|
||||
var $studio = $.parseHTML(
|
||||
"<div class='o_web_studio_client_action'>" +
|
||||
"<div class='o_web_studio_editor_manager o_web_studio_report_editor_manager'/>" +
|
||||
"</div>" +
|
||||
"</div>");
|
||||
self.$el.append($studio);
|
||||
});
|
||||
},
|
||||
});
|
||||
var parent = new Parent();
|
||||
weTestUtils.patch();
|
||||
params.data = weTestUtils.wysiwygData(params.data);
|
||||
await testUtils.mock.addMockEnvironment(parent, params);
|
||||
|
||||
var selector = params.debug ? 'body' : '#qunit-fixture';
|
||||
return parent.appendTo(selector).then(function () {
|
||||
var editor = new ReportEditor(parent, params);
|
||||
// override 'destroy' of editor so that it calls 'destroy' on the parent
|
||||
// instead
|
||||
editor.destroy = function () {
|
||||
// remove the override to properly destroy editor and its children
|
||||
// when it will be called the second time (by its parent)
|
||||
delete editor.destroy;
|
||||
// TODO: call super?
|
||||
parent.destroy();
|
||||
weTestUtils.unpatch();
|
||||
};
|
||||
return editor.appendTo(parent.$('.o_web_studio_editor_manager')).then(function () {
|
||||
return editor;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ReportEditorManager widget.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @return {Promise<ReportEditorManager>}
|
||||
*/
|
||||
async function createReportEditorManager(params) {
|
||||
var parent = new StudioEnvironment();
|
||||
await testUtils.mock.addMockEnvironment(parent, params);
|
||||
weTestUtils.patch();
|
||||
params.data = weTestUtils.wysiwygData(params.data);
|
||||
|
||||
var rem = new ReportEditorManager(parent, params);
|
||||
// also destroy to parent widget to avoid memory leak
|
||||
rem.destroy = function () {
|
||||
delete rem.destroy;
|
||||
parent.destroy();
|
||||
weTestUtils.unpatch();
|
||||
};
|
||||
|
||||
var fragment = document.createDocumentFragment();
|
||||
var selector = params.debug ? 'body' : '#qunit-fixture';
|
||||
if (params.debug) {
|
||||
$('body').addClass('debug');
|
||||
}
|
||||
await parent.prependTo(selector);
|
||||
await rem.appendTo(fragment)
|
||||
// use dom.append to call on_attach_callback
|
||||
dom.append(parent.$('.o_web_studio_client_action'), fragment, {
|
||||
callbacks: [{widget: rem}],
|
||||
in_DOM: true,
|
||||
});
|
||||
await rem.editorIframeDef
|
||||
return rem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sidebar widget.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @return {Promise<ReportEditorSidebar>}
|
||||
*/
|
||||
async function createSidebar(params) {
|
||||
var Parent = Widget.extend({
|
||||
start: function () {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments).then(function () {
|
||||
var $studio = $.parseHTML(
|
||||
"<div class='o_web_studio_client_action'>" +
|
||||
"<div class='o_web_studio_editor_manager o_web_studio_report_editor_manager'/>" +
|
||||
"</div>");
|
||||
self.$el.append($studio);
|
||||
});
|
||||
},
|
||||
});
|
||||
var parent = new Parent();
|
||||
weTestUtils.patch();
|
||||
params.data = weTestUtils.wysiwygData(params.data);
|
||||
await testUtils.mock.addMockEnvironment(parent, params);
|
||||
|
||||
var sidebar = new ReportEditorSidebar(parent, params);
|
||||
sidebar.destroy = function () {
|
||||
// remove the override to properly destroy sidebar and its children
|
||||
// when it will be called the second time (by its parent)
|
||||
delete sidebar.destroy;
|
||||
parent.destroy();
|
||||
weTestUtils.unpatch();
|
||||
};
|
||||
|
||||
var selector = params.debug ? 'body' : '#qunit-fixture';
|
||||
if (params.debug) {
|
||||
$('body').addClass('debug');
|
||||
}
|
||||
parent.appendTo(selector);
|
||||
|
||||
var fragment = document.createDocumentFragment();
|
||||
return sidebar.appendTo(fragment).then(function () {
|
||||
sidebar.$el.appendTo(parent.$('.o_web_studio_editor_manager'));
|
||||
return sidebar;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This class let us instanciate a widget via createWebClient and get it
|
||||
* afterwards in order to use it during tests.
|
||||
*/
|
||||
const { onMounted, onWillUnmount } = owl;
|
||||
class StudioEnvironmentComponent extends ComponentAdapter {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.env = owl.Component.env;
|
||||
onMounted(() => {
|
||||
StudioEnvironmentComponent.currentWidget = this.widget;
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
StudioEnvironmentComponent.currentWidget = undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ViewEditorManager widget.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @return {ViewEditorManager}
|
||||
*/
|
||||
async function createViewEditorManager(params) {
|
||||
weTestUtils.patch();
|
||||
params.serverData = params.serverData || {};
|
||||
params.serverData.models = params.serverData.models || {};
|
||||
params.serverData.models = weTestUtils.wysiwygData(params.serverData.models);
|
||||
registry.category('main_components').add('StudioEnvironmentContainer', {
|
||||
Component: StudioEnvironmentComponent,
|
||||
props: { Component: StudioEnvironment },
|
||||
});
|
||||
const { env: wowlEnv } = await start({
|
||||
...params,
|
||||
legacyParams: { withLegacyMockServer: true },
|
||||
});
|
||||
const parent = StudioEnvironmentComponent.currentWidget;
|
||||
const fieldsView = testUtils.mock.getView(MockServer.currentMockServer, params);
|
||||
if (params.viewID) {
|
||||
fieldsView.view_id = params.viewID;
|
||||
}
|
||||
const type = fieldsView.type;
|
||||
|
||||
const viewDescriptions = {
|
||||
fields: fieldsView.fields,
|
||||
relatedModels: {
|
||||
[params.model]: fieldsView.fields,
|
||||
},
|
||||
views: {
|
||||
[type]: {
|
||||
arch: fieldsView.arch,
|
||||
id: fieldsView.view_id || false,
|
||||
custom_view_id: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const vem = new ViewEditorManager(parent, {
|
||||
action: {
|
||||
context: params.context || {},
|
||||
domain: params.domain || [],
|
||||
res_model: params.model,
|
||||
},
|
||||
controllerState: {
|
||||
currentId: 'res_id' in params ? params.res_id : undefined,
|
||||
resIds: 'res_id' in params ? [params.res_id] : undefined,
|
||||
},
|
||||
fields_view: fieldsView,
|
||||
viewType: fieldsView.type,
|
||||
studio_view_id: params.studioViewID,
|
||||
chatter_allowed: params.chatter_allowed,
|
||||
wowlEnv,
|
||||
viewDescriptions,
|
||||
});
|
||||
|
||||
registerCleanup(() => {
|
||||
vem.destroy();
|
||||
weTestUtils.unpatch();
|
||||
});
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
await parent.prependTo(testUtils.prepareTarget(params.debug));
|
||||
await vem.appendTo(fragment);
|
||||
dom.append(parent.$('.o_web_studio_client_action'), fragment, {
|
||||
callbacks: [{widget: vem}],
|
||||
in_DOM: true,
|
||||
});
|
||||
return vem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a list of templates.
|
||||
*
|
||||
* @param {Array<Object>} templates
|
||||
* @param {Object} data
|
||||
* @param {String} [data.dataOeContext]
|
||||
* @returns {string}
|
||||
*/
|
||||
function getReportHTML(templates, data) {
|
||||
_brandTemplates(templates, data && data.dataOeContext);
|
||||
|
||||
var qweb = new QWeb();
|
||||
_.each(templates, function (template) {
|
||||
qweb.add_template(template.arch);
|
||||
});
|
||||
var render = qweb.render('template0', data);
|
||||
return render;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the report views object.
|
||||
*
|
||||
* @param {Array<Object>} templates
|
||||
* @param {Object} [data]
|
||||
* @param {String} [data.dataOeContext]
|
||||
* @returns {Object}
|
||||
*/
|
||||
function getReportViews(templates, data) {
|
||||
_brandTemplates(templates, data && data.dataOeContext);
|
||||
|
||||
var reportViews = {};
|
||||
_.each(templates, function (template) {
|
||||
reportViews[template.view_id] = {
|
||||
arch: template.arch,
|
||||
key: template.key,
|
||||
studio_arch: '</data>',
|
||||
studio_view_id: false,
|
||||
view_id: template.view_id,
|
||||
};
|
||||
});
|
||||
return reportViews;
|
||||
}
|
||||
|
||||
/**
|
||||
* Brands (in place) a list of templates.
|
||||
*
|
||||
* @private
|
||||
* @param {Array<Object>} templates
|
||||
* @param {String} [dataOeContext]
|
||||
*/
|
||||
function _brandTemplates(templates, dataOeContext) {
|
||||
|
||||
_.each(templates, function (template) {
|
||||
brandTemplate(template);
|
||||
});
|
||||
|
||||
function brandTemplate(template) {
|
||||
var doc = $.parseXML(template.arch).documentElement;
|
||||
var rootNode = utils.xml_to_json(doc, true);
|
||||
brandNode([rootNode], rootNode, '');
|
||||
|
||||
function brandNode(siblings, node, xpath) {
|
||||
// do not brand already branded nodes
|
||||
if (_.isObject(node) && !node.attrs['data-oe-id']) {
|
||||
if (node.tag !== 'kikou') {
|
||||
xpath += ('/' + node.tag);
|
||||
var index = _.filter(siblings, {tag: node.tag}).indexOf(node);
|
||||
if (index > 0) {
|
||||
xpath += '[' + index + ']';
|
||||
}
|
||||
node.attrs['data-oe-id'] = template.view_id;
|
||||
node.attrs['data-oe-xpath'] = xpath;
|
||||
node.attrs['data-oe-context'] = dataOeContext || '{}';
|
||||
}
|
||||
|
||||
_.each(node.children, function (child) {
|
||||
brandNode(node.children, child, xpath);
|
||||
});
|
||||
}
|
||||
}
|
||||
template.arch = utils.json_node_to_xml(rootNode);
|
||||
}
|
||||
}
|
||||
|
||||
var StudioEnvironment = Widget.extend({
|
||||
className: 'o_web_client o_in_studio',
|
||||
start: function () {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments).then(function () {
|
||||
// reproduce the DOM environment of Studio
|
||||
var $studio = $.parseHTML(
|
||||
"<div class='o_content'>" +
|
||||
"<div class='o_web_studio_client_action'/>" +
|
||||
"</div>"
|
||||
);
|
||||
self.$el.append($studio);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
createReportEditor: createReportEditor,
|
||||
createReportEditorManager: createReportEditorManager,
|
||||
createSidebar: createSidebar,
|
||||
createViewEditorManager: createViewEditorManager,
|
||||
getData: weTestUtils.wysiwygData,
|
||||
getReportHTML: getReportHTML,
|
||||
getReportViews: getReportViews,
|
||||
patch: weTestUtils.patch,
|
||||
unpatch: weTestUtils.unpatch,
|
||||
};
|
||||
|
||||
});
|
||||
990
web_studio/static/tests/legacy/tours/web_studio.js
Normal file
990
web_studio/static/tests/legacy/tours/web_studio.js
Normal file
@@ -0,0 +1,990 @@
|
||||
odoo.define('web_studio.tests.tour', function (require) {
|
||||
"use strict";
|
||||
|
||||
const localStorage = require('web.local_storage');
|
||||
const tour = require('web_tour.tour');
|
||||
|
||||
const { randomString } = require('web_studio.utils');
|
||||
|
||||
let createdAppString = null;
|
||||
let createdMenuString = null;
|
||||
|
||||
tour.register('web_studio_tests_tour', {
|
||||
url: "/web",
|
||||
test: true,
|
||||
}, [{
|
||||
// open studio
|
||||
trigger: '.o_main_navbar .o_web_studio_navbar_item',
|
||||
}, {
|
||||
trigger: '.o_web_studio_new_app',
|
||||
}, {
|
||||
// the next steps are here to create a new app
|
||||
trigger: '.o_web_studio_app_creator_next',
|
||||
}, {
|
||||
trigger: '.o_web_studio_app_creator_name > input',
|
||||
run: 'text ' + (createdAppString = randomString(6)),
|
||||
}, {
|
||||
trigger: '.o_web_studio_app_creator_next.is_ready',
|
||||
}, {
|
||||
trigger: '.o_web_studio_app_creator_menu > input',
|
||||
run: 'text ' + (createdMenuString = randomString(6)),
|
||||
}, {
|
||||
trigger: '.o_web_studio_app_creator_next.is_ready',
|
||||
}, {
|
||||
// disable chatter in model configurator, we'll test adding it on later
|
||||
trigger: 'input[name="use_mail"]',
|
||||
}, {
|
||||
// disable company if visible, otherwise it might make the test uncertain
|
||||
trigger: 'body',
|
||||
run: () => {
|
||||
const $input = $('input[name="use_company"]');
|
||||
if ($input) {
|
||||
$input.click();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
trigger: '.o_web_studio_model_configurator_next',
|
||||
}, {
|
||||
// toggle the home menu outside of studio and come back in studio
|
||||
extra_trigger: '.o_menu_toggle:not(.o_menu_toggle_back)',
|
||||
trigger: '.o_web_studio_leave > a.btn',
|
||||
timeout: 60000, /* previous step reloads registry, etc. - could take a long time */
|
||||
}, {
|
||||
extra_trigger: `.o_web_client:not(.o_in_studio)`, /* wait to be out of studio */
|
||||
trigger: '.o_menu_toggle:not(.o_menu_toggle_back)',
|
||||
timeout: 60000, /* previous step reloads registry, etc. - could take a long time */
|
||||
}, {
|
||||
trigger: '.o_main_navbar .o_web_studio_navbar_item',
|
||||
extra_trigger: '.o_home_menu',
|
||||
}, {
|
||||
// open the app creator and leave it
|
||||
trigger: '.o_web_studio_new_app',
|
||||
}, {
|
||||
extra_trigger: '.o_web_studio_app_creator',
|
||||
trigger: '.o_web_studio_leave > a.btn',
|
||||
}, {
|
||||
// go back to the previous app
|
||||
trigger: '.o_home_menu',
|
||||
run: () => {
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
bubbles: true,
|
||||
key: 'Escape',
|
||||
}));
|
||||
},
|
||||
}, {
|
||||
// this should open the previous app outside of studio
|
||||
extra_trigger: `.o_web_client:not(.o_in_studio) .o_menu_brand:contains(${createdAppString})`,
|
||||
// go back to the home menu
|
||||
trigger: '.o_menu_toggle:not(.o_menu_toggle_back)',
|
||||
}, {
|
||||
trigger: 'input.o_search_hidden',
|
||||
// Open Command Palette
|
||||
run: 'text ' + createdMenuString[0],
|
||||
}, {
|
||||
trigger: '.o_command_palette_search input',
|
||||
run: 'text ' + "/" + createdMenuString,
|
||||
}, {
|
||||
// search results should have been updated
|
||||
extra_trigger: `.o_command.focused:contains(${createdAppString} / ${createdMenuString})`,
|
||||
trigger: '.o_command_palette',
|
||||
// Close the Command Palette
|
||||
run: () => {
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
key: 'Escape',
|
||||
}));
|
||||
},
|
||||
}, {
|
||||
// enter Studio
|
||||
trigger: '.o_main_navbar .o_web_studio_navbar_item',
|
||||
}, {
|
||||
// edit an app
|
||||
extra_trigger: '.o_studio_home_menu',
|
||||
trigger: `.o_app[data-menu-xmlid*="studio"]:contains(${createdAppString})`,
|
||||
run: function () {
|
||||
// We can't emulate a hover to display the edit icon
|
||||
const editIcon = this.$anchor[0].querySelector('.o_web_studio_edit_icon');
|
||||
editIcon.style.visibility = 'visible';
|
||||
editIcon.click();
|
||||
},
|
||||
}, {
|
||||
// design the icon
|
||||
// TODO: we initially tested this (change an app icon) at the end but a
|
||||
// long-standing bug (KeyError: ir.ui.menu.display_name, caused by a registry
|
||||
// issue with multiple workers) on runbot prevent us from doing it. It thus have
|
||||
// been moved at the beginning of this test to avoid the registry to be reloaded
|
||||
// before the write on ir.ui.menu.
|
||||
trigger: '.o_web_studio_selector:eq(0)',
|
||||
}, {
|
||||
trigger: '.o_web_studio_palette > .o_web_studio_selector:first',
|
||||
}, {
|
||||
trigger: '.modal-footer .btn.btn-primary',
|
||||
}, {
|
||||
// click on the created app
|
||||
trigger: `.o_app[data-menu-xmlid*="studio"]:contains(${createdAppString})`,
|
||||
}, {
|
||||
// create a new menu
|
||||
trigger: '.o_main_navbar .o_web_edit_menu',
|
||||
}, {
|
||||
trigger: 'footer.modal-footer .js_add_menu',
|
||||
}, {
|
||||
trigger: 'input[name="name"]',
|
||||
run: 'text ' + (createdMenuString = randomString(6)),
|
||||
}, {
|
||||
trigger: 'div[name="model_choice"] input[data-value="existing"]',
|
||||
}, {
|
||||
trigger: '.o_field_many2one[name="model"] input',
|
||||
run: 'text a',
|
||||
}, {
|
||||
trigger: '.ui-autocomplete > .ui-menu-item:first > a',
|
||||
in_modal: false,
|
||||
}, {
|
||||
trigger: 'button:contains(Confirm):not(".disabled")',
|
||||
}, {
|
||||
trigger: 'button:contains(Confirm):not(".disabled")',
|
||||
}, {
|
||||
// check that the Studio menu is still there
|
||||
extra_trigger: '.o_web_studio_menu',
|
||||
// switch to form view
|
||||
trigger: '.o_web_studio_views_icons > a[title="Form"]',
|
||||
}, {
|
||||
// wait for the form editor to be rendered because the sidebar is the same
|
||||
extra_trigger: '.o_web_studio_form_view_editor',
|
||||
// unfold 'Existing Fieldqs' section
|
||||
trigger: '.o_web_studio_existing_fields_header',
|
||||
}, {
|
||||
// add an new field
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_field_type_container:eq(1) .o_web_studio_field_char',
|
||||
run: 'drag_and_drop .o_web_studio_form_view_editor .o_inner_group',
|
||||
}, {
|
||||
// click on the field
|
||||
trigger: '.o_web_studio_form_view_editor .o_wrap_label:first label',
|
||||
// when it's there
|
||||
extra_trigger: 'input[data-type="field_name"]',
|
||||
}, {
|
||||
// rename the label
|
||||
trigger: '.o_web_studio_sidebar_content.o_display_field input[name="string"]',
|
||||
run: 'text My Coucou Field',
|
||||
}, {
|
||||
// verify that the field name has changed and change it
|
||||
trigger: 'input[data-type="field_name"][value="my_coucou_field"]',
|
||||
run: 'text coucou',
|
||||
// the rename operation (/web_studio/rename_field + /web_studio/edit_view)
|
||||
// takes a while and sometimes reaches the default 10s timeout
|
||||
timeout: 20000,
|
||||
}, {
|
||||
// click on "Add" tab
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_new',
|
||||
// the rename operation (/web_studio/rename_field + /web_studio/edit_view)
|
||||
// takes a while and sometimes reaches the default 10s timeout
|
||||
timeout: 20000,
|
||||
async run() {
|
||||
// During the rename, the UI is blocked. When the rpc returns, the UI is
|
||||
// unblocked and the sidebar is re-rendered. Without this, the step is
|
||||
// sometimes executed exactly when the sidebar is about to be replaced,
|
||||
// and it doesn't work. We thus here wait for 1s to ensure that the
|
||||
// sidebar has been re-rendered, before going further.
|
||||
// note1: there's nothing in the DOM that could be used to determine that
|
||||
// we're ready to continue (the sidebar is just replaced by itself, same state)
|
||||
// note2: ideally, it should work whenever we click, but with the current
|
||||
// architecture of studio, it's really hard to fix. Hopefully, when studio
|
||||
// will be converted to owl, this should no longer be an issue.
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
$(".o_web_studio_sidebar .o_web_studio_new").click();
|
||||
}
|
||||
}, {
|
||||
// add a new field
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_field_type_container:eq(1) .o_web_studio_field_char',
|
||||
run: 'drag_and_drop .o_web_studio_form_view_editor .o_inner_group',
|
||||
}, {
|
||||
// rename the field with the same name
|
||||
trigger: 'input[data-type="field_name"]',
|
||||
run: 'text coucou',
|
||||
}, {
|
||||
// an alert dialog should be opened
|
||||
trigger: '.modal-footer > button:first',
|
||||
}, {
|
||||
// rename the label
|
||||
trigger: '.o_web_studio_sidebar_content.o_display_field input[name="string"]',
|
||||
run: 'text COUCOU',
|
||||
}, {
|
||||
// verify that the field name has changed (post-fixed by _1)
|
||||
extra_trigger: 'input[data-type="field_name"][value="coucou_1"]',
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_new',
|
||||
// the rename operation (/web_studio/rename_field + /web_studio/edit_view)
|
||||
// takes a while and sometimes reaches the default 10s timeout
|
||||
timeout: 20000,
|
||||
}, {
|
||||
// add a monetary field --> create a currency field
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_field_type_container:eq(1) .o_web_studio_field_monetary',
|
||||
run: 'drag_and_drop .o_web_studio_form_view_editor .o_inner_group',
|
||||
}, {
|
||||
trigger: '.modal-footer .btn.btn-primary',
|
||||
}, {
|
||||
// verify that the currency field is in the view
|
||||
extra_trigger: '.o_web_studio_form_view_editor .o_wrap_label label:contains("Currency")',
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_new',
|
||||
async run() {
|
||||
// When adding a new field, the UI is blocked. When the rpc returns, the UI is
|
||||
// unblocked and the sidebar is re-rendered. Without this, the step is
|
||||
// sometimes executed exactly when the sidebar is about to be replaced,
|
||||
// and it doesn't work. We thus here wait for 1s to ensure that the
|
||||
// sidebar has been re-rendered, before going further.
|
||||
// note1: there's nothing in the DOM that could be used to determine that
|
||||
// we're ready to continue (the sidebar is just replaced by itself, same state)
|
||||
// note2: ideally, it should work whenever we click, but with the current
|
||||
// architecture of studio, it's really hard to fix. Hopefully, when studio
|
||||
// will be converted to owl, this should no longer be an issue.
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
$(".o_web_studio_sidebar .o_web_studio_new").click();
|
||||
}
|
||||
}, {
|
||||
// add a monetary field
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_field_type_container:eq(1) .o_web_studio_field_monetary',
|
||||
run: 'drag_and_drop (.o_web_studio_form_view_editor .o_inner_group:first .o_web_studio_hook:eq(1))',
|
||||
}, {
|
||||
// verify that the monetary field is in the view
|
||||
extra_trigger: '.o_web_studio_form_view_editor .o_wrap_label:eq(1) label:contains("New Monetary")',
|
||||
// switch the two first fields
|
||||
trigger: '.o_web_studio_form_view_editor .o_inner_group:first .o-draggable:eq(1)',
|
||||
run: 'drag_and_drop_native .o_inner_group:first .o_web_studio_hook:first',
|
||||
}, {
|
||||
// click on "Add" tab
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_new',
|
||||
}, {
|
||||
// verify that the fields have been switched
|
||||
extra_trigger: '.o_web_studio_form_view_editor .o_wrap_label:eq(0) label:contains("New Monetary")',
|
||||
// add a m2m field
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_field_type_container:eq(1) .o_web_studio_field_many2many',
|
||||
run: 'drag_and_drop .o_inner_group:first .o_web_studio_hook:first',
|
||||
}, {
|
||||
// type something in the modal
|
||||
trigger: '.o_field_many2one[name="model"] input',
|
||||
run: 'text a',
|
||||
}, {
|
||||
// select the first model
|
||||
trigger: '.ui-autocomplete > .ui-menu-item:first > a',
|
||||
in_modal: false,
|
||||
}, {
|
||||
trigger: 'button:contains(Confirm)',
|
||||
}, {
|
||||
// select the m2m to set its properties
|
||||
trigger: '.o_wrap_input:has(.o_field_many2many)',
|
||||
timeout: 15000, // creating M2M relations can take some time...
|
||||
}, {
|
||||
// change the `widget` attribute
|
||||
trigger: '.o_web_studio_sidebar select[name="widget"]',
|
||||
run: function () {
|
||||
this.$anchor.val('many2many_tags').trigger('change');
|
||||
},
|
||||
}, {
|
||||
// use colors on the m2m tags
|
||||
trigger: '.o_web_studio_sidebar label[for="option_color_field"]',
|
||||
}, {
|
||||
// add a statusbar
|
||||
trigger: '.o_web_studio_statusbar_hook',
|
||||
}, {
|
||||
trigger: '.modal-footer .btn.btn-primary',
|
||||
}, {
|
||||
trigger: '.o_statusbar_status',
|
||||
}, {
|
||||
// verify that a default value has been set for the statusbar
|
||||
trigger: '.o_web_studio_sidebar select[name="default_value"]:contains(First Status)',
|
||||
}, {
|
||||
trigger: '.o_web_studio_views_icons a[title=Form]',
|
||||
}, {
|
||||
// verify Chatter can be added after changing view to form
|
||||
extra_trigger: '.o_web_studio_add_chatter',
|
||||
// edit action
|
||||
trigger: '.o_web_studio_menu .o_menu_sections li a:contains(Views)',
|
||||
}, {
|
||||
// edit form view
|
||||
trigger: '.o_web_studio_view_category .o_web_studio_view_type[data-type="form"] .o_web_studio_thumbnail',
|
||||
}, {
|
||||
// verify Chatter can be added after changing view to form
|
||||
extra_trigger: '.o_web_studio_add_chatter',
|
||||
// switch in list view
|
||||
trigger: '.o_web_studio_menu .o_web_studio_views_icons a[title="List"]',
|
||||
}, {
|
||||
// wait for the list editor to be rendered because the sidebar is the same
|
||||
extra_trigger: '.o_web_studio_list_view_editor',
|
||||
// unfold 'Existing Fieldqs' section
|
||||
trigger: '.o_web_studio_existing_fields_header',
|
||||
}, {
|
||||
// add an existing field (display_name)
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_field_type_container:eq(1) .o_web_studio_field_char',
|
||||
run: 'drag_and_drop .o_web_studio_list_view_editor th.o_web_studio_hook:first',
|
||||
}, {
|
||||
// verify that the field is correctly named
|
||||
extra_trigger: '.o_web_studio_list_view_editor th:contains("COUCOU")',
|
||||
// leave Studio
|
||||
trigger: '.o_web_studio_leave > a.btn',
|
||||
}, {
|
||||
// come back to the home menu to check if the menu data have changed
|
||||
extra_trigger: '.o_web_client:not(.o_in_studio)',
|
||||
trigger: '.o_menu_toggle:not(.o_menu_toggle_back)',
|
||||
}, {
|
||||
trigger: 'input.o_search_hidden',
|
||||
// Open Command Palette
|
||||
run: 'text ' + createdMenuString[0],
|
||||
}, {
|
||||
trigger: '.o_command_palette_search input',
|
||||
run: 'text ' + "/" + createdMenuString,
|
||||
}, {
|
||||
// search results should have been updated
|
||||
extra_trigger: `.o_command.focused:contains(${createdAppString} / ${createdMenuString})`,
|
||||
trigger: '.o_command_palette',
|
||||
// Close the Command Palette
|
||||
run: () => {
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
bubbles: true,
|
||||
key: 'Escape',
|
||||
}));
|
||||
},
|
||||
}, {
|
||||
trigger: '.o_home_menu',
|
||||
// go back again to the app (using keyboard)
|
||||
run: () => {
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
bubbles: true,
|
||||
key: 'Escape',
|
||||
}));
|
||||
},
|
||||
}, {
|
||||
// wait to be back in the list view
|
||||
extra_trigger: '.o_list_view',
|
||||
// re-open studio
|
||||
trigger: '.o_web_studio_navbar_item',
|
||||
}, {
|
||||
// modify the list view
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_view'
|
||||
}, {
|
||||
//select field you want to sort and based on that sorting will be applied on List view
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_sidebar_select #sort_field',
|
||||
run: function () {
|
||||
$('#sort_field option:eq(1)').attr('selected', 'selected');
|
||||
$('#sort_field option:eq(1)').change();
|
||||
}
|
||||
}, {
|
||||
//change order of sorting, Select order and change it
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_sidebar_select #sort_order',
|
||||
run: function () {
|
||||
$('#sort_order option:eq(1)').attr('selected', 'selected');
|
||||
$('#sort_order option:eq(1)').change();
|
||||
}
|
||||
}, {
|
||||
// edit action
|
||||
trigger: '.o_web_studio_menu .o_menu_sections li a:contains("Views")',
|
||||
}, {
|
||||
// add a kanban
|
||||
trigger: '.o_web_studio_view_category .o_web_studio_view_type.o_web_studio_inactive[data-type="kanban"] .o_web_studio_thumbnail',
|
||||
}, {
|
||||
// add a dropdown
|
||||
trigger: '.o_dropdown_kanban.o_web_studio_add_dropdown',
|
||||
}, {
|
||||
trigger: '.modal-footer .btn.btn-primary',
|
||||
}, {
|
||||
// select the dropdown for edition
|
||||
trigger: '.o_dropdown_kanban:not(.o_web_studio_add_dropdown)',
|
||||
}, {
|
||||
// enable "Set Cover" feature
|
||||
trigger: '.o_web_studio_sidebar input[name=set_cover]',
|
||||
}, {
|
||||
trigger: '.modal-footer .btn.btn-primary',
|
||||
}, {
|
||||
// edit action
|
||||
trigger: '.o_web_studio_menu .o_menu_sections li a:contains("Views")',
|
||||
}, {
|
||||
// check that the kanban view is now active
|
||||
extra_trigger: '.o_web_studio_view_category .o_web_studio_view_type:not(.o_web_studio_inactive)[data-type="kanban"]',
|
||||
// add an activity view
|
||||
trigger: '.o_web_studio_view_category .o_web_studio_view_type.o_web_studio_inactive[data-type="activity"] .o_web_studio_thumbnail',
|
||||
}, {
|
||||
extra_trigger: '.o_activity_view',
|
||||
// edit action
|
||||
trigger: '.o_web_studio_menu .o_menu_sections li a:contains("Views")',
|
||||
timeout: 20000, // activating a view takes a while and sometimes reaches the default 10s timeout
|
||||
}, {
|
||||
// add a graph view
|
||||
trigger: '.o_web_studio_view_category .o_web_studio_view_type.o_web_studio_inactive[data-type="graph"] .o_web_studio_thumbnail',
|
||||
}, {
|
||||
extra_trigger: '.o_graph_renderer',
|
||||
trigger: '.o_web_studio_menu .o_menu_sections li a:contains("Views")',
|
||||
}, {
|
||||
extra_trigger: '.o_web_studio_views',
|
||||
// edit the search view
|
||||
trigger: '.o_web_studio_view_category .o_web_studio_view_type[data-type="search"] .o_web_studio_thumbnail',
|
||||
}, {
|
||||
extra_trigger: '.o_web_studio_search_view_editor',
|
||||
trigger: '.o_menu_toggle:not(.o_menu_toggle_back)',
|
||||
}, {
|
||||
trigger: '.o_web_studio_home_studio_menu .dropdown-toggle',
|
||||
}, {
|
||||
// export all modifications
|
||||
trigger: '.o_web_studio_export',
|
||||
}, {
|
||||
// click on the created app
|
||||
trigger: '.o_app[data-menu-xmlid*="studio"]:last',
|
||||
}, {
|
||||
// switch to form view
|
||||
trigger: '.o_web_studio_views_icons > a[title="Form"]',
|
||||
}, {
|
||||
extra_trigger: '.o_web_studio_form_view_editor',
|
||||
// click on the view tab
|
||||
trigger: '.o_web_studio_view',
|
||||
}, {
|
||||
// click on the restore default view button
|
||||
trigger: '.o_web_studio_restore',
|
||||
}, {
|
||||
// click on the ok button
|
||||
trigger: '.modal-footer .btn.btn-primary',
|
||||
}, {
|
||||
// checks that the field doesn't exist anymore
|
||||
extra_trigger: 'label.o_form_label:not(:contains("COUCOU"))',
|
||||
trigger: '.o_web_studio_leave > a.btn'
|
||||
}]);
|
||||
|
||||
tour.register('web_studio_hide_fields_tour', {
|
||||
url: "/web#action=studio&mode=home_menu",
|
||||
test: true,
|
||||
}, [{
|
||||
trigger: '.o_web_studio_new_app',
|
||||
}, {
|
||||
trigger: '.o_web_studio_app_creator_next',
|
||||
}, {
|
||||
trigger: `
|
||||
.o_web_studio_app_creator_name
|
||||
> input`,
|
||||
run: `text ${randomString(6)}`,
|
||||
}, {
|
||||
// make another interaction to show "next" button
|
||||
trigger: `
|
||||
.o_web_studio_selectors
|
||||
.o_web_studio_selector:eq(2)`,
|
||||
}, {
|
||||
trigger: '.o_web_studio_app_creator_next',
|
||||
}, {
|
||||
trigger: `
|
||||
.o_web_studio_app_creator_menu
|
||||
> input`,
|
||||
run: `text ${randomString(6)}`,
|
||||
}, {
|
||||
trigger: '.o_web_studio_app_creator_next',
|
||||
}, {
|
||||
trigger: '.o_web_studio_model_configurator_next',
|
||||
}, {
|
||||
// check that the Studio menu is still there
|
||||
extra_trigger: '.o_web_studio_menu',
|
||||
trigger: '.o_web_studio_leave > a.btn',
|
||||
timeout: 60000, /* previous step reloads registry, etc. - could take a long time */
|
||||
}, {
|
||||
trigger: '.oe_title input',
|
||||
run: 'text Test',
|
||||
}, {
|
||||
trigger: '.o_form_button_save',
|
||||
}, {
|
||||
trigger: '.o_web_studio_navbar_item',
|
||||
}, {
|
||||
extra_trigger: '.o_web_studio_menu',
|
||||
trigger: `
|
||||
.o_web_studio_views_icons
|
||||
> a[title="List"]`,
|
||||
}, {
|
||||
// wait for the list editor to be rendered because the sidebar is the same
|
||||
extra_trigger: '.o_web_studio_list_view_editor',
|
||||
trigger: '.o_web_studio_existing_fields_icon',
|
||||
}, {
|
||||
trigger: `
|
||||
.o_web_studio_sidebar
|
||||
.o_web_studio_existing_fields
|
||||
.o_web_studio_component:has(.o_web_studio_component_description:contains(display_name))`,
|
||||
run: 'drag_and_drop .o_web_studio_list_view_editor .o_web_studio_hook',
|
||||
}, {
|
||||
trigger: `
|
||||
.o_list_table
|
||||
th[data-name="display_name"]`,
|
||||
}, {
|
||||
trigger: `
|
||||
.o_web_studio_sidebar
|
||||
select[name="optional"]`,
|
||||
run: "text Hide by default",
|
||||
}, {
|
||||
extra_trigger: '.o_list_table:not(:has(th[data-name="display_name"]))',
|
||||
trigger: `
|
||||
.o_web_studio_sidebar_header
|
||||
.o_web_studio_view`,
|
||||
}, {
|
||||
trigger: `
|
||||
.o_web_studio_sidebar_checkbox
|
||||
input#show_invisible`,
|
||||
}, {
|
||||
extra_trigger: `
|
||||
.o_list_table
|
||||
th[data-name="display_name"].o_web_studio_show_invisible`,
|
||||
trigger: '.o_web_studio_leave > a.btn',
|
||||
}]);
|
||||
|
||||
tour.register('web_studio_model_option_value_tour', {
|
||||
url: "/web?debug=tests#action=studio&mode=home_menu",
|
||||
test: true,
|
||||
}, [{
|
||||
trigger: '.o_web_studio_new_app',
|
||||
}, {
|
||||
trigger: '.o_web_studio_app_creator_next',
|
||||
}, {
|
||||
trigger: `
|
||||
.o_web_studio_app_creator_name
|
||||
> input`,
|
||||
run: `text ${randomString(6)}`,
|
||||
}, {
|
||||
trigger: `
|
||||
.o_web_studio_selectors
|
||||
.o_web_studio_selector:eq(2)`,
|
||||
}, {
|
||||
trigger: '.o_web_studio_app_creator_next',
|
||||
}, {
|
||||
trigger: `
|
||||
.o_web_studio_app_creator_menu
|
||||
> input`,
|
||||
run: `text ${randomString(6)}`,
|
||||
}, {
|
||||
trigger: '.o_web_studio_app_creator_next',
|
||||
}, {
|
||||
// check monetary value in model configurator
|
||||
trigger: 'input[name="use_value"]',
|
||||
}, {
|
||||
// check lines value in model configurator
|
||||
trigger: 'input[name="lines"]',
|
||||
}, {
|
||||
trigger: '.o_web_studio_model_configurator_next',
|
||||
}, {
|
||||
trigger: '.o_web_studio_menu .o_web_studio_views_icons > a[title="Graph"]',
|
||||
timeout: 60000, /* previous step reloads registry, etc. - could take a long time */
|
||||
}, {
|
||||
// wait for the graph editor to be rendered and also check for sample data
|
||||
extra_trigger: '.o_view_sample_data .o_graph_renderer',
|
||||
trigger: '.o_web_studio_menu .o_web_studio_views_icons a[title="Pivot"]',
|
||||
}, {
|
||||
// wait for the pivot editor to be rendered and also check for sample data
|
||||
extra_trigger: '.o_pivot_view .o_view_sample_data .o_view_nocontent_empty_folder',
|
||||
trigger: '.o_web_studio_leave > a.btn',
|
||||
}]);
|
||||
|
||||
tour.register('web_studio_new_report_tour', {
|
||||
url: "/web",
|
||||
test: true,
|
||||
}, [{
|
||||
// open studio
|
||||
trigger: '.o_main_navbar .o_web_studio_navbar_item',
|
||||
}, {
|
||||
// click on the created app
|
||||
trigger: '.o_app[data-menu-xmlid*="studio"]:first',
|
||||
extra_trigger: 'body.o_in_studio',
|
||||
}, {
|
||||
// edit reports
|
||||
trigger: '.o_web_studio_menu li a:contains(Reports)',
|
||||
}, {
|
||||
// create a new report
|
||||
trigger: '.o_control_panel .o-kanban-button-new',
|
||||
}, {
|
||||
// select external layout
|
||||
trigger: '.o_web_studio_report_layout_dialog div[data-layout="web.external_layout"]',
|
||||
}, {
|
||||
// sidebar should display add tab
|
||||
extra_trigger: '.o_web_studio_report_editor_manager .o_web_studio_sidebar_header div.active[name="new"]',
|
||||
// switch to 'Report' tab
|
||||
trigger: '.o_web_studio_report_editor_manager .o_web_studio_sidebar_header div[name="report"]',
|
||||
}, {
|
||||
// edit report name
|
||||
trigger: '.o_web_studio_sidebar input[name="name"]',
|
||||
run: 'text My Awesome Report',
|
||||
}, {
|
||||
// switch to 'Add' in Sidebar
|
||||
trigger: '.o_web_studio_sidebar div[name="new"]',
|
||||
}, {
|
||||
// wait for the iframe to be loaded
|
||||
extra_trigger: '.o_web_studio_report_editor iframe #wrapwrap',
|
||||
// add a 'title' building block
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_component:contains(Title Block)',
|
||||
run: 'drag_and_drop .o_web_studio_report_editor iframe .article > .page',
|
||||
auto: true,
|
||||
}, {
|
||||
// click on the newly added field
|
||||
trigger: '.o_web_studio_report_editor iframe .h2 > span:contains(New Title)',
|
||||
}, {
|
||||
// change the text of the H2 to 'test'
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_text .note-editable',
|
||||
run: function () {
|
||||
this.$anchor.focusIn();
|
||||
this.$anchor[0].firstChild.textContent = 'Test';
|
||||
this.$anchor.keydown();
|
||||
this.$anchor.blur();
|
||||
}
|
||||
}, {
|
||||
// click outside to blur the field
|
||||
trigger: '.o_web_studio_report_editor',
|
||||
extra_trigger: '.o_web_studio_sidebar .o_web_studio_text .note-editable:contains(Test)',
|
||||
}, {
|
||||
extra_trigger: '.o_web_studio_report_editor iframe .h2:contains(Test)',
|
||||
// add a new group on the node
|
||||
trigger: '.o_web_studio_sidebar .o_field_many2manytags[name="groups"] input',
|
||||
run: function () {
|
||||
this.$anchor.click();
|
||||
},
|
||||
}, {
|
||||
trigger: '.ui-autocomplete:visible li:contains(Access Rights)',
|
||||
}, {
|
||||
// wait for the group to appear
|
||||
extra_trigger: '.o_web_studio_sidebar .o_field_many2manytags[name="groups"] .o_badge_text:contains(Access Rights)',
|
||||
// switch to 'Add' in Sidebar
|
||||
trigger: '.o_web_studio_sidebar div[name="new"]',
|
||||
}, {
|
||||
// add a 'title' building block Data Table
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_component:contains(Data table)',
|
||||
run: 'drag_and_drop .o_web_studio_report_editor iframe .article > .page',
|
||||
}, {
|
||||
// expand the model selector in the popup
|
||||
trigger: 'div.o_field_selector_value',
|
||||
run: function () {
|
||||
$('div.o_field_selector_value').focusin();
|
||||
}
|
||||
}, {
|
||||
// select the second element of the model (followers)
|
||||
trigger: '.o_field_selector_popover_body > ul > li:contains(Followers)'
|
||||
}, {
|
||||
trigger:'.modal-content button>span:contains(Confirm)', // button
|
||||
extra_trigger:'.o_field_selector_chain_part:contains(Followers)',//content of the field is set
|
||||
}, {
|
||||
// select the content of the first field of the newly added table
|
||||
trigger: '.o_web_studio_report_editor iframe span[t-field="table_line.display_name"]'
|
||||
}, {
|
||||
// change the bound field
|
||||
trigger: '.o_web_studio_sidebar .card:last() div.o_field_selector_value',
|
||||
run: function () {
|
||||
$('.o_web_studio_sidebar .card:last() div.o_field_selector_value').focusin();
|
||||
}
|
||||
}, {
|
||||
trigger: 'ul.o_field_selector_page li:contains(ID)'
|
||||
}, {
|
||||
// update the title of the column
|
||||
extra_trigger: '.o_web_studio_report_editor iframe span[t-field="table_line.id"]',
|
||||
trigger: '.o_web_studio_report_editor iframe table thead span:contains(Name) ', // the name title
|
||||
//extra_trigger: '.o_web_studio_report_editor iframe span[t-field="table_line.display_name"]:not(:contains(YourCompany, Administrator))', // the id has been updated in the iframe
|
||||
}, {
|
||||
// update column title 'name' into another title
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_text .note-editable',
|
||||
run: function () {
|
||||
this.$anchor.focusIn();
|
||||
this.$anchor[0].firstChild.textContent = 'new column title';
|
||||
this.$anchor.keydown();
|
||||
this.$anchor.blur();
|
||||
}
|
||||
}, {
|
||||
// click outside to blur the field
|
||||
trigger: '.o_web_studio_report_editor',
|
||||
extra_trigger: '.o_web_studio_sidebar .o_web_studio_text .note-editable:contains(new column title)',
|
||||
}, {
|
||||
// wait to be sure the modification has been correctly applied
|
||||
extra_trigger: '.o_web_studio_report_editor iframe table thead span:contains(new column title) ',
|
||||
// leave the report
|
||||
trigger: '.o_web_studio_breadcrumb .o_back_button:contains(Reports)',
|
||||
}, {
|
||||
// a invisible element cannot be used as a trigger so this small hack is
|
||||
// mandatory for the next step
|
||||
run: function () {
|
||||
$('.o_kanban_record:contains(My Awesome Report) .dropdown-toggle').css('visibility', 'visible');
|
||||
},
|
||||
trigger: '.o_legacy_kanban_view',
|
||||
}, {
|
||||
// open the dropdown
|
||||
trigger: '.o_kanban_record:contains(My Awesome Report) .dropdown-toggle',
|
||||
}, {
|
||||
// duplicate the report
|
||||
trigger: '.o_kanban_record:contains(My Awesome Report) .dropdown-menu a:contains(Duplicate)',
|
||||
}, {
|
||||
// open the duplicate report
|
||||
trigger: '.o_kanban_record:contains(My Awesome Report copy(1))',
|
||||
}, {
|
||||
// switch to 'Report' tab
|
||||
trigger: '.o_web_studio_report_editor_manager .o_web_studio_sidebar_header div[name="report"]',
|
||||
}, {
|
||||
// wait for the duplicated report to be correctly loaded
|
||||
extra_trigger: '.o_web_studio_sidebar input[name="name"][value="My Awesome Report copy(1)"]',
|
||||
// leave Studio
|
||||
trigger: '.o_web_studio_leave > a.btn',
|
||||
}]);
|
||||
|
||||
tour.register('web_studio_new_report_basic_layout_tour', {
|
||||
url: "/web",
|
||||
test: true,
|
||||
}, [{
|
||||
// open studio
|
||||
trigger: '.o_main_navbar .o_web_studio_navbar_item',
|
||||
}, {
|
||||
// click on the created app
|
||||
trigger: '.o_app[data-menu-xmlid*="studio"]:first',
|
||||
extra_trigger: 'body.o_in_studio',
|
||||
}, {
|
||||
// edit reports
|
||||
trigger: '.o_web_studio_menu li a:contains(Reports)',
|
||||
}, {
|
||||
// create a new report
|
||||
trigger: '.o_control_panel .o-kanban-button-new',
|
||||
}, {
|
||||
// select external layout
|
||||
trigger: '.o_web_studio_report_layout_dialog div[data-layout="web.basic_layout"]',
|
||||
}, {
|
||||
// sidebar should display add tab
|
||||
extra_trigger: '.o_web_studio_report_editor_manager .o_web_studio_sidebar_header div.active[name="new"]',
|
||||
// switch to 'Report' tab
|
||||
trigger: '.o_web_studio_report_editor_manager .o_web_studio_sidebar_header div[name="report"]',
|
||||
}, {
|
||||
// edit report name
|
||||
trigger: '.o_web_studio_sidebar input[name="name"]',
|
||||
run: 'text My Awesome basic layout Report',
|
||||
}, {
|
||||
// switch to 'Add' in Sidebar
|
||||
trigger: '.o_web_studio_sidebar div[name="new"]',
|
||||
}, {
|
||||
// wait for the iframe to be loaded
|
||||
extra_trigger: '.o_web_studio_report_editor iframe #wrapwrap',
|
||||
// add a 'title' building block
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_component:contains(Title Block)',
|
||||
run: 'drag_and_drop .o_web_studio_report_editor iframe .article > .page',
|
||||
auto: true,
|
||||
}, {
|
||||
// click on the newly added field
|
||||
trigger: '.o_web_studio_report_editor iframe .h2 > span:contains(New Title)',
|
||||
}, {
|
||||
// change the text of the H2 to 'test'
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_text .note-editable',
|
||||
run: function () {
|
||||
this.$anchor.focusIn();
|
||||
this.$anchor[0].firstChild.textContent = 'Test';
|
||||
this.$anchor.keydown();
|
||||
this.$anchor.blur();
|
||||
}
|
||||
}, {
|
||||
// click outside to blur the field
|
||||
trigger: '.o_web_studio_report_editor',
|
||||
extra_trigger: '.o_web_studio_sidebar .o_web_studio_text .note-editable:contains(Test)',
|
||||
}, {
|
||||
extra_trigger: '.o_web_studio_report_editor iframe .h2:contains(Test)',
|
||||
// add a new group on the node
|
||||
trigger: '.o_web_studio_sidebar .o_field_many2manytags[name="groups"] input',
|
||||
run: function () {
|
||||
this.$anchor.click();
|
||||
},
|
||||
}, {
|
||||
trigger: '.ui-autocomplete:visible li:contains(Access Rights)',
|
||||
}, {
|
||||
// wait for the group to appear
|
||||
extra_trigger: '.o_web_studio_sidebar .o_field_many2manytags[name="groups"] .o_badge_text:contains(Access Rights)',
|
||||
// switch to 'Add' in Sidebar
|
||||
trigger: '.o_web_studio_sidebar div[name="new"]',
|
||||
}, {
|
||||
// add a 'title' building block Data Table
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_component:contains(Data table)',
|
||||
run: 'drag_and_drop .o_web_studio_report_editor iframe .article > .page',
|
||||
}, {
|
||||
// expand the model selector in the popup
|
||||
trigger: 'div.o_field_selector_value',
|
||||
run: function () {
|
||||
$('div.o_field_selector_value').focusin();
|
||||
}
|
||||
}, {
|
||||
// select the second element of the model (followers)
|
||||
trigger: '.o_field_selector_popover_body > ul > li:contains(Followers)'
|
||||
}, {
|
||||
trigger:'.modal-content button>span:contains(Confirm)', // button
|
||||
extra_trigger:'.o_field_selector_chain_part:contains(Followers)', //content of the field is set
|
||||
}, {
|
||||
// select the content of the first field of the newly added table
|
||||
trigger: '.o_web_studio_report_editor iframe span[t-field="table_line.display_name"]'
|
||||
}, {
|
||||
// change the bound field
|
||||
trigger: '.o_web_studio_sidebar .card:last() div.o_field_selector_value',
|
||||
run: function () {
|
||||
$('.o_web_studio_sidebar .card:last() div.o_field_selector_value').focusin();
|
||||
}
|
||||
}, {
|
||||
trigger: 'ul.o_field_selector_page li:contains(ID)'
|
||||
}, {
|
||||
// update the title of the column
|
||||
trigger: '.o_web_studio_report_editor iframe table thead span:contains(Name) ', // the name title
|
||||
//extra_trigger: '.o_web_studio_report_editor iframe span[t-field="table_line.display_name"]:not(:contains(YourCompany, Administrator))', // the id has been updated in the iframe
|
||||
}, {
|
||||
// update column title 'name' into another title
|
||||
trigger: '.o_web_studio_sidebar .o_web_studio_text .note-editable',
|
||||
run: function () {
|
||||
this.$anchor.focusIn();
|
||||
this.$anchor[0].firstChild.textContent = 'new column title';
|
||||
this.$anchor.keydown();
|
||||
this.$anchor.blur();
|
||||
}
|
||||
}, {
|
||||
// click outside to blur the field
|
||||
trigger: '.o_web_studio_report_editor',
|
||||
extra_trigger: '.o_web_studio_sidebar .o_web_studio_text .note-editable:contains(new column title)',
|
||||
}, {
|
||||
// wait to be sure the modification has been correctly applied
|
||||
extra_trigger: '.o_web_studio_report_editor iframe table thead span:contains(new column title) ',
|
||||
// leave the report
|
||||
trigger: '.o_web_studio_breadcrumb .o_back_button:contains(Reports)',
|
||||
}, {
|
||||
// a invisible element cannot be used as a trigger so this small hack is
|
||||
// mandatory for the next step
|
||||
run: function () {
|
||||
$('.o_kanban_record:contains(My Awesome basic layout Report) .dropdown-toggle').css('visibility', 'visible');
|
||||
},
|
||||
trigger: '.o_legacy_kanban_view',
|
||||
}, {
|
||||
// open the dropdown
|
||||
trigger: '.o_kanban_record:contains(My Awesome basic layout Report) .dropdown-toggle',
|
||||
}, {
|
||||
// duplicate the report
|
||||
trigger: '.o_kanban_record:contains(My Awesome basic layout Report) .dropdown-menu a:contains(Duplicate)',
|
||||
}, {
|
||||
// open the duplicate report
|
||||
trigger: '.o_kanban_record:contains(My Awesome basic layout Report copy(1))',
|
||||
}, {
|
||||
// switch to 'Report' tab
|
||||
trigger: '.o_web_studio_report_editor_manager .o_web_studio_sidebar_header div[name="report"]',
|
||||
}, {
|
||||
// wait for the duplicated report to be correctly loaded
|
||||
extra_trigger: '.o_web_studio_sidebar input[name="name"][value="My Awesome basic layout Report copy(1)"]',
|
||||
// leave Studio
|
||||
trigger: '.o_web_studio_leave > a.btn',
|
||||
}]);
|
||||
|
||||
tour.register('web_studio_approval_tour', {
|
||||
url: "/web",
|
||||
test: true,
|
||||
}, [{
|
||||
// go to Apps menu
|
||||
trigger: '.o_app[data-menu-xmlid="base.menu_management"]',
|
||||
}, {
|
||||
// open studio
|
||||
trigger: '.o_main_navbar .o_web_studio_navbar_item',
|
||||
extra_trigger: '.o_cp_switch_buttons',
|
||||
}, {
|
||||
// switch to form view editor
|
||||
trigger: '.o_web_studio_views_icons > a[title="Form"]',
|
||||
}, {
|
||||
// click on first button it finds that has a node id
|
||||
trigger: '.o_web_studio_form_view_editor button.o-web-studio-editor--element-clickable',
|
||||
}, {
|
||||
// enable approvals for the button
|
||||
trigger: '.o_web_studio_sidebar label[for="studio_approval"]',
|
||||
}, {
|
||||
// set approval message
|
||||
trigger: '.o_web_studio_sidebar_approval input[name*="approval_message"]',
|
||||
run: 'text nope',
|
||||
}, {
|
||||
// add approval rule
|
||||
trigger: '.o_web_studio_sidebar_approval .o_approval_new',
|
||||
extra_trigger: '.o_web_studio_snackbar .fa-check',
|
||||
}, {
|
||||
// set domain on first rule
|
||||
trigger: '.o_web_studio_sidebar_approval .o_approval_domain',
|
||||
extra_trigger: '.o_studio_sidebar_approval_rule:eq(1)',
|
||||
}, {
|
||||
// set stupid domain that is always truthy
|
||||
trigger: '.o_domain_debug_container textarea',
|
||||
run: function () {
|
||||
this.$anchor.focusIn();
|
||||
this.$anchor.val('[["id", "!=", False]]');
|
||||
this.$anchor.change();
|
||||
}
|
||||
}, {
|
||||
// save domain and close modal
|
||||
trigger:' .modal-footer .btn-primary',
|
||||
}, {
|
||||
// add second approval rule when the first is set
|
||||
trigger: '.o_web_studio_sidebar_approval .o_approval_new',
|
||||
extra_trigger: '.o_web_studio_snackbar .fa-check',
|
||||
}, {
|
||||
// enable 'force different users' for one rule (doesn't matter which)
|
||||
trigger: '.o_web_studio_sidebar label[for*="exclusive_user"]',
|
||||
extra_trigger: '.o_web_studio_snackbar .fa-check',
|
||||
}, {
|
||||
// leave studio
|
||||
trigger: '.o_web_studio_leave > a.btn',
|
||||
extra_trigger: '.o_web_studio_snackbar .fa-check',
|
||||
}, {
|
||||
// go back to kanban
|
||||
trigger: '.o_menu_brand',
|
||||
extra_trigger: '.o_web_client:not(.o_in_studio)'
|
||||
}, {
|
||||
// open first record (should be the one that was used, so the button should be there)
|
||||
trigger: '.o_kanban_view .o_kanban_record .o_dropdown_kanban .dropdown-toggle',
|
||||
}, {
|
||||
trigger: '.o_kanban_view .o_kanban_record .o-dropdown--menu .dropdown-item',
|
||||
},{
|
||||
// try to do the action
|
||||
trigger: 'button[studio_approval]',
|
||||
}, {
|
||||
// there should be a warning
|
||||
trigger: '.o_notification.border-warning'
|
||||
}, {
|
||||
trigger: '.breadcrumb .o_back_button'
|
||||
}]);
|
||||
|
||||
tour.register('web_studio_custom_field_tour', {
|
||||
url: "/web",
|
||||
test: true,
|
||||
}, [{
|
||||
// go to Apps menu
|
||||
trigger: '.o_app[data-menu-xmlid="base.menu_management"]',
|
||||
}, {
|
||||
// click on the list view
|
||||
trigger: '.o_switch_view.o_list',
|
||||
}, {
|
||||
// click on optional column dropdown
|
||||
trigger: '.o_optional_columns_dropdown_toggle'
|
||||
}, {
|
||||
// click on add custom field
|
||||
trigger: '.dropdown-item-studio'
|
||||
}, {
|
||||
// go to home menu
|
||||
trigger: '.o_menu_toggle',
|
||||
extra_trigger: '.o_web_client.o_in_studio'
|
||||
}, {
|
||||
//leave studio
|
||||
trigger: '.o_web_studio_leave > a.btn'
|
||||
}, {
|
||||
// studio left.
|
||||
trigger: '.o_app[data-menu-xmlid="base.menu_management"]',
|
||||
extra_trigger: '.o_web_client:not(.o_in_studio)',
|
||||
}]);
|
||||
|
||||
tour.register('web_studio_local_storage_tour', {
|
||||
url: "/web",
|
||||
test: true,
|
||||
}, [{
|
||||
trigger: '.o_app[data-menu-xmlid="base.menu_management"]',
|
||||
run: function () {
|
||||
localStorage.setItem('openStudioOnReload', "main");
|
||||
window.location.reload();
|
||||
},
|
||||
}, {
|
||||
// should be directly in studio mode
|
||||
trigger: '.o_app[data-menu-xmlid="base.menu_management"]',
|
||||
extra_trigger: '.o_web_client.o_in_studio'
|
||||
}, {
|
||||
trigger: '.o_menu_toggle',
|
||||
}, {
|
||||
trigger: '.o_web_studio_leave > a.btn',
|
||||
}, {
|
||||
// studio left.
|
||||
trigger: '.o_app[data-menu-xmlid="base.menu_management"]',
|
||||
extra_trigger: '.o_web_client:not(.o_in_studio)',
|
||||
run: function () {
|
||||
window.location.reload();
|
||||
},
|
||||
}, {
|
||||
// studio left after refresh.
|
||||
trigger: '.o_app[data-menu-xmlid="base.menu_management"]',
|
||||
extra_trigger: '.o_web_client:not(.o_in_studio)'
|
||||
}]);
|
||||
|
||||
});
|
||||
271
web_studio/static/tests/legacy/views/form_tests.js
Normal file
271
web_studio/static/tests/legacy/views/form_tests.js
Normal file
@@ -0,0 +1,271 @@
|
||||
odoo.define('web_studio.form_tests', function (require) {
|
||||
'use strict';
|
||||
|
||||
const FormView = require('web.FormView');
|
||||
const testUtils = require('web.test_utils');
|
||||
require('web_studio.FormRenderer');
|
||||
require('web_studio.FormController');
|
||||
const { legacyExtraNextTick } = require("@web/../tests/helpers/utils");
|
||||
|
||||
const createView = testUtils.createView;
|
||||
|
||||
QUnit.module(
|
||||
'Studio',
|
||||
{
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: 'Displayed name', type: 'char' },
|
||||
int_field: { string: 'int_field', type: 'integer', sortable: true },
|
||||
bar: { string: 'Bar', type: 'boolean' },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: 'first record',
|
||||
int_field: 42,
|
||||
bar: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: 'second record',
|
||||
int_field: 27,
|
||||
bar: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
function () {
|
||||
QUnit.module('Form Approvals');
|
||||
|
||||
QUnit.test('approval widget basic rendering', async function (assert) {
|
||||
assert.expect(14);
|
||||
|
||||
const form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
debug: true, // need to be in the viewport because of popover
|
||||
arch: `<form string="Partners">
|
||||
<sheet>
|
||||
<header>
|
||||
<button type="object=" name="someMethod" string="Apply Method" studio_approval="True"/>
|
||||
</header>
|
||||
<div name="button_box">
|
||||
<button class="oe_stat_button" studio_approval="True" id="visibleStat">
|
||||
<field name="int_field"/>
|
||||
</button>
|
||||
<button class="oe_stat_button" studio_approval="True"
|
||||
attrs='{"invisible": [["bar", "=", true]]}' id="invisibleStat">
|
||||
<field name="bar"/>
|
||||
</button>
|
||||
</div>
|
||||
<group>
|
||||
<group style="background-color: red">
|
||||
<field name="display_name" studio_approval="True"/>
|
||||
<field name="bar"/>
|
||||
<field name="int_field"/>
|
||||
</group>
|
||||
</group>
|
||||
<button type="object=" name="anotherMethod"
|
||||
string="Apply Second Method" studio_approval="True"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
res_id: 2,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'get_approval_spec') {
|
||||
assert.step('fetch_approval_spec');
|
||||
return Promise.resolve({
|
||||
rules: [
|
||||
{
|
||||
id: 1,
|
||||
group_id: [1, 'Internal User'],
|
||||
domain: false,
|
||||
can_validate: true,
|
||||
message: false,
|
||||
exclusive_user: false,
|
||||
},
|
||||
],
|
||||
entries: [],
|
||||
groups: [[1, 'Internal User']],
|
||||
});
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
session: { uid: 42 },
|
||||
});
|
||||
|
||||
await legacyExtraNextTick(); // wait for the approval button (owl) to render
|
||||
|
||||
// check that the widget was inserted on visible buttons only
|
||||
assert.containsOnce(form, 'button[name="someMethod"] .o_web_studio_approval');
|
||||
assert.containsOnce(form, '#visibleStat .o_web_studio_approval');
|
||||
assert.containsNone(form, '#invisibleStat .o_web_studio_approval');
|
||||
assert.containsOnce(form, 'button[name="anotherMethod"] .o_web_studio_approval');
|
||||
assert.containsNone(form, '.o_group .o_web_studio_approval');
|
||||
// should have fetched spec for exactly 3 buttons
|
||||
assert.verifySteps(['fetch_approval_spec', 'fetch_approval_spec', 'fetch_approval_spec']);
|
||||
// display popover
|
||||
await testUtils.dom.click('button[name="someMethod"] .o_web_studio_approval');
|
||||
assert.containsOnce($(document), '.o_popover');
|
||||
const popover = $(document).find('.o_popover');
|
||||
assert.containsOnce(popover, '.o_web_studio_approval_no_entry');
|
||||
assert.containsOnce(popover, '.o_web_approval_approve');
|
||||
assert.containsOnce(popover, '.o_web_approval_reject');
|
||||
assert.containsNone(popover, '.o_web_approval_cancel');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('approval check', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: `<form string="Partners">
|
||||
<sheet>
|
||||
<header>
|
||||
<button type="object" id="mainButton" name="someMethod"
|
||||
string="Apply Method" studio_approval="True"/>
|
||||
</header>
|
||||
<group>
|
||||
<group style="background-color: red">
|
||||
<field name="display_name"/>
|
||||
<field name="bar"/>
|
||||
<field name="int_field"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
res_id: 2,
|
||||
mockRPC: function (route, args) {
|
||||
const rule = {
|
||||
id: 1,
|
||||
group_id: [1, 'Internal User'],
|
||||
domain: false,
|
||||
can_validate: true,
|
||||
message: false,
|
||||
exclusive_user: false,
|
||||
};
|
||||
if (args.method === 'get_approval_spec') {
|
||||
assert.step('fetch_approval_spec');
|
||||
return Promise.resolve({
|
||||
rules: [rule],
|
||||
entries: [],
|
||||
groups: [[1, 'Internal User']],
|
||||
});
|
||||
} else if (args.method === 'check_approval') {
|
||||
assert.step('attempt_action');
|
||||
return Promise.resolve({
|
||||
approved: false,
|
||||
rules: [rule],
|
||||
entries: [],
|
||||
});
|
||||
} else if (args.method === 'someMethod') {
|
||||
/* the action of the button should not be
|
||||
called, as the approval is refused! if this
|
||||
code is traversed, the test *must* fail!
|
||||
that's why it's not included in the expected count
|
||||
or in the verifySteps call */
|
||||
assert.step('should_not_happen!');
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.dom.click('#mainButton');
|
||||
// first render, handle click, rerender after click
|
||||
assert.verifySteps(['fetch_approval_spec', 'attempt_action', 'fetch_approval_spec']);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('approval widget basic flow', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
let hasValidatedRule;
|
||||
|
||||
const form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
debug: true, // need to be in the viewport because of popover
|
||||
arch: `<form string="Partners">
|
||||
<sheet>
|
||||
<header>
|
||||
<button type="object=" name="someMethod" string="Apply Method" studio_approval="True"/>
|
||||
</header>
|
||||
<group>
|
||||
<group style="background-color: red">
|
||||
<field name="display_name"/>
|
||||
<field name="bar"/>
|
||||
<field name="int_field"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
res_id: 2,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'get_approval_spec') {
|
||||
const spec = {
|
||||
rules: [
|
||||
{
|
||||
id: 1,
|
||||
group_id: [1, 'Internal User'],
|
||||
domain: false,
|
||||
can_validate: true,
|
||||
message: false,
|
||||
exclusive_user: false,
|
||||
},
|
||||
],
|
||||
entries: [],
|
||||
groups: [[1, 'Internal User']],
|
||||
};
|
||||
if (hasValidatedRule !== undefined) {
|
||||
spec.entries = [
|
||||
{
|
||||
id: 1,
|
||||
approved: hasValidatedRule,
|
||||
user_id: [42, 'Some rando'],
|
||||
write_date: '2020-04-07 12:43:48',
|
||||
rule_id: [1, 'someMethod/partner (Internal User)'],
|
||||
model: 'partner',
|
||||
res_id: 2,
|
||||
},
|
||||
];
|
||||
}
|
||||
return Promise.resolve(spec);
|
||||
} else if (args.method === 'set_approval') {
|
||||
hasValidatedRule = args.kwargs.approved;
|
||||
assert.step(hasValidatedRule ? 'approve_rule' : 'reject_rule');
|
||||
return Promise.resolve(true);
|
||||
} else if (args.method === 'delete_approval') {
|
||||
hasValidatedRule = undefined;
|
||||
assert.step('delete_approval');
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
session: { uid: 42 },
|
||||
});
|
||||
|
||||
await legacyExtraNextTick(); // wait for the approval button (owl) to render
|
||||
|
||||
// display popover and validate a rule, then cancel, then reject
|
||||
await testUtils.dom.click('button[name="someMethod"] .o_web_studio_approval');
|
||||
assert.containsOnce($(document), '.o_popover');
|
||||
await testUtils.dom.click('.o_popover button.o_web_approval_approve');
|
||||
await testUtils.dom.click('.o_popover button.o_web_approval_cancel');
|
||||
await testUtils.dom.click('.o_popover button.o_web_approval_reject');
|
||||
assert.verifySteps(['approve_rule', 'delete_approval', 'reject_rule']);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
6328
web_studio/static/tests/legacy/views/view_editor_manager_tests.js
Normal file
6328
web_studio/static/tests/legacy/views/view_editor_manager_tests.js
Normal file
File diff suppressed because it is too large
Load Diff
97
web_studio/static/tests/mock_server.js
Normal file
97
web_studio/static/tests/mock_server.js
Normal file
@@ -0,0 +1,97 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { MockServer } from "@web/../tests/helpers/mock_server";
|
||||
|
||||
patch(MockServer.prototype, "web_studio.MockServer", {
|
||||
performRPC(route, args) {
|
||||
if (route === "/web_studio/activity_allowed") {
|
||||
return Promise.resolve(this.mockActivityAllowed());
|
||||
}
|
||||
if (route === "/web_studio/get_studio_view_arch") {
|
||||
return Promise.resolve(this.mockGetStudioViewArch());
|
||||
}
|
||||
if (route === "/web_studio/chatter_allowed") {
|
||||
return Promise.resolve(this.mockChatterAllowed());
|
||||
}
|
||||
if (route === "/web_studio/get_default_value") {
|
||||
return Promise.resolve(this.mockGetDefaultValue());
|
||||
}
|
||||
if (route === "/web_studio/get_studio_action") {
|
||||
return Promise.resolve(this.mockGetStudioAction(args));
|
||||
}
|
||||
if (route === "/web_studio/edit_view") {
|
||||
return Promise.resolve(this.mockEditView(args));
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
|
||||
mockActivityAllowed() {
|
||||
return false;
|
||||
},
|
||||
|
||||
mockChatterAllowed() {
|
||||
return false;
|
||||
},
|
||||
|
||||
mockGetStudioViewArch() {
|
||||
return {
|
||||
studio_view_id: false,
|
||||
studio_view_arch: "<data/>",
|
||||
};
|
||||
},
|
||||
|
||||
mockGetDefaultValue() {
|
||||
return {};
|
||||
},
|
||||
|
||||
mockGetStudioAction(args) {
|
||||
if (args.action_name === "reports") {
|
||||
return {
|
||||
name: "Reports",
|
||||
res_model: "ir.actions.report",
|
||||
target: "current",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "kanban"]],
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
mockEditView(args) {
|
||||
const viewId = args.view_id;
|
||||
if (!viewId) {
|
||||
throw new Error(
|
||||
"To use the 'edit_view' mocked controller, you should specify a unique id on the view you are editing"
|
||||
);
|
||||
}
|
||||
const uniqueViewKey = Object.keys(this.archs)
|
||||
.map((k) => k.split(","))
|
||||
.filter(([model, vid, vtype]) => vid === `${viewId}`);
|
||||
|
||||
if (!uniqueViewKey.length) {
|
||||
throw new Error(`No view with id "${viewId}" in edit_view`);
|
||||
}
|
||||
if (uniqueViewKey.length > 1) {
|
||||
throw new Error(
|
||||
`There are multiple views with id "${viewId}", and probably for different models.`
|
||||
);
|
||||
}
|
||||
const [modelName, , viewType] = uniqueViewKey[0];
|
||||
|
||||
const view = this.getView(modelName, [viewId, viewType], {
|
||||
context: args.context,
|
||||
options: {},
|
||||
});
|
||||
const models = {};
|
||||
for (const modelName of Object.keys(view.models)) {
|
||||
models[modelName] = this.mockFieldsGet(modelName);
|
||||
}
|
||||
return {
|
||||
views: {
|
||||
[viewType]: view,
|
||||
},
|
||||
models,
|
||||
studio_view_id: false,
|
||||
};
|
||||
},
|
||||
});
|
||||
31
web_studio/static/tests/model_definitions_setup.js
Normal file
31
web_studio/static/tests/model_definitions_setup.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { addFakeModel, addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers';
|
||||
|
||||
addModelNamesToFetch(['res.groups', 'ir.ui.view']);
|
||||
addFakeModel('product', {
|
||||
partner_ids: { string: "Attachments", type: "one2many", relation: "partner" },
|
||||
coucou_id: { string: "coucou", type: "many2one", relation: "coucou" },
|
||||
display_name: { string: "Display Name", type: "char" },
|
||||
m2m: { string: "M2M", type: "many2many", relation: "product" },
|
||||
m2o: { string: "M2O", type: "many2one", relation: 'partner' },
|
||||
related: { type: "char", related: "partner.display_name", string: "myRelatedField" },
|
||||
sign: { string: "Signature", type: "binary" },
|
||||
toughness: { manual: true, string: "toughness", type: 'selection', selection: [['0', "Hard"], ['1', "Harder"]] },
|
||||
});
|
||||
addFakeModel('coucou', {
|
||||
display_name: { string: "Display Name", type: "char" },
|
||||
char_field: { string: "A char", type: "char" },
|
||||
croissant: { string: "Croissant", type: "integer" },
|
||||
m2o: { string: "M2O", type: "many2one", relation: 'product' },
|
||||
message_attachment_count: { string: 'Attachment count', type: 'integer' },
|
||||
priority: { string: "Priority", type: "selection", manual: true, selection: [['1', "Low"], ['2', "Medium"], ['3', "High"]] },
|
||||
product_ids: { string: "Products", type: "one2many", relation: "product" },
|
||||
start: { string: "Start Date", type: 'datetime' },
|
||||
stop: { string: "Stop Date", type: 'datetime' },
|
||||
});
|
||||
addFakeModel('partner', {
|
||||
display_name: { string: "Display Name", type: "char" },
|
||||
image: { string: "Image", type: "binary" },
|
||||
displayed_image_id: { string: "cover", type: "many2one", relation: "ir.attachment" },
|
||||
});
|
||||
390
web_studio/static/tests/navbar_tests.js
Normal file
390
web_studio/static/tests/navbar_tests.js
Normal file
@@ -0,0 +1,390 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { StudioNavbar } from "@web_studio/client_action/navbar/navbar";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
mount,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
legacyExtraNextTick,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { menuService } from "@web/webclient/menus/menu_service";
|
||||
import { actionService } from "@web/webclient/actions/action_service";
|
||||
import { makeFakeDialogService } from "@web/../tests/helpers/mock_services";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { registerStudioDependencies, openStudio, leaveStudio } from "./helpers";
|
||||
import { createEnterpriseWebClient } from "@web_enterprise/../tests/helpers";
|
||||
import { getActionManagerServerData, loadState } from "@web/../tests/webclient/helpers";
|
||||
import { companyService } from "@web/webclient/company_service";
|
||||
import { viewService } from "@web/views/view_service";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
let target;
|
||||
|
||||
QUnit.module("Studio > Navbar", (hooks) => {
|
||||
let baseConfig;
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
registerStudioDependencies();
|
||||
serviceRegistry.add("action", actionService).add("view", viewService); // #action-serv-leg-compat-js-class
|
||||
serviceRegistry.add("dialog", makeFakeDialogService());
|
||||
serviceRegistry.add("menu", menuService);
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (handler, delay, ...args) => handler(...args),
|
||||
clearTimeout: () => {},
|
||||
});
|
||||
const menus = {
|
||||
root: { id: "root", children: [1], name: "root", appID: "root" },
|
||||
1: { id: 1, children: [10, 11, 12], name: "App0", appID: 1 },
|
||||
10: { id: 10, children: [], name: "Section 10", appID: 1 },
|
||||
11: { id: 11, children: [], name: "Section 11", appID: 1 },
|
||||
12: { id: 12, children: [120, 121, 122], name: "Section 12", appID: 1 },
|
||||
120: { id: 120, children: [], name: "Section 120", appID: 1 },
|
||||
121: { id: 121, children: [], name: "Section 121", appID: 1 },
|
||||
122: { id: 122, children: [], name: "Section 122", appID: 1 },
|
||||
};
|
||||
const serverData = { menus };
|
||||
baseConfig = { serverData };
|
||||
});
|
||||
|
||||
QUnit.test("menu buttons will not be placed under 'more' menu", async (assert) => {
|
||||
assert.expect(12);
|
||||
|
||||
class MyStudioNavbar extends StudioNavbar {
|
||||
async adapt() {
|
||||
const prom = super.adapt();
|
||||
const sectionsCount = this.currentAppSections.length;
|
||||
const hiddenSectionsCount = this.currentAppSectionsExtra.length;
|
||||
assert.step(`adapt -> hide ${hiddenSectionsCount}/${sectionsCount} sections`);
|
||||
return prom;
|
||||
}
|
||||
}
|
||||
|
||||
const env = await makeTestEnv(baseConfig);
|
||||
patchWithCleanup(env.services.studio, {
|
||||
get mode() {
|
||||
// Will force the the navbar in the studio editor state
|
||||
return "editor";
|
||||
},
|
||||
});
|
||||
// Force the parent width, to make this test independent of screen size
|
||||
target.style.width = "100%";
|
||||
|
||||
// Set menu and mount
|
||||
env.services.menu.setCurrentMenu(1);
|
||||
await mount(MyStudioNavbar, target, { env });
|
||||
await nextTick();
|
||||
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_menu_sections > *:not(.o_menu_sections_more):not(.d-none)",
|
||||
3,
|
||||
"should have 3 menu sections displayed (that are not the 'more' menu)"
|
||||
);
|
||||
assert.containsNone(target, ".o_menu_sections_more", "the 'more' menu should not exist");
|
||||
assert.containsN(
|
||||
target,
|
||||
".o-studio--menu > *",
|
||||
2,
|
||||
"should have 2 studio menu elements displayed"
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o-studio--menu > *")].map((el) => el.innerText),
|
||||
["Edit Menu", "New Model"]
|
||||
);
|
||||
|
||||
// Force minimal width and dispatch window resize event
|
||||
target.style.width = "0%";
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
await nextTick();
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_menu_sections > *:not(.d-none)",
|
||||
"only one menu section should be displayed"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_menu_sections_more:not(.d-none)",
|
||||
"the displayed menu section should be the 'more' menu"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o-studio--menu > *",
|
||||
2,
|
||||
"should have 2 studio menu elements displayed"
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o-studio--menu > *")].map((el) => el.innerText),
|
||||
["Edit Menu", "New Model"]
|
||||
);
|
||||
|
||||
// Open the more menu
|
||||
await click(target, ".o_menu_sections_more .dropdown-toggle");
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".dropdown-menu > *")].map((el) => el.textContent),
|
||||
["Section 10", "Section 11", "Section 12", "Section 120", "Section 121", "Section 122"],
|
||||
"'more' menu should contain all hidden sections in correct order"
|
||||
);
|
||||
|
||||
// Check the navbar adaptation calls
|
||||
assert.verifySteps(["adapt -> hide 0/3 sections", "adapt -> hide 3/3 sections"]);
|
||||
});
|
||||
|
||||
QUnit.test("homemenu customizer rendering", async (assert) => {
|
||||
assert.expect(6);
|
||||
|
||||
serviceRegistry.add("company", companyService);
|
||||
|
||||
const fakeHTTPService = {
|
||||
start() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("http", fakeHTTPService);
|
||||
|
||||
const env = await makeTestEnv(baseConfig);
|
||||
|
||||
patchWithCleanup(env.services.studio, {
|
||||
get mode() {
|
||||
// Will force the navbar in the studio home_menu state
|
||||
return "home_menu";
|
||||
},
|
||||
});
|
||||
|
||||
const target = getFixture();
|
||||
|
||||
// Set menu and mount
|
||||
await mount(StudioNavbar, target, { env });
|
||||
await nextTick();
|
||||
|
||||
assert.containsOnce(target, ".o_studio_navbar");
|
||||
assert.containsOnce(target, ".o_web_studio_home_studio_menu");
|
||||
|
||||
await click(target.querySelector(".o_web_studio_home_studio_menu .dropdown-toggle"));
|
||||
|
||||
assert.containsOnce(target, ".o_web_studio_change_background");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_web_studio_change_background input").accept,
|
||||
"image/*",
|
||||
"Input should now only accept images"
|
||||
);
|
||||
|
||||
assert.containsOnce(target, ".o_web_studio_import");
|
||||
assert.containsOnce(target, ".o_web_studio_export");
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("Studio > navbar coordination", (hooks) => {
|
||||
let serverData;
|
||||
let target;
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = getActionManagerServerData();
|
||||
registerStudioDependencies();
|
||||
serviceRegistry.add("company", companyService);
|
||||
});
|
||||
|
||||
QUnit.test("adapt navbar when leaving studio", async (assert) => {
|
||||
assert.expect(8);
|
||||
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (handler, delay, ...args) => handler(...args),
|
||||
clearTimeout: () => {},
|
||||
});
|
||||
target.style.width = "1080px";
|
||||
|
||||
serverData.menus[1].actionID = 1;
|
||||
serverData.actions[1].xml_id = "action_xml_id";
|
||||
|
||||
const webClient = await createEnterpriseWebClient({ serverData });
|
||||
const width = document.body.style.width;
|
||||
document.body.style.width = "1080px";
|
||||
registerCleanup(() => {
|
||||
document.body.style.width = width;
|
||||
});
|
||||
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
await click(target.querySelector(".o_app[data-menu-xmlid=menu_1]"));
|
||||
await legacyExtraNextTick();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_menu_sections .o_menu_sections_more");
|
||||
|
||||
await openStudio(target);
|
||||
await legacyExtraNextTick();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_studio .o_menu_sections");
|
||||
assert.containsNone(target, ".o_studio .o_menu_sections .o_menu_sections_more");
|
||||
|
||||
Object.assign(serverData.menus, {
|
||||
10: {
|
||||
id: 10,
|
||||
children: [],
|
||||
name: "The chain",
|
||||
appID: 1,
|
||||
actionID: 1001,
|
||||
xmlid: "menu_1",
|
||||
},
|
||||
11: {
|
||||
id: 11,
|
||||
children: [111],
|
||||
name: "Running in the shadows, damn your love, damn your lies",
|
||||
appID: 1,
|
||||
actionID: 1001,
|
||||
xmlid: "menu_1",
|
||||
},
|
||||
12: {
|
||||
id: 12,
|
||||
children: [],
|
||||
name: "You would never break the chain (Never break the chain)",
|
||||
appID: 1,
|
||||
actionID: 1001,
|
||||
xmlid: "menu_1",
|
||||
},
|
||||
13: {
|
||||
id: 13,
|
||||
children: [],
|
||||
name: "Chain keep us together (running in the shadow)",
|
||||
appID: 1,
|
||||
actionID: 1001,
|
||||
xmlid: "menu_1",
|
||||
},
|
||||
111: {
|
||||
id: 111,
|
||||
children: [],
|
||||
name: "You will never love me again",
|
||||
appID: 1,
|
||||
actionID: 1001,
|
||||
xmlid: "menu_1",
|
||||
},
|
||||
});
|
||||
serverData.menus[1].children = [10, 11, 12, 13];
|
||||
await webClient.env.services.menu.reload();
|
||||
// two ticks to allow the navbar to adapt
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_studio header .o_menu_sections > *:not(.d-none)").length,
|
||||
3
|
||||
);
|
||||
assert.containsOnce(target, ".o_studio .o_menu_sections .o_menu_sections_more");
|
||||
|
||||
await leaveStudio(target);
|
||||
await legacyExtraNextTick();
|
||||
// two more ticks to allow the navbar to adapt
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_studio");
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll("header .o_menu_sections > *:not(.d-none)").length,
|
||||
4
|
||||
);
|
||||
assert.containsOnce(target, ".o_menu_sections .o_menu_sections_more");
|
||||
});
|
||||
|
||||
QUnit.test("adapt navbar when refreshing studio (loadState)", async (assert) => {
|
||||
assert.expect(7);
|
||||
|
||||
target.style.width = "1080px";
|
||||
|
||||
const adapted = [];
|
||||
patchWithCleanup(StudioNavbar.prototype, {
|
||||
async adapt() {
|
||||
const prom = this._super();
|
||||
adapted.push(prom);
|
||||
return prom;
|
||||
},
|
||||
});
|
||||
|
||||
serverData.menus[1].actionID = 1;
|
||||
serverData.actions[1].xml_id = "action_xml_id";
|
||||
serverData.actions[1].id = 1;
|
||||
serverData.menus[1].children = [10, 11, 12, 13];
|
||||
|
||||
Object.assign(serverData.menus, {
|
||||
10: {
|
||||
id: 10,
|
||||
children: [],
|
||||
name: "The chain",
|
||||
appID: 1,
|
||||
actionID: 1001,
|
||||
xmlid: "menu_1",
|
||||
},
|
||||
11: {
|
||||
id: 11,
|
||||
children: [111],
|
||||
name: "Running in the shadows, damn your love, damn your lies",
|
||||
appID: 1,
|
||||
actionID: 1001,
|
||||
xmlid: "menu_1",
|
||||
},
|
||||
12: {
|
||||
id: 12,
|
||||
children: [],
|
||||
name: "You would never break the chain (Never break the chain)",
|
||||
appID: 1,
|
||||
actionID: 1001,
|
||||
xmlid: "menu_1",
|
||||
},
|
||||
13: {
|
||||
id: 13,
|
||||
children: [],
|
||||
name: "Chain keep us together (running in the shadow)",
|
||||
appID: 1,
|
||||
actionID: 1001,
|
||||
xmlid: "menu_1",
|
||||
},
|
||||
111: {
|
||||
id: 111,
|
||||
children: [],
|
||||
name: "You will never love me again",
|
||||
appID: 1,
|
||||
actionID: 1001,
|
||||
xmlid: "menu_1",
|
||||
},
|
||||
});
|
||||
|
||||
const webClient = await createEnterpriseWebClient({ serverData });
|
||||
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
await Promise.all(adapted);
|
||||
await click(target.querySelector(".o_app[data-menu-xmlid=menu_1]"));
|
||||
await legacyExtraNextTick();
|
||||
await Promise.all(adapted);
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_studio");
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll("header .o_menu_sections > *:not(.d-none)").length,
|
||||
4
|
||||
);
|
||||
assert.containsOnce(target, ".o_menu_sections .o_menu_sections_more");
|
||||
|
||||
await openStudio(target);
|
||||
await Promise.all(adapted);
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_studio header .o_menu_sections > *:not(.d-none)").length,
|
||||
3
|
||||
);
|
||||
assert.containsOnce(target, ".o_studio .o_menu_sections .o_menu_sections_more");
|
||||
|
||||
const state = webClient.env.services.router.current.hash;
|
||||
await loadState(webClient, state);
|
||||
await Promise.all(adapted);
|
||||
assert.strictEqual(
|
||||
target.querySelectorAll(".o_studio header .o_menu_sections > *:not(.d-none)").length,
|
||||
3
|
||||
);
|
||||
assert.containsOnce(target, ".o_studio .o_menu_sections .o_menu_sections_more");
|
||||
});
|
||||
});
|
||||
628
web_studio/static/tests/navigation_tests.js
Normal file
628
web_studio/static/tests/navigation_tests.js
Normal file
@@ -0,0 +1,628 @@
|
||||
/** @odoo-module **/
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
legacyExtraNextTick,
|
||||
makeDeferred,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { companyService } from "@web/webclient/company_service";
|
||||
import { createEnterpriseWebClient } from "@web_enterprise/../tests/helpers";
|
||||
import { getActionManagerServerData } from "@web/../tests/webclient/helpers";
|
||||
import { leaveStudio, openStudio, registerStudioDependencies } from "@web_studio/../tests/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { session } from "@web/session";
|
||||
import { patch, unpatch } from "@web/core/utils/patch";
|
||||
import { StudioView } from "@web_studio/client_action/studio_view";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Tests
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
QUnit.module("Studio", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
serverData = getActionManagerServerData();
|
||||
registerStudioDependencies();
|
||||
const serviceRegistry = registry.category("services");
|
||||
serviceRegistry.add("company", companyService);
|
||||
|
||||
// tweak a bit the default config to better fit with studio needs:
|
||||
// - add some menu items we can click on to test the navigation
|
||||
// - add a one2many field in a form view to test the one2many edition
|
||||
serverData.menus = {
|
||||
root: { id: "root", children: [1, 2, 3], name: "root", appID: "root" },
|
||||
1: {
|
||||
id: 1,
|
||||
children: [11, 12],
|
||||
name: "Partners",
|
||||
appID: 1,
|
||||
actionID: 4,
|
||||
xmlid: "app_1",
|
||||
},
|
||||
11: {
|
||||
id: 11,
|
||||
children: [],
|
||||
name: "Partners (Action 4)",
|
||||
appID: 1,
|
||||
actionID: 4,
|
||||
xmlid: "menu_11",
|
||||
},
|
||||
12: {
|
||||
id: 12,
|
||||
children: [],
|
||||
name: "Partners (Action 3)",
|
||||
appID: 1,
|
||||
actionID: 3,
|
||||
xmlid: "menu_12",
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
children: [],
|
||||
name: "Ponies",
|
||||
appID: 2,
|
||||
actionID: 8,
|
||||
xmlid: "app_2",
|
||||
},
|
||||
3: {
|
||||
id: 3,
|
||||
children: [],
|
||||
name: "Client Action",
|
||||
appID: 3,
|
||||
actionID: 9,
|
||||
xmlid: "app_3",
|
||||
},
|
||||
};
|
||||
serverData.models.partner.fields.date = { string: "Date", type: "date" };
|
||||
serverData.models.partner.fields.pony_id = {
|
||||
string: "Pony",
|
||||
type: "many2one",
|
||||
relation: "pony",
|
||||
};
|
||||
serverData.models.pony.fields.partner_ids = {
|
||||
string: "Partners",
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
relation_field: "pony_id",
|
||||
};
|
||||
serverData.views["pony,false,form"] = `
|
||||
<form>
|
||||
<field name="name"/>
|
||||
<field name='partner_ids'>
|
||||
<form>
|
||||
<sheet>
|
||||
<field name='display_name'/>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</form>`;
|
||||
});
|
||||
|
||||
QUnit.module("Studio Navigation");
|
||||
|
||||
QUnit.test("Studio not available for non system users", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
patchWithCleanup(session, { is_system: false });
|
||||
await createEnterpriseWebClient({ serverData });
|
||||
assert.containsOnce(target, ".o_main_navbar");
|
||||
|
||||
assert.containsNone(target, ".o_main_navbar .o_web_studio_navbar_item a");
|
||||
});
|
||||
|
||||
QUnit.test("open Studio with act_window", async function (assert) {
|
||||
assert.expect(22);
|
||||
|
||||
const mockRPC = async (route) => {
|
||||
assert.step(route);
|
||||
};
|
||||
await createEnterpriseWebClient({ serverData, mockRPC });
|
||||
assert.containsOnce(target, ".o_home_menu");
|
||||
|
||||
// open app Partners (act window action)
|
||||
await click(target.querySelector(".o_app[data-menu-xmlid=app_1]"));
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.containsOnce(target, ".o_kanban_view");
|
||||
assert.verifySteps(
|
||||
[
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"/web/dataset/call_kw/partner/get_views",
|
||||
"/web/dataset/call_kw/partner/web_search_read",
|
||||
],
|
||||
"should have loaded the action"
|
||||
);
|
||||
assert.containsOnce(target, ".o_main_navbar .o_web_studio_navbar_item a");
|
||||
|
||||
await openStudio(target);
|
||||
|
||||
assert.verifySteps(
|
||||
[
|
||||
"/web_studio/activity_allowed",
|
||||
"/web_studio/get_studio_view_arch",
|
||||
"/web/dataset/call_kw/partner/get_views",
|
||||
"/web/dataset/search_read",
|
||||
],
|
||||
"should have opened the action in Studio"
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_web_studio_client_action .o_web_studio_kanban_view_editor",
|
||||
"the kanban view should be opened"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_kanban_record:contains(yop)",
|
||||
"the first partner should be displayed"
|
||||
);
|
||||
assert.containsOnce(target, ".o_studio_navbar .o_web_studio_leave a");
|
||||
|
||||
await leaveStudio(target);
|
||||
|
||||
assert.verifySteps(
|
||||
[
|
||||
"/web/action/load",
|
||||
"/web/dataset/call_kw/partner/get_views",
|
||||
"/web/dataset/call_kw/partner/web_search_read",
|
||||
],
|
||||
"should have reloaded the previous action edited by Studio"
|
||||
);
|
||||
|
||||
assert.containsNone(target, ".o_web_studio_client_action", "Studio should be closed");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_kanban_view .o_kanban_record:contains(yop)",
|
||||
"the first partner should be displayed in kanban"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("open Studio with act_window and viewType", async function (assert) {
|
||||
await createEnterpriseWebClient({ serverData });
|
||||
|
||||
// open app Partners (act window action), sub menu Partners (action 3)
|
||||
await click(target.querySelector(".o_app[data-menu-xmlid=app_1]"));
|
||||
// the menu is rendered once the action is ready, so potentially in the next animation frame
|
||||
await nextTick();
|
||||
await click(target, ".o_menu_sections .o_nav_entry:nth-child(2)");
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
|
||||
await click(target.querySelector(".o_data_row .o_data_cell")); // open a record
|
||||
assert.containsOnce(target, ".o_form_view");
|
||||
|
||||
await openStudio(target);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_web_studio_client_action .o_web_studio_form_view_editor",
|
||||
"the form view should be opened"
|
||||
);
|
||||
assert.strictEqual(
|
||||
$(target).find('.o_field_widget[name="foo"]').text(),
|
||||
"yop",
|
||||
"the first partner should be displayed"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("reload the studio view", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const webClient = await createEnterpriseWebClient({ serverData });
|
||||
|
||||
// open app Partners (act window action), sub menu Partners (action 3)
|
||||
await click(target.querySelector(".o_app[data-menu-xmlid=app_1]"));
|
||||
await legacyExtraNextTick();
|
||||
assert.strictEqual(
|
||||
$(target).find(".o_kanban_record:contains(yop)").length,
|
||||
1,
|
||||
"the first partner should be displayed"
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_kanban_record")); // open a record
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce(target, ".o_form_view");
|
||||
assert.strictEqual(
|
||||
$(target).find(".o_form_view input:propValue(yop)").length,
|
||||
1,
|
||||
"should have open the same record"
|
||||
);
|
||||
|
||||
let prom = makeDeferred();
|
||||
patch(StudioView.prototype, "web_studio.Test.StudioView", {
|
||||
setup() {
|
||||
this._super();
|
||||
owl.onMounted(() => {
|
||||
prom.resolve();
|
||||
});
|
||||
},
|
||||
});
|
||||
await openStudio(target);
|
||||
await prom;
|
||||
prom = makeDeferred();
|
||||
await webClient.env.services.studio.reload();
|
||||
await prom;
|
||||
unpatch(StudioView.prototype, "web_studio.Test.StudioView");
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_web_studio_client_action .o_web_studio_form_view_editor",
|
||||
"the studio view should be opened after reloading"
|
||||
);
|
||||
assert.strictEqual(
|
||||
$(target).find(".o_form_view span:contains(yop)").length,
|
||||
1,
|
||||
"should have open the same record"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("switch view and close Studio", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
await createEnterpriseWebClient({ serverData });
|
||||
// open app Partners (act window action)
|
||||
await click(target.querySelector(".o_app[data-menu-xmlid=app_1]"));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce(target, ".o_kanban_view");
|
||||
|
||||
await openStudio(target);
|
||||
assert.containsOnce(target, ".o_web_studio_client_action .o_web_studio_kanban_view_editor");
|
||||
|
||||
// click on tab "Views"
|
||||
await click(target.querySelector(".o_web_studio_menu .o_web_studio_menu_item a"));
|
||||
assert.containsOnce(target, ".o_web_studio_action_editor");
|
||||
|
||||
// open list view
|
||||
await click(
|
||||
target.querySelector(
|
||||
".o_web_studio_views .o_web_studio_view_type[data-type=list] .o_web_studio_thumbnail"
|
||||
)
|
||||
);
|
||||
assert.containsOnce(target, ".o_web_studio_client_action .o_web_studio_list_view_editor");
|
||||
|
||||
await leaveStudio(target);
|
||||
|
||||
assert.containsNone(target, ".o_web_studio_client_action", "Studio should be closed");
|
||||
assert.containsOnce(target, ".o_list_view", "the list view should be opened");
|
||||
});
|
||||
|
||||
QUnit.test("navigation in Studio with act_window", async function (assert) {
|
||||
assert.expect(28);
|
||||
|
||||
const mockRPC = async (route) => {
|
||||
assert.step(route);
|
||||
};
|
||||
|
||||
await createEnterpriseWebClient({ serverData, mockRPC });
|
||||
// open app Partners (act window action)
|
||||
await click(target.querySelector(".o_app[data-menu-xmlid=app_1]"));
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.verifySteps(
|
||||
[
|
||||
"/web/webclient/load_menus",
|
||||
"/web/action/load",
|
||||
"/web/dataset/call_kw/partner/get_views",
|
||||
"/web/dataset/call_kw/partner/web_search_read",
|
||||
],
|
||||
"should have loaded the action"
|
||||
);
|
||||
|
||||
await openStudio(target);
|
||||
|
||||
assert.verifySteps(
|
||||
[
|
||||
"/web_studio/activity_allowed",
|
||||
"/web_studio/get_studio_view_arch",
|
||||
"/web/dataset/call_kw/partner/get_views",
|
||||
"/web/dataset/search_read",
|
||||
],
|
||||
"should have opened the action in Studio"
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_web_studio_client_action .o_web_studio_kanban_view_editor",
|
||||
"the kanban view should be opened"
|
||||
);
|
||||
assert.strictEqual(
|
||||
$(target).find(".o_kanban_record:contains(yop)").length,
|
||||
1,
|
||||
"the first partner should be displayed"
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_studio_navbar .o_menu_toggle"));
|
||||
|
||||
assert.containsOnce(target, ".o_studio_home_menu");
|
||||
|
||||
// open app Ponies (act window action)
|
||||
await click(target.querySelector(".o_app[data-menu-xmlid=app_2]"));
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.verifySteps(
|
||||
[
|
||||
"/web/action/load",
|
||||
"/web_studio/activity_allowed",
|
||||
"/web_studio/get_studio_view_arch",
|
||||
"/web/dataset/call_kw/pony/get_views",
|
||||
"/web/dataset/search_read",
|
||||
],
|
||||
"should have opened the navigated action in Studio"
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_web_studio_client_action .o_web_studio_list_view_editor",
|
||||
"the list view should be opened"
|
||||
);
|
||||
assert.strictEqual(
|
||||
$(target).find(".o_legacy_list_view .o_data_cell").text(),
|
||||
"Twilight SparkleApplejackFluttershy",
|
||||
"the list of ponies should be correctly displayed"
|
||||
);
|
||||
|
||||
await leaveStudio(target);
|
||||
|
||||
assert.verifySteps(
|
||||
[
|
||||
"/web/action/load",
|
||||
"/web/dataset/call_kw/pony/get_views",
|
||||
"/web/dataset/call_kw/pony/web_search_read",
|
||||
],
|
||||
"should have reloaded the previous action edited by Studio"
|
||||
);
|
||||
|
||||
assert.containsNone(target, ".o_web_studio_client_action", "Studio should be closed");
|
||||
assert.containsOnce(target, ".o_list_view", "the list view should be opened");
|
||||
assert.strictEqual(
|
||||
$(target).find(".o_list_view .o_data_cell").text(),
|
||||
"Twilight SparkleApplejackFluttershy",
|
||||
"the list of ponies should be correctly displayed"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("keep action context when leaving Studio", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
let nbLoadAction = 0;
|
||||
const mockRPC = async (route, args) => {
|
||||
if (route === "/web/action/load") {
|
||||
nbLoadAction++;
|
||||
if (nbLoadAction === 2) {
|
||||
assert.strictEqual(
|
||||
args.additional_context.active_id,
|
||||
1,
|
||||
"the context should be correctly passed when leaving Studio"
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
serverData.actions[4].context = "{'active_id': 1}";
|
||||
|
||||
await createEnterpriseWebClient({
|
||||
serverData,
|
||||
mockRPC,
|
||||
});
|
||||
// open app Partners (act window action)
|
||||
await click(target.querySelector(".o_app[data-menu-xmlid=app_1]"));
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.containsOnce(target, ".o_kanban_view");
|
||||
|
||||
await openStudio(target);
|
||||
|
||||
assert.containsOnce(target, ".o_web_studio_kanban_view_editor");
|
||||
|
||||
await leaveStudio(target);
|
||||
|
||||
assert.containsOnce(target, ".o_kanban_view");
|
||||
assert.strictEqual(nbLoadAction, 2, "the action should have been loaded twice");
|
||||
});
|
||||
|
||||
QUnit.test("open same record when leaving form", async function (assert) {
|
||||
await createEnterpriseWebClient({ serverData });
|
||||
// open app Ponies (act window action)
|
||||
await click(target.querySelector(".o_app[data-menu-xmlid=app_2]"));
|
||||
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
|
||||
await click(target.querySelector(".o_data_row .o_data_cell"));
|
||||
|
||||
assert.containsOnce(target, ".o_form_view");
|
||||
|
||||
await openStudio(target);
|
||||
|
||||
assert.containsOnce(target, ".o_web_studio_client_action .o_web_studio_form_view_editor");
|
||||
|
||||
await leaveStudio(target);
|
||||
|
||||
assert.containsOnce(target, ".o_form_view");
|
||||
assert.containsOnce(target, ".o_form_view .o_field_widget[name=name] input");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_form_view .o_field_widget[name=name] input").value,
|
||||
"Twilight Sparkle"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("open Studio with non editable view", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
serverData.menus[99] = {
|
||||
id: 9,
|
||||
children: [],
|
||||
name: "Action with Grid view",
|
||||
appID: 9,
|
||||
actionID: 99,
|
||||
xmlid: "app_9",
|
||||
};
|
||||
serverData.menus.root.children.push(99);
|
||||
serverData.actions[99] = {
|
||||
id: 99,
|
||||
xml_id: "some.xml_id",
|
||||
name: "Partners Action 99",
|
||||
res_model: "partner",
|
||||
type: "ir.actions.act_window",
|
||||
views: [
|
||||
[42, "grid"],
|
||||
[2, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
};
|
||||
serverData.views["partner,42,grid"] = `
|
||||
<grid>
|
||||
<field name="foo" type="row"/>
|
||||
<field name="id" type="measure"/>
|
||||
<field name="date" type="col">
|
||||
<range name="week" string="Week" span="week" step="day"/>
|
||||
</field>
|
||||
</grid>`;
|
||||
|
||||
await createEnterpriseWebClient({
|
||||
serverData,
|
||||
legacyParams: { withLegacyMockServer: true },
|
||||
});
|
||||
await click(target.querySelector(".o_app[data-menu-xmlid=app_9]"));
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.containsOnce(target, ".o_grid_view");
|
||||
|
||||
await openStudio(target);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_web_studio_action_editor",
|
||||
"action editor should be opened (grid is not editable)"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"open list view with sample data gives empty list view in studio",
|
||||
async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
serverData.models.pony.records = [];
|
||||
serverData.views["pony,false,list"] = `<tree sample="1"><field name="name"/></tree>`;
|
||||
|
||||
await createEnterpriseWebClient({
|
||||
serverData,
|
||||
});
|
||||
// open app Ponies (act window action)
|
||||
await click(target.querySelector(".o_app[data-menu-xmlid=app_2]"));
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.ok(
|
||||
[...target.querySelectorAll(".o_list_table .o_data_row")].length > 0,
|
||||
"there should be some sample data in the list view"
|
||||
);
|
||||
|
||||
await openStudio(target);
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_list_table .o_data_row",
|
||||
"the list view should not contain any data"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"open Studio with editable form view and check context propagation",
|
||||
async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
serverData.menus[43] = {
|
||||
id: 43,
|
||||
children: [],
|
||||
name: "Form with context",
|
||||
appID: 43,
|
||||
actionID: 43,
|
||||
xmlid: "app_43",
|
||||
};
|
||||
serverData.menus.root.children.push(43);
|
||||
serverData.actions[43] = {
|
||||
id: 43,
|
||||
name: "Pony Action 43",
|
||||
res_model: "pony",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "form"]],
|
||||
context: "{'default_type': 'foo'}",
|
||||
res_id: 4,
|
||||
xml_id: "action_43",
|
||||
};
|
||||
|
||||
const mockRPC = async (route, args) => {
|
||||
if (route === "/web/dataset/call_kw/pony/read") {
|
||||
// We pass here twice: once for the "classic" action
|
||||
// and once when entering studio
|
||||
assert.strictEqual(args.kwargs.context.default_type, "foo");
|
||||
}
|
||||
if (route === "/web/dataset/call_kw/partner/onchange") {
|
||||
assert.ok(
|
||||
!("default_type" in args.kwargs.context),
|
||||
"'default_x' context value should not be propaged to x2m model"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
await createEnterpriseWebClient({
|
||||
serverData,
|
||||
mockRPC,
|
||||
});
|
||||
await click(target.querySelector(".o_app[data-menu-xmlid=app_43]"));
|
||||
await legacyExtraNextTick();
|
||||
|
||||
assert.containsOnce(target, ".o_form_view");
|
||||
|
||||
await openStudio(target);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_web_studio_client_action .o_web_studio_form_view_editor",
|
||||
"the form view should be opened"
|
||||
);
|
||||
|
||||
await click(target.querySelector(".o_web_studio_form_view_editor .o_field_one2many"));
|
||||
await click(
|
||||
target.querySelector(
|
||||
'.o_web_studio_form_view_editor .o_field_one2many .o_web_studio_editX2Many[data-type="form"]'
|
||||
)
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_web_studio_client_action .o_web_studio_form_view_editor",
|
||||
"the form view should be opened"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"concurrency: execute a non editable action and try to enter studio",
|
||||
async function (assert) {
|
||||
// the purpose of this test is to ensure that there's no time window
|
||||
// during which if the icon isn't disabled, but the current action isn't
|
||||
// editable (typically, just after the current action has changed).
|
||||
assert.expect(5);
|
||||
|
||||
const def = makeDeferred();
|
||||
serverData.actions[4].xml_id = false; // make action 4 non editable
|
||||
const webClient = await createEnterpriseWebClient({ serverData });
|
||||
assert.containsOnce(target, ".o_home_menu");
|
||||
|
||||
webClient.env.bus.on("ACTION_MANAGER:UI-UPDATED", null, () => {
|
||||
assert.containsOnce(target, ".o_kanban_view");
|
||||
assert.hasClass(target.querySelector(".o_web_studio_navbar_item"), "o_disabled");
|
||||
def.resolve();
|
||||
});
|
||||
|
||||
// open app Partners (non editable act window action)
|
||||
await click(target.querySelector(".o_app[data-menu-xmlid=app_1]"));
|
||||
await def;
|
||||
|
||||
assert.containsOnce(target, ".o_kanban_view");
|
||||
assert.hasClass(target.querySelector(".o_web_studio_navbar_item"), "o_disabled");
|
||||
}
|
||||
);
|
||||
});
|
||||
339
web_studio/static/tests/studio_home_menu_tests.js
Normal file
339
web_studio/static/tests/studio_home_menu_tests.js
Normal file
@@ -0,0 +1,339 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { IconCreator } from "@web_studio/client_action/icon_creator/icon_creator";
|
||||
import { StudioHomeMenu } from "@web_studio/client_action/studio_home_menu/studio_home_menu";
|
||||
import { MODES } from "@web_studio/studio_service";
|
||||
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { enterpriseSubscriptionService } from "@web_enterprise/webclient/home_menu/enterprise_subscription_service";
|
||||
|
||||
import {
|
||||
fakeCommandService,
|
||||
makeFakeNotificationService,
|
||||
makeFakeRPCService,
|
||||
} from "@web/../tests/helpers/mock_services";
|
||||
import { userService } from "@web/core/user_service";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { click, getFixture, mount } from "@web/../tests/helpers/utils";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const { Component, EventBus, xml } = owl;
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
const genericHomeMenuProps = {
|
||||
apps: [
|
||||
{
|
||||
actionID: 121,
|
||||
id: 1,
|
||||
appID: 1,
|
||||
label: "Discuss",
|
||||
parents: "",
|
||||
webIcon: "mail,static/description/icon.png",
|
||||
webIconData: "/web_enterprise/static/img/default_icon_app.png",
|
||||
xmlid: "app.1",
|
||||
},
|
||||
{
|
||||
actionID: 122,
|
||||
id: 2,
|
||||
appID: 2,
|
||||
label: "Calendar",
|
||||
parents: "",
|
||||
webIcon: {
|
||||
backgroundColor: "#C6572A",
|
||||
color: "#FFFFFF",
|
||||
iconClass: "fa fa-diamond",
|
||||
},
|
||||
xmlid: "app.2",
|
||||
},
|
||||
{
|
||||
actionID: 123,
|
||||
id: 3,
|
||||
appID: 3,
|
||||
label: "Contacts",
|
||||
parents: "",
|
||||
webIcon: false,
|
||||
webIconData: "/web_enterprise/static/img/default_icon_app.png",
|
||||
xmlid: "app.3",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const createStudioHomeMenu = async () => {
|
||||
class Parent extends Component {
|
||||
get DialogContainer() {
|
||||
return registry.category("main_components").get("DialogContainer");
|
||||
}
|
||||
}
|
||||
Parent.components = { StudioHomeMenu };
|
||||
Parent.template = xml`
|
||||
<div>
|
||||
<StudioHomeMenu t-props="props.homeMenuProps" />
|
||||
<div class="o_dialog_container" />
|
||||
<t t-component="DialogContainer.Component" t-props="DialogContainer.props" />
|
||||
</div>`;
|
||||
|
||||
const env = await makeTestEnv();
|
||||
const target = getFixture();
|
||||
await mount(Parent, target, { env, props: { homeMenuProps: { ...genericHomeMenuProps } } });
|
||||
return target;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Tests
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
let bus;
|
||||
|
||||
QUnit.module("Studio", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
IconCreator.enableTransitions = false;
|
||||
registerCleanup(() => {
|
||||
IconCreator.enableTransitions = true;
|
||||
});
|
||||
|
||||
bus = new EventBus();
|
||||
|
||||
const fakeNotificationService = makeFakeNotificationService();
|
||||
const fakeHomeMenuService = {
|
||||
start() {
|
||||
return {
|
||||
toggle() {},
|
||||
};
|
||||
},
|
||||
};
|
||||
const fakeMenuService = {
|
||||
start() {
|
||||
return {
|
||||
setCurrentMenu(menu) {
|
||||
bus.trigger("menu:setCurrentMenu", menu.id);
|
||||
},
|
||||
reload() {
|
||||
bus.trigger("menu:reload");
|
||||
},
|
||||
getMenu() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const fakeStudioService = {
|
||||
start() {
|
||||
return {
|
||||
MODES,
|
||||
open(...args) {
|
||||
bus.trigger("studio:open", args);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
const fakeHTTPService = {
|
||||
start() {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
serviceRegistry.add("orm", ormService);
|
||||
serviceRegistry.add("enterprise_subscription", enterpriseSubscriptionService);
|
||||
serviceRegistry.add("home_menu", fakeHomeMenuService);
|
||||
serviceRegistry.add("http", fakeHTTPService);
|
||||
serviceRegistry.add("menu", fakeMenuService);
|
||||
serviceRegistry.add("notification", fakeNotificationService);
|
||||
serviceRegistry.add("user", userService);
|
||||
serviceRegistry.add("studio", fakeStudioService);
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
serviceRegistry.add("dialog", dialogService);
|
||||
serviceRegistry.add("ui", uiService);
|
||||
serviceRegistry.add("command", fakeCommandService);
|
||||
});
|
||||
|
||||
QUnit.module("StudioHomeMenu");
|
||||
|
||||
QUnit.test("simple rendering", async (assert) => {
|
||||
assert.expect(20);
|
||||
|
||||
const target = await createStudioHomeMenu();
|
||||
|
||||
// Main div
|
||||
assert.containsOnce(target, ".o_home_menu");
|
||||
|
||||
// Hidden elements
|
||||
assert.isNotVisible(
|
||||
target.querySelector(".database_expiration_panel"),
|
||||
"Expiration panel should not be visible"
|
||||
);
|
||||
|
||||
// App list
|
||||
assert.containsOnce(target, "div.o_apps");
|
||||
assert.containsN(
|
||||
target,
|
||||
"div.o_apps > a.o_app.o_menuitem",
|
||||
4,
|
||||
"should contain 3 normal app icons + the new app button"
|
||||
);
|
||||
|
||||
// App with image
|
||||
const firstApp = target.querySelector("div.o_apps > a.o_app.o_menuitem");
|
||||
assert.strictEqual(firstApp.dataset.menuXmlid, "app.1");
|
||||
assert.containsOnce(firstApp, "img.o_app_icon");
|
||||
assert.strictEqual(
|
||||
firstApp.querySelector("img.o_app_icon").dataset.src,
|
||||
"/web_enterprise/static/img/default_icon_app.png"
|
||||
);
|
||||
assert.containsOnce(firstApp, "div.o_caption");
|
||||
assert.strictEqual(firstApp.querySelector("div.o_caption").innerText, "Discuss");
|
||||
assert.containsOnce(firstApp, ".o_web_studio_edit_icon i");
|
||||
|
||||
// App with custom icon
|
||||
const secondApp = target.querySelectorAll("div.o_apps > a.o_app.o_menuitem")[1];
|
||||
assert.strictEqual(secondApp.dataset.menuXmlid, "app.2");
|
||||
assert.containsOnce(secondApp, "div.o_app_icon");
|
||||
assert.strictEqual(
|
||||
secondApp.querySelector("div.o_app_icon").style.backgroundColor,
|
||||
"rgb(198, 87, 42)",
|
||||
"Icon background color should be #C6572A"
|
||||
);
|
||||
assert.containsOnce(secondApp, "i.fa.fa-diamond");
|
||||
assert.strictEqual(
|
||||
secondApp.querySelector("i.fa.fa-diamond").style.color,
|
||||
"rgb(255, 255, 255)",
|
||||
"Icon color should be #FFFFFF"
|
||||
);
|
||||
assert.containsOnce(secondApp, ".o_web_studio_edit_icon i");
|
||||
|
||||
// New app button
|
||||
assert.containsOnce(
|
||||
target,
|
||||
"div.o_apps > a.o_app.o_web_studio_new_app",
|
||||
'should contain a "New App icon"'
|
||||
);
|
||||
const newApp = target.querySelector("a.o_app.o_web_studio_new_app");
|
||||
assert.strictEqual(
|
||||
newApp.querySelector("img.o_app_icon").dataset.src,
|
||||
"/web_studio/static/src/img/default_icon_app.png",
|
||||
"Image source URL should end with '/web_studio/static/src/img/default_icon_app.png'"
|
||||
);
|
||||
assert.containsOnce(newApp, "div.o_caption");
|
||||
assert.strictEqual(newApp.querySelector("div.o_caption").innerText, "New App");
|
||||
});
|
||||
|
||||
QUnit.test("Click on a normal App", async (assert) => {
|
||||
assert.expect(2);
|
||||
|
||||
bus.on("studio:open", null, (modeAndActionId) => {
|
||||
assert.deepEqual(modeAndActionId, [MODES.EDITOR, 121]);
|
||||
});
|
||||
bus.on("menu:setCurrentMenu", null, (menuId) => {
|
||||
assert.strictEqual(menuId, 1);
|
||||
});
|
||||
const target = await createStudioHomeMenu();
|
||||
|
||||
await click(target.querySelector(".o_menuitem"));
|
||||
});
|
||||
|
||||
QUnit.test("Click on new App", async (assert) => {
|
||||
assert.expect(1);
|
||||
|
||||
bus.on("studio:open", null, ([mode]) => {
|
||||
assert.strictEqual(mode, MODES.APP_CREATOR);
|
||||
});
|
||||
bus.on("menu:setCurrentMenu", null, () => {
|
||||
throw new Error("should not update the current menu");
|
||||
});
|
||||
const target = await createStudioHomeMenu();
|
||||
|
||||
await click(target, "a.o_app.o_web_studio_new_app");
|
||||
});
|
||||
|
||||
QUnit.test("Click on edit icon button", async (assert) => {
|
||||
assert.expect(11);
|
||||
|
||||
const target = await createStudioHomeMenu();
|
||||
|
||||
// TODO: we should maybe check icon visibility comes on mouse over
|
||||
const firstEditIconButton = target.querySelector(".o_web_studio_edit_icon i");
|
||||
await click(firstEditIconButton);
|
||||
|
||||
const dialog = document.querySelector("div.modal");
|
||||
assert.containsOnce(dialog, "header.modal-header");
|
||||
assert.strictEqual(
|
||||
dialog.querySelector("header.modal-header h4").innerText,
|
||||
"Edit Application Icon"
|
||||
);
|
||||
|
||||
assert.containsOnce(
|
||||
dialog,
|
||||
".modal-content.o_web_studio_edit_menu_icon_modal .o_web_studio_icon_creator"
|
||||
);
|
||||
|
||||
assert.containsOnce(dialog, "footer.modal-footer");
|
||||
assert.containsN(dialog, "footer button", 2);
|
||||
|
||||
const buttons = dialog.querySelectorAll("footer button");
|
||||
const firstButton = buttons[0];
|
||||
const secondButton = buttons[1];
|
||||
|
||||
assert.strictEqual(firstButton.innerText, "CONFIRM");
|
||||
assert.hasClass(firstButton, "btn-primary");
|
||||
|
||||
assert.strictEqual(secondButton.innerText, "CANCEL");
|
||||
assert.hasClass(secondButton, "btn-secondary");
|
||||
|
||||
await click(secondButton);
|
||||
|
||||
assert.strictEqual(document.querySelector("div.modal"), null);
|
||||
|
||||
await click(firstEditIconButton);
|
||||
await click(document.querySelector("footer button"));
|
||||
|
||||
assert.strictEqual(document.querySelector("div.modal"), null);
|
||||
});
|
||||
|
||||
QUnit.test("edit an icon", async (assert) => {
|
||||
assert.expect(3);
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (route === "/web_studio/edit_menu_icon") {
|
||||
assert.deepEqual(args, {
|
||||
context: {
|
||||
lang: "en",
|
||||
tz: "taht",
|
||||
uid: 7,
|
||||
},
|
||||
icon: ["fa fa-balance-scale", "#f1c40f", "#34495e"],
|
||||
menu_id: 1,
|
||||
});
|
||||
}
|
||||
};
|
||||
registry.category("services").add("rpc", makeFakeRPCService(mockRPC), { force: true });
|
||||
|
||||
const target = await createStudioHomeMenu();
|
||||
|
||||
await click(target.querySelector(".o_web_studio_edit_icon i"));
|
||||
const dialog = document.querySelector("div.modal");
|
||||
await click(dialog.querySelector(".o_web_studio_upload a"));
|
||||
|
||||
assert.doesNotHaveClass(
|
||||
dialog.querySelector(".o_web_studio_icon .o_app_icon i"),
|
||||
"fa-balance-scale"
|
||||
);
|
||||
|
||||
// Change the icon's pictogram
|
||||
await click(dialog.querySelectorAll(".o_web_studio_selector")[2]);
|
||||
await click(dialog, ".o_web_studio_selector .fa.fa-balance-scale");
|
||||
|
||||
assert.hasClass(
|
||||
dialog.querySelector(".o_web_studio_icon .o_app_icon i"),
|
||||
"fa-balance-scale"
|
||||
);
|
||||
|
||||
await click(dialog.querySelector("footer button")); // trigger save
|
||||
});
|
||||
});
|
||||
8
web_studio/static/tests/views/disable_patch.js
Normal file
8
web_studio/static/tests/views/disable_patch.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { unpatch } from "@web/core/utils/patch";
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
|
||||
import "@web_studio/views/list/list_renderer";
|
||||
|
||||
unpatch(ListRenderer.prototype, "web_studio.ListRenderer");
|
||||
80
web_studio/static/tests/views/list_tests.js
Normal file
80
web_studio/static/tests/views/list_tests.js
Normal file
@@ -0,0 +1,80 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { click, getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { doAction, getActionManagerServerData } from "@web/../tests/webclient/helpers";
|
||||
import { patch, unpatch } from "@web/core/utils/patch";
|
||||
import { session } from "@web/session";
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
import { createEnterpriseWebClient } from "@web_enterprise/../tests/helpers";
|
||||
import { patchListRendererDesktop } from "@web_enterprise/views/list/list_renderer_desktop";
|
||||
import { registerStudioDependencies } from "@web_studio/../tests/helpers";
|
||||
import { patchListRendererStudio } from "@web_studio/views/list/list_renderer";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Studio", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = getActionManagerServerData();
|
||||
registerStudioDependencies();
|
||||
patchWithCleanup(session, { is_system: true });
|
||||
target = getFixture();
|
||||
patch(
|
||||
ListRenderer.prototype,
|
||||
"web_enterprise.ListRendererDesktop",
|
||||
patchListRendererDesktop
|
||||
);
|
||||
patch(ListRenderer.prototype, "web_studio.ListRenderer", patchListRendererStudio);
|
||||
});
|
||||
|
||||
hooks.afterEach(() => {
|
||||
unpatch(ListRenderer.prototype, "web_enterprise.ListRendererDesktop");
|
||||
unpatch(ListRenderer.prototype, "web_studio.ListRenderer");
|
||||
});
|
||||
|
||||
QUnit.module("ListView");
|
||||
|
||||
QUnit.test("add custom field button with other optional columns", async function (assert) {
|
||||
serverData.views["partner,false,list"] = `
|
||||
<tree>
|
||||
<field name="foo"/>
|
||||
<field name="bar" optional="hide"/>
|
||||
</tree>`;
|
||||
|
||||
const webClient = await createEnterpriseWebClient({ serverData });
|
||||
await doAction(webClient, 3);
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
assert.containsOnce(target, ".o_list_view .o_optional_columns_dropdown_toggle");
|
||||
|
||||
await click(target.querySelector(".o_optional_columns_dropdown_toggle"));
|
||||
assert.containsN(target, ".o_optional_columns_dropdown .dropdown-item", 2);
|
||||
assert.containsOnce(target, ".o_optional_columns_dropdown .dropdown-item-studio");
|
||||
|
||||
await click(target.querySelector(".o_optional_columns_dropdown .dropdown-item-studio"));
|
||||
assert.containsNone(target, ".modal-studio");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_studio .o_web_studio_editor .o_web_studio_list_view_editor"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("add custom field button without other optional columns", async function (assert) {
|
||||
// by default, the list in serverData doesn't contain optional fields
|
||||
const webClient = await createEnterpriseWebClient({ serverData });
|
||||
await doAction(webClient, 3);
|
||||
|
||||
assert.containsOnce(target, ".o_list_view");
|
||||
assert.containsOnce(target, ".o_list_view .o_optional_columns_dropdown_toggle");
|
||||
await click(target.querySelector(".o_optional_columns_dropdown_toggle"));
|
||||
|
||||
assert.containsOnce(target, ".o_optional_columns_dropdown .dropdown-item");
|
||||
assert.containsOnce(target, ".o_optional_columns_dropdown .dropdown-item-studio");
|
||||
|
||||
await click(target.querySelector(".o_optional_columns_dropdown .dropdown-item-studio"));
|
||||
assert.containsNone(target, ".modal-studio");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_studio .o_web_studio_editor .o_web_studio_list_view_editor"
|
||||
);
|
||||
});
|
||||
});
|
||||
312
web_studio/static/tests/views/studio_approval_tests.js
Normal file
312
web_studio/static/tests/views/studio_approval_tests.js
Normal file
@@ -0,0 +1,312 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import {
|
||||
getFixture,
|
||||
patchWithCleanup,
|
||||
click,
|
||||
nextTick,
|
||||
makeDeferred,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { session } from "@web/session";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const fakeStudioService = {
|
||||
start() {
|
||||
return {
|
||||
mode: null,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
QUnit.module("Studio Approval", (hooks) => {
|
||||
let target;
|
||||
let serverData;
|
||||
|
||||
hooks.beforeEach(() => {
|
||||
target = getFixture();
|
||||
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
int_field: { string: "int_field", type: "integer", sortable: true },
|
||||
bar: { string: "Bar", type: "boolean" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "first record",
|
||||
int_field: 42,
|
||||
bar: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "second record",
|
||||
int_field: 27,
|
||||
bar: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setupViewRegistries();
|
||||
registry.category("services").add("studio", fakeStudioService);
|
||||
});
|
||||
|
||||
QUnit.test("approval components are synchronous", async (assert) => {
|
||||
const prom = makeDeferred();
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `<form><button studio_approval="True" type="object" name="myMethod"/></form>`,
|
||||
async mockRPC(route, args) {
|
||||
if (args.method === "get_approval_spec") {
|
||||
assert.step(args.method);
|
||||
await prom;
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
id: 1,
|
||||
group_id: [1, "Internal User"],
|
||||
domain: false,
|
||||
can_validate: true,
|
||||
message: false,
|
||||
exclusive_user: false,
|
||||
},
|
||||
],
|
||||
entries: [],
|
||||
groups: [[1, "Internal User"]],
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
assert.verifySteps(["get_approval_spec"]);
|
||||
assert.containsOnce(target, "button .o_web_studio_approval .fa-circle-o-notch.fa-spin");
|
||||
prom.resolve();
|
||||
await nextTick();
|
||||
assert.containsNone(target, "button .o_web_studio_approval .fa-circle-o-notch.fa-spin");
|
||||
assert.containsOnce(target, "button .o_web_studio_approval .o_web_studio_approval_avatar");
|
||||
});
|
||||
|
||||
QUnit.test("approval widget basic rendering", async function (assert) {
|
||||
assert.expect(14);
|
||||
|
||||
patchWithCleanup(session, {
|
||||
uid: 42,
|
||||
});
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `<form string="Partners">
|
||||
<sheet>
|
||||
<header>
|
||||
<button type="object" name="someMethod" string="Apply Method" studio_approval="True"/>
|
||||
</header>
|
||||
<div name="button_box">
|
||||
<button class="oe_stat_button" studio_approval="True" id="visibleStat">
|
||||
<field name="int_field"/>
|
||||
</button>
|
||||
<button class="oe_stat_button" studio_approval="True"
|
||||
attrs='{"invisible": [["bar", "=", true]]}' id="invisibleStat">
|
||||
<field name="bar"/>
|
||||
</button>
|
||||
</div>
|
||||
<group>
|
||||
<group style="background-color: red">
|
||||
<field name="display_name" studio_approval="True"/>
|
||||
<field name="bar"/>
|
||||
<field name="int_field"/>
|
||||
</group>
|
||||
</group>
|
||||
<button type="object" name="anotherMethod"
|
||||
string="Apply Second Method" studio_approval="True"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 2,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === "get_approval_spec") {
|
||||
assert.step("fetch_approval_spec");
|
||||
return Promise.resolve({
|
||||
rules: [
|
||||
{
|
||||
id: 1,
|
||||
group_id: [1, "Internal User"],
|
||||
domain: false,
|
||||
can_validate: true,
|
||||
message: false,
|
||||
exclusive_user: false,
|
||||
},
|
||||
],
|
||||
entries: [],
|
||||
groups: [[1, "Internal User"]],
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// check that the widget was inserted on visible buttons only
|
||||
assert.containsOnce(target, 'button[name="someMethod"] .o_web_studio_approval');
|
||||
assert.containsOnce(target, "#visibleStat .o_web_studio_approval");
|
||||
assert.containsNone(target, "#invisibleStat .o_web_studio_approval");
|
||||
assert.containsOnce(target, 'button[name="anotherMethod"] .o_web_studio_approval');
|
||||
assert.containsNone(target, ".o_group .o_web_studio_approval");
|
||||
// should have fetched spec for exactly 3 buttons
|
||||
assert.verifySteps(["fetch_approval_spec", "fetch_approval_spec", "fetch_approval_spec"]);
|
||||
// display popover
|
||||
await click(target, 'button[name="someMethod"] .o_web_studio_approval');
|
||||
assert.containsOnce(target, ".o-approval-popover");
|
||||
const popover = target.querySelector(".o-approval-popover");
|
||||
assert.containsOnce(popover, ".o_web_studio_approval_no_entry");
|
||||
assert.containsOnce(popover, ".o_web_approval_approve");
|
||||
assert.containsOnce(popover, ".o_web_approval_reject");
|
||||
assert.containsNone(popover, ".o_web_approval_cancel");
|
||||
});
|
||||
|
||||
QUnit.test("approval check", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `<form string="Partners">
|
||||
<sheet>
|
||||
<header>
|
||||
<button type="object" id="mainButton" name="someMethod"
|
||||
string="Apply Method" studio_approval="True"/>
|
||||
</header>
|
||||
<group>
|
||||
<group style="background-color: red">
|
||||
<field name="display_name"/>
|
||||
<field name="bar"/>
|
||||
<field name="int_field"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 2,
|
||||
mockRPC: function (route, args) {
|
||||
const rule = {
|
||||
id: 1,
|
||||
group_id: [1, "Internal User"],
|
||||
domain: false,
|
||||
can_validate: true,
|
||||
message: false,
|
||||
exclusive_user: false,
|
||||
};
|
||||
if (args.method === "get_approval_spec") {
|
||||
assert.step("fetch_approval_spec");
|
||||
return Promise.resolve({
|
||||
rules: [rule],
|
||||
entries: [],
|
||||
groups: [[1, "Internal User"]],
|
||||
});
|
||||
} else if (args.method === "check_approval") {
|
||||
assert.step("attempt_action");
|
||||
return Promise.resolve({
|
||||
approved: false,
|
||||
rules: [rule],
|
||||
entries: [],
|
||||
});
|
||||
} else if (args.method === "someMethod") {
|
||||
/* the action of the button should not be
|
||||
called, as the approval is refused! if this
|
||||
code is traversed, the test *must* fail!
|
||||
that's why it's not included in the expected count
|
||||
or in the verifySteps call */
|
||||
assert.step("should_not_happen!");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await click(target, "#mainButton");
|
||||
// first render, handle click, rerender after click
|
||||
assert.verifySteps(["fetch_approval_spec", "attempt_action", "fetch_approval_spec"]);
|
||||
});
|
||||
|
||||
QUnit.test("approval widget basic flow", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
patchWithCleanup(session, {
|
||||
uid: 42,
|
||||
});
|
||||
|
||||
let hasValidatedRule;
|
||||
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: `<form string="Partners">
|
||||
<sheet>
|
||||
<header>
|
||||
<button type="object=" name="someMethod" string="Apply Method" studio_approval="True"/>
|
||||
</header>
|
||||
<group>
|
||||
<group style="background-color: red">
|
||||
<field name="display_name"/>
|
||||
<field name="bar"/>
|
||||
<field name="int_field"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>`,
|
||||
resId: 2,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === "get_approval_spec") {
|
||||
const spec = {
|
||||
rules: [
|
||||
{
|
||||
id: 1,
|
||||
group_id: [1, "Internal User"],
|
||||
domain: false,
|
||||
can_validate: true,
|
||||
message: false,
|
||||
exclusive_user: false,
|
||||
},
|
||||
],
|
||||
entries: [],
|
||||
groups: [[1, "Internal User"]],
|
||||
};
|
||||
if (hasValidatedRule !== undefined) {
|
||||
spec.entries = [
|
||||
{
|
||||
id: 1,
|
||||
approved: hasValidatedRule,
|
||||
user_id: [42, "Some rando"],
|
||||
write_date: "2020-04-07 12:43:48",
|
||||
rule_id: [1, "someMethod/partner (Internal User)"],
|
||||
model: "partner",
|
||||
res_id: 2,
|
||||
},
|
||||
];
|
||||
}
|
||||
return Promise.resolve(spec);
|
||||
} else if (args.method === "set_approval") {
|
||||
hasValidatedRule = args.kwargs.approved;
|
||||
assert.step(hasValidatedRule ? "approve_rule" : "reject_rule");
|
||||
return Promise.resolve(true);
|
||||
} else if (args.method === "delete_approval") {
|
||||
hasValidatedRule = undefined;
|
||||
assert.step("delete_approval");
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// display popover and validate a rule, then cancel, then reject
|
||||
await click(target, 'button[name="someMethod"] .o_web_studio_approval');
|
||||
assert.containsOnce(target, ".o_popover");
|
||||
await click(target, ".o_popover button.o_web_approval_approve");
|
||||
await nextTick();
|
||||
await click(target, ".o_popover button.o_web_approval_cancel");
|
||||
await click(target, ".o_popover button.o_web_approval_reject");
|
||||
assert.verifySteps(["approve_rule", "delete_approval", "reject_rule"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user