import { describe, expect, test } from "@odoo/hoot"; import { waitFor, waitForNone } from "@odoo/hoot-dom"; import { animationFrame } from "@odoo/hoot-mock"; import { contains, defineModels, mockService, onRpc, patchWithCleanup, selectFieldDropdownItem, serverState, } from "@web/../tests/web_test_helpers"; import { user } from "@web/core/user"; import { DocumentsModels, getDocumentsTestServerModelsData, makeDocumentRecordData, } from "./helpers/data"; import { makeDocumentsMockEnv } from "./helpers/model"; import { embeddedActionsServerData } from "./helpers/test_server_data"; import { mountDocumentsListView } from "./helpers/views/list"; describe.current.tags("desktop"); defineModels(DocumentsModels); /** * Shortcut for details panel selector * @param selector * @return {`.o_documents_details_panel ${string}`} */ const dp = (selector) => `.o_documents_details_panel ${selector}`; test("Open share with view user_permission", async function () { onRpc("/documents/touch/accessTokenFolder1", () => ({})); const serverData = getDocumentsTestServerModelsData(); const { id: folder1Id, name: folder1Name } = serverData["documents.document"][0]; mockService("document.document", { openSharingDialog: (documentIds) => { expect(documentIds).toEqual([folder1Id]); expect.step("open_share"); }, }); await makeDocumentsMockEnv({ serverData }); await mountDocumentsListView(); await contains(`.o_data_row:contains(${folder1Name}) .o_list_record_selector`).click(); await contains("button:contains(Share)").click(); expect.verifySteps(["open_share"]); }); test("Right panel shows and updates focused or container record only", async function () { onRpc("/documents/touch/", () => ({})); const file2Id = 3; const serverData = getDocumentsTestServerModelsData([ makeDocumentRecordData(2, "File 1", { attachment_id: 1, folder_id: 1 }), makeDocumentRecordData(file2Id, "File 2", { attachment_id: 2, folder_id: 1 }), makeDocumentRecordData(4, "File 3", { attachment_id: 3, folder_id: 1 }), ]); serverData["ir.attachment"] = [ { id: 1, name: "One" }, { id: 2, name: "Two" }, { id: 3, name: "Three" }, ]; const { name: folder1Name } = serverData["documents.document"][0]; onRpc("web_save", ({ args }) => { if (args[0].length === 1 && args[0][0] === file2Id) { expect.step("edit_request_2"); } }); onRpc("ir.model", "display_name_for", ({ args }) => args[0].map((model) => ({ model, display_name: model })) ); await makeDocumentsMockEnv({ serverData }); await mountDocumentsListView(); // Open right panel await contains(".o_control_panel_navigation .fa-info-circle").click(); await waitFor(".documents_chatter_disabled_overlay"); // Focus without selection await contains(`.o_data_row td[name='name']:contains(${folder1Name})`).click(); await animationFrame(); expect(dp(".o_documents_details_panel_name input")).toHaveValue(folder1Name); await contains(`.o_list_renderer`).click(); // de-focus await waitFor(".documents_chatter_disabled_overlay"); // As we're in all/company // Enter folder await contains(`.o_data_row:contains(${folder1Name}) .fa-folder-o`).click(); await animationFrame(); expect(`.o_data_row .o_list_record_selector`).toHaveCount(3); expect(dp(".o_documents_details_panel_name input")).toHaveValue(folder1Name); // Focus without selection await contains(".o_data_row :contains('File 1')").click(); expect(dp(".o_documents_details_panel_name input")).toHaveValue("File 1"); // Unfocus await contains(`.o_list_renderer`).click(); expect(dp(".o_documents_details_panel_name input")).toHaveValue(folder1Name); // select record focuses it await contains(".o_data_row:contains('File 1') .o_list_record_selector").click(); expect(dp(".o_documents_details_panel_name input")).toHaveValue("File 1"); // Focus without selection await contains(".o_data_row :contains('File 2')").click(); expect(dp(".o_documents_details_panel_name input")).toHaveValue("File 2"); // Editing unselected File 2 only await contains(dp(".o_documents_details_panel_name input")).edit("File 4"); // Row is modified, not File 1 await waitFor(".o_data_row :contains('File 4')"); await waitFor(".o_data_row :contains('File 1')"); expect.verifySteps(["edit_request_2"]); }); test("Document actions are hidden when focused record is not selected", async function () { onRpc("/documents/touch/", () => ({})); onRpc("ir.model", "display_name_for", ({ args }) => args[0].map((model) => ({ model, display_name: model })) ); const serverData = getDocumentsTestServerModelsData([ makeDocumentRecordData(2, "File 1", { attachment_id: 1, folder_id: 1 }), makeDocumentRecordData(3, "File 2", { attachment_id: 2, folder_id: 1 }), ]); serverData["ir.attachment"] = [ { id: 1, name: "One" }, { id: 2, name: "Two" }, ]; await makeDocumentsMockEnv({ serverData }); await mountDocumentsListView(); // select record focuses it await contains(".o_data_row:contains('File 1') .o_list_record_selector").click(); // Actions are visible as selection is focused await waitFor(".o_control_panel_actions:contains('Download')"); // Focus without selection await contains(".o_data_row :contains('File 2')").click(); await waitFor(".o_selection_container"); // Actions are no longer visible as focused is not selected await waitFor(".o_control_panel_actions:not(:contains('Download'))"); // Select it to show actions again await contains(".o_data_row:contains('File 2') .o_list_record_selector").click(); await waitFor(".o_control_panel_actions:contains('Download')"); }); test("Actions menu opens after selecting multiple records", async function () { onRpc("/documents/touch/", () => ({})); const serverData = getDocumentsTestServerModelsData([ makeDocumentRecordData(2, "File 1", { attachment_id: 1, folder_id: 1 }), makeDocumentRecordData(3, "File 2", { attachment_id: 2, folder_id: 1 }), ]); serverData["ir.attachment"] = [ { id: 1, name: "One" }, { id: 2, name: "Two" }, ]; await makeDocumentsMockEnv({ serverData }); await mountDocumentsListView(); await contains(".o_data_row:contains('File 1') .o_list_record_selector").click(); await contains(".o_data_row:contains('File 2') .o_list_record_selector").click(); await waitFor(".o_selection_box:contains('2')"); await contains(".o_cp_action_menus button:contains('Actions')").click(); await waitFor(".o-dropdown--menu .o_menu_item"); }); test("only show common available actions", async function () { await makeDocumentsMockEnv({ serverData: embeddedActionsServerData }); await mountDocumentsListView(); await contains(`.o_data_row:contains('Request 1') .o_list_record_selector`).click(); await waitFor(".o_control_panel_actions:contains('Action 1')"); await contains(`.o_data_row:contains('Request 1') .o_list_record_selector`).click(); await contains(`.o_data_row:contains('Request 2') .o_list_record_selector`).click(); await waitForNone(".o_control_panel_actions:contains('Action 1')"); await waitFor(".o_control_panel_actions:contains('Action 2 only')"); await waitFor(".o_control_panel_actions:contains('Action 2 and 3')"); await contains(`.o_data_row:contains('Request 3') .o_list_record_selector`).click(); await waitForNone(".o_control_panel_actions:contains('Action 2 only')"); await waitFor(".o_control_panel_actions:contains('Action 2 and 3')"); }); test("Required document name", async function () { const serverData = getDocumentsTestServerModelsData([ makeDocumentRecordData(2, "Testing file", { folder_id: 1 }), makeDocumentRecordData(3, "Testing folder", { folder_id: 1 }), ]); await makeDocumentsMockEnv({ serverData }); await mountDocumentsListView(); const lr = (documentName, selector) => `.o_data_row:contains('${documentName}') ${selector}`; for (const documentName of ["Testing folder", "Testing file"]) { await contains(lr(documentName, ".o_list_record_selector")).click(); await contains(lr(documentName, ".o_data_cell[name='name']")).click(); await expect(lr(documentName, ".o_data_cell[name='name'] input")).toHaveCount(1); await expect(lr(documentName, ".o_data_cell[name='name'] input")).toHaveValue(documentName); // Set empty name await contains(lr(documentName, ".o_data_cell[name='name'] input")).edit(""); await animationFrame(); expect(".o_notification").toHaveCount(1); expect(".o_notification").toHaveText("Name cannot be empty."); await contains(".o_notification .o_notification_close").click(); await expect(lr(documentName, ".o_data_cell[name='name']")).toHaveText(documentName); await expect(lr(documentName, ".o_data_cell[name='name'] input")).toHaveCount(0); // Remove selection and close record edition await contains(".o_list_renderer").click(); await animationFrame(); } }); test("documents list: don't unselect all when interacting with the headers", async () => { await makeDocumentsMockEnv({ serverData: embeddedActionsServerData }); await mountDocumentsListView(); await contains(".o_data_row:eq(0) .o_list_record_selector input").click(); await contains(".o_data_row:eq(1) .o_list_record_selector input").click(); expect(".o_data_row_selected").toHaveCount(2); await contains("th:eq(2) .o_resize", { visible: false }).dragAndDrop("th:eq(3)"); expect(".o_data_row_selected").toHaveCount(2); }); test("company_id field visibility for internal in multicompany", async function () { serverState.companies = [ { id: 1, name: "Company 1", sequence: 1, parent_id: false, child_ids: [] }, { id: 2, name: "Company 2", sequence: 2, parent_id: false, child_ids: [] }, ]; const serverData = getDocumentsTestServerModelsData(); await makeDocumentsMockEnv({ serverData }); await mountDocumentsListView({ context: { allowed_company_ids: [1, 2], }, }); expect("thead th[data-name='company_id']").toHaveCount(1); }); test("company_id field visibility for portal in multicompany", async function () { serverState.companies = [ { id: 1, name: "Company 1", sequence: 1, parent_id: false, child_ids: [] }, { id: 2, name: "Company 2", sequence: 2, parent_id: false, child_ids: [] }, ]; const testUserGroups = ["base.group_portal", "base.group_multi_company"]; // We need to do this here and not on the model because has_group("base.group_user") // is already in cache before the model method is called the first time. patchWithCleanup(user, { hasGroup: (group) => testUserGroups.includes(group), }); const serverData = getDocumentsTestServerModelsData(); const currentUser = serverData["res.users"].find((u) => u.id === serverState.userId); // Sync server data for consistency even if it is not really used. Object.assign(currentUser, { group_ids: [], share: true }); await makeDocumentsMockEnv({ serverData }); await mountDocumentsListView({ context: { allowed_company_ids: [1, 2], }, }); expect("thead th[data-name='company_id']").toHaveCount(0); }); test("file sharing via link with multiple subfolders", async function () { let accessFolder1 = false; let accessFolder2 = false; let addFolder4 = false; onRpc("/documents/touch/accessTokenFolder1", () => { expect.step("touch 1"); accessFolder1 = true; return { reload: true }; }); onRpc("/documents/touch/accessTokenFolder2", () => { expect.step("touch 2"); accessFolder2 = true; return { reload: true }; }); onRpc("/documents/touch/accessTokenFolder3", () => { return { reload: true }; }); onRpc("/documents/touch/accessTokenFolder4", () => { return { reload: true }; }); // Set active true/false to control the folders display const folder2 = makeDocumentRecordData(2, "Folder 2", { type: "folder", is_folder: true, folder_id: 1, access_token: "accessTokenFolder2", active: false, }); const folder3 = makeDocumentRecordData(3, "Folder 3", { type: "folder", is_folder: true, folder_id: 2, access_token: "accessTokenFolder3", active: false, }); const folder4 = makeDocumentRecordData(4, "Folder 4", { type: "folder", is_folder: true, folder_id: 2, access_token: "accessTokenFolder4", active: false, }); const serverData = getDocumentsTestServerModelsData([folder2, folder3, folder4]); const docEnv = await makeDocumentsMockEnv({ serverData }); const activateFolders = ({ args }) => { folder2.active = accessFolder1; folder3.active = accessFolder2; if (addFolder4) { folder4.active = accessFolder2; // Ensure only one folder button available folder3.active = false; } }; onRpc("search_panel_select_range", activateFolders); onRpc("web_search_read", activateFolders); const docService = docEnv.services["document.document"]; // Avoid logAccess 1000ms debounce timer patchWithCleanup(docService, { logAccess: (token) => docService._logAccess(token), // Avoid duplicates due to focusRecord logAccess with no debounce focusRecord: () => false, }); await mountDocumentsListView(); await contains(`.o_search_panel_label[data-tooltip="Company"] .o_toggle_fold`).click(); expect(`.o_data_row .o_field_cell[name="name"]:contains("Folder 1")`).toHaveCount(1); await contains(`.o_data_row .o_field_cell .o_field_documents_type_icon`).click(); expect(`.o_data_row .o_field_cell[name="name"]:contains("Folder 2")`).toHaveCount(1); await contains(`.o_data_row .o_field_cell .o_field_documents_type_icon`).click(); expect(`.o_data_row .o_field_cell[name="name"]:contains("Folder 3")`).toHaveCount(1); await contains(`.o_data_row .o_field_cell .o_field_documents_type_icon`).click(); expect.verifySteps(["touch 1", "touch 2"]); expect(`.o_search_panel_label[data-tooltip="Folder 4"]`).toHaveCount(0); // New sub-folder added without reloading addFolder4 = true; await contains(`.o_search_panel_label_title:contains("Folder 2")`).click(); expect(`.o_search_panel_label[data-tooltip="Folder 4"]`).toHaveCount(0); expect(`.o_data_row .o_field_cell[name="name"]:contains("Folder 4")`).toHaveCount(1); await contains(`.o_data_row .o_field_cell .o_field_documents_type_icon`).click(); expect(`.o_search_panel_label[data-tooltip="Folder 4"]`).toHaveCount(1); expect.verifySteps(["touch 2"]); }); test("edit mode is preserved when interacting with modals or control panel", async function () { onRpc("/documents/touch/", () => ({})); const serverData = getDocumentsTestServerModelsData(); await makeDocumentsMockEnv({ serverData }); await mountDocumentsListView(); // Enter edit mode and open "Search More" modal await contains(".o_data_row:nth-child(1) td.o_list_record_selector").click(); expect(".o_data_row_selected").toHaveCount(1); await contains('.o_data_row:nth-child(1) td.o_list_many2one[name="partner_id"]').click(); await selectFieldDropdownItem("partner_id", "Search more..."); // Clicks inside the modal do not exit edit mode await contains(`.modal-title:contains("Search: Related partner")`).click(); await animationFrame(); expect(".modal-title").toHaveText("Search: Related partner"); expect(".o_data_row_selected").toHaveCount(1); await contains(".modal-footer .btn-secondary").click(); // Clicks near searchbar exit edit mode await contains(".o_control_panel_breadcrumbs").click(); expect(".o_list_button_discard").toHaveCount(0); expect(".o_data_row_selected").toHaveCount(1); // Click below the list panel unselects the row await contains(".o_renderer_with_searchpanel").click(); expect(".o_data_row_selected").toHaveCount(0); });