forked from Mapan/odoo17e
577 lines
21 KiB
JavaScript
577 lines
21 KiB
JavaScript
/** @odoo-module **/
|
|
|
|
import { registry } from "@web/core/registry";
|
|
import { browser } from "@web/core/browser/browser";
|
|
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
|
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
|
import { setupViewRegistries } from "@web/../tests/views/helpers";
|
|
import { AppCreator } from "@web_studio/client_action/app_creator/app_creator";
|
|
import { IconCreator } from "@web_studio/client_action/icon_creator/icon_creator";
|
|
import { makeFakeHTTPService } from "@web/../tests/helpers/mock_services";
|
|
import {
|
|
click,
|
|
getFixture,
|
|
nextTick,
|
|
triggerEvent,
|
|
editInput,
|
|
mount,
|
|
patchWithCleanup,
|
|
} from "@web/../tests/helpers/utils";
|
|
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
|
|
|
|
const serviceRegistry = registry.category("services");
|
|
const sampleIconUrl = "/web_enterprise/Parent.src/img/default_icon_app.png";
|
|
|
|
function makeFakeUIService({ block = () => {}, unblock = () => {} } = {}) {
|
|
return {
|
|
start(env) {
|
|
const ui = {
|
|
block,
|
|
unblock,
|
|
};
|
|
Object.defineProperty(env, "isSmall", {
|
|
get() {
|
|
return false;
|
|
},
|
|
});
|
|
return ui;
|
|
},
|
|
};
|
|
}
|
|
|
|
async function startAtStep(target, startStep) {
|
|
if (["app", "model", "model_configuration"].includes(startStep)) {
|
|
// From welcome to app
|
|
await click(target, ".o_web_studio_app_creator_next");
|
|
}
|
|
if (["model", "model_configuration"].includes(startStep)) {
|
|
// From app to model
|
|
await editInput(target, "input[name='appName']", "testApp");
|
|
await click(target, ".o_web_studio_app_creator_next");
|
|
}
|
|
if (["model_configuration"].includes(startStep)) {
|
|
// From model to model_configuration
|
|
await editInput(target, "input[name='menuName']", "testMenu");
|
|
await click(target, ".o_web_studio_app_creator_next");
|
|
}
|
|
}
|
|
|
|
async function createAppCreator(params = {}) {
|
|
const onNewAppCreated = params.onNewAppCreated || (() => {});
|
|
|
|
for (const serviceKey in params.services) {
|
|
serviceRegistry.add(serviceKey, params.services[serviceKey], { force: true });
|
|
}
|
|
|
|
const { mockRPC, serverData, startStep } = params;
|
|
const target = getFixture();
|
|
const component = await mount(AppCreator, target, {
|
|
props: { onNewAppCreated },
|
|
env: await makeTestEnv({
|
|
serverData,
|
|
mockRPC,
|
|
}),
|
|
});
|
|
|
|
if (startStep) {
|
|
await startAtStep(target, startStep);
|
|
}
|
|
|
|
return { state: component.state };
|
|
}
|
|
|
|
QUnit.module("AppCreator", (hooks) => {
|
|
let serverData;
|
|
let target;
|
|
hooks.beforeEach(() => {
|
|
target = getFixture();
|
|
IconCreator.enableTransitions = false;
|
|
registerCleanup(() => {
|
|
IconCreator.enableTransitions = true;
|
|
});
|
|
|
|
patchWithCleanup(browser, {
|
|
setTimeout: (fn) => fn(),
|
|
clearTimeout: () => {},
|
|
});
|
|
patchWithCleanup(AutoComplete, {
|
|
timeout: 0,
|
|
});
|
|
|
|
setupViewRegistries();
|
|
|
|
serviceRegistry.add("http", makeFakeHTTPService(), { force: true });
|
|
serviceRegistry.add("ui", makeFakeUIService(), { force: true });
|
|
|
|
serverData = {
|
|
models: {
|
|
"ir.model": {
|
|
fields: {
|
|
display_name: { type: "char" },
|
|
transient: { type: "boolean" },
|
|
abstract: { type: "boolean" },
|
|
},
|
|
records: [{ id: 69, display_name: "The Value" }],
|
|
},
|
|
},
|
|
};
|
|
});
|
|
|
|
QUnit.test("app creator: standard flow with model creation", async (assert) => {
|
|
assert.expect(39);
|
|
|
|
const fakeHttpRequestService = {
|
|
start() {
|
|
return async (route) => {
|
|
if (route === "/web/binary/upload_attachment") {
|
|
assert.step(route);
|
|
return `[{ "id": 666 }]`;
|
|
}
|
|
};
|
|
},
|
|
};
|
|
|
|
const { state } = await createAppCreator({
|
|
serverData,
|
|
services: {
|
|
ui: makeFakeUIService({
|
|
block: () => assert.step("UI blocked"),
|
|
unblock: () => assert.step("UI unblocked"),
|
|
}),
|
|
httpRequest: fakeHttpRequestService,
|
|
http: makeFakeHTTPService(null, async (route) => {
|
|
if (route === "/web/binary/upload_attachment") {
|
|
assert.step(route);
|
|
return `[{ "id": 666 }]`;
|
|
}
|
|
}),
|
|
},
|
|
onNewAppCreated: () => assert.step("new-app-created"),
|
|
mockRPC: async (route, params) => {
|
|
if (typeof route === "object") {
|
|
assert.strictEqual(route.model, "ir.attachment");
|
|
return [{ datas: sampleIconUrl }];
|
|
}
|
|
|
|
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'"
|
|
);
|
|
return true;
|
|
}
|
|
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.data.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_field_warning",
|
|
"Input should be in warning mode"
|
|
);
|
|
|
|
await editInput(target, 'input[name="appName"]', "Kikou");
|
|
assert.doesNotHaveClass(
|
|
appNameInput,
|
|
"o_web_studio_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_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);
|
|
|
|
await createAppCreator({
|
|
serverData,
|
|
startStep: "model_configuration",
|
|
mockRPC: 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'"
|
|
);
|
|
return true;
|
|
}
|
|
},
|
|
});
|
|
|
|
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(17);
|
|
|
|
patchWithCleanup(odoo, { debug: "1" });
|
|
|
|
const { state } = await createAppCreator({
|
|
serverData,
|
|
startStep: "model",
|
|
async mockRPC(route, params) {
|
|
switch (route) {
|
|
case "/web/dataset/call_kw/ir.model/name_search": {
|
|
assert.deepEqual(params.kwargs.args, [
|
|
"&",
|
|
"&",
|
|
["transient", "=", false],
|
|
["abstract", "=", false],
|
|
"!",
|
|
["id", "in", []],
|
|
]);
|
|
assert.step(route);
|
|
assert.strictEqual(
|
|
params.model,
|
|
"ir.model",
|
|
"request should target the right model"
|
|
);
|
|
break;
|
|
}
|
|
case "/web_studio/create_new_app": {
|
|
assert.step(route);
|
|
assert.strictEqual(
|
|
params.model_id,
|
|
69,
|
|
"model id should be the one provided"
|
|
);
|
|
return true;
|
|
}
|
|
}
|
|
},
|
|
});
|
|
|
|
await editInput(target, "input[name='menuName']", "testMenuName");
|
|
|
|
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_menu_creator_model"),
|
|
"o_web_studio_field_warning"
|
|
);
|
|
assert.doesNotHaveClass(buttonNext, "is_ready");
|
|
assert.containsOnce(
|
|
target,
|
|
".o_record_selector",
|
|
"There should be a many2one to select a model"
|
|
);
|
|
|
|
await click(buttonNext);
|
|
|
|
assert.hasClass(
|
|
target.querySelector(".o_web_studio_menu_creator_model"),
|
|
"o_web_studio_field_warning"
|
|
);
|
|
assert.doesNotHaveClass(buttonNext, "is_ready");
|
|
|
|
await editInput(target, ".o_record_selector input", "The");
|
|
await click(target.querySelector(".o-autocomplete--dropdown-item"));
|
|
|
|
assert.strictEqual(
|
|
target.querySelector(".o_record_selector input").value,
|
|
"The Value",
|
|
"Correct value should be selected."
|
|
);
|
|
|
|
assert.doesNotHaveClass(
|
|
target.querySelector(".o_web_studio_menu_creator_model"),
|
|
"o_web_studio_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 } = await createAppCreator({
|
|
serverData,
|
|
services: {
|
|
ui: makeFakeUIService({
|
|
block: () => assert.step("UI blocked"),
|
|
unblock: () => assert.step("UI unblocked"),
|
|
}),
|
|
},
|
|
onNewAppCreated: () => assert.step("new-app-created"),
|
|
async mockRPC(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");
|
|
return true;
|
|
}
|
|
},
|
|
});
|
|
|
|
// step: 'welcome'
|
|
assert.strictEqual(state.step, "welcome", "Current step should be set to welcome");
|
|
|
|
// 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_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"]);
|
|
});
|
|
});
|