base_import/static/tests/import_action_tests.js

1533 lines
59 KiB
JavaScript
Raw Permalink Normal View History

/** @odoo-module */
import { browser } from "@web/core/browser/browser";
import {
click,
editInput,
editSelect,
editSelectMenu,
getFixture,
nextTick,
patchWithCleanup,
} from "@web/../tests/helpers/utils";
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
import { registry } from "@web/core/registry";
import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services";
import { ImportDataProgress } from "../src/import_data_progress/import_data_progress";
import { ImportAction } from "../src/import_action/import_action";
import { ImportBlockUI } from "../src/import_block_ui";
import { useEffect } from "@odoo/owl";
const serviceRegistry = registry.category("services");
// -----------------------------------------------------------------------------
//#region Helpers
// -----------------------------------------------------------------------------
let serverData;
let target;
let totalRows;
function registerFakeHTTPService(validate = (route, params) => {}) {
const fakeHTTPService = {
start() {
return {
post: (route, params) => {
validate(route, params);
const file = {
id: 10,
name: params.ufile[0].name,
mimetype: "text/plain",
};
return JSON.stringify([file]);
},
};
},
};
serviceRegistry.add("http", fakeHTTPService);
}
function getFieldsTree(serverData) {
const fields = Object.entries(serverData.models.partner.fields);
fields.forEach(([k, v]) => {
v.id = k;
v.fields = [];
});
const mappedFields = fields.map((e) => e[1]);
return mappedFields.filter((e) => ["id", "__last_update", "name"].includes(e.id) === false);
}
function getMatches(serverData, headers) {
// basic implementation for testing purposes which matches if the first line is the
// name of a field, or corresponds to the string value of a field from serverData
const matches = [];
for (const header of headers) {
if (serverData.models.partner.fields[header]) {
matches.push([header]);
}
const serverDataIndex = Object.values(serverData.models.partner.fields).findIndex(
(e) => e.string === header
);
if (serverDataIndex !== -1) {
matches.push([Object.keys(serverData.models.partner.fields)[serverDataIndex]]);
}
}
return Object.assign({}, matches);
}
async function executeImport(data, shouldWait = false) {
const res = {
ids: [],
};
const matching = data[1].filter((f) => f !== false);
if (matching.length) {
res.ids.push(1);
} else {
res.messages = [
{
type: "error",
not_matching_error: true,
message: "You must configure at least one field to import",
},
];
}
if (data[3].skip + 1 < (data[3].has_headers ? totalRows - 1 : totalRows)) {
res.nextrow = data[3].skip + data[3].limit;
}
if (shouldWait) {
// make sure the progress bar is shown
await nextTick();
}
return res;
}
function parsePreview(opts) {
const fakePreviewData = [
["Foo", "Deco addict", "Azure Interior", "Brandon Freeman"],
["Bar", "1", "1", "0"],
["Display name", "Azure Interior"],
];
const headers = opts.has_headers && fakePreviewData.map((col) => col[0]);
totalRows = [...fakePreviewData].sort((a, b) => (a.length > b.length ? -1 : 1))[0].length;
return Promise.resolve({
advanced_mode: opts.advanced,
batch: false,
fields: getFieldsTree(serverData),
file_length: opts.has_headers ? totalRows - 1 : totalRows,
header_types: false,
headers: headers,
matches: opts.has_headers && getMatches(serverData, headers),
options: {
...opts,
sheet: opts.sheet.length ? opts.sheet : "Template",
sheets: ["Template", "Template 2"],
},
preview: opts.has_headers
? fakePreviewData.map((col) => col.shift() && col)
: [...fakePreviewData],
});
}
function customParsePreview(opts, { fields, headers, rowCount, matches, preview }) {
totalRows = rowCount;
return Promise.resolve({
advanced_mode: opts.advanced,
batch: false,
fields: fields,
file_length: opts.has_headers ? totalRows - 1 : totalRows,
header_types: false,
headers: headers,
matches: matches,
options: {
...opts,
sheet: opts.sheet.length ? opts.sheet : "Template",
sheets: ["Template", "Template 2"],
},
preview: preview,
});
}
// since executing a real import would be difficult, this method simply returns
// some error messages to help testing the UI
function executeFailingImport(field, isMultiline, field_path = "") {
let moreInfo = [];
if (serverData.models.partner.fields[field].type === "selection") {
moreInfo = serverData.models.partner.fields[field].selection;
}
return {
ids: false,
messages: isMultiline
? [
{
field,
field_name: serverData.models.partner.fields[field].string,
field_path,
message: "Invalid value",
moreInfo,
record: 0,
rows: { from: 0, to: 0 },
value: "Invalid value",
priority: "info",
},
{
field,
field_name: serverData.models.partner.fields[field].string,
field_path,
message: "Duplicate value",
moreInfo,
record: 0,
rows: { from: 1, to: 1 },
priority: "error",
},
{
field,
field_name: serverData.models.partner.fields[field].string,
field_path,
message: "Wrong values",
moreInfo,
record: 0,
rows: { from: 2, to: 3 },
priority: "warning",
},
{
field,
field_name: serverData.models.partner.fields[field].string,
field_path,
message: "Bad value here",
moreInfo,
record: 0,
rows: { from: 4, to: 4 },
value: "Bad value",
priority: "warning",
},
{
field,
field_name: serverData.models.partner.fields[field].string,
field_path,
message: "Duplicate value",
moreInfo,
record: 0,
rows: { from: 5, to: 5 },
priority: "error",
},
]
: [
{
field,
field_name: serverData.models.partner.fields[field].string,
field_path,
message: "Incorrect value",
moreInfo,
record: 0,
rows: { from: 0, to: 0 },
},
],
name: ["Some invalid content", "Wrong content", "Bad content"],
nextrow: 0,
};
}
async function createImportAction(customRouter = {}) {
const router = {
"/web/dataset/call_kw/partner/get_import_templates": (route, args) => Promise.resolve([]),
"/web/dataset/call_kw/base_import.import/parse_preview": (route, args) =>
parsePreview(args[1]),
"/web/dataset/call_kw/base_import.import/execute_import": (route, args) =>
executeImport(args),
"/web/dataset/call_kw/base_import.import/create": (route, args) => Promise.resolve(11),
"base_import.import/get_fields": (route, args) =>
Promise.resolve(serverData.models.partner.fields),
};
for (const key in customRouter) {
router["/web/dataset/call_kw/" + key] = customRouter[key];
}
const webClient = await createWebClient({
serverData,
mockRPC: function (route, { args }) {
if (route in router) {
return router[route](route.replace("/web/dataset/call_kw/", ""), args);
}
},
});
await doAction(webClient, 1);
}
// -----------------------------------------------------------------------------
//#endregion
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
QUnit.module("Base Import Tests", (hooks) => {
hooks.beforeEach(async () => {
target = getFixture();
serverData = {
actions: {
1: {
name: "Import Data",
tag: "import",
target: "current",
type: "ir.actions.client",
params: {
model: "partner",
},
},
},
models: {
partner: {
fields: {
display_name: { string: "Display name", type: "char" },
foo: { string: "Foo", type: "char" },
bar: { string: "Bar", type: "boolean", model_name: "partner" },
selection: {
string: "Selection",
type: "selection",
selection: [
["item_1", "First Item"],
["item_2", "Second item"],
],
model_name: "partner",
},
many2many_field: {
string: "Many2Many",
type: "many2many",
relation: "partner",
comodel_name: "comodel.test",
},
},
records: [],
},
},
};
target = getFixture();
});
QUnit.module("ImportAction");
QUnit.test("Import view: UI before file upload", async function (assert) {
const templateURL = "/myTemplateURL.xlsx";
await createImportAction({
"partner/get_import_templates": (route, args) => {
assert.step(route);
return Promise.resolve([
{
label: "Some Import Template",
template: templateURL,
},
]);
},
"base_import.import/create": (route, args) => {
assert.step(route);
return Promise.resolve(11);
},
});
assert.containsOnce(target, ".o_import_action", "import view is displayed");
assert.strictEqual(
target.querySelector(".o_nocontent_help .btn-outline-primary").textContent,
" Some Import Template"
);
assert.strictEqual(
target.querySelector(".o_nocontent_help .btn-outline-primary").href,
window.location.origin + templateURL,
"button has the right download url"
);
assert.verifySteps(["partner/get_import_templates", "base_import.import/create"]);
// Contains invisible mobile buttons
assert.containsN(
target,
".o_control_panel button",
5,
"only two buttons are visible by default"
);
});
QUnit.test("Import view: import a file with multiple sheets", async function (assert) {
registerFakeHTTPService((route, params) => {
assert.strictEqual(route, "/base_import/set_file");
assert.strictEqual(
params.ufile[0].name,
"fake_file.xlsx",
"file is correctly uploaded to the server"
);
});
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
await createImportAction({
"partner/get_import_templates": (route, args) => {
assert.step(route);
return Promise.resolve([]);
},
"base_import.import/parse_preview": (route, args) => {
assert.step(route);
return parsePreview(args[1]);
},
"base_import.import/create": (route, args) => {
assert.step(route);
return Promise.resolve(11);
},
});
// Set and trigger the change of a file for the input
const file = new File(["fake_file"], "fake_file.xlsx", { type: "text/plain" });
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
assert.verifySteps([
"partner/get_import_templates",
"base_import.import/create",
"base_import.import/parse_preview",
]);
assert.containsOnce(
target,
".o_import_action .o_import_data_sidepanel",
"side panel is visible"
);
assert.containsOnce(
target,
".o_import_action .o_import_data_content",
"content panel is visible"
);
assert.strictEqual(
target.querySelector(".o_import_data_sidepanel .fst-italic.truncate").textContent,
"fake_file",
"filename is shown and can be truncated"
);
assert.strictEqual(
target.querySelector(".o_import_data_sidepanel .fst-italic:not(.truncate)").textContent,
".xlsx",
"file extension is displayed on its own"
);
assert.strictEqual(
target.querySelector(".o_import_data_sidepanel [name=o_import_sheet]")
.selectedOptions[0].textContent,
"Template",
"first sheet is selected by default"
);
assert.containsN(
target,
".o_import_data_content tbody > tr",
3,
"recognized values are displayed in the view"
);
assert.strictEqual(
target.querySelector(".o_import_data_content tr:first-child td span:first-child")
.textContent,
"Foo",
"column title is shown"
);
assert.strictEqual(
target.querySelector(".o_import_data_content tr:first-child td span:nth-child(2)")
.textContent,
"Deco addict",
"first example is shown"
);
assert.strictEqual(
target.querySelector(".o_import_data_content tr:first-child td span:nth-child(2)")
.dataset.tooltipInfo,
'{"lines":["Deco addict","Azure Interior","Brandon Freeman"]}',
"tooltip contains other examples"
);
assert.containsNone(
target,
".o_import_data_content tbody td:nth-child(3) .alert-info",
"no comments are shown"
);
// Select a field already selected for another column
await editSelectMenu(target, ".o_import_data_content .o_select_menu", "Display name");
assert.containsN(
target,
".o_import_data_content tbody td:nth-child(3) .alert-info",
2,
"two comments are shown"
);
assert.strictEqual(
target.querySelector(".o_import_data_content tbody td:nth-child(3) .alert-info")
.textContent,
"This column will be concatenated in field Display name"
);
// Preview the second sheet
await editSelect(target, ".o_import_data_sidepanel [name=o_import_sheet]", "Template 2");
assert.verifySteps(
["base_import.import/parse_preview"],
"changing sheet has sent a new parse_preview request"
);
assert.strictEqual(
target.querySelector(".o_import_data_sidepanel [name=o_import_sheet]")
.selectedOptions[0].textContent,
"Template 2",
"second sheet is now selected"
);
assert.containsNone(
target,
".o_import_data_content tbody td:nth-child(3) .alert-info",
"no comments are shown"
);
});
QUnit.test("Import view: default import options are correctly", async function (assert) {
registerFakeHTTPService();
await createImportAction({
"base_import.import/parse_preview": async (route, args) => {
return parsePreview(args[1]);
},
"base_import.import/execute_import": (route, args) => {
assert.step("execute_import");
assert.deepEqual(
args[3],
{
advanced: true,
date_format: "",
datetime_format: "",
encoding: "",
fallback_values: {},
float_decimal_separator: ".",
float_thousand_separator: ",",
has_headers: true,
import_set_empty_fields: [],
import_skip_records: [],
keep_matches: false,
limit: 2000,
name_create_enabled_fields: {},
quoting: '"',
separator: "",
sheet: "Template",
sheets: ["Template", "Template 2"],
skip: 0,
tracking_disable: true,
},
"options are defaulted as expected"
);
return executeImport(args);
},
});
// Set and trigger the change of a file for the input
const file = new File(["fake_file"], "fake_file.xls", { type: "text/plain" });
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
await click(
target.querySelector(".o_control_panel_main_buttons .d-none button:first-child")
);
assert.verifySteps(["execute_import"]);
});
QUnit.test("Import view: import a CSV file with one sheet", async function (assert) {
registerFakeHTTPService((route, params) => {
assert.strictEqual(route, "/base_import/set_file");
assert.strictEqual(
params.ufile[0].name,
"fake_file.csv",
"file is correctly uploaded to the server"
);
});
await createImportAction();
// Set and trigger the change of a file for the input
const file = new File(["fake_file"], "fake_file.csv", { type: "text/plain" });
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
assert.containsOnce(
target,
".o_import_data_sidepanel .o_import_formatting",
"formatting options are present in the side panel"
);
assert.containsOnce(
target,
".o_import_action .o_import_data_content",
"content panel is visible"
);
});
QUnit.test("Import view: additional options in debug", async function (assert) {
patchWithCleanup(odoo, { debug: true });
registerFakeHTTPService();
await createImportAction({
"base_import.import/parse_preview": (route, args) => {
assert.strictEqual(
args[1].advanced,
true,
"in debug, advanced_mode is set in parse_preview"
);
return parsePreview(args[1]);
},
});
// Set and trigger the change of a file for the input
const file = new File(["fake_file"], "fake_file.csv", { type: "text/plain" });
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
await nextTick();
assert.containsOnce(
target,
".o_import_data_sidepanel .o_import_debug_options",
"additional options are present in the side panel in debug mode"
);
});
QUnit.test(
"Import view: execute import with option 'use first row as headers'",
async function (assert) {
registerFakeHTTPService();
const notificationMock = (message) => {
assert.step(message);
return () => {};
};
registry
.category("services")
.add("notification", makeFakeNotificationService(notificationMock), {
force: true,
});
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
await createImportAction({
"base_import.import/parse_preview": async (route, args) => {
assert.step(route);
return parsePreview(args[1]);
},
"base_import.import/execute_import": (route, args) => {
assert.step(route);
return executeImport(args);
},
});
// Set and trigger the change of a file for the input
const file = new File(["fake_file"], "fake_file.xls", { type: "text/plain" });
await editInput(
target,
".o_control_panel_main_buttons .d-none input[type='file']",
file
);
assert.strictEqual(
target.querySelector(".o_import_data_sidepanel input[type=checkbox]").checked,
true,
"by default, the checkbox is enabled"
);
assert.verifySteps(["base_import.import/parse_preview"]);
assert.strictEqual(
target.querySelector(".o_import_data_content tr:first-child td span:first-child")
.textContent,
"Foo",
"first row is used as column title"
);
assert.strictEqual(
target.querySelector(".o_import_data_content .o_select_menu").textContent,
"Foo",
"as the column header could match with a database field, it is selected by default"
);
await click(target.querySelector(".o_import_data_sidepanel input[type=checkbox]"));
assert.verifySteps(["base_import.import/parse_preview"]);
assert.strictEqual(
target.querySelector(".o_import_data_content tr:first-child td span:first-child")
.textContent,
"Foo, Deco addict, Azure Interior, Brandon Freeman",
"column title is shown as a list of rows elements"
);
assert.strictEqual(
target.querySelector(".o_import_data_content .o_select_menu").textContent,
"To import, select a field...",
"as the column couldn't match with the database, user must make a choice"
);
await click(
target.querySelector(".o_control_panel_main_buttons .d-none button:first-child")
);
assert.containsNone(
target,
".o_notification_body",
"should not display a notification"
);
assert.verifySteps(["base_import.import/execute_import"]);
assert.containsOnce(
target,
".o_import_data_content .alert-info",
"if no fields were selected to match, the import fails with a message"
);
assert.containsOnce(
target,
".o_import_data_content .alert-danger",
"an error is also displayed"
);
assert.strictEqual(
target.querySelector(".o_import_data_content .alert-danger").textContent,
"You must configure at least one field to import"
);
await editSelectMenu(target, ".o_import_data_content .o_select_menu", "Display name");
await click(
target.querySelector(".o_control_panel_main_buttons .d-none button:first-child")
);
assert.verifySteps([
"base_import.import/execute_import",
"1 records successfully imported",
]);
}
);
QUnit.test("Import view: import data that don't match (selection)", async function (assert) {
serverData.models.partner.fields.selection.required = true;
let shouldFail = true;
registerFakeHTTPService();
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
await createImportAction({
"base_import.import/execute_import": (route, args) => {
if (shouldFail) {
shouldFail = false;
return executeFailingImport(args[1][0]);
}
assert.deepEqual(
args[3].fallback_values,
{
selection: {
fallback_value: "item_2",
field_model: "partner",
field_type: "selection",
},
},
"selected fallback value has been given to the request"
);
return executeImport(args);
},
});
// Set and trigger the change of a file for the input
const file = new File(["fake_file"], "fake_file.xlsx", { type: "text/plain" });
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
// For this test, we force the display of an error message if this field is set
await editSelectMenu(target, ".o_import_data_content .o_select_menu", "Selection");
await click(
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
);
assert.strictEqual(
target.querySelector(".o_import_data_content .alert-danger").textContent,
"The file contains blocking errors (see below)",
"a message is shown if the import was blocked"
);
assert.strictEqual(
target.querySelector(".o_import_report p").textContent,
"Incorrect value",
"the message is displayed in the view"
);
assert.containsOnce(
target,
".o_import_field_selection",
"an action can be set when the column cannot match a field"
);
assert.strictEqual(
target.querySelector(".o_import_field_selection select").textContent,
"Prevent importSet to: First ItemSet to: Second item",
"'skip' option is not available, since the field is required"
);
assert.strictEqual(
target.querySelector(".o_import_field_selection select").selectedOptions[0].textContent,
"Prevent import",
"prevent option is selected by default"
);
editSelect(target, ".o_import_field_selection select", "item_2");
await click(
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
);
assert.strictEqual(
target.querySelector(".o_import_data_content .alert-info").textContent,
"Everything seems valid.",
"import is now successful"
);
assert.containsOnce(
target,
".o_import_field_selection",
"options are still present to change the action to do when the column don't match"
);
});
QUnit.test("Import view: import data that don't match (boolean)", async function (assert) {
let shouldFail = true;
registerFakeHTTPService();
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
await createImportAction({
"base_import.import/execute_import": (route, args) => {
if (shouldFail) {
shouldFail = false;
return executeFailingImport(args[1][0]);
}
assert.deepEqual(
args[3].fallback_values,
{
bar: {
fallback_value: "false",
field_model: "partner",
field_type: "boolean",
},
},
"selected fallback value has been given to the request"
);
return executeImport(args);
},
});
// Set and trigger the change of a file for the input
const file = new File(["fake_file"], "fake_file.xlsx", { type: "text/plain" });
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
// For this test, we force the display of an error message if this field is set
await editSelectMenu(target, ".o_import_data_content .o_select_menu", "Bar");
await click(
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
);
assert.strictEqual(
target.querySelector(".o_import_data_content .alert-danger").textContent,
"The file contains blocking errors (see below)",
"a message is shown if the import was blocked"
);
assert.strictEqual(
target.querySelector(".o_import_field_boolean select").textContent,
"Prevent importSet to: FalseSet to: TrueSkip record",
"options are 'prevent', choose a default boolean value or 'skip'"
);
editSelect(target, ".o_import_field_boolean select", "false");
await click(
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
);
assert.strictEqual(
target.querySelector(".o_import_data_content .alert-info").textContent,
"Everything seems valid.",
"import is now successful"
);
});
QUnit.test("Import view: import data that don't match (many2many)", async function (assert) {
let executeCount = 0;
registerFakeHTTPService();
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
await createImportAction({
"base_import.import/execute_import": (route, args) => {
executeCount++;
if (executeCount === 1) {
return executeFailingImport(args[1][0]);
}
if (executeCount === 2) {
assert.deepEqual(
args[3].name_create_enabled_fields,
{
many2many_field: true,
},
"selected fallback value has been given to the request"
);
} else {
assert.deepEqual(
args[3].name_create_enabled_fields,
{},
"selected fallback value has been given to the request"
);
assert.deepEqual(
args[3].import_skip_records,
["many2many_field"],
"selected fallback value has been given to the request"
);
}
return executeImport(args);
},
});
// Set and trigger the change of a file for the input
const file = new File(["fake_file"], "fake_file.xlsx", { type: "text/plain" });
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
// For this test, we force the display of an error message if this field is set
await editSelectMenu(target, ".o_import_data_content .o_select_menu", "Many2Many");
await click(
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
);
assert.strictEqual(
target.querySelector(".o_import_data_content .alert-danger").textContent,
"The file contains blocking errors (see below)",
"a message is shown if the import was blocked"
);
assert.strictEqual(
target.querySelector(".o_import_field_many2many select").textContent,
"Prevent importSet value as emptySkip recordCreate new values",
"options are 'prevent', choose a default boolean value or 'skip'"
);
editSelect(target, ".o_import_field_many2many select", "name_create_enabled_fields");
await click(
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
);
assert.strictEqual(
target.querySelector(".o_import_data_content .alert-info").textContent,
"Everything seems valid.",
"import is now successful"
);
editSelect(target, ".o_import_field_many2many select", "import_skip_records");
await click(
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
);
assert.strictEqual(
target.querySelector(".o_import_data_content .alert-info").textContent,
"Everything seems valid.",
"import is still successful"
);
});
QUnit.test("Import view: import messages are grouped and sorted", async function (assert) {
const fakeHTTPService = {
start() {
return {
post: (route, params) => {
const file = {
id: 10,
name: params.ufile[0].name,
mimetype: "text/plain",
};
return JSON.stringify([file]);
},
};
},
};
serviceRegistry.add("http", fakeHTTPService);
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
const webClient = await createWebClient({
serverData,
mockRPC: function (route, { args }) {
if (route === "/web/dataset/call_kw/partner/get_import_templates") {
return Promise.resolve([]);
}
if (route === "/web/dataset/call_kw/base_import.import/parse_preview") {
return parsePreview(args[1]);
}
if (route === "/web/dataset/call_kw/base_import.import/get_fields") {
assert.step(route);
return Promise.resolve(serverData.models.partner.fields);
}
if (route === "/web/dataset/call_kw/base_import.import/execute_import") {
return executeFailingImport(args[1][0], true);
}
if (route === "/web/dataset/call_kw/base_import.import/create") {
return Promise.resolve(11);
}
},
});
await doAction(webClient, 1);
// Set and trigger the change of a file for the input
const file = new File(["fake_file"], "fake_file.xlsx", { type: "text/plain" });
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
await click(
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(1)")
);
assert.strictEqual(
target.querySelector(".o_import_data_content .alert-danger").textContent,
"The file contains blocking errors (see below)",
"a message is shown if the import was blocked"
);
// Check that errors have been sorted and grouped
assert.strictEqual(
target.querySelector(".o_import_report p").textContent.trim().toLowerCase(),
"multiple errors occurred in field foo:"
);
assert.strictEqual(
target.querySelector(".o_import_report li:first-child").textContent.trim(),
"Duplicate value at multiple rows"
);
assert.strictEqual(
target.querySelector(".o_import_report li:nth-child(2)").textContent.trim(),
"Wrong values at multiple rows"
);
assert.strictEqual(
target.querySelector(".o_import_report li:nth-child(3)").textContent.trim(),
"Bad value at row 5"
);
assert.containsN(target, ".o_import_report li", 3, "only 3 errors are visible by default");
assert.strictEqual(
target.querySelector(".o_import_report_count").textContent.trim(),
"1 more"
);
await click(target, ".o_import_report_count");
assert.strictEqual(
target.querySelector(".o_import_report_count + li").textContent.trim(),
"Invalid value at row 1 (Some invalid content)"
);
});
QUnit.test("Import view: test import in batches", async function (assert) {
let executeImportCount = 0;
registerFakeHTTPService();
patchWithCleanup(ImportAction.prototype, {
get isBatched() {
// make sure the UI displays the batched import options
return true;
},
});
await createImportAction({
"base_import.import/execute_import": (route, args) => {
assert.deepEqual(
args[1],
["foo", "bar", "display_name"],
"param contains the list of matching fields"
);
assert.deepEqual(
args[2],
["foo", "bar", "display name"],
"param contains the list of associated columns"
);
assert.strictEqual(
args[3].limit,
1,
"limit option is equal to the value set in the view"
);
assert.strictEqual(
args[3].skip,
executeImportCount * args[3].limit,
"skip option increments at each import"
);
executeImportCount++;
return executeImport(args);
},
});
// Set and trigger the change of a file for the input
const file = new File(["fake_file"], "fake_file.xls", { type: "text/plain" });
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
assert.strictEqual(
target.querySelector("input#o_import_batch_limit").value,
"2000",
"by default, the batch limit is set to 2000 rows"
);
assert.strictEqual(
target.querySelector("input#o_import_row_start").value,
"1",
"by default, the import starts at line 1"
);
await editInput(target, "input#o_import_batch_limit", 1);
await click(
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
);
assert.strictEqual(
target.querySelector(".o_import_data_content .alert-info").textContent,
"Everything seems valid.",
"a message is shown if the import test was successfull"
);
assert.strictEqual(executeImportCount, 3, "execute_import was called 3 times");
});
QUnit.test("Import view: execute and pause import in batches", async function (assert) {
registerFakeHTTPService();
patchWithCleanup(ImportAction.prototype, {
get isBatched() {
// make sure the UI displays the batched import options
return true;
},
});
patchWithCleanup(ImportBlockUI.prototype, {
setup() {
super.setup();
if (this.props.message === "Importing") {
assert.step("Block UI received the right text");
}
},
});
patchWithCleanup(ImportDataProgress.prototype, {
setup() {
super.setup();
useEffect(
() => {
if (this.props.importProgress.step === 1) {
// Trigger a pause at this step to resume later from the view
assert.step("pause triggered during step 2");
this.interrupt();
}
},
() => [this.props.importProgress.step]
);
assert.strictEqual(
this.props.totalSteps,
3,
"progress bar receives the number of steps"
);
assert.deepEqual(
this.props.importProgress,
{
value: 0,
step: 1,
},
"progress status has been given to the progress bar"
);
},
});
await createImportAction({
"base_import.import/execute_import": (route, args) => executeImport(args, true),
});
// Set and trigger the change of a file for the input
const file = new File(["fake_file"], "fake_file.xls", { type: "text/plain" });
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
await editInput(target, "input#o_import_batch_limit", 1);
await click(
target.querySelector(".o_control_panel_main_buttons .d-none button:first-child")
);
// Since a nextTick is added to each batch, we must wait twice before the end of the second batch
await nextTick();
await nextTick();
assert.verifySteps(["Block UI received the right text", "pause triggered during step 2"]);
assert.containsOnce(
target,
".o_import_data_content div .alert-warning",
"a message is shown to indicate the user to resume from the third row"
);
assert.strictEqual(
target.querySelector(".o_import_data_content .alert-warning b:first-child").textContent,
"Click 'Resume' to proceed with the import, resuming at line 2.",
"a message is shown to indicate the user to resume from the third row"
);
assert.strictEqual(
target.querySelector(".o_control_panel_main_buttons .d-none button:first-child")
.textContent,
"Resume",
"button contains the right text"
);
assert.strictEqual(
target.querySelector("input#o_import_row_start").value,
"2",
"the import will resume at line 2"
);
assert.strictEqual(
target.querySelector(".o_notification_body").textContent,
"1 records successfully imported",
"display a notification with the quantity of imported values"
);
});
QUnit.test("Import view: test and pause import in batches", async function (assert) {
registerFakeHTTPService();
patchWithCleanup(ImportAction.prototype, {
get isBatched() {
// make sure the UI displays the batched import options
return true;
},
});
patchWithCleanup(ImportBlockUI.prototype, {
setup() {
super.setup();
if (this.props.message === "Testing") {
assert.step("Block UI received the right text");
}
},
});
patchWithCleanup(ImportDataProgress.prototype, {
setup() {
super.setup();
useEffect(
() => {
if (this.props.importProgress.step === 1) {
// Trigger a pause at this step to resume later from the view
assert.step("pause triggered during step 2");
this.interrupt();
}
},
() => [this.props.importProgress.step]
);
assert.strictEqual(
this.props.totalSteps,
3,
"progress bar receives the number of steps"
);
assert.deepEqual(
this.props.importProgress,
{
value: 0,
step: 1,
},
"progress status has been given to the progress bar"
);
},
});
await createImportAction({
"base_import.import/execute_import": (route, args) => executeImport(args, true),
});
// Set and trigger the change of a file for the input
const file = new File(["fake_file"], "fake_file.xls", { type: "text/plain" });
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
await editInput(target, "input#o_import_batch_limit", 1);
await click(
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
);
// Since a nextTick is added to each batch, we must wait twice before the end of the second batch
await nextTick();
await nextTick();
assert.verifySteps(["Block UI received the right text", "pause triggered during step 2"]);
assert.strictEqual(
target.querySelector(".o_import_data_content .alert-info").textContent,
"Everything seems valid."
);
assert.strictEqual(
target.querySelector(".o_control_panel_main_buttons .d-none button:first-child")
.textContent,
"Import",
"after testing, 'Resume' text is not shown"
);
assert.strictEqual(
target.querySelector("input#o_import_row_start").value,
"1",
"the import will resume at line 1"
);
});
QUnit.test(
"Import view: relational fields correctly mapped on preview",
async function (assert) {
assert.expect(4);
registerFakeHTTPService();
await createImportAction({
"base_import.import/parse_preview": async (route, args) => {
return customParsePreview(args[1], {
fields: [
{ id: "id", name: "id", string: "External ID", fields: [], type: "id" },
{
id: "display_name",
name: "display_name",
string: "Display Name",
fields: [],
type: "id",
},
{
id: "many2many_field",
name: "many2many_field",
string: "Many2Many",
fields: [
{
id: "id",
name: "id",
string: "External ID",
fields: [],
type: "id",
},
],
type: "id",
},
],
headers: ["id", "display_name", "many2many_field/id"],
rowCount: 5,
matches: {
0: ["id"],
1: ["display_name"],
2: ["many2many_field", "id"],
},
preview: [
["0", "1", "2"],
["Name 1", "Name 2", "Name 3"],
["", "1", "2"],
],
});
},
"base_import.import/execute_import": (route, args) => {
assert.deepEqual(
args[1],
["id", "display_name", "many2many_field/id"],
"The proper arguments are given for the import"
);
assert.deepEqual(
args[2],
["id", "display_name", "many2many_field/id"],
"The proper arguments are given for the import"
);
return executeImport(args);
},
});
// Set and trigger the change of a file for the input
const file = new File(["fake_file"], "fake_file.xls", { type: "text/plain" });
await editInput(
target,
".o_control_panel_main_buttons .d-none input[type='file']",
file
);
assert.strictEqual(
target.querySelector(
"tr:nth-child(3) .o_import_file_column_cell span.text-truncate"
).innerText,
"many2many_field/id",
"The third row should be the relational field"
);
assert.strictEqual(
target.querySelector("tr:nth-child(3) .o_select_menu_toggler_slot span").innerText,
"Many2Many / External ID",
"The relational field should be selected by default and the name should be the full path."
);
await click(
target.querySelector(".o_control_panel_main_buttons .d-none button:first-child")
);
}
);
QUnit.test("Import view: import errors with relational fields", async function (assert) {
registerFakeHTTPService();
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
await createImportAction({
"base_import.import/parse_preview": async (route, args) => {
return customParsePreview(args[1], {
fields: [
{ id: "id", name: "id", string: "External ID", fields: [], type: "id" },
{
id: "display_name",
name: "display_name",
string: "Display Name",
fields: [],
type: "id",
},
{
id: "many2many_field",
name: "many2many_field",
string: "Many2Many",
fields: [
{
id: "id",
name: "id",
string: "External ID",
fields: [],
type: "id",
},
],
type: "id",
},
],
headers: ["id", "display_name", "many2many_field/id"],
rowCount: 5,
matches: {
0: ["id"],
1: ["display_name"],
2: ["many2many_field", "id"],
},
preview: [
["0", "1", "2"],
["Name 1", "Name 2", "Name 3"],
["", "1", "2"],
],
});
},
"base_import.import/execute_import": (route, args) => {
return executeFailingImport(args[1][0], true, ["many2many_field", "id"]);
},
});
// Set and trigger the change of a file for the input
const file = new File(["fake_file"], "fake_file.xlsx", { type: "text/plain" });
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
// For this test, we force the display of an error message if this field is set
await editSelectMenu(target, ".o_import_data_content .o_select_menu", "Many2Many");
await click(
target.querySelector(".o_control_panel_main_buttons .d-none button:nth-child(2)")
);
assert.strictEqual(
target.querySelector(".o_import_data_content .alert-danger").textContent,
"The file contains blocking errors (see below)",
"A message is shown if the import was blocked"
);
assert.strictEqual(
target.querySelector("tr:nth-child(3) .o_import_file_column_cell span.text-truncate")
.innerText,
"many2many_field/id",
"The third row should be the relational field"
);
assert.strictEqual(
target.querySelector("tr:nth-child(3) .o_select_menu_toggler_slot span").innerText,
"Many2Many / External ID",
"The relational field is properly mapped"
);
assert.containsOnce(
target,
"tr:nth-child(3) .o_import_report.alert",
"The relational field should have error messages on his row"
);
assert.strictEqual(
target.querySelector("tr:nth-child(3) .o_import_report.alert p b").innerText,
"Many2Many / External ID",
"The error should contain the full path of the relational field"
);
});
QUnit.test("Import view: date format should be converted to strftime", async function (assert) {
assert.expect(5);
registerFakeHTTPService();
let parseCount = 0;
await createImportAction({
"base_import.import/parse_preview": async (route, args) => {
parseCount++;
const response = await parsePreview(args[1]);
if (parseCount === 2) {
assert.strictEqual(
response.options.date_format,
"%Y%m%d",
"server sends back a strftime formatted date"
);
}
return response;
},
"base_import.import/execute_import": (route, args) => {
assert.step("execute_import");
assert.strictEqual(
args[3].date_format,
"%Y%m%d",
"date is converted to strftime as expected during the import"
);
return executeImport(args);
},
});
// Set and trigger the change of a file for the input
const file = new File(["fake_file"], "fake_file.csv", { type: "text/plain" });
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
await editInput(target, ".o_import_date_format#date_format-3", "YYYYMMDD");
// Parse the file again with the updated date format to check that
// the format is correctly formatted in the UI
await click(target.querySelector(".o_import_formatting button"));
await click(
$(target).find(
".o_control_panel_main_buttons > div:visible > button:contains(Import)"
)[0]
);
assert.verifySteps(["execute_import"]);
assert.strictEqual(
target.querySelector(".o_import_date_format").value,
"YYYYMMDD",
"UI displays the human formatted date"
);
});
QUnit.test("Import action: field selection has a clear button", async function (assert) {
registerFakeHTTPService();
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
await createImportAction();
// Set and trigger the change of a file for the input
const file = new File(["fake_file"], "fake_file.xlsx", { type: "text/plain" });
await editInput(target, ".o_control_panel_main_buttons .d-none input[type='file']", file);
await editSelectMenu(target, ".o_import_data_content .o_select_menu", "Bar");
assert.containsN(
target,
".o_select_menu_toggler_clear",
2,
"clear button is present for each field to unselect it"
);
await click(target.querySelector(".o_select_menu_toggler_clear"));
assert.strictEqual(
target.querySelector("tr:nth-child(2) .o_select_menu").textContent,
"To import, select a field...",
"field has been unselected"
);
});
QUnit.test(
"Import a CSV: formatting options for date and datetime options",
async function (assert) {
registerFakeHTTPService((route, params) => {
assert.strictEqual(route, "/base_import/set_file");
assert.strictEqual(
params.ufile[0].name,
"fake_file.csv",
"file is correctly uploaded to the server"
);
});
await createImportAction();
// Set and trigger the change of a file for the input
const file = new File(["fake_file"], "fake_file.csv", { type: "text/plain" });
await editInput(target, ".o_control_panel_collapsed_create input[type='file']", file);
assert.strictEqual(
target.querySelector(".o_import_date_format").list.id,
"list-3",
"a datalist is associated to the date input"
);
assert.containsOnce(
target.querySelector(".o_import_date_format").previousElementSibling,
"sup",
"a tooltip is displayed on the label of the option"
);
assert.strictEqual(
target.querySelector(".o_import_date_format").list.options[0].value,
"YYYY-MM-DD",
"an option for datetime has the right value"
);
assert.strictEqual(
target.querySelector(".o_import_datetime_format").list.id,
"list-4",
"a datalist is associated to the datetime input"
);
assert.containsOnce(
target.querySelector(".o_import_datetime_format").previousElementSibling,
"sup",
"a tooltip is displayed on the label of the option"
);
assert.strictEqual(
target.querySelector(".o_import_datetime_format").list.options[0].value,
"YYYY-MM-DD HH:mm:SS",
"an option for datetime has the right value"
);
assert.containsOnce(
target,
".o_import_action .o_import_data_content",
"content panel is visible"
);
}
);
});