mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-21 17:12:05 +02:00
vanilla 18.0
This commit is contained in:
parent
5454004ff9
commit
d7f6d2725e
979 changed files with 428093 additions and 0 deletions
|
|
@ -0,0 +1,557 @@
|
|||
/** @odoo-module alias=@web/../tests/views/calendar/helpers default=false */
|
||||
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { createElement } from "@web/core/utils/xml";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Field } from "@web/views/fields/field";
|
||||
import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env";
|
||||
import { click, getFixture, mount, nextTick, triggerEvent } from "../../helpers/utils";
|
||||
import { setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
export function makeEnv(services = {}) {
|
||||
clearRegistryWithCleanup(registry.category("main_components"));
|
||||
setupViewRegistries();
|
||||
services = Object.assign(
|
||||
{
|
||||
ui: uiService,
|
||||
},
|
||||
services
|
||||
);
|
||||
|
||||
for (const [key, service] of Object.entries(services)) {
|
||||
registry.category("services").add(key, service, { force: true });
|
||||
}
|
||||
|
||||
return makeTestEnv({
|
||||
config: {
|
||||
setDisplayName: () => {},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export async function mountComponent(C, env, props) {
|
||||
const target = getFixture();
|
||||
return await mount(C, target, { env, props });
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export function makeFakeDate() {
|
||||
return luxon.DateTime.local(2021, 7, 16, 8, 0, 0, 0);
|
||||
}
|
||||
|
||||
export function makeFakeRecords() {
|
||||
return {
|
||||
1: {
|
||||
id: 1,
|
||||
title: "1 day, all day in July",
|
||||
start: makeFakeDate(),
|
||||
isAllDay: true,
|
||||
end: makeFakeDate(),
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
title: "3 days, all day in July",
|
||||
start: makeFakeDate().plus({ days: 2 }),
|
||||
isAllDay: true,
|
||||
end: makeFakeDate().plus({ days: 4 }),
|
||||
},
|
||||
3: {
|
||||
id: 3,
|
||||
title: "1 day, all day in June",
|
||||
start: makeFakeDate().plus({ months: -1 }),
|
||||
isAllDay: true,
|
||||
end: makeFakeDate().plus({ months: -1 }),
|
||||
},
|
||||
4: {
|
||||
id: 4,
|
||||
title: "3 days, all day in June",
|
||||
start: makeFakeDate().plus({ months: -1, days: 2 }),
|
||||
isAllDay: true,
|
||||
end: makeFakeDate().plus({ months: -1, days: 4 }),
|
||||
},
|
||||
5: {
|
||||
id: 5,
|
||||
title: "Over June and July",
|
||||
start: makeFakeDate().startOf("month").plus({ days: -2 }),
|
||||
isAllDay: true,
|
||||
end: makeFakeDate().startOf("month").plus({ days: 2 }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const FAKE_FILTER_SECTIONS = [
|
||||
{
|
||||
label: "Attendees",
|
||||
fieldName: "partner_ids",
|
||||
avatar: {
|
||||
model: "res.partner",
|
||||
field: "avatar_128",
|
||||
},
|
||||
hasAvatar: true,
|
||||
write: {
|
||||
model: "filter_partner",
|
||||
field: "partner_id",
|
||||
},
|
||||
canCollapse: true,
|
||||
canAddFilter: true,
|
||||
filters: [
|
||||
{
|
||||
type: "user",
|
||||
label: "Mitchell Admin",
|
||||
active: true,
|
||||
value: 3,
|
||||
colorIndex: 3,
|
||||
recordId: null,
|
||||
canRemove: false,
|
||||
hasAvatar: true,
|
||||
},
|
||||
{
|
||||
type: "all",
|
||||
label: "Everybody's calendar",
|
||||
active: false,
|
||||
value: "all",
|
||||
colorIndex: null,
|
||||
recordId: null,
|
||||
canRemove: false,
|
||||
hasAvatar: false,
|
||||
},
|
||||
{
|
||||
type: "record",
|
||||
label: "Brandon Freeman",
|
||||
active: true,
|
||||
value: 4,
|
||||
colorIndex: 4,
|
||||
recordId: 1,
|
||||
canRemove: true,
|
||||
hasAvatar: true,
|
||||
},
|
||||
{
|
||||
type: "record",
|
||||
label: "Marc Demo",
|
||||
active: false,
|
||||
value: 6,
|
||||
colorIndex: 6,
|
||||
recordId: 2,
|
||||
canRemove: true,
|
||||
hasAvatar: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Users",
|
||||
fieldName: "user_id",
|
||||
avatar: {
|
||||
model: null,
|
||||
field: null,
|
||||
},
|
||||
hasAvatar: false,
|
||||
write: {
|
||||
model: null,
|
||||
field: null,
|
||||
},
|
||||
canCollapse: false,
|
||||
canAddFilter: false,
|
||||
filters: [
|
||||
{
|
||||
type: "record",
|
||||
label: "Brandon Freeman",
|
||||
active: false,
|
||||
value: 1,
|
||||
colorIndex: false,
|
||||
recordId: null,
|
||||
canRemove: true,
|
||||
hasAvatar: true,
|
||||
},
|
||||
{
|
||||
type: "record",
|
||||
label: "Marc Demo",
|
||||
active: false,
|
||||
value: 2,
|
||||
colorIndex: false,
|
||||
recordId: null,
|
||||
canRemove: true,
|
||||
hasAvatar: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const FAKE_FIELDS = {
|
||||
id: { string: "Id", type: "integer" },
|
||||
user_id: { string: "User", type: "many2one", relation: "user", default: -1 },
|
||||
partner_id: {
|
||||
string: "Partner",
|
||||
type: "many2one",
|
||||
relation: "partner",
|
||||
related: "user_id.partner_id",
|
||||
default: 1,
|
||||
},
|
||||
name: { string: "Name", type: "char" },
|
||||
start_date: { string: "Start Date", type: "date" },
|
||||
stop_date: { string: "Stop Date", type: "date" },
|
||||
start: { string: "Start Datetime", type: "datetime" },
|
||||
stop: { string: "Stop Datetime", type: "datetime" },
|
||||
delay: { string: "Delay", type: "float" },
|
||||
allday: { string: "Is All Day", type: "boolean" },
|
||||
partner_ids: {
|
||||
string: "Attendees",
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
default: [[6, 0, [1]]],
|
||||
},
|
||||
type: { string: "Type", type: "integer" },
|
||||
event_type_id: { string: "Event Type", type: "many2one", relation: "event_type" },
|
||||
color: { string: "Color", type: "integer", related: "event_type_id.color" },
|
||||
};
|
||||
|
||||
function makeFakeModelState() {
|
||||
const fakeFieldNode = createElement("field", { name: "name" });
|
||||
const fakeModels = { event: { fields: FAKE_FIELDS } };
|
||||
return {
|
||||
canCreate: true,
|
||||
canDelete: true,
|
||||
canEdit: true,
|
||||
date: makeFakeDate(),
|
||||
fieldMapping: {
|
||||
date_start: "start_date",
|
||||
date_stop: "stop_date",
|
||||
date_delay: "delay",
|
||||
all_day: "allday",
|
||||
color: "color",
|
||||
},
|
||||
fieldNames: ["start_date", "stop_date", "color", "delay", "allday", "user_id"],
|
||||
fields: FAKE_FIELDS,
|
||||
filterSections: FAKE_FILTER_SECTIONS,
|
||||
firstDayOfWeek: 0,
|
||||
isDateHidden: false,
|
||||
isTimeHidden: false,
|
||||
hasAllDaySlot: true,
|
||||
hasEditDialog: false,
|
||||
quickCreate: false,
|
||||
popoverFieldNodes: {
|
||||
name: Field.parseFieldNode(fakeFieldNode, fakeModels, "event", "calendar"),
|
||||
},
|
||||
activeFields: {
|
||||
name: {
|
||||
context: "{}",
|
||||
invisible: false,
|
||||
readonly: false,
|
||||
required: false,
|
||||
onChange: false,
|
||||
},
|
||||
},
|
||||
rangeEnd: makeFakeDate().endOf("month"),
|
||||
rangeStart: makeFakeDate().startOf("month"),
|
||||
records: makeFakeRecords(),
|
||||
resModel: "event",
|
||||
scale: "month",
|
||||
scales: ["day", "week", "month", "year"],
|
||||
unusualDays: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function makeFakeModel(state = {}) {
|
||||
return {
|
||||
...makeFakeModelState(),
|
||||
load() {},
|
||||
createFilter() {},
|
||||
createRecord() {},
|
||||
unlinkFilter() {},
|
||||
unlinkRecord() {},
|
||||
updateFilter() {},
|
||||
updateRecord() {},
|
||||
...state,
|
||||
};
|
||||
}
|
||||
|
||||
// DOM Utils
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
async function scrollTo(el, scrollParam) {
|
||||
el.scrollIntoView(scrollParam);
|
||||
await new Promise(window.requestAnimationFrame);
|
||||
}
|
||||
|
||||
export function findPickedDate(target) {
|
||||
return target.querySelector(".o_datetime_picker .o_selected");
|
||||
}
|
||||
|
||||
export async function pickDate(target, date) {
|
||||
const day = date.split("-")[2];
|
||||
const iDay = parseInt(day, 10) - 1;
|
||||
const el = target.querySelectorAll(`.o_datetime_picker .o_date_item_cell:not(.o_out_of_range)`)[
|
||||
iDay
|
||||
];
|
||||
el.scrollIntoView();
|
||||
await click(el);
|
||||
}
|
||||
|
||||
export function expandCalendarView(target) {
|
||||
// Expends Calendar view and FC too
|
||||
let tmpElement = target.querySelector(".fc");
|
||||
do {
|
||||
tmpElement = tmpElement.parentElement;
|
||||
tmpElement.classList.add("h-100");
|
||||
} while (!tmpElement.classList.contains("o_view_controller"));
|
||||
}
|
||||
|
||||
export function findAllDaySlot(target, date) {
|
||||
return target.querySelector(`.fc-daygrid-body .fc-day[data-date="${date}"]`);
|
||||
}
|
||||
|
||||
export function findDateCell(target, date) {
|
||||
return target.querySelector(`.fc-day[data-date="${date}"]`);
|
||||
}
|
||||
|
||||
export function findEvent(target, eventId) {
|
||||
return target.querySelector(`.o_event[data-event-id="${eventId}"]`);
|
||||
}
|
||||
|
||||
export function findDateCol(target, date) {
|
||||
return target.querySelector(`.fc-col-header-cell.fc-day[data-date="${date}"]`);
|
||||
}
|
||||
|
||||
export function findTimeRow(target, time) {
|
||||
return target.querySelector(`.fc-timegrid-slot[data-time="${time}"]`);
|
||||
}
|
||||
|
||||
export async function triggerEventForCalendar(el, type, position = {}) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = position.x || rect.x + rect.width / 2;
|
||||
const y = position.y || rect.y + rect.height / 2;
|
||||
const attrs = {
|
||||
which: 1,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
};
|
||||
await triggerEvent(el, null, type, attrs);
|
||||
}
|
||||
|
||||
export async function clickAllDaySlot(target, date) {
|
||||
const el = findAllDaySlot(target, date);
|
||||
await scrollTo(el);
|
||||
await triggerEventForCalendar(el, "mousedown");
|
||||
await triggerEventForCalendar(el, "mouseup");
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function clickDate(target, date) {
|
||||
const el = findDateCell(target, date);
|
||||
await scrollTo(el);
|
||||
await triggerEventForCalendar(el, "mousedown");
|
||||
await triggerEventForCalendar(el, "mouseup");
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function clickEvent(target, eventId) {
|
||||
const el = findEvent(target, eventId);
|
||||
await scrollTo(el);
|
||||
await click(el);
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function selectTimeRange(target, startDateTime, endDateTime) {
|
||||
const [startDate, startTime] = startDateTime.split(" ");
|
||||
const [endDate, endTime] = endDateTime.split(" ");
|
||||
|
||||
const startCol = findDateCol(target, startDate);
|
||||
const endCol = findDateCol(target, endDate);
|
||||
const startRow = findTimeRow(target, startTime);
|
||||
const endRow = findTimeRow(target, endTime);
|
||||
|
||||
await scrollTo(startRow);
|
||||
const startColRect = startCol.getBoundingClientRect();
|
||||
const startRowRect = startRow.getBoundingClientRect();
|
||||
|
||||
await triggerEventForCalendar(startRow, "mousedown", {
|
||||
x: startColRect.x + startColRect.width / 2,
|
||||
y: startRowRect.y + 2,
|
||||
});
|
||||
|
||||
await scrollTo(endRow, false);
|
||||
const endColRect = endCol.getBoundingClientRect();
|
||||
const endRowRect = endRow.getBoundingClientRect();
|
||||
|
||||
await triggerEventForCalendar(endRow, "mousemove", {
|
||||
x: endColRect.x + endColRect.width / 2,
|
||||
y: endRowRect.y - 2,
|
||||
});
|
||||
await triggerEventForCalendar(endRow, "mouseup", {
|
||||
x: endColRect.x + endColRect.width / 2,
|
||||
y: endRowRect.y - 2,
|
||||
});
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function selectDateRange(target, startDate, endDate) {
|
||||
const start = findDateCell(target, startDate);
|
||||
const end = findDateCell(target, endDate);
|
||||
await scrollTo(start);
|
||||
await triggerEventForCalendar(start, "mousedown");
|
||||
await scrollTo(end);
|
||||
await triggerEventForCalendar(end, "mousemove");
|
||||
await triggerEventForCalendar(end, "mouseup");
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function selectAllDayRange(target, startDate, endDate) {
|
||||
const start = findAllDaySlot(target, startDate);
|
||||
const end = findAllDaySlot(target, endDate);
|
||||
await scrollTo(start);
|
||||
await triggerEventForCalendar(start, "mousedown");
|
||||
await scrollTo(end);
|
||||
await triggerEventForCalendar(end, "mousemove");
|
||||
await triggerEventForCalendar(end, "mouseup");
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function moveEventToDate(target, eventId, date, options = {}) {
|
||||
const event = findEvent(target, eventId);
|
||||
const cell = findDateCell(target, date);
|
||||
|
||||
await scrollTo(event);
|
||||
await triggerEventForCalendar(event, "mousedown");
|
||||
|
||||
await scrollTo(cell);
|
||||
await triggerEventForCalendar(cell, "mousemove");
|
||||
|
||||
if (!options.disableDrop) {
|
||||
await triggerEventForCalendar(cell, "mouseup");
|
||||
}
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function moveEventToTime(target, eventId, dateTime) {
|
||||
const event = findEvent(target, eventId);
|
||||
const [date, time] = dateTime.split(" ");
|
||||
|
||||
const col = findDateCol(target, date);
|
||||
const row = findTimeRow(target, time);
|
||||
|
||||
// Find event position
|
||||
await scrollTo(event);
|
||||
const eventRect = event.getBoundingClientRect();
|
||||
const eventPos = {
|
||||
x: eventRect.x + eventRect.width / 2,
|
||||
y: eventRect.y,
|
||||
};
|
||||
|
||||
await triggerEventForCalendar(event, "mousedown", eventPos);
|
||||
|
||||
// Find target position
|
||||
await scrollTo(row, false);
|
||||
const colRect = col.getBoundingClientRect();
|
||||
const rowRect = row.getBoundingClientRect();
|
||||
const toPos = {
|
||||
x: colRect.x + colRect.width / 2,
|
||||
y: rowRect.y - 1,
|
||||
};
|
||||
|
||||
await triggerEventForCalendar(row, "mousemove", toPos);
|
||||
await triggerEventForCalendar(row, "mouseup", toPos);
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function moveEventToAllDaySlot(target, eventId, date) {
|
||||
const event = findEvent(target, eventId);
|
||||
const slot = findAllDaySlot(target, date);
|
||||
|
||||
// Find event position
|
||||
await scrollTo(event);
|
||||
const eventRect = event.getBoundingClientRect();
|
||||
const eventPos = {
|
||||
x: eventRect.x + eventRect.width / 2,
|
||||
y: eventRect.y,
|
||||
};
|
||||
await triggerEventForCalendar(event, "mousedown", eventPos);
|
||||
|
||||
// Find target position
|
||||
await scrollTo(slot);
|
||||
const slotRect = slot.getBoundingClientRect();
|
||||
const toPos = {
|
||||
x: slotRect.x + slotRect.width / 2,
|
||||
y: slotRect.y - 1,
|
||||
};
|
||||
await triggerEventForCalendar(slot, "mousemove", toPos);
|
||||
await triggerEventForCalendar(slot, "mouseup", toPos);
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function resizeEventToTime(target, eventId, dateTime) {
|
||||
const event = findEvent(target, eventId);
|
||||
const [date, time] = dateTime.split(" ");
|
||||
|
||||
const col = findDateCol(target, date);
|
||||
const row = findTimeRow(target, time);
|
||||
|
||||
// Find event position
|
||||
await scrollTo(event);
|
||||
await triggerEventForCalendar(event, "mouseover");
|
||||
|
||||
// Find event resizer
|
||||
const resizer = event.querySelector(".fc-event-resizer-end");
|
||||
resizer.style.display = "block";
|
||||
resizer.style.width = "100%";
|
||||
resizer.style.height = "1em";
|
||||
resizer.style.bottom = "0";
|
||||
const resizerRect = resizer.getBoundingClientRect();
|
||||
const resizerPos = {
|
||||
x: resizerRect.x + resizerRect.width / 2,
|
||||
y: resizerRect.y + resizerRect.height / 2,
|
||||
};
|
||||
await triggerEventForCalendar(resizer, "mousedown", resizerPos);
|
||||
|
||||
// Find target position
|
||||
await scrollTo(row, false);
|
||||
const colRect = col.getBoundingClientRect();
|
||||
const rowRect = row.getBoundingClientRect();
|
||||
const toPos = {
|
||||
x: colRect.x + colRect.width / 2,
|
||||
y: rowRect.y - 1,
|
||||
};
|
||||
|
||||
await triggerEventForCalendar(row, "mousemove", toPos);
|
||||
await triggerEventForCalendar(row, "mouseup", toPos);
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function changeScale(target, scale) {
|
||||
await click(target, `.o_view_scale_selector .scale_button_selection`);
|
||||
await click(target, `.o-dropdown--menu .o_scale_button_${scale}`);
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function navigate(target, direction) {
|
||||
await click(target, `.o_calendar_navigation_buttons .o_calendar_button_${direction}`);
|
||||
}
|
||||
|
||||
export function findFilterPanelSection(target, sectionName) {
|
||||
return target.querySelector(`.o_calendar_filter[data-name="${sectionName}"]`);
|
||||
}
|
||||
|
||||
export function findFilterPanelFilter(target, sectionName, filterValue) {
|
||||
return findFilterPanelSection(target, sectionName).querySelector(
|
||||
`.o_calendar_filter_item[data-value="${filterValue}"]`
|
||||
);
|
||||
}
|
||||
|
||||
export function findFilterPanelSectionFilter(target, sectionName) {
|
||||
return findFilterPanelSection(target, sectionName).querySelector(
|
||||
`.o_calendar_filter_items_checkall`
|
||||
);
|
||||
}
|
||||
|
||||
export async function toggleFilter(target, sectionName, filterValue) {
|
||||
const el = findFilterPanelFilter(target, sectionName, filterValue).querySelector(`input`);
|
||||
await scrollTo(el);
|
||||
await click(el);
|
||||
}
|
||||
|
||||
export async function toggleSectionFilter(target, sectionName) {
|
||||
const el = findFilterPanelSectionFilter(target, sectionName).querySelector(`input`);
|
||||
await scrollTo(el);
|
||||
await click(el);
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/** @odoo-module alias=@web/../tests/views/graph_view_tests default=false */
|
||||
|
||||
import { click, findChildren, triggerEvent } from "@web/../tests/helpers/utils";
|
||||
import { ensureArray } from "@web/core/utils/arrays";
|
||||
|
||||
// TODO: remove when dependant modules are converted
|
||||
|
||||
export function checkLabels(assert, graph, expectedLabels) {
|
||||
const labels = getGraphRenderer(graph).chart.data.labels.map((l) => l.toString());
|
||||
assert.deepEqual(labels, expectedLabels);
|
||||
}
|
||||
|
||||
export function checkLegend(assert, graph, expectedLegendLabels) {
|
||||
expectedLegendLabels = ensureArray(expectedLegendLabels);
|
||||
const { chart } = getGraphRenderer(graph);
|
||||
const actualLegendLabels = chart.config.options.plugins.legend.labels
|
||||
.generateLabels(chart)
|
||||
.map((o) => o.text);
|
||||
assert.deepEqual(actualLegendLabels, expectedLegendLabels);
|
||||
}
|
||||
|
||||
export function clickOnDataset(graph) {
|
||||
const { chart } = getGraphRenderer(graph);
|
||||
const meta = chart.getDatasetMeta(0);
|
||||
const rectangle = chart.canvas.getBoundingClientRect();
|
||||
const point = meta.data[0].getCenterPoint();
|
||||
return triggerEvent(chart.canvas, null, "click", {
|
||||
pageX: rectangle.left + point.x,
|
||||
pageY: rectangle.top + point.y,
|
||||
});
|
||||
}
|
||||
|
||||
export function getGraphRenderer(graph) {
|
||||
for (const { component } of Object.values(findChildren(graph).children)) {
|
||||
if (component.chart) {
|
||||
return component;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function selectMode(el, mode) {
|
||||
return click(el, `.o_graph_button[data-mode="${mode}"`);
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
/** @odoo-module alias=@web/../tests/views/helpers default=false */
|
||||
|
||||
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
|
||||
import { getFixture, mount, nextTick } from "@web/../tests/helpers/utils";
|
||||
import { createDebugContext } from "@web/core/debug/debug_context";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { View, getDefaultConfig } from "@web/views/view";
|
||||
import {
|
||||
fakeCompanyService,
|
||||
makeFakeLocalizationService,
|
||||
patchUserWithCleanup,
|
||||
} from "../helpers/mock_services";
|
||||
import {
|
||||
setupControlPanelFavoriteMenuRegistry,
|
||||
setupControlPanelServiceRegistry,
|
||||
} from "../search/helpers";
|
||||
|
||||
import { Component, useSubEnv, xml } from "@odoo/owl";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
const rootDialogTemplate = xml`<Dialog><View t-props="props.viewProps"/></Dialog>`;
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* serverData: Object,
|
||||
* mockRPC?: Function,
|
||||
* type: string,
|
||||
* resModel: string,
|
||||
* [prop:string]: any
|
||||
* }} MakeViewParams
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Component} T
|
||||
* @param {MakeViewParams} params
|
||||
* @param {boolean} [inDialog=false]
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
async function _makeView(params, inDialog = false) {
|
||||
const props = { resId: false, ...params };
|
||||
const serverData = props.serverData;
|
||||
const mockRPC = props.mockRPC;
|
||||
const config = {
|
||||
...getDefaultConfig(),
|
||||
...props.config,
|
||||
};
|
||||
|
||||
delete props.serverData;
|
||||
delete props.mockRPC;
|
||||
delete props.config;
|
||||
|
||||
if (props.arch) {
|
||||
serverData.views = serverData.views || {};
|
||||
props.viewId = params.viewId || 100000001; // hopefully will not conflict with an id already in views
|
||||
serverData.views[`${props.resModel},${props.viewId},${props.type}`] = props.arch;
|
||||
delete props.arch;
|
||||
props.searchViewId = 100000002; // hopefully will not conflict with an id already in views
|
||||
const searchViewArch = props.searchViewArch || "<search/>";
|
||||
serverData.views[`${props.resModel},${props.searchViewId},search`] = searchViewArch;
|
||||
delete props.searchViewArch;
|
||||
}
|
||||
|
||||
const env = await makeTestEnv({ serverData, mockRPC });
|
||||
Object.assign(env, createDebugContext(env)); // This is needed if the views are in debug mode
|
||||
|
||||
const target = getFixture();
|
||||
const viewEnv = Object.assign(Object.create(env), { config });
|
||||
|
||||
await mount(MainComponentsContainer, target, { env });
|
||||
let viewNode;
|
||||
if (inDialog) {
|
||||
let root;
|
||||
class RootDialog extends Component {
|
||||
static components = { Dialog, View };
|
||||
static template = rootDialogTemplate;
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
root = this;
|
||||
useSubEnv(viewEnv);
|
||||
}
|
||||
}
|
||||
env.services.dialog.add(RootDialog, { viewProps: props });
|
||||
await nextTick();
|
||||
const rootNode = root.__owl__;
|
||||
const dialogNode = Object.values(rootNode.children)[0];
|
||||
viewNode = Object.values(dialogNode.children)[0];
|
||||
} else {
|
||||
const view = await mount(View, target, { env: viewEnv, props });
|
||||
await nextTick();
|
||||
viewNode = view.__owl__;
|
||||
}
|
||||
const withSearchNode = Object.values(viewNode.children)[0];
|
||||
const concreteViewNode = Object.values(withSearchNode.children)[0];
|
||||
const concreteView = concreteViewNode.component;
|
||||
|
||||
return concreteView;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MakeViewParams} params
|
||||
*/
|
||||
export function makeView(params) {
|
||||
return _makeView(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MakeViewParams} params
|
||||
*/
|
||||
export function makeViewInDialog(params) {
|
||||
return _makeView(params, true);
|
||||
}
|
||||
|
||||
export function setupViewRegistries() {
|
||||
setupControlPanelFavoriteMenuRegistry();
|
||||
setupControlPanelServiceRegistry();
|
||||
patchUserWithCleanup({
|
||||
hasGroup: async (group) => {
|
||||
return [
|
||||
"base.group_allow_export",
|
||||
"base.group_user",
|
||||
].includes(group);
|
||||
},
|
||||
isInternalUser: true,
|
||||
});
|
||||
serviceRegistry.add("localization", makeFakeLocalizationService());
|
||||
serviceRegistry.add("company", fakeCompanyService);
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
/** @odoo-module alias=@web/../tests/views/kanban/helpers default=false */
|
||||
|
||||
import { makeFakeDialogService } from "@web/../tests/helpers/mock_services";
|
||||
import { click, editInput, getDropdownMenu, nextTick } from "@web/../tests/helpers/utils";
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
export function patchDialog(addDialog) {
|
||||
registry.category("services").add("dialog", makeFakeDialogService(addDialog), { force: true });
|
||||
}
|
||||
|
||||
// Kanban
|
||||
// WOWL remove this helper and use the control panel instead
|
||||
export async function reload(kanban, params = {}) {
|
||||
kanban.env.searchModel.reload(params);
|
||||
kanban.env.searchModel.search();
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export function getCard(target, cardIndex = 0) {
|
||||
return target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")[cardIndex];
|
||||
}
|
||||
|
||||
export function getColumn(target, groupIndex = 0, ignoreFolded = false) {
|
||||
let selector = ".o_kanban_group";
|
||||
if (ignoreFolded) {
|
||||
selector += ":not(.o_column_folded)";
|
||||
}
|
||||
return target.querySelectorAll(selector)[groupIndex];
|
||||
}
|
||||
|
||||
export function getColumnDropdownMenu(target, groupIndex = 0, ignoreFolded = false) {
|
||||
let selector = ".o_kanban_group";
|
||||
if (ignoreFolded) {
|
||||
selector += ":not(.o_column_folded)";
|
||||
}
|
||||
const column = target.querySelectorAll(selector)[groupIndex];
|
||||
return getDropdownMenu(target, column);
|
||||
}
|
||||
|
||||
export function getCardTexts(target, groupIndex) {
|
||||
const root = groupIndex >= 0 ? getColumn(target, groupIndex) : target;
|
||||
return [...root.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")]
|
||||
.map((card) => card.innerText.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function getCounters(target) {
|
||||
return [...target.querySelectorAll(".o_animated_number")].map((counter) => counter.innerText);
|
||||
}
|
||||
|
||||
export function getProgressBars(target, columnIndex) {
|
||||
const column = getColumn(target, columnIndex);
|
||||
return [...column.querySelectorAll(".o_column_progress .progress-bar")];
|
||||
}
|
||||
|
||||
export function getTooltips(target, groupIndex) {
|
||||
const root = groupIndex >= 0 ? getColumn(target, groupIndex) : target;
|
||||
return [...root.querySelectorAll(".o_column_progress .progress-bar")]
|
||||
.map((card) => card.dataset.tooltip)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
// Record
|
||||
export async function createRecord(target) {
|
||||
await click(target, ".o_control_panel_main_buttons button.o-kanban-button-new");
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function quickCreateRecord(target, groupIndex) {
|
||||
await click(getColumn(target, groupIndex), ".o_kanban_quick_add");
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export async function editQuickCreateInput(target, field, value) {
|
||||
await editInput(target, `.o_kanban_quick_create .o_field_widget[name=${field}] input`, value);
|
||||
}
|
||||
|
||||
export async function validateRecord(target) {
|
||||
await click(target, ".o_kanban_quick_create .o_kanban_add");
|
||||
}
|
||||
|
||||
export async function editRecord(target) {
|
||||
await click(target, ".o_kanban_quick_create .o_kanban_edit");
|
||||
}
|
||||
|
||||
export async function discardRecord(target) {
|
||||
await click(target, ".o_kanban_quick_create .o_kanban_cancel");
|
||||
}
|
||||
|
||||
export async function toggleRecordDropdown(target, recordIndex) {
|
||||
const group = target.querySelectorAll(`.o_kanban_record`)[recordIndex];
|
||||
await click(group, ".o_dropdown_kanban .dropdown-toggle");
|
||||
}
|
||||
|
||||
// Column
|
||||
export async function createColumn(target) {
|
||||
await click(target, ".o_column_quick_create > .o_quick_create_folded");
|
||||
}
|
||||
|
||||
export async function editColumnName(target, value) {
|
||||
await editInput(target, ".o_column_quick_create input", value);
|
||||
}
|
||||
|
||||
export async function validateColumn(target) {
|
||||
await click(target, ".o_column_quick_create .o_kanban_add");
|
||||
}
|
||||
|
||||
export async function toggleColumnActions(target, columnIndex) {
|
||||
const group = getColumn(target, columnIndex);
|
||||
await click(group, ".o_kanban_config .dropdown-toggle");
|
||||
const buttons = getDropdownMenu(target, group).querySelectorAll(".dropdown-item");
|
||||
return (buttonText) => {
|
||||
const re = new RegExp(`\\b${buttonText}\\b`, "i");
|
||||
const button = [...buttons].find((b) => re.test(b.innerText));
|
||||
return click(button);
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadMore(target, columnIndex) {
|
||||
await click(getColumn(target, columnIndex), ".o_kanban_load_more button");
|
||||
}
|
||||
|
|
@ -0,0 +1,879 @@
|
|||
/** @odoo-module alias=@web/../tests/views/list_view_tests default=false */
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { tooltipService } from "@web/core/tooltip/tooltip_service";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import {
|
||||
click,
|
||||
dragAndDrop,
|
||||
editInput,
|
||||
getFixture,
|
||||
getNodesTextContent,
|
||||
nextTick,
|
||||
patchWithCleanup,
|
||||
triggerEvent,
|
||||
triggerEvents,
|
||||
triggerHotkey,
|
||||
} from "../helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "./helpers";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
function getGroup(position) {
|
||||
return target.querySelectorAll(".o_group_header")[position - 1];
|
||||
}
|
||||
|
||||
QUnit.module("Views", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = {
|
||||
models: {
|
||||
foo: {
|
||||
fields: {
|
||||
foo: { string: "Foo", type: "char" },
|
||||
bar: { string: "Bar", type: "boolean" },
|
||||
date: { string: "Some Date", type: "date" },
|
||||
int_field: {
|
||||
string: "int_field",
|
||||
type: "integer",
|
||||
sortable: true,
|
||||
aggregator: "sum",
|
||||
},
|
||||
text: { string: "text field", type: "text" },
|
||||
qux: { string: "my float", type: "float", aggregator: "sum" },
|
||||
m2o: { string: "M2O field", type: "many2one", relation: "bar" },
|
||||
o2m: { string: "O2M field", type: "one2many", relation: "bar" },
|
||||
m2m: { string: "M2M field", type: "many2many", relation: "bar" },
|
||||
amount: { string: "Monetary field", type: "monetary", aggregator: "sum" },
|
||||
amount_currency: {
|
||||
string: "Monetary field (currency)",
|
||||
type: "monetary",
|
||||
currency_field: "company_currency_id",
|
||||
},
|
||||
currency_id: {
|
||||
string: "Currency",
|
||||
type: "many2one",
|
||||
relation: "res_currency",
|
||||
default: 1,
|
||||
},
|
||||
currency_test: {
|
||||
string: "Currency",
|
||||
type: "many2one",
|
||||
relation: "res_currency",
|
||||
default: 1,
|
||||
},
|
||||
company_currency_id: {
|
||||
string: "Company Currency",
|
||||
type: "many2one",
|
||||
relation: "res_currency",
|
||||
default: 2,
|
||||
},
|
||||
datetime: { string: "Datetime Field", type: "datetime" },
|
||||
reference: {
|
||||
string: "Reference Field",
|
||||
type: "reference",
|
||||
selection: [
|
||||
["bar", "Bar"],
|
||||
["res_currency", "Currency"],
|
||||
["event", "Event"],
|
||||
],
|
||||
},
|
||||
properties: {
|
||||
type: "properties",
|
||||
definition_record: "m2o",
|
||||
definition_record_field: "definitions",
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
bar: true,
|
||||
foo: "yop",
|
||||
int_field: 10,
|
||||
qux: 0.4,
|
||||
m2o: 1,
|
||||
m2m: [1, 2],
|
||||
amount: 1200,
|
||||
amount_currency: 1100,
|
||||
currency_id: 2,
|
||||
company_currency_id: 1,
|
||||
date: "2017-01-25",
|
||||
datetime: "2016-12-12 10:55:05",
|
||||
reference: "bar,1",
|
||||
properties: [],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
bar: true,
|
||||
foo: "blip",
|
||||
int_field: 9,
|
||||
qux: 13,
|
||||
m2o: 2,
|
||||
m2m: [1, 2, 3],
|
||||
amount: 500,
|
||||
reference: "res_currency,1",
|
||||
properties: [],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
bar: true,
|
||||
foo: "gnap",
|
||||
int_field: 17,
|
||||
qux: -3,
|
||||
m2o: 1,
|
||||
m2m: [],
|
||||
amount: 300,
|
||||
reference: "res_currency,2",
|
||||
properties: [],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
bar: false,
|
||||
foo: "blip",
|
||||
int_field: -4,
|
||||
qux: 9,
|
||||
m2o: 1,
|
||||
m2m: [1],
|
||||
amount: 0,
|
||||
properties: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
bar: {
|
||||
fields: {
|
||||
definitions: { type: "properties_definitions" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, display_name: "Value 1", definitions: [] },
|
||||
{ id: 2, display_name: "Value 2", definitions: [] },
|
||||
{ id: 3, display_name: "Value 3", definitions: [] },
|
||||
],
|
||||
},
|
||||
res_currency: {
|
||||
fields: {
|
||||
symbol: { string: "Symbol", type: "char" },
|
||||
position: {
|
||||
string: "Position",
|
||||
type: "selection",
|
||||
selection: [
|
||||
["after", "A"],
|
||||
["before", "B"],
|
||||
],
|
||||
},
|
||||
},
|
||||
records: [
|
||||
{ id: 1, display_name: "USD", symbol: "$", position: "before" },
|
||||
{ id: 2, display_name: "EUR", symbol: "€", position: "after" },
|
||||
],
|
||||
},
|
||||
event: {
|
||||
fields: {
|
||||
id: { string: "ID", type: "integer" },
|
||||
name: { string: "name", type: "char" },
|
||||
},
|
||||
records: [{ id: "2-20170808020000", name: "virtual" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
setupViewRegistries();
|
||||
serviceRegistry.add("tooltip", tooltipService);
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
clearTimeout: () => {},
|
||||
});
|
||||
target = getFixture();
|
||||
serviceRegistry.add("ui", uiService);
|
||||
});
|
||||
|
||||
QUnit.module("ListView");
|
||||
|
||||
QUnit.test(
|
||||
"multi_edit: edit a required field with invalid value and click 'Ok' of alert dialog",
|
||||
async function (assert) {
|
||||
serverData.models.foo.fields.foo.required = true;
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch: `
|
||||
<list multi_edit="1">
|
||||
<field name="foo"/>
|
||||
<field name="int_field"/>
|
||||
</list>
|
||||
`,
|
||||
mockRPC(route, args) {
|
||||
assert.step(args.method);
|
||||
},
|
||||
});
|
||||
assert.containsN(target, ".o_data_row", 4);
|
||||
assert.verifySteps(["get_views", "web_search_read"]);
|
||||
|
||||
const rows = target.querySelectorAll(".o_data_row");
|
||||
await click(rows[0], ".o_list_record_selector input");
|
||||
await click(rows[0].querySelector(".o_data_cell"));
|
||||
await editInput(target, "[name='foo'] input", "");
|
||||
await click(target, ".o_list_view");
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.strictEqual(target.querySelector(".modal .btn").textContent, "Ok");
|
||||
|
||||
await click(target.querySelector(".modal .btn"));
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row .o_data_cell[name='foo']").textContent,
|
||||
"yop"
|
||||
);
|
||||
assert.hasClass(target.querySelector(".o_data_row"), "o_data_row_selected");
|
||||
|
||||
assert.verifySteps([]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"multi_edit: edit a required field with invalid value and dismiss alert dialog",
|
||||
async function (assert) {
|
||||
serverData.models.foo.fields.foo.required = true;
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch: `
|
||||
<list multi_edit="1">
|
||||
<field name="foo"/>
|
||||
<field name="int_field"/>
|
||||
</list>`,
|
||||
mockRPC(route, args) {
|
||||
assert.step(args.method);
|
||||
},
|
||||
});
|
||||
assert.containsN(target, ".o_data_row", 4);
|
||||
assert.verifySteps(["get_views", "web_search_read"]);
|
||||
|
||||
const rows = target.querySelectorAll(".o_data_row");
|
||||
await click(rows[0], ".o_list_record_selector input");
|
||||
await click(rows[0].querySelector(".o_data_cell"));
|
||||
await editInput(target, "[name='foo'] input", "");
|
||||
await click(target, ".o_list_view");
|
||||
|
||||
assert.containsOnce(target, ".modal");
|
||||
await click(target.querySelector(".modal-header .btn-close"));
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_data_row .o_data_cell[name='foo']").textContent,
|
||||
"yop"
|
||||
);
|
||||
assert.hasClass(target.querySelector(".o_data_row"), "o_data_row_selected");
|
||||
assert.verifySteps([]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("column widths are re-computed on window resize", async function (assert) {
|
||||
serverData.models.foo.records[0].text =
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
|
||||
"Sed blandit, justo nec tincidunt feugiat, mi justo suscipit libero, sit amet tempus " +
|
||||
"ipsum purus bibendum est.";
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="datetime"/>
|
||||
<field name="text"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
const initialTextWidth = target.querySelectorAll('th[data-name="text"]')[0].offsetWidth;
|
||||
const selectorWidth = target.querySelectorAll("th.o_list_record_selector")[0].offsetWidth;
|
||||
|
||||
// simulate a window resize
|
||||
target.style.width = target.getBoundingClientRect().width / 2 + "px";
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
|
||||
const postResizeTextWidth = target.querySelectorAll('th[data-name="text"]')[0].offsetWidth;
|
||||
const postResizeSelectorWidth = target.querySelectorAll("th.o_list_record_selector")[0]
|
||||
.offsetWidth;
|
||||
assert.ok(postResizeTextWidth < initialTextWidth);
|
||||
assert.strictEqual(selectorWidth, postResizeSelectorWidth);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"editable list view: multi edition error and cancellation handling",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch: `
|
||||
<list multi_edit="1">
|
||||
<field name="foo" required="1"/>
|
||||
<field name="int_field"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
assert.containsN(target, ".o_list_record_selector input:enabled", 5);
|
||||
|
||||
// select two records
|
||||
const rows = target.querySelectorAll(".o_data_row");
|
||||
await click(rows[0], ".o_list_record_selector input");
|
||||
await click(rows[1], ".o_list_record_selector input");
|
||||
|
||||
// edit a line and cancel
|
||||
await click(rows[0].querySelector(".o_data_cell"));
|
||||
assert.containsNone(target, ".o_list_record_selector input:enabled");
|
||||
await editInput(target, ".o_selected_row [name=foo] input", "abc");
|
||||
await click(target, ".modal .btn.btn-secondary");
|
||||
assert.strictEqual(
|
||||
$(target).find(".o_data_row:eq(0) .o_data_cell").text(),
|
||||
"yop10",
|
||||
"first cell should have discarded any change"
|
||||
);
|
||||
assert.containsN(target, ".o_list_record_selector input:enabled", 5);
|
||||
|
||||
// edit a line with an invalid format type
|
||||
await click(rows[0].querySelectorAll(".o_data_cell")[1]);
|
||||
assert.containsNone(target, ".o_list_record_selector input:enabled");
|
||||
|
||||
await editInput(target, ".o_selected_row [name=int_field] input", "hahaha");
|
||||
assert.containsOnce(target, ".modal", "there should be an opened modal");
|
||||
|
||||
await click(target, ".modal .btn-primary");
|
||||
assert.strictEqual(
|
||||
$(target).find(".o_data_row:eq(0) .o_data_cell").text(),
|
||||
"yop10",
|
||||
"changes should be discarded"
|
||||
);
|
||||
assert.containsN(target, ".o_list_record_selector input:enabled", 5);
|
||||
|
||||
// edit a line with an invalid value
|
||||
await click(rows[0].querySelector(".o_data_cell"));
|
||||
assert.containsNone(target, ".o_list_record_selector input:enabled");
|
||||
|
||||
await editInput(target, ".o_selected_row [name=foo] input", "");
|
||||
assert.containsOnce(target, ".modal", "there should be an opened modal");
|
||||
await click(target, ".modal .btn-primary");
|
||||
assert.strictEqual(
|
||||
$(target).find(".o_data_row:eq(0) .o_data_cell").text(),
|
||||
"yop10",
|
||||
"changes should be discarded"
|
||||
);
|
||||
assert.containsN(target, ".o_list_record_selector input:enabled", 5);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
'editable list view: mousedown on "Discard", mouseup somewhere else (no multi-edit)',
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
arch: `
|
||||
<list editable="top">
|
||||
<field name="foo"/>
|
||||
</list>`,
|
||||
mockRPC(route, args) {
|
||||
assert.step(args.method);
|
||||
},
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
});
|
||||
|
||||
// select two records
|
||||
const rows = target.querySelectorAll(".o_data_row");
|
||||
await click(rows[0], ".o_list_record_selector input");
|
||||
await click(rows[1], ".o_list_record_selector input");
|
||||
await click(rows[0].querySelector(".o_data_cell"));
|
||||
target.querySelector(".o_data_row .o_data_cell input").value = "oof";
|
||||
|
||||
await triggerEvents($(".o_list_button_discard:visible").get(0), null, ["mousedown"]);
|
||||
await triggerEvents(target, ".o_data_row .o_data_cell input", [
|
||||
"change",
|
||||
"blur",
|
||||
"focusout",
|
||||
]);
|
||||
await triggerEvents(target, null, ["focus"]);
|
||||
await triggerEvents(target, null, ["mouseup"]);
|
||||
await click(target);
|
||||
|
||||
assert.containsNone(document.body, ".modal", "should not open modal");
|
||||
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_data_cell")), [
|
||||
"oof",
|
||||
"blip",
|
||||
"gnap",
|
||||
"blip",
|
||||
]);
|
||||
assert.verifySteps(["get_views", "web_search_read", "web_save"]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"editable readonly list view: single edition does not behave like a multi-edition",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
arch: `
|
||||
<list multi_edit="1">
|
||||
<field name="foo" required="1"/>
|
||||
</list>`,
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
});
|
||||
|
||||
// select a record
|
||||
const rows = target.querySelectorAll(".o_data_row");
|
||||
await click(rows[0], ".o_list_record_selector input");
|
||||
|
||||
// edit a field (invalid input)
|
||||
await click(rows[0].querySelector(".o_data_cell"));
|
||||
await editInput(target, ".o_data_row [name=foo] input", "");
|
||||
assert.containsOnce(target, ".modal", "should have a modal (invalid fields)");
|
||||
|
||||
await click(target, ".modal button.btn");
|
||||
|
||||
// edit a field
|
||||
await click(rows[0].querySelector(".o_data_cell"));
|
||||
await editInput(target, ".o_data_row [name=foo] input", "bar");
|
||||
assert.containsNone(target, ".modal", "should not have a modal");
|
||||
assert.strictEqual(
|
||||
$(target).find(".o_data_row:eq(0) .o_data_cell").text(),
|
||||
"bar",
|
||||
"the first row should be updated"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"pressing ESC in editable grouped list should discard the current line changes",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch: '<list editable="top"><field name="foo"/><field name="bar"/></list>',
|
||||
groupBy: ["bar"],
|
||||
});
|
||||
|
||||
await click(target.querySelectorAll(".o_group_header")[1]); // open second group
|
||||
assert.containsN(target, "tr.o_data_row", 3);
|
||||
|
||||
await click(target.querySelector(".o_data_cell"));
|
||||
|
||||
// update foo field of edited row
|
||||
await editInput(target, ".o_data_cell [name=foo] input", "new_value");
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector(".o_data_cell [name=foo] input")
|
||||
);
|
||||
// discard by pressing ESC
|
||||
triggerHotkey("Escape");
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".modal");
|
||||
|
||||
assert.containsOnce(target, "tbody tr td:contains(yop)");
|
||||
assert.containsN(target, "tr.o_data_row", 3);
|
||||
assert.containsNone(target, "tr.o_data_row.o_selected_row");
|
||||
assert.isNotVisible(target.querySelector(".o_list_button_save"));
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("editing then pressing TAB in editable grouped list", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch: '<list editable="bottom"><field name="foo"/></list>',
|
||||
mockRPC(route, args) {
|
||||
assert.step(args.method || route);
|
||||
},
|
||||
groupBy: ["bar"],
|
||||
});
|
||||
|
||||
// open two groups
|
||||
await click(getGroup(1));
|
||||
assert.containsN(target, ".o_data_row", 1, "first group contains 1 rows");
|
||||
await click(getGroup(2));
|
||||
assert.containsN(target, ".o_data_row", 4, "first group contains 3 row");
|
||||
|
||||
// select and edit last row of first group
|
||||
await click(target.querySelector(".o_data_row").querySelector(".o_data_cell"));
|
||||
assert.hasClass($(target).find(".o_data_row:nth(0)"), "o_selected_row");
|
||||
await editInput(target, '.o_selected_row [name="foo"] input', "new value");
|
||||
|
||||
// Press 'Tab' -> should create a new record as we edited the previous one
|
||||
triggerHotkey("Tab");
|
||||
await nextTick();
|
||||
assert.containsN(target, ".o_data_row", 5);
|
||||
assert.hasClass($(target).find(".o_data_row:nth(1)"), "o_selected_row");
|
||||
|
||||
// fill foo field for the new record and press 'tab' -> should create another record
|
||||
await editInput(target, '.o_selected_row [name="foo"] input', "new record");
|
||||
triggerHotkey("Tab");
|
||||
await nextTick();
|
||||
|
||||
assert.containsN(target, ".o_data_row", 6);
|
||||
assert.hasClass($(target).find(".o_data_row:nth(2)"), "o_selected_row");
|
||||
|
||||
// leave this new row empty and press tab -> should discard the new record and move to the
|
||||
// next group
|
||||
triggerHotkey("Tab");
|
||||
await nextTick();
|
||||
assert.containsN(target, ".o_data_row", 5);
|
||||
assert.hasClass($(target).find(".o_data_row:nth(2)"), "o_selected_row");
|
||||
|
||||
assert.verifySteps([
|
||||
"get_views",
|
||||
"web_read_group",
|
||||
"web_search_read",
|
||||
"web_search_read",
|
||||
"web_save",
|
||||
"onchange",
|
||||
"web_save",
|
||||
"onchange",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("cell-level keyboard navigation in editable grouped list", async function (assert) {
|
||||
serverData.models.foo.records[0].bar = false;
|
||||
serverData.models.foo.records[1].bar = false;
|
||||
serverData.models.foo.records[2].bar = false;
|
||||
serverData.models.foo.records[3].bar = true;
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch: `
|
||||
<list editable="bottom">
|
||||
<field name="foo" required="1"/>
|
||||
</list>`,
|
||||
groupBy: ["bar"],
|
||||
});
|
||||
|
||||
await click(target.querySelector(".o_group_name"));
|
||||
const secondDataRow = target.querySelectorAll(".o_data_row")[1];
|
||||
await click(secondDataRow, "[name=foo]");
|
||||
assert.hasClass(secondDataRow, "o_selected_row");
|
||||
|
||||
await editInput(secondDataRow, "[name=foo] input", "blipbloup");
|
||||
|
||||
triggerHotkey("Escape");
|
||||
await nextTick();
|
||||
|
||||
assert.containsNone(document.body, ".modal");
|
||||
|
||||
assert.doesNotHaveClass(secondDataRow, "o_selected_row");
|
||||
|
||||
assert.strictEqual(document.activeElement, secondDataRow.querySelector("[name=foo]"));
|
||||
|
||||
assert.strictEqual(document.activeElement.textContent, "blip");
|
||||
|
||||
triggerHotkey("ArrowLeft");
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
secondDataRow.querySelector("input[type=checkbox]")
|
||||
);
|
||||
|
||||
triggerHotkey("ArrowUp");
|
||||
triggerHotkey("ArrowRight");
|
||||
|
||||
const firstDataRow = target.querySelector(".o_data_row");
|
||||
assert.strictEqual(document.activeElement, firstDataRow.querySelector("[name=foo]"));
|
||||
|
||||
triggerHotkey("Enter");
|
||||
await nextTick();
|
||||
|
||||
assert.hasClass(firstDataRow, "o_selected_row");
|
||||
await editInput(firstDataRow, "[name=foo] input", "Zipadeedoodah");
|
||||
|
||||
triggerHotkey("Enter");
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(firstDataRow.querySelector("[name=foo]").innerText, "Zipadeedoodah");
|
||||
assert.doesNotHaveClass(firstDataRow, "o_selected_row");
|
||||
assert.hasClass(secondDataRow, "o_selected_row");
|
||||
assert.strictEqual(document.activeElement, secondDataRow.querySelector("[name=foo] input"));
|
||||
assert.strictEqual(document.activeElement.value, "blip");
|
||||
|
||||
triggerHotkey("ArrowUp");
|
||||
triggerHotkey("ArrowRight");
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(document.activeElement, secondDataRow.querySelector("[name=foo] input"));
|
||||
assert.strictEqual(document.activeElement.value, "blip");
|
||||
|
||||
triggerHotkey("ArrowDown");
|
||||
triggerHotkey("ArrowLeft");
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
secondDataRow.querySelector("td[name=foo] input")
|
||||
);
|
||||
assert.strictEqual(document.activeElement.value, "blip");
|
||||
|
||||
triggerHotkey("Escape");
|
||||
await nextTick();
|
||||
|
||||
assert.doesNotHaveClass(secondDataRow, "o_selected_row");
|
||||
|
||||
assert.strictEqual(document.activeElement, secondDataRow.querySelector("td[name=foo]"));
|
||||
|
||||
triggerHotkey("ArrowDown");
|
||||
triggerHotkey("ArrowDown");
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector(".o_group_field_row_add a")
|
||||
);
|
||||
|
||||
triggerHotkey("ArrowDown");
|
||||
|
||||
const secondGroupHeader = target.querySelectorAll(".o_group_name")[1];
|
||||
assert.strictEqual(document.activeElement, secondGroupHeader);
|
||||
|
||||
assert.containsN(target, ".o_data_row", 3);
|
||||
|
||||
triggerHotkey("Enter");
|
||||
await nextTick();
|
||||
|
||||
assert.containsN(target, ".o_data_row", 4);
|
||||
|
||||
assert.strictEqual(document.activeElement, secondGroupHeader);
|
||||
|
||||
triggerHotkey("ArrowDown");
|
||||
|
||||
const fourthDataRow = target.querySelectorAll(".o_data_row")[3];
|
||||
assert.strictEqual(document.activeElement, fourthDataRow.querySelector("[name=foo]"));
|
||||
|
||||
triggerHotkey("ArrowDown");
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelectorAll(".o_group_field_row_add a")[1]
|
||||
);
|
||||
|
||||
triggerHotkey("ArrowDown");
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelectorAll(".o_group_field_row_add a")[1]
|
||||
);
|
||||
|
||||
// default Enter on a A tag
|
||||
const event = await triggerEvent(document.activeElement, null, "keydown", { key: "Enter" });
|
||||
assert.ok(!event.defaultPrevented);
|
||||
await click(target.querySelectorAll(".o_group_field_row_add a")[1]);
|
||||
|
||||
const fifthDataRow = target.querySelectorAll(".o_data_row")[4];
|
||||
assert.strictEqual(document.activeElement, fifthDataRow.querySelector("[name=foo] input"));
|
||||
|
||||
await editInput(
|
||||
fifthDataRow.querySelector("[name=foo] input"),
|
||||
null,
|
||||
"cheateur arrete de cheater"
|
||||
);
|
||||
|
||||
triggerHotkey("Enter");
|
||||
await nextTick();
|
||||
|
||||
assert.containsN(target, ".o_data_row", 6);
|
||||
|
||||
triggerHotkey("Escape");
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelectorAll(".o_group_field_row_add a")[1]
|
||||
);
|
||||
|
||||
// come back to the top
|
||||
for (let i = 0; i < 9; i++) {
|
||||
triggerHotkey("ArrowUp");
|
||||
}
|
||||
|
||||
assert.strictEqual(document.activeElement, target.querySelector("thead th:nth-child(2)"));
|
||||
|
||||
triggerHotkey("ArrowLeft");
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector("thead th.o_list_record_selector input")
|
||||
);
|
||||
|
||||
triggerHotkey("ArrowDown");
|
||||
triggerHotkey("ArrowDown");
|
||||
triggerHotkey("ArrowRight");
|
||||
|
||||
assert.strictEqual(document.activeElement, firstDataRow.querySelector("td[name=foo]"));
|
||||
|
||||
triggerHotkey("ArrowUp");
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector(".o_group_header:nth-child(1) .o_group_name")
|
||||
);
|
||||
|
||||
assert.containsN(target, ".o_data_row", 5);
|
||||
|
||||
triggerHotkey("Enter");
|
||||
await nextTick();
|
||||
|
||||
assert.containsN(target, ".o_data_row", 2);
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector(".o_group_header:nth-child(1) .o_group_name")
|
||||
);
|
||||
|
||||
triggerHotkey("ArrowRight");
|
||||
await nextTick();
|
||||
|
||||
assert.containsN(target, ".o_data_row", 5);
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector(".o_group_header:nth-child(1) .o_group_name")
|
||||
);
|
||||
|
||||
triggerHotkey("ArrowRight");
|
||||
await nextTick();
|
||||
|
||||
assert.containsN(target, ".o_data_row", 5);
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector(".o_group_header:nth-child(1) .o_group_name")
|
||||
);
|
||||
|
||||
triggerHotkey("ArrowLeft");
|
||||
await nextTick();
|
||||
|
||||
assert.containsN(target, ".o_data_row", 2);
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector(".o_group_header:nth-child(1) .o_group_name")
|
||||
);
|
||||
|
||||
triggerHotkey("ArrowLeft");
|
||||
await nextTick();
|
||||
|
||||
assert.containsN(target, ".o_data_row", 2);
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector(".o_group_header:nth-child(1) .o_group_name")
|
||||
);
|
||||
|
||||
triggerHotkey("ArrowDown");
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector(".o_group_header:nth-child(2) .o_group_name")
|
||||
);
|
||||
|
||||
triggerHotkey("ArrowDown");
|
||||
|
||||
const firstVisibleDataRow = target.querySelector(".o_data_row");
|
||||
assert.strictEqual(document.activeElement, firstVisibleDataRow.querySelector("[name=foo]"));
|
||||
|
||||
triggerHotkey("ArrowDown");
|
||||
|
||||
const secondVisibleDataRow = target.querySelectorAll(".o_data_row")[1];
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
secondVisibleDataRow.querySelector("[name=foo]")
|
||||
);
|
||||
|
||||
triggerHotkey("ArrowDown");
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
target.querySelector(".o_group_field_row_add a")
|
||||
);
|
||||
|
||||
triggerHotkey("ArrowUp");
|
||||
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
secondVisibleDataRow.querySelector("[name=foo]")
|
||||
);
|
||||
|
||||
triggerHotkey("ArrowUp");
|
||||
assert.strictEqual(document.activeElement, firstVisibleDataRow.querySelector("[name=foo]"));
|
||||
});
|
||||
|
||||
QUnit.test("editable list: resize column headers", async function (assert) {
|
||||
// This test will ensure that, on resize list header,
|
||||
// the resized element have the correct size and other elements are not resized
|
||||
serverData.models.foo.records[0].foo = "a".repeat(200);
|
||||
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch: `
|
||||
<list editable="top">
|
||||
<field name="foo"/>
|
||||
<field name="bar"/>
|
||||
<field name="reference" optional="hide"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
// Target handle
|
||||
const th = target.querySelector("th:nth-child(2)");
|
||||
const thNext = target.querySelector("th:nth-child(3)");
|
||||
const resizeHandle = th.querySelector(".o_resize");
|
||||
const nextResizeHandle = thNext.querySelector(".o_resize");
|
||||
const thOriginalWidth = th.getBoundingClientRect().width;
|
||||
const thNextOriginalWidth = thNext.getBoundingClientRect().width;
|
||||
const thExpectedWidth = thOriginalWidth + thNextOriginalWidth;
|
||||
|
||||
await dragAndDrop(resizeHandle, nextResizeHandle);
|
||||
|
||||
const thFinalWidth = th.getBoundingClientRect().width;
|
||||
const thNextFinalWidth = thNext.getBoundingClientRect().width;
|
||||
|
||||
assert.ok(
|
||||
Math.abs(Math.floor(thFinalWidth) - Math.floor(thExpectedWidth)) <= 1,
|
||||
`Wrong width on resize (final: ${thFinalWidth}, expected: ${thExpectedWidth})`
|
||||
);
|
||||
assert.strictEqual(
|
||||
Math.floor(thNextOriginalWidth),
|
||||
Math.floor(thNextFinalWidth),
|
||||
"Width must not have been changed"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"continue creating new lines in editable=top on keyboard nav",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch: `
|
||||
<list editable="top">
|
||||
<field name="int_field"/>
|
||||
</list>`,
|
||||
});
|
||||
|
||||
const initialRowCount = $(".o_data_cell[name=int_field]").length;
|
||||
|
||||
// click on int_field cell of first row
|
||||
await click($(".o_list_button_add:visible").get(0));
|
||||
|
||||
await editInput(target, ".o_data_cell[name=int_field] input", "1");
|
||||
triggerHotkey("Tab");
|
||||
await nextTick();
|
||||
|
||||
await editInput(target, ".o_data_cell[name=int_field] input", "2");
|
||||
triggerHotkey("Enter");
|
||||
await nextTick();
|
||||
|
||||
// 3 new rows: the two created ("1" and "2", and a new still in edit mode)
|
||||
assert.strictEqual($(".o_data_cell[name=int_field]").length, initialRowCount + 3);
|
||||
}
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue