/** @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"]); }); });