19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:02 +01:00
parent 62d197ac8b
commit 184bb0e321
667 changed files with 691406 additions and 239886 deletions

View file

@ -0,0 +1,33 @@
import { describe, expect, test } from "@odoo/hoot";
import { Model } from "@odoo/o-spreadsheet";
import { insertChartInSpreadsheet } from "@spreadsheet/../tests/helpers/chart";
import { makeSpreadsheetMockEnv } from "@spreadsheet/../tests/helpers/model";
import { OdooDataProvider } from "@spreadsheet/data_sources/odoo_data_provider";
import { createDashboardActionWithData } from "@spreadsheet_dashboard/../tests/helpers/dashboard_action";
import { defineSpreadsheetDashboardModels } from "@spreadsheet_dashboard/../tests/helpers/data";
import { contains } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
defineSpreadsheetDashboardModels();
test("can change granularity", async () => {
const env = await makeSpreadsheetMockEnv();
const setupModel = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
const chartId = insertChartInSpreadsheet(setupModel, "odoo_line", {
metaData: {
groupBy: ["date:month"],
resModel: "partner",
measure: "__count",
order: null,
},
});
const { model } = await createDashboardActionWithData(setupModel.exportData());
expect("select.o-chart-dashboard-item").toHaveValue("month");
await contains("select.o-chart-dashboard-item", { visible: false }).select("quarter");
expect(model.getters.getChartGranularity(chartId)).toEqual({
fieldName: "date",
granularity: "quarter",
});
expect(model.getters.getChartDefinition(chartId).metaData.groupBy).toEqual(["date:quarter"]);
});

View file

@ -0,0 +1,68 @@
import { describe, expect, test } from "@odoo/hoot";
import { animationFrame } from "@odoo/hoot-dom";
import { Model, components } from "@odoo/o-spreadsheet";
import { createBasicChart } from "@spreadsheet/../tests/helpers/commands";
import { makeSpreadsheetMockEnv } from "@spreadsheet/../tests/helpers/model";
import { OdooDataProvider } from "@spreadsheet/data_sources/odoo_data_provider";
import { createDashboardActionWithData } from "@spreadsheet_dashboard/../tests/helpers/dashboard_action";
import { defineSpreadsheetDashboardModels } from "@spreadsheet_dashboard/../tests/helpers/data";
import { patchWithCleanup } from "@web/../tests/web_test_helpers";
describe.current.tags("desktop");
defineSpreadsheetDashboardModels();
function spyCharts() {
const charts = {};
patchWithCleanup(components.ChartJsComponent.prototype, {
createChart(chartData) {
super.createChart(chartData);
charts[this.props.chartId] = this.chart;
},
});
return charts;
}
test("Charts are animated only at first render", async () => {
const env = await makeSpreadsheetMockEnv();
const setupModel = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
createBasicChart(setupModel, "chartId");
const charts = spyCharts();
const { model } = await createDashboardActionWithData(setupModel.exportData());
expect(".o-figure").toHaveCount(1);
expect(charts["chartId"].config.options.animation.animateRotate).toBe(true);
// Scroll the figure out of the viewport and back in
model.dispatch("SET_VIEWPORT_OFFSET", { offsetX: 0, offsetY: 500 });
await animationFrame();
await animationFrame();
expect(".o-figure").toHaveCount(0);
model.dispatch("SET_VIEWPORT_OFFSET", { offsetX: 0, offsetY: 0 });
await animationFrame();
expect(".o-figure").toHaveCount(1);
expect(charts["chartId"].config.options.animation).toBe(false);
});
test("Charts are animated when chart type changes", async () => {
const env = await makeSpreadsheetMockEnv();
const setupModel = new Model({}, { custom: { odooDataProvider: new OdooDataProvider(env) } });
createBasicChart(setupModel, "chartId");
const charts = spyCharts();
const { model } = await createDashboardActionWithData(setupModel.exportData());
expect(".o-figure").toHaveCount(1);
expect(charts["chartId"].config.options.animation.animateRotate).toBe(true);
delete charts["chartId"];
const definition = model.getters.getChartDefinition("chartId");
model.dispatch("UPDATE_CHART", {
definition: { ...definition, type: "pie" },
chartId: "chartId",
figureId: model.getters.getFigureIdFromChartId("chartId"),
sheetId: model.getters.getActiveSheetId(),
});
await animationFrame();
expect(charts["chartId"].config.options.animation.animateRotate).toBe(true);
});

View file

@ -0,0 +1,209 @@
import { beforeEach, expect, test } from "@odoo/hoot";
import { click } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { components } from "@odoo/o-spreadsheet";
import { insertChartInSpreadsheet } from "@spreadsheet/../tests/helpers/chart";
import {
createBasicChart,
createScorecardChart,
createGaugeChart,
addChartFigureToCarousel,
createCarousel,
} from "@spreadsheet/../tests/helpers/commands";
import { getBasicData } from "@spreadsheet/../tests/helpers/data";
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
import { mountSpreadsheet } from "@spreadsheet/../tests/helpers/ui";
import { defineSpreadsheetDashboardModels } from "@spreadsheet_dashboard/../tests/helpers/data";
import { contains, mockService, patchWithCleanup } from "@web/../tests/web_test_helpers";
defineSpreadsheetDashboardModels();
/**
* @typedef {import("@spreadsheet/../tests/helpers/data").ServerData} ServerData
*/
const chartId = "uuid1";
let serverData = /** @type {ServerData} */ ({});
function mockActionService(doActionStep) {
const fakeActionService = {
doAction: async (actionRequest, options = {}) => {
if (actionRequest === "menuAction2") {
expect.step(doActionStep);
}
},
};
mockService("action", fakeActionService);
}
beforeEach(() => {
serverData = {};
serverData.menus = {
2: {
id: 2,
name: "test menu 2",
xmlid: "spreadsheet.test.menu2",
appID: 1,
actionID: "menuAction2",
},
};
serverData.models = {
...getBasicData(),
"ir.ui.menu": {
records: [{ id: 2, name: "test menu 2", action: "action2", group_ids: [10] }],
},
"res.group": { records: [{ id: 10, name: "test group" }] },
};
});
test("Click on chart in dashboard mode redirect to the odoo menu", async function () {
const doActionStep = "doAction";
mockActionService(doActionStep);
const { model } = await createModelWithDataSource({ serverData });
const fixture = await mountSpreadsheet(model);
createBasicChart(model, chartId);
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
chartId,
odooMenuId: 2,
});
const chartMenu = model.getters.getChartOdooMenu(chartId);
expect(chartMenu.id).toBe(2, { message: "Odoo menu is linked to chart" });
await animationFrame();
await click(fixture.querySelector(".o-chart-container canvas"));
await animationFrame();
// Clicking on a chart while not dashboard mode do nothing
expect.verifySteps([]);
model.updateMode("dashboard");
await animationFrame();
await click(fixture.querySelector(".o-chart-container canvas"));
await animationFrame();
// Clicking on a chart while on dashboard mode redirect to the odoo menu
expect.verifySteps([doActionStep]);
});
test("Click on chart element in dashboard mode do not redirect twice", async function () {
patchWithCleanup(components.ChartJsComponent.prototype, {
enableAnimationInChartData(chartData) {
return chartData; // disable animation for the test
},
});
mockService("action", {
doAction: async (actionRequest, options = {}) => {
if (actionRequest === "menuAction2") {
expect.step("chartMenuRedirect");
} else if (
actionRequest.type === "ir.actions.act_window" &&
actionRequest.res_model === "partner"
) {
expect.step("chartElementRedirect");
}
},
});
const { model } = await createModelWithDataSource({ serverData });
const fixture = await mountSpreadsheet(model);
const chartId = insertChartInSpreadsheet(model, "odoo_pie");
model.dispatch("LINK_ODOO_MENU_TO_CHART", { chartId, odooMenuId: 2 });
await animationFrame();
model.updateMode("dashboard");
await animationFrame();
// Click pie element
const chartCanvas = fixture.querySelector(".o-chart-container canvas");
const canvasRect = chartCanvas.getBoundingClientRect();
const canvasCenter = {
x: canvasRect.left + canvasRect.width / 2,
y: canvasRect.top + canvasRect.height / 2,
};
await click(".o-chart-container canvas", { position: canvasCenter, relative: true });
await animationFrame();
expect.verifySteps(["chartElementRedirect"]);
// Click outside the pie element
await click(".o-chart-container canvas", { position: "top-left" });
await animationFrame();
expect.verifySteps(["chartMenuRedirect"]);
});
test("Clicking on a scorecard or gauge redirects to the linked menu id", async function () {
mockService("action", {
doAction: async (actionRequest) => expect.step(actionRequest),
});
const { model } = await createModelWithDataSource({ serverData });
await mountSpreadsheet(model);
createScorecardChart(model, "scorecardId");
createGaugeChart(model, "gaugeId");
model.dispatch("LINK_ODOO_MENU_TO_CHART", { chartId: "scorecardId", odooMenuId: 2 });
model.dispatch("LINK_ODOO_MENU_TO_CHART", { chartId: "gaugeId", odooMenuId: 2 });
await animationFrame();
model.updateMode("dashboard");
await animationFrame();
const chartCanvas = document.querySelectorAll(".o-figure canvas");
await click(chartCanvas[0]);
expect.verifySteps(["menuAction2"]);
await click(chartCanvas[1]);
expect.verifySteps(["menuAction2"]);
});
test.tags("desktop");
test("Middle-click on chart in dashboard mode open the odoo menu in a new tab", async function () {
const { model } = await createModelWithDataSource({ serverData });
await mountSpreadsheet(model);
mockService("action", {
doAction(_, options) {
expect.step("doAction");
expect(options).toEqual({
newWindow: true,
});
return Promise.resolve(true);
},
});
createBasicChart(model, chartId);
model.dispatch("LINK_ODOO_MENU_TO_CHART", {
chartId,
odooMenuId: 2,
});
model.updateMode("dashboard");
await animationFrame();
await contains(".o-chart-container canvas").click({ ctrlKey: true });
expect.verifySteps(["doAction"]);
await contains(".o-chart-container canvas").click({ button: 1 }); // middle mouse click
expect.verifySteps(["doAction"]);
});
test("Clicking on the carousel header doesn't redirect to its chart's linked menu", async function () {
mockService("action", {
doAction: async (actionRequest) => expect.step(actionRequest),
});
const { model } = await createModelWithDataSource({ serverData });
await mountSpreadsheet(model);
createBasicChart(model, chartId);
const sheetId = model.getters.getActiveSheetId();
const chartFigureId = model.getters.getFigures(sheetId)[0].id;
createCarousel(model, { items: [] }, "carouselId");
addChartFigureToCarousel(model, "carouselId", chartFigureId);
model.dispatch("LINK_ODOO_MENU_TO_CHART", { chartId, odooMenuId: 2 });
model.updateMode("dashboard");
await animationFrame();
await contains(".o-carousel-header").click();
expect.verifySteps([]);
await contains(".o-carousel canvas").click();
expect.verifySteps(["menuAction2"]);
});

View file

@ -1,96 +0,0 @@
/** @odoo-module */
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
import { getFixture } from "@web/../tests/helpers/utils";
import { getDashboardServerData } from "../utils/data";
import { createSpreadsheetDashboard } from "../utils/dashboard_action";
import { getBasicData } from "@spreadsheet/../tests/utils/data";
const { Model } = spreadsheet;
async function createDashboardWithModel(model) {
return createDashboardWithData(model.exportData());
}
async function createDashboardWithData(spreadsheetData) {
const serverData = getDashboardServerData();
const json = JSON.stringify(spreadsheetData);
const dashboard = serverData.models["spreadsheet.dashboard"].records[0];
dashboard.raw = json;
dashboard.json_data = json;
serverData.models = {
...serverData.models,
...getBasicData(),
};
await createSpreadsheetDashboard({ serverData, spreadsheetId: dashboard.id });
return getFixture();
}
QUnit.module("spreadsheet_dashboard > clickable cells");
QUnit.test("A link in a dashboard should be clickable", async (assert) => {
const data = {
sheets: [
{
cells: { A1: { content: "[Odoo](https://odoo.com)" } },
},
],
};
const model = new Model(data, { mode: "dashboard" });
const target = await createDashboardWithModel(model);
assert.containsOnce(target, ".o-dashboard-clickable-cell");
});
QUnit.test("Invalid pivot/list formulas should not be clickable", async (assert) => {
const data = {
sheets: [
{
cells: {
A1: { content: `=ODOO.PIVOT("1", "measure")` },
A2: { content: `=ODOO.LIST("1", 1, "name")` },
},
},
],
};
const model = new Model(data, { mode: "dashboard" });
const target = await createDashboardWithModel(model);
assert.containsNone(target, ".o-dashboard-clickable-cell");
});
QUnit.test("pivot/list formulas should be clickable", async (assert) => {
const data = {
sheets: [
{
cells: {
A1: { content: '=ODOO.PIVOT(1,"probability")' },
A2: { content: '=ODOO.LIST(1, 1, "foo")' },
},
},
],
pivots: {
1: {
id: 1,
colGroupBys: [],
domain: [],
measures: [{ field: "probability", operator: "avg" }],
model: "partner",
rowGroupBys: [],
context: {},
fieldMatching: {},
},
},
lists: {
1: {
id: 1,
columns: ["foo", "contact_name"],
domain: [],
model: "partner",
orderBy: [],
context: {},
fieldMatching: {},
},
},
};
const target = await createDashboardWithData(data);
assert.containsN(target, ".o-dashboard-clickable-cell", 2);
});

View file

@ -0,0 +1,208 @@
import { describe, expect, test } from "@odoo/hoot";
import { click, queryAll, queryFirst } from "@odoo/hoot-dom";
import { createDashboardActionWithData } from "@spreadsheet_dashboard/../tests/helpers/dashboard_action";
import { defineSpreadsheetDashboardModels } from "@spreadsheet_dashboard/../tests/helpers/data";
import { Partner } from "@spreadsheet/../tests/helpers/data";
import { getCellIcons } from "@spreadsheet/../tests/helpers/getters";
import { fields } from "@web/../tests/web_test_helpers";
import { animationFrame } from "@odoo/hoot-mock";
describe.current.tags("desktop");
defineSpreadsheetDashboardModels();
test("A link in a dashboard should be clickable", async () => {
const data = {
sheets: [
{
cells: { A1: "[Odoo](https://odoo.com)" },
},
],
};
await createDashboardActionWithData(data);
expect(".o-dashboard-clickable-cell").toHaveCount(1);
});
test("Invalid pivot/list formulas should not be clickable", async () => {
const data = {
sheets: [
{
cells: {
A1: '=PIVOT.VALUE("1", "measure")',
A2: '=ODOO.LIST("1", 1, "name")',
},
},
],
};
await createDashboardActionWithData(data);
expect(".o-dashboard-clickable-cell").toHaveCount(0);
});
test("pivot/list formulas should be clickable", async () => {
const data = {
version: 16,
sheets: [
{
cells: {
A1: { content: '=PIVOT.VALUE("1", "probability", "bar", "false")' },
A2: { content: '=ODOO.LIST(1, 1, "foo")' },
},
},
],
lists: {
1: {
id: 1,
columns: ["foo"],
domain: [],
model: "partner",
orderBy: [],
},
},
pivots: {
1: {
id: 1,
colGroupBys: ["foo"],
domain: [],
measures: [{ field: "probability", operator: "avg" }],
model: "partner",
rowGroupBys: ["bar"],
context: {},
},
},
};
await createDashboardActionWithData(data);
expect(".o-dashboard-clickable-cell").toHaveCount(2);
});
test("list sorting clickable cell", async () => {
Partner._fields.foo = fields.Integer({ sortable: true });
Partner._fields.bar = fields.Boolean({ sortable: false });
const data = {
sheets: [
{
cells: {
A1: '=ODOO.LIST.HEADER(1, "foo")',
A2: '=ODOO.LIST(1, 1, "foo")',
},
},
],
lists: {
1: {
id: 1,
columns: [],
domain: [],
model: "partner",
orderBy: [],
},
},
};
const { model } = await createDashboardActionWithData(data);
expect(getCellIcons(model, "A1")).toHaveLength(0);
expect(".o-dashboard-clickable-cell .fa-sort").toHaveCount(1);
await click(queryFirst(".o-dashboard-clickable-cell .sorting-icon"));
expect(model.getters.getListDefinition(1).orderBy).toEqual([{ name: "foo", asc: true }]);
await animationFrame();
expect(getCellIcons(model, "A1")).toMatchObject([{ type: "list_dashboard_sorting_asc" }]);
await click(queryFirst(".o-dashboard-clickable-cell"));
expect(model.getters.getListDefinition(1).orderBy).toEqual([{ name: "foo", asc: false }]);
await animationFrame();
expect(getCellIcons(model, "A1")).toMatchObject([{ type: "list_dashboard_sorting_desc" }]);
await click(queryFirst(".o-dashboard-clickable-cell"));
expect(getCellIcons(model, "A1")).toHaveLength(0);
expect(model.getters.getListDefinition(1).orderBy).toEqual([]);
});
test("list sort multiple fields", async () => {
Partner._fields.foo = fields.Integer({ sortable: true });
Partner._fields.bar = fields.Boolean({ sortable: true });
const data = {
sheets: [
{
cells: {
A1: '=ODOO.LIST.HEADER(1, "foo")',
A2: '=ODOO.LIST.HEADER(1, "bar")',
},
},
],
lists: {
1: {
id: 1,
columns: [],
domain: [],
model: "partner",
orderBy: [],
},
},
};
const { model } = await createDashboardActionWithData(data);
await click(queryAll(".o-dashboard-clickable-cell")[0]);
expect(model.getters.getListDefinition(1).orderBy).toEqual([{ name: "foo", asc: true }]);
await animationFrame();
await click(queryAll(".o-dashboard-clickable-cell")[1]);
expect(model.getters.getListDefinition(1).orderBy).toEqual([
{ name: "bar", asc: true },
{ name: "foo", asc: true },
]);
await click(queryAll(".o-dashboard-clickable-cell")[0]);
expect(model.getters.getListDefinition(1).orderBy).toEqual([
{ name: "foo", asc: true },
{ name: "bar", asc: true },
]);
await animationFrame();
await click(queryAll(".o-dashboard-clickable-cell")[0]);
expect(model.getters.getListDefinition(1).orderBy).toEqual([
{ name: "foo", asc: false },
{ name: "bar", asc: true },
]);
await animationFrame();
await click(queryAll(".o-dashboard-clickable-cell")[0]);
expect(model.getters.getListDefinition(1).orderBy).toEqual([]);
await animationFrame();
});
test("Clickable ignores spill and empty cells for list sorting", async () => {
const data = {
sheets: [
{
cells: {
A1: "foo",
B1: "bar",
// spill cells
A2: "=ODOO.LIST.HEADER(1, A1:B1)",
A3: '=ODOO.LIST(1, sequence(2), "foo")',
},
},
],
lists: {
1: {
id: 1,
columns: [],
domain: [],
model: "partner",
orderBy: [],
},
},
};
const { model } = await createDashboardActionWithData(data);
expect(getCellIcons(model, "A2")).toHaveLength(0);
expect(".o-dashboard-clickable-cell .fa-sort").toHaveCount(0);
expect(getCellIcons(model, "B2")).toHaveLength(0);
expect(".o-dashboard-clickable-cell .fa-sort").toHaveCount(0);
expect(getCellIcons(model, "A3")).toHaveLength(0);
expect(".o-dashboard-clickable-cell .fa-sort").toHaveCount(0);
expect(getCellIcons(model, "A4")).toHaveLength(0);
expect(".o-dashboard-clickable-cell .fa-sort").toHaveCount(0);
expect(getCellIcons(model, "C10")).toHaveLength(0); // unrelated empty cell
expect(".o-dashboard-clickable-cell .fa-sort").toHaveCount(0);
});

View file

@ -0,0 +1,734 @@
import { describe, expect, test } from "@odoo/hoot";
import { queryAll, press, queryAllTexts } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { getBasicData, Product } from "@spreadsheet/../tests/helpers/data";
import { createSpreadsheetDashboard } from "@spreadsheet_dashboard/../tests/helpers/dashboard_action";
import {
defineSpreadsheetDashboardModels,
getDashboardServerData,
} from "@spreadsheet_dashboard/../tests/helpers/data";
import { contains, onRpc, patchWithCleanup } from "@web/../tests/web_test_helpers";
import { browser } from "@web/core/browser/browser";
import { RPCError } from "@web/core/network/rpc";
import { Deferred } from "@web/core/utils/concurrency";
import { range } from "@web/core/utils/numbers";
import { THIS_YEAR_GLOBAL_FILTER } from "@spreadsheet/../tests/helpers/global_filter";
describe.current.tags("desktop");
defineSpreadsheetDashboardModels();
function getServerData(spreadsheetData) {
const serverData = getDashboardServerData();
serverData.models = {
...serverData.models,
...getBasicData(),
};
serverData.models["spreadsheet.dashboard.group"].records = [
{
published_dashboard_ids: [789],
id: 1,
name: "Pivot",
},
];
serverData.models["spreadsheet.dashboard"].records = [
{
id: 789,
name: "Spreadsheet with Pivot",
json_data: JSON.stringify(spreadsheetData),
spreadsheet_data: JSON.stringify(spreadsheetData),
dashboard_group_id: 1,
},
];
return serverData;
}
test("display available spreadsheets", async () => {
await createSpreadsheetDashboard();
expect(".o_search_panel section").toHaveCount(2);
expect(".o_search_panel li").toHaveCount(3);
});
test("display the active spreadsheet", async () => {
await createSpreadsheetDashboard();
expect(".o_search_panel li.active").toHaveCount(1, {
message: "It should have one active element",
});
expect(".o-spreadsheet").toHaveCount(1, { message: "It should display the spreadsheet" });
});
test("Fold/unfold the search panel", async function () {
await createSpreadsheetDashboard();
await contains(".o_spreadsheet_dashboard_search_panel button").click();
expect(".o_spreadsheet_dashboard_search_panel").toHaveCount(0);
expect(".o_search_panel_sidebar").toHaveText("Container 1 / Dashboard CRM 1");
await contains(".o_search_panel_sidebar button").click();
expect(".o_search_panel_sidebar").toHaveCount(0);
expect(".o_spreadsheet_dashboard_search_panel").toHaveCount(1);
});
test("Folding dashboard from 'FAVORITES' group shows correct active dashboard group", async function () {
await createSpreadsheetDashboard({
mockRPC: async function (route, args) {
if (
args.method === "action_toggle_favorite" &&
args.model === "spreadsheet.dashboard"
) {
expect.step("action_toggle_favorite");
return true;
}
},
});
await contains(".o_dashboard_star").click();
expect(".o_search_panel_section").toHaveCount(3);
expect(".o_search_panel_category header b:first").toHaveText("FAVORITES");
expect.verifySteps(["action_toggle_favorite"]);
await contains(".o_spreadsheet_dashboard_search_panel button").click();
expect(".o_spreadsheet_dashboard_search_panel").toHaveCount(0);
expect(".o_search_panel_sidebar").toHaveText("Container 1 / Dashboard CRM 1");
});
test("Fold button invisible in the search panel without any dashboard", async function () {
const serverData = getDashboardServerData();
serverData.models["spreadsheet.dashboard"].records = [];
serverData.models["spreadsheet.dashboard.group"].records = [];
await createSpreadsheetDashboard({ serverData });
expect(".o_spreadsheet_dashboard_search_panel button").toHaveCount(0);
});
test("load action with specific dashboard", async () => {
await createSpreadsheetDashboard({ spreadsheetId: 3 });
expect(".o_search_panel li.active").toHaveText("Dashboard Accounting 1");
});
test("can switch spreadsheet", async () => {
await createSpreadsheetDashboard();
const spreadsheets = queryAll(".o_search_panel li");
expect(spreadsheets[0]).toHaveClass("active");
expect(spreadsheets[1]).not.toHaveClass("active");
expect(spreadsheets[2]).not.toHaveClass("active");
await contains(spreadsheets[1]).click();
expect(spreadsheets[0]).not.toHaveClass("active");
expect(spreadsheets[1]).toHaveClass("active");
expect(spreadsheets[2]).not.toHaveClass("active");
});
test("display no dashboard message", async () => {
await createSpreadsheetDashboard({
mockRPC: function (route, { model, method, args }) {
if (method === "web_search_read" && model === "spreadsheet.dashboard.group") {
return {
records: [],
length: 0,
};
}
},
});
expect(".o_search_panel li").toHaveCount(0, {
message: "It should not display any spreadsheet",
});
expect(".dashboard-loading-status").toHaveText("No available dashboard", {
message: "It should display no dashboard message",
});
});
test("display error message", async () => {
expect.errors(1);
onRpc("/spreadsheet/dashboard/data/2", () => {
const error = new RPCError();
error.data = {};
throw error;
});
await createSpreadsheetDashboard();
expect(".o-spreadsheet").toHaveCount(1, { message: "It should display the spreadsheet" });
await contains(".o_search_panel li:eq(1)").click();
expect(".o_spreadsheet_dashboard_action .dashboard-loading-status.error").toHaveCount(1, {
message: "It should display an error",
});
await contains(".o_search_panel li:eq(0)").click();
expect(".o-spreadsheet").toHaveCount(1, { message: "It should display the spreadsheet" });
expect(".o_renderer .error").toHaveCount(0, { message: "It should not display an error" });
expect.verifyErrors(["RPC_ERROR"]);
});
test("load dashboard that doesn't exist", async () => {
expect.errors(1);
await createSpreadsheetDashboard({
spreadsheetId: 999,
});
expect(".o_spreadsheet_dashboard_action .dashboard-loading-status.error").toHaveCount(1, {
message: "It should display an error",
});
expect.verifyErrors(["RPC_ERROR"]);
});
test("Last selected spreadsheet is kept when go back from breadcrumb", async function () {
const spreadsheetData = {
version: 16,
sheets: [
{
id: "sheet1",
cells: { A1: { content: '=PIVOT.VALUE("1", "probability")' } },
},
],
pivots: {
1: {
id: 1,
colGroupBys: ["foo"],
domain: [],
measures: [{ field: "probability", operator: "avg" }],
model: "partner",
rowGroupBys: ["bar"],
},
},
};
const serverData = getServerData(spreadsheetData);
serverData.models["spreadsheet.dashboard"].records.push({
id: 790,
name: "Second dashboard",
json_data: JSON.stringify(spreadsheetData),
spreadsheet_data: JSON.stringify(spreadsheetData),
dashboard_group_id: 1,
});
serverData.models["spreadsheet.dashboard.group"].records[0].published_dashboard_ids.push(790);
await createSpreadsheetDashboard({ serverData });
await contains(".o_search_panel li:last-child").click();
await contains(".o-dashboard-clickable-cell").click();
expect(".o_list_view").toHaveCount(1);
await contains(".o_back_button").click();
expect(".o_search_panel li:last-child").toHaveClass("active");
});
test("Can clear filter date filter value that defaults to current period", async function () {
const spreadsheetData = {
globalFilters: [
{
id: "1",
type: "date",
label: "Period",
},
{
id: "2",
type: "date",
label: "This Year",
defaultValue: "this_year",
},
],
};
const serverData = getServerData(spreadsheetData);
await createSpreadsheetDashboard({ serverData });
const year = luxon.DateTime.local().year;
expect(".o_control_panel_actions .o_facet_value").toHaveText(String(year));
await contains(".o_searchview_facet_label").click();
await contains('.o-filter-item[data-id="2"] input').click();
await contains(".o-dropdown-item[data-id='year'] .btn-previous").click();
await contains(".o-filter-values-footer .btn-primary").click();
expect(".o_control_panel_actions .o_facet_value").toHaveText(String(year - 1));
expect(".o_control_panel_actions .o_facet_remove").toHaveCount(1);
await contains(".o_control_panel_actions .o_facet_remove").click();
expect(".o_control_panel_actions .o_facet_remove").toHaveCount(0);
});
test("share dashboard from dashboard view", async function () {
patchWithCleanup(browser.navigator.clipboard, {
writeText: (url) => {
expect.step("share url copied");
expect(url).toBe("localhost:8069/share/url/132465");
},
});
const def = new Deferred();
await createSpreadsheetDashboard({
mockRPC: async function (route, args) {
if (args.method === "action_get_share_url") {
await def;
expect.step("dashboard_shared");
expect(args.model).toBe("spreadsheet.dashboard.share");
return "localhost:8069/share/url/132465";
}
},
});
expect(".spreadsheet_share_dropdown").toHaveCount(0);
await contains("i.fa-share-alt").click();
await animationFrame();
expect(".spreadsheet_share_dropdown .o_loading_state").toHaveText("Generating sharing link");
def.resolve();
await animationFrame();
expect(".spreadsheet_share_dropdown .o_loading_state").toHaveCount(0);
expect.verifySteps(["dashboard_shared", "share url copied"]);
expect(".o_field_CopyClipboardChar").toHaveText("localhost:8069/share/url/132465");
await contains(".fa-clipboard").click();
expect.verifySteps(["share url copied"]);
});
test("Changing filter values will create a new share", async function () {
const spreadsheetData = {
globalFilters: [
{
id: "1",
type: "date",
label: "Period",
},
{
id: "2",
type: "date",
label: "This Year",
defaultValue: "this_year",
},
],
};
const serverData = getServerData(spreadsheetData);
let counter = 0;
await createSpreadsheetDashboard({
serverData,
mockRPC: async function (route, args) {
if (args.method === "action_get_share_url") {
return `localhost:8069/share/url/${++counter}`;
}
},
});
await contains("i.fa-share-alt").click();
await animationFrame();
expect(".o_field_CopyClipboardChar").toHaveText(`localhost:8069/share/url/1`);
await contains("i.fa-share-alt").click(); // close share dropdown
await contains("i.fa-share-alt").click();
await animationFrame();
expect(".o_field_CopyClipboardChar").toHaveText(`localhost:8069/share/url/1`);
await contains("i.fa-share-alt").click();
const year = luxon.DateTime.local().year;
expect(".o_control_panel_actions .o_facet_value").toHaveText(String(year));
await contains(".o_searchview_facet_label").click();
await contains(".o-filter-value input").click();
await contains(".o-dropdown-item[data-id='year'] .btn-previous").click();
await contains(".o-filter-values-footer .btn-primary").click();
await contains("i.fa-share-alt").click();
await animationFrame();
expect(".o_field_CopyClipboardChar").toHaveText(`localhost:8069/share/url/2`);
});
test("Should toggle favorite status of a dashboard when the 'Favorite' icon is clicked", async function () {
onRpc("spreadsheet.dashboard", "action_toggle_favorite", ({ method }) => {
expect.step(method);
return true;
});
await createSpreadsheetDashboard();
expect(".o_search_panel_section").toHaveCount(2);
await contains(".o_dashboard_star").click();
expect(".o_dashboard_star").toHaveClass("fa-star", {
message: "The star should be filled",
});
expect(".o_search_panel_section").toHaveCount(3);
expect.verifySteps(["action_toggle_favorite"]);
expect(".o_search_panel_section.o_search_panel_category:first header b:first").toHaveText(
"FAVORITES"
);
await contains(".o_dashboard_star").click();
expect(".o_dashboard_star").not.toHaveClass("fa-star", {
message: "The star should not be filled",
});
expect.verifySteps(["action_toggle_favorite"]);
expect(".o_search_panel_section").toHaveCount(2);
});
test("Global filter with same id is not shared between dashboards", async function () {
const spreadsheetData = {
globalFilters: [
{
id: "1",
type: "relation",
label: "Relation Filter",
modelName: "product",
},
],
};
const serverData = getServerData(spreadsheetData);
serverData.models["spreadsheet.dashboard"].records.push({
id: 790,
name: "Spreadsheet dup. with Pivot",
json_data: JSON.stringify(spreadsheetData),
spreadsheet_data: JSON.stringify(spreadsheetData),
dashboard_group_id: 1,
});
serverData.models["spreadsheet.dashboard.group"].records[0].published_dashboard_ids = [
789, 790,
];
await createSpreadsheetDashboard({ serverData });
expect(".o_searchview_facet").toHaveCount(0);
await contains(".o_spreadsheet_dashboard_action .dropdown-toggle").click();
await contains(".o-autocomplete--input.o_input").click();
expect(".o-filter-value .o_tag_badge_text").toHaveCount(0);
await contains(".dropdown-item:first").click();
expect(".o-filter-value .o_tag_badge_text").toHaveCount(1);
await contains(".o-filter-values-footer .btn-primary").click();
expect(".o_searchview_facet").toHaveCount(1);
await contains(".o_search_panel li:last-child").click();
expect(".o_searchview_facet").toHaveCount(0);
});
test("Search bar is not present if there is no global filters", async function () {
const spreadsheetData = {
globalFilters: [],
};
const serverData = getServerData(spreadsheetData);
await createSpreadsheetDashboard({ serverData });
expect(".o_sp_dashboard_search").toHaveCount(0);
});
test("Can add a new global filter from the search bar", async function () {
const spreadsheetData = {
globalFilters: [
{
id: "1",
type: "relation",
label: "Relation Filter",
modelName: "product",
},
],
};
const serverData = getServerData(spreadsheetData);
await createSpreadsheetDashboard({ serverData });
await contains(".o_spreadsheet_dashboard_action .dropdown-toggle").click();
expect(".o-autocomplete--input.o_input").toHaveCount(1);
expect(".o-autocomplete--input.o_input").toHaveValue("");
await contains(".o-autocomplete--input.o_input").click();
await contains(".o-autocomplete--dropdown-item").click();
await contains(".o-filter-values-footer .btn-primary").click();
expect(".o_searchview_facet").toHaveCount(1);
expect(".o_searchview_facet .o_searchview_facet_label").toHaveText("Relation Filter");
expect(".o_searchview_facet .o_facet_value").toHaveText("xphone");
});
test("Can open the dialog by clicking on a facet", async function () {
const spreadsheetData = {
globalFilters: [
{
id: "1",
type: "relation",
label: "Relation Filter",
modelName: "product",
defaultValue: { operator: "in", ids: [37] }, // xphone
},
],
};
const serverData = getServerData(spreadsheetData);
await createSpreadsheetDashboard({ serverData });
expect(".o_searchview_facet").toHaveCount(1);
await contains(".o_searchview_facet .o_searchview_facet_label ").click();
expect(".o-filter-values").toHaveCount(1);
});
test("Can open the dialog by clicking on the search bar", async function () {
const spreadsheetData = {
globalFilters: [
{
id: "1",
type: "relation",
label: "Relation Filter",
modelName: "product",
defaultValue: { operator: "in", ids: [37] }, // xphone
},
],
};
const serverData = getServerData(spreadsheetData);
await createSpreadsheetDashboard({ serverData });
await contains(".o_searchview input").click();
expect(".o-filter-values").toHaveCount(1);
});
test("Changes of global filters are not dispatched while inside the dialog", async function () {
const spreadsheetData = {
globalFilters: [
{
id: "1",
type: "relation",
label: "Relation Filter",
modelName: "product",
},
],
};
const serverData = getServerData(spreadsheetData);
const { model } = await createSpreadsheetDashboard({ serverData });
expect(model.getters.getGlobalFilterValue("1")).toBe(undefined);
await contains(".o_spreadsheet_dashboard_action .dropdown-toggle").click();
await contains(".o-autocomplete--input.o_input").click();
await contains(".o-autocomplete--dropdown-item").click();
expect(model.getters.getGlobalFilterValue("1")).toBe(undefined);
await contains(".o-filter-values-footer .btn-primary").click();
expect(model.getters.getGlobalFilterValue("1")).toEqual({ operator: "in", ids: [37] });
});
test("First global filter date is displayed as button", async function () {
const spreadsheetData = {
globalFilters: [
{
id: "1",
type: "relation",
label: "Relation Filter",
modelName: "product",
defaultValue: { operator: "in", ids: [37] },
},
{
id: "2",
type: "date",
label: "Period",
defaultValue: "this_year",
},
],
};
const serverData = getServerData(spreadsheetData);
await createSpreadsheetDashboard({ serverData });
expect(".o_sp_date_filter_button").toHaveCount(1);
expect(".o_searchview_facet").toHaveCount(1);
});
test("No date buttons are displayed if there is no date filter", async function () {
const spreadsheetData = {
globalFilters: [
{
id: "1",
type: "relation",
label: "Relation Filter",
modelName: "product",
},
],
};
const serverData = getServerData(spreadsheetData);
await createSpreadsheetDashboard({ serverData });
expect(".o_sp_date_filter_button").toHaveCount(0);
});
test("Unknown value for relation filter is displayed as inaccessible", async function () {
const spreadsheetData = {
globalFilters: [
{
id: "1",
type: "relation",
label: "Relation Filter",
modelName: "product",
defaultValue: { operator: "in", ids: [9999] }, // unknown product
},
],
};
const serverData = getServerData(spreadsheetData);
await createSpreadsheetDashboard({ serverData });
expect(".o_searchview_facet").toHaveCount(1);
expect(".o_searchview_facet .o_facet_value").toHaveText("Inaccessible/missing record ID");
});
describe("Quick search bar", () => {
const productFilter = {
id: "1",
type: "relation",
label: "Product",
modelName: "product",
};
const selectionFilter = {
id: "55",
type: "selection",
label: "Selection Filter",
resModel: "res.currency",
selectionField: "position",
};
test("Can quick search a string in a relational filter", async function () {
const spreadsheetData = { globalFilters: [productFilter] };
const serverData = getServerData(spreadsheetData);
const { model } = await createSpreadsheetDashboard({ serverData });
await contains(".o_searchview_input").edit("phone");
expect(".o-dropdown-item.focus").toHaveText("Search Product for: phone");
await press("Enter");
const filterValue = model.getters.getGlobalFilterValue(productFilter.id);
expect(filterValue).toEqual({ operator: "ilike", strings: ["phone"] });
});
test("Can quick search a string in a relational filter if a record was already selected", async function () {
const filter = { ...productFilter, defaultValue: { operator: "in", ids: [37] } };
const spreadsheetData = { globalFilters: [filter] };
const serverData = getServerData(spreadsheetData);
const { model } = await createSpreadsheetDashboard({ serverData });
await contains(".o_searchview_input").edit("test");
expect(".o-dropdown-item.focus").toHaveText("Search Product for: test");
await press("Enter");
const filterValue = model.getters.getGlobalFilterValue(productFilter.id);
expect(filterValue).toEqual({ operator: "ilike", strings: ["test"] });
});
test("Can quick search a specific record in a relational filter", async function () {
const spreadsheetData = { globalFilters: [productFilter] };
const serverData = getServerData(spreadsheetData);
const { model } = await createSpreadsheetDashboard({ serverData });
await contains(".o_searchview_input").edit("x");
expect(".o-dropdown-item.focus").toHaveText("Search Product for: x");
await contains(".o-dropdown-item.focus .o_expand").click();
const children = queryAll(".o-dropdown-item.o_indent");
expect(children.map((el) => el.innerText)).toEqual(["xphone", "xpad"]);
await contains(children[0]).click();
const filterValue = model.getters.getGlobalFilterValue(productFilter.id);
expect(filterValue).toEqual({ operator: "in", ids: [37] });
});
test("Can load more records in the quick search", async function () {
for (let i = 0; i < 15; i++) {
Product._records.push({ id: i, display_name: "name" + i });
}
const serverData = getServerData({ globalFilters: [productFilter] });
await createSpreadsheetDashboard({ serverData });
await contains(".o_searchview_input").edit("name");
expect(".o-dropdown-item.focus").toHaveText("Search Product for: name");
await contains(".o-dropdown-item.focus .o_expand").click();
const children = queryAll(".o-dropdown-item.o_indent");
expect(children.map((el) => el.innerText)).toEqual([
...range(0, 9).map((i) => "name" + i),
"Load more",
]);
await contains(children.at(-1)).click();
expect(queryAllTexts(".o-dropdown-item.o_indent")).toEqual(
range(0, 15).map((i) => "name" + i)
);
});
test("Can quick search a string in a text filter", async function () {
const spreadsheetData = { globalFilters: [{ id: "2", type: "text", label: "Text" }] };
const serverData = getServerData(spreadsheetData);
const { model } = await createSpreadsheetDashboard({ serverData });
await contains(".o_searchview_input").edit("phone");
expect(".o-dropdown-item.focus").toHaveText("Search Text for: phone");
await press("Enter");
const filterValue = model.getters.getGlobalFilterValue("2");
expect(filterValue).toEqual({ operator: "ilike", strings: ["phone"] });
});
test("Can quick search a string in a text filter with a range of allowed values", async function () {
const spreadsheetData = {
sheets: [{ id: "sh1", name: "Sh1", cells: { A1: "phone", A2: "tablet", A3: "table" } }],
globalFilters: [
{
id: "2",
type: "text",
label: "Text",
rangesOfAllowedValues: ["Sh1!A1:A5"],
},
],
};
const serverData = getServerData(spreadsheetData);
const { model } = await createSpreadsheetDashboard({ serverData });
await contains(".o_searchview_input").edit("a");
expect(".o-dropdown-item.focus").toHaveText("Search Text for: a");
await press("ArrowRight");
await animationFrame();
const children = queryAll(".o-dropdown-item.o_indent");
expect(children.map((el) => el.innerText)).toEqual(["tablet", "table"]);
await contains(children[1]).click();
const filterValue = model.getters.getGlobalFilterValue("2");
expect(filterValue).toEqual({ operator: "ilike", strings: ["table"] });
});
test("Cannot search for a string that is not in rangesOfAllowedValues", async function () {
const spreadsheetData = {
sheets: [{ id: "sh1", name: "Sh1", cells: { A1: "phone", A2: "tablet", A3: "table" } }],
globalFilters: [
{
id: "2",
type: "text",
label: "Text",
rangesOfAllowedValues: ["Sh1!A1:A5"],
},
],
};
const serverData = getServerData(spreadsheetData);
const { model } = await createSpreadsheetDashboard({ serverData });
await contains(".o_searchview_input").edit("desk");
expect(".o-dropdown-item.focus").toHaveText("Search Text for: desk");
await press("Enter");
const filterValue = model.getters.getGlobalFilterValue("2");
expect(filterValue).toEqual(undefined);
});
test("Can quick search a selection filter value", async function () {
const spreadsheetData = { globalFilters: [selectionFilter] };
const serverData = getServerData(spreadsheetData);
const { model } = await createSpreadsheetDashboard({ serverData });
await contains(".o_searchview_input").edit("a");
expect(".o-dropdown-item.focus").toHaveText("Search Selection Filter for: a");
await contains(".o-dropdown-item.focus .o_expand").click();
const children = queryAll(".o-dropdown-item.o_indent");
expect(children.map((el) => el.innerText)).toEqual(["A"]);
await contains(children[0]).click();
const filterValue = model.getters.getGlobalFilterValue(selectionFilter.id);
expect(filterValue).toEqual({ operator: "in", selectionValues: ["after"] });
});
test("Date and numeric filters are not in the quick search results", async function () {
const numericFilter = { id: "255", type: "numeric", label: "Numeric Filter" };
const spreadsheetData = {
globalFilters: [productFilter, THIS_YEAR_GLOBAL_FILTER, numericFilter, selectionFilter],
};
const serverData = getServerData(spreadsheetData);
await createSpreadsheetDashboard({ serverData });
await contains(".o_searchview_input").edit("phone");
expect(queryAllTexts(".o-dropdown-item")).toEqual([
"Search Product for: phone",
"Search Selection Filter for: phone",
]);
});
test("Pressing backspace will remove the last facet", async function () {
const filter = { ...productFilter, defaultValue: { operator: "in", ids: [37] } };
const spreadsheetData = { globalFilters: [filter] };
const serverData = getServerData(spreadsheetData);
const { model } = await createSpreadsheetDashboard({ serverData });
let filterValue = model.getters.getGlobalFilterValue(productFilter.id);
expect(filterValue).toEqual({ operator: "in", ids: [37] });
await contains(".o_searchview_input").focus();
await press("Backspace");
filterValue = model.getters.getGlobalFilterValue(productFilter.id);
expect(filterValue).toEqual(undefined);
});
});

View file

@ -0,0 +1,67 @@
import { describe, expect, test, getFixture } from "@odoo/hoot";
import { getBasicData } from "@spreadsheet/../tests/helpers/data";
import { createSpreadsheetDashboard } from "@spreadsheet_dashboard/../tests/helpers/dashboard_action";
import {
defineSpreadsheetDashboardModels,
getDashboardServerData,
} from "@spreadsheet_dashboard/../tests/helpers/data";
import { contains } from "@web/../tests/web_test_helpers";
describe.current.tags("mobile");
defineSpreadsheetDashboardModels();
function getServerData(spreadsheetData) {
const serverData = getDashboardServerData();
serverData.models = {
...serverData.models,
...getBasicData(),
};
serverData.models["spreadsheet.dashboard.group"].records = [
{
published_dashboard_ids: [789],
id: 1,
name: "Pivot",
},
];
serverData.models["spreadsheet.dashboard"].records = [
{
id: 789,
name: "Spreadsheet with Pivot",
json_data: JSON.stringify(spreadsheetData),
spreadsheet_data: JSON.stringify(spreadsheetData),
dashboard_group_id: 1,
},
];
return serverData;
}
test("Search input can be toggled", async () => {
const productFilter = { id: "1", type: "relation", label: "Product", modelName: "product" };
const spreadsheetData = { globalFilters: [productFilter] };
const serverData = getServerData(spreadsheetData);
await createSpreadsheetDashboard({ serverData });
expect(".o_searchview_input").toHaveCount(0);
await contains(".o_search_toggler button").click();
expect(".o_searchview_input").toHaveCount(1);
});
test("Search input is not focusable in mobile", async () => {
const productFilter = {
id: "1",
type: "relation",
label: "Product",
modelName: "product",
};
const spreadsheetData = { globalFilters: [productFilter] };
const serverData = getServerData(spreadsheetData);
await createSpreadsheetDashboard({ serverData });
await contains(".o_search_toggler button").click();
await contains(".o_searchview_input").click();
const input = getFixture().querySelector(".o_searchview_input");
expect(document.activeElement).not.toBe(input);
expect(".o_bottom_sheet .o-filter-values").toHaveCount(1);
});

View file

@ -1,254 +0,0 @@
/** @odoo-module */
import {
getFixture,
click,
legacyExtraNextTick,
nextTick,
editInput,
} from "@web/../tests/helpers/utils";
import { getDashboardServerData } from "../utils/data";
import { getBasicData, getBasicListArchs } from "@spreadsheet/../tests/utils/data";
import { createSpreadsheetDashboard } from "../utils/dashboard_action";
import { registry } from "@web/core/registry";
import { errorService } from "@web/core/errors/error_service";
import { RPCError } from "@web/core/network/rpc_service";
QUnit.module("spreadsheet_dashboard > Dashboard > Dashboard action");
function getServerData(spreadsheetData) {
const serverData = getDashboardServerData();
serverData.models = {
...serverData.models,
...getBasicData(),
};
serverData.views = getBasicListArchs();
serverData.models["spreadsheet.dashboard.group"].records = [
{
dashboard_ids: [789],
id: 1,
name: "Pivot",
},
];
serverData.models["spreadsheet.dashboard"].records = [
{
id: 789,
name: "Spreadsheet with Pivot",
json_data: JSON.stringify(spreadsheetData),
raw: JSON.stringify(spreadsheetData),
dashboard_group_id: 1,
},
];
return serverData;
}
QUnit.test("display available spreadsheets", async (assert) => {
await createSpreadsheetDashboard();
assert.containsN(getFixture(), ".o_search_panel section", 2);
assert.containsN(getFixture(), ".o_search_panel li", 3);
});
QUnit.test("display the active spreadsheet", async (assert) => {
await createSpreadsheetDashboard();
assert.containsOnce(
getFixture(),
".o_search_panel li.active",
"It should have one active element"
);
assert.containsOnce(getFixture(), ".o-spreadsheet", "It should display the spreadsheet");
});
QUnit.test("load action with specific dashboard", async (assert) => {
await createSpreadsheetDashboard({ spreadsheetId: 3 });
const active = getFixture().querySelector(".o_search_panel li.active");
assert.strictEqual(active.innerText, "Dashboard Accounting 1");
});
QUnit.test("can switch spreadsheet", async (assert) => {
await createSpreadsheetDashboard();
const fixture = getFixture();
const spreadsheets = fixture.querySelectorAll(".o_search_panel li");
assert.ok(spreadsheets[0].className.includes("active"));
assert.notOk(spreadsheets[1].className.includes("active"));
assert.notOk(spreadsheets[2].className.includes("active"));
await click(spreadsheets[1]);
assert.notOk(spreadsheets[0].className.includes("active"));
assert.ok(spreadsheets[1].className.includes("active"));
assert.notOk(spreadsheets[2].className.includes("active"));
});
QUnit.test("display no dashboard message", async (assert) => {
await createSpreadsheetDashboard({
mockRPC: function (route, { model, method, args }) {
if (method === "search_read" && model === "spreadsheet.dashboard.group") {
return [];
}
},
});
const fixture = getFixture();
assert.containsNone(fixture, ".o_search_panel li", "It should not display any spreadsheet");
assert.strictEqual(
fixture.querySelector(".dashboard-loading-status").innerText,
"No available dashboard",
"It should display no dashboard message"
);
});
QUnit.test("display error message", async (assert) => {
registry.category("services").add("error", errorService);
await createSpreadsheetDashboard({
mockRPC: function (route, args) {
if (
args.model === "spreadsheet.dashboard" &&
((args.method === "read" && args.args[0][0] === 2 && args.args[1][0] === "raw") ||
// this is not correct from a module dependency POV but it's required for the test
// to pass when `spreadsheet_dashboard_edition` module is installed
(args.method === "join_spreadsheet_session" && args.args[0] === 2))
) {
const error = new RPCError();
error.data = {};
throw error;
}
},
});
const fixture = getFixture();
const spreadsheets = fixture.querySelectorAll(".o_search_panel li");
assert.containsOnce(fixture, ".o-spreadsheet", "It should display the spreadsheet");
await click(spreadsheets[1]);
assert.containsOnce(
fixture,
".o_spreadsheet_dashboard_action .dashboard-loading-status.error",
"It should display an error"
);
await click(spreadsheets[0]);
assert.containsOnce(fixture, ".o-spreadsheet", "It should display the spreadsheet");
assert.containsNone(fixture, ".o_renderer .error", "It should not display an error");
});
QUnit.test("load dashboard that doesn't exist", async (assert) => {
registry.category("services").add("error", errorService);
await createSpreadsheetDashboard({
spreadsheetId: 999,
});
const fixture = getFixture();
assert.containsOnce(
fixture,
".o_spreadsheet_dashboard_action .dashboard-loading-status.error",
"It should display an error"
);
});
QUnit.test(
"Last selected spreadsheet is kept when go back from breadcrumb",
async function (assert) {
const spreadsheetData = {
sheets: [
{
id: "sheet1",
cells: { A1: { content: `=PIVOT("1", "probability")` } },
},
],
pivots: {
1: {
id: 1,
colGroupBys: ["foo"],
domain: [],
measures: [{ field: "probability", operator: "avg" }],
model: "partner",
rowGroupBys: ["bar"],
},
},
};
const serverData = getServerData(spreadsheetData);
const fixture = getFixture();
await createSpreadsheetDashboard({ serverData });
await click(fixture, ".o_search_panel li:last-child");
await click(fixture, ".o-dashboard-clickable-cell");
await legacyExtraNextTick();
assert.containsOnce(fixture, ".o_list_view");
await click(document.body.querySelector(".o_back_button"));
await legacyExtraNextTick();
assert.hasClass(fixture.querySelector(".o_search_panel li:last-child"), "active");
}
);
QUnit.test(
"Can clear filter date filter value that defaults to current period",
async function (assert) {
const spreadsheetData = {
globalFilters: [
{
id: "1",
type: "date",
label: "Date Filter",
rangeType: "year",
defaultValue: {},
defaultsToCurrentPeriod: true,
pivotFields: {},
},
],
};
const serverData = getServerData(spreadsheetData);
const fixture = getFixture();
await createSpreadsheetDashboard({ serverData });
const year = fixture.querySelector(".o_cp_top_right input.o_datepicker_input");
const this_year = luxon.DateTime.local().year;
assert.equal(year.value, String(this_year));
const input = fixture.querySelector(
"input.o_datepicker_input.o_input.datetimepicker-input"
);
await click(input);
await editInput(input, null, String(this_year - 1));
await nextTick();
assert.equal(year.value, String(this_year - 1));
assert.containsOnce(fixture, ".o_cp_top_right .fa-times");
await click(fixture.querySelector(".o_cp_top_right .fa-times"));
assert.containsNone(fixture, ".o_cp_top_right .fa-times");
assert.equal(year.value, "");
}
);
QUnit.test("Global filter with same id is not shared between dashboards", async function (assert) {
const spreadsheetData = {
globalFilters: [
{
id: "1",
type: "relation",
label: "Relation Filter",
modelName: "product",
},
],
};
const serverData = getServerData(spreadsheetData);
serverData.models["spreadsheet.dashboard"].records.push({
id: 790,
name: "Spreadsheet dup. with Pivot",
json_data: JSON.stringify(spreadsheetData),
raw: JSON.stringify(spreadsheetData),
dashboard_group_id: 1,
});
serverData.models["spreadsheet.dashboard.group"].records[0].dashboard_ids = [789, 790];
const fixture = getFixture();
await createSpreadsheetDashboard({ serverData });
assert.containsNone(
fixture,
".o-filter-value .o_tag_badge_text",
"It should not display any filter value"
);
await click(fixture.querySelector(".o-autocomplete--input.o_input"));
await click(fixture.querySelector(".dropdown-item"));
assert.containsN(
fixture,
".o-filter-value .o_tag_badge_text",
1,
"It should not display any filter value"
);
await click(fixture.querySelector(".o_search_panel li:last-child"));
assert.containsNone(
fixture,
".o-filter-value .o_tag_badge_text",
"It should not display any filter value"
);
});

View file

@ -0,0 +1,51 @@
import { describe, expect, test } from "@odoo/hoot";
import { makeMockEnv, contains, mountWithCleanup } from "@web/../tests/web_test_helpers";
import { defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
import { DashboardDateFilter } from "@spreadsheet_dashboard/bundle/dashboard_action/dashboard_date_filter/dashboard_date_filter";
describe.current.tags("desktop");
defineSpreadsheetModels();
/**
*
* @param {{ model: Model, filter: object}} props
*/
async function mountDashboardFilterValue(env, props) {
await mountWithCleanup(DashboardDateFilter, { props, env });
}
test("Can display the input as a button", async function () {
const env = await makeMockEnv();
await mountDashboardFilterValue(env, {
value: { type: "range", from: "2023-01-01", to: "2023-01-31" },
update: () => {},
});
expect("button").toHaveCount(3);
expect(".o-date-filter-value").toHaveText("January 1 31, 2023");
});
test("Can navigate with buttons to select the next period", async function () {
const env = await makeMockEnv();
await mountDashboardFilterValue(env, {
value: { type: "month", month: 1, year: 2023 },
update: (value) => {
expect.step("update");
expect(value).toEqual({ type: "month", month: 2, year: 2023 });
},
});
await contains(".btn-next-date").click();
expect.verifySteps(["update"]);
});
test("Can navigate with buttons to select the previous period", async function () {
const env = await makeMockEnv();
await mountDashboardFilterValue(env, {
value: { type: "month", month: 1, year: 2023 },
update: (value) => {
expect.step("update");
expect(value).toEqual({ type: "month", month: 12, year: 2022 });
},
});
await contains(".btn-previous-date").click();
expect.verifySteps(["update"]);
});

View file

@ -0,0 +1,287 @@
import { expect, test } from "@odoo/hoot";
import { animationFrame } from "@odoo/hoot-mock";
import { getCellValue } from "@spreadsheet/../tests/helpers/getters";
import { makeSpreadsheetMockEnv } from "@spreadsheet/../tests/helpers/model";
import { waitForDataLoaded } from "@spreadsheet/helpers/model";
import {
defineSpreadsheetDashboardModels,
getDashboardServerData,
} from "@spreadsheet_dashboard/../tests/helpers/data";
import {
DashboardLoader,
Status,
} from "@spreadsheet_dashboard/bundle/dashboard_action/dashboard_loader_service";
import { onRpc, patchWithCleanup } from "@web/../tests/web_test_helpers";
import { RPCError } from "@web/core/network/rpc";
defineSpreadsheetDashboardModels();
/**
* @param {object} [params]
* @param {object} [params.serverData]
* @param {function} [params.mockRPC]
* @returns {Promise<DashboardLoader>}
*/
async function createDashboardLoader(params = {}) {
const env = await makeSpreadsheetMockEnv({
serverData: params.serverData || getDashboardServerData(),
mockRPC: params.mockRPC,
});
return new DashboardLoader(env, env.services.orm, async (dashboardId) => {
const [record] = await env.services.orm.read(
"spreadsheet.dashboard",
[dashboardId],
["spreadsheet_data"]
);
return { data: JSON.parse(record.spreadsheet_data), revisions: [] };
});
}
test("load all dashboards of all containers", async () => {
const loader = await createDashboardLoader();
loader.load();
expect(loader.getDashboardGroups()).toEqual([]);
await animationFrame();
expect(loader.getDashboardGroups()).toEqual([
{
id: 1,
name: "Container 1",
dashboards: [
{
data: {
id: 1,
name: "Dashboard CRM 1",
is_favorite: false,
},
status: Status.NotLoaded,
},
{
data: {
id: 2,
name: "Dashboard CRM 2",
is_favorite: false,
},
status: Status.NotLoaded,
},
],
},
{
id: 2,
name: "Container 2",
dashboards: [
{
data: {
id: 3,
name: "Dashboard Accounting 1",
is_favorite: false,
},
status: Status.NotLoaded,
},
],
},
]);
});
test("load twice does not duplicate spreadsheets", async () => {
const loader = await createDashboardLoader();
await loader.load();
expect(loader.getDashboardGroups()[1].dashboards).toMatchObject([{ status: Status.NotLoaded }]);
await loader.load();
expect(loader.getDashboardGroups()[1].dashboards).toMatchObject([{ status: Status.NotLoaded }]);
});
test("load spreadsheet data", async () => {
const loader = await createDashboardLoader();
await loader.load();
const result = loader.getDashboard(3);
expect(result.status).toBe(Status.Loading);
await animationFrame();
expect(result.status).toBe(Status.Loaded);
expect(result.model).not.toBe(undefined);
});
test("load spreadsheet data only once", async () => {
onRpc("/spreadsheet/dashboard/data/3", () => expect.step("spreadsheet 3 loaded"));
const loader = await createDashboardLoader({
mockRPC: function (route, args) {
if (args.model === "spreadsheet.dashboard" && args.method === "read") {
// read names
expect.step(`spreadsheet ${args.args[0]} loaded`);
}
},
});
await loader.load();
let result = loader.getDashboard(3);
await animationFrame();
expect(result.status).toBe(Status.Loaded);
expect.verifySteps(["spreadsheet 3 loaded"]);
result = loader.getDashboard(3);
await animationFrame();
expect(result.status).toBe(Status.Loaded);
expect.verifySteps([]);
});
test("don't return empty dashboard group", async () => {
const loader = await createDashboardLoader({
mockRPC: async function (route, args) {
if (args.method === "web_search_read" && args.model === "spreadsheet.dashboard.group") {
return {
length: 2,
records: [
{
id: 45,
name: "Group A",
published_dashboard_ids: [{ id: 1, name: "Dashboard CRM 1" }],
},
{
id: 46,
name: "Group B",
published_dashboard_ids: [],
},
],
};
}
},
});
await loader.load();
expect(loader.getDashboardGroups()).toEqual([
{
id: 45,
name: "Group A",
dashboards: [
{
data: { id: 1, name: "Dashboard CRM 1" },
status: Status.NotLoaded,
},
],
},
]);
});
test("load multiple spreadsheets", async () => {
onRpc("/spreadsheet/dashboard/data/1", () => expect.step("spreadsheet 1 loaded"));
onRpc("/spreadsheet/dashboard/data/2", () => expect.step("spreadsheet 2 loaded"));
const loader = await createDashboardLoader({
mockRPC: function (route, args) {
if (args.method === "web_search_read" && args.model === "spreadsheet.dashboard.group") {
expect.step("load groups");
}
if (args.method === "read" && args.model === "spreadsheet.dashboard") {
// read names
expect.step(`spreadsheet ${args.args[0]} loaded`);
}
},
});
await loader.load();
expect.verifySteps(["load groups"]);
loader.getDashboard(1);
await animationFrame();
expect.verifySteps(["spreadsheet 1 loaded"]);
loader.getDashboard(2);
await animationFrame();
expect.verifySteps(["spreadsheet 2 loaded"]);
loader.getDashboard(1);
await animationFrame();
expect.verifySteps([]);
});
test("load spreadsheet data with error", async () => {
onRpc("/spreadsheet/dashboard/data/*", () => {
const error = new RPCError();
error.data = { message: "Bip" };
throw error;
});
const loader = await createDashboardLoader();
await loader.load();
const result = loader.getDashboard(3);
expect(result.status).toBe(Status.Loading);
await result.promise.catch(() => expect.step("error"));
expect(result.status).toBe(Status.Error);
expect(result.error.data.message).toBe("Bip");
// error is thrown
expect.verifySteps(["error"]);
});
test("async formulas are correctly evaluated", async () => {
const spreadsheetData = {
sheets: [
{
id: "sheet1",
cells: {
A1: '=ODOO.CURRENCY.RATE("EUR","USD")', // an async formula
},
},
],
};
const serverData = getDashboardServerData();
const dashboardId = 15;
serverData.models["spreadsheet.dashboard.group"].records = [
{ id: 1, name: "Container 1", published_dashboard_ids: [dashboardId] },
];
serverData.models["spreadsheet.dashboard"].records = [
{
id: dashboardId,
spreadsheet_data: JSON.stringify(spreadsheetData),
json_data: JSON.stringify(spreadsheetData),
name: "Dashboard Accounting 1",
dashboard_group_id: 1,
},
];
const loader = await createDashboardLoader({
serverData,
mockRPC: function (route, args) {
if (args.method === "get_rates_for_spreadsheet") {
const info = args.args[0][0];
return [{ ...info, rate: 0.9 }];
}
},
});
await loader.load();
loader.getDashboard(dashboardId);
await animationFrame();
const { model } = loader.getDashboard(dashboardId);
await waitForDataLoaded(model);
expect(await getCellValue(model, "A1")).toBe(0.9);
});
test("Model is in dashboard mode", async () => {
const loader = await createDashboardLoader();
await loader.load();
loader.getDashboard(3);
await animationFrame();
const { model } = loader.getDashboard(3);
expect(model.config.mode).toBe("dashboard");
});
test("Model is in dashboard mode [2]", async () => {
patchWithCleanup(DashboardLoader.prototype, {
_activateFirstSheet: () => {
expect.step("activate sheet");
},
});
const loader = await createDashboardLoader();
await loader.load();
loader.getDashboard(3);
await animationFrame();
expect.verifySteps(["activate sheet"]);
});
test("default currency format", async () => {
onRpc("/spreadsheet/dashboard/data/*", () => ({
data: {},
revisions: [],
default_currency: {
code: "Odoo",
symbol: "θ",
position: "after",
decimalPlaces: 2,
},
}));
const loader = await createDashboardLoader();
await loader.load();
const result = loader.getDashboard(3);
expect(result.status).toBe(Status.Loading);
await animationFrame();
const { model } = loader.getDashboard(3);
expect(model.getters.getCompanyCurrencyFormat()).toBe("#,##0.00[$θ]");
});

View file

@ -1,259 +0,0 @@
/** @odoo-module */
import { ormService } from "@web/core/orm_service";
import { registry } from "@web/core/registry";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import {
DashboardLoader,
Status,
} from "@spreadsheet_dashboard/bundle/dashboard_action/dashboard_loader";
import { nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
import { getDashboardServerData } from "../utils/data";
import { waitForDataSourcesLoaded } from "@spreadsheet/../tests/utils/model";
import { getCellValue } from "@spreadsheet/../tests/utils/getters";
import { RPCError } from "@web/core/network/rpc_service";
/**
* @param {object} [params]
* @param {object} [params.serverData]
* @param {function} [params.mockRPC]
* @returns {Promise<DashboardLoader>}
*/
async function createDashboardLoader(params = {}) {
registry.category("services").add("orm", ormService);
const env = await makeTestEnv({
serverData: params.serverData || getDashboardServerData(),
mockRPC: params.mockRPC,
});
return new DashboardLoader(env, env.services.orm, async (dashboardId) => {
const [record] = await env.services.orm.read(
"spreadsheet.dashboard",
[dashboardId],
["raw"]
);
return { data: record.raw, revisions: [] };
});
}
QUnit.module("spreadsheet_dashboard > Dashboard loader");
QUnit.test("load all dashboards of all containers", async (assert) => {
const loader = await createDashboardLoader();
loader.load();
assert.deepEqual(loader.getDashboardGroups(), []);
await nextTick();
assert.deepEqual(loader.getDashboardGroups(), [
{
id: 1,
name: "Container 1",
dashboards: [
{
id: 1,
displayName: "Dashboard CRM 1",
status: Status.NotLoaded,
},
{
id: 2,
displayName: "Dashboard CRM 2",
status: Status.NotLoaded,
},
],
},
{
id: 2,
name: "Container 2",
dashboards: [
{
id: 3,
displayName: "Dashboard Accounting 1",
status: Status.NotLoaded,
},
],
},
]);
});
QUnit.test("load twice does not duplicate spreadsheets", async (assert) => {
const loader = await createDashboardLoader();
await loader.load();
assert.deepEqual(loader.getDashboardGroups()[1].dashboards, [
{ id: 3, displayName: "Dashboard Accounting 1", status: Status.NotLoaded },
]);
await loader.load();
assert.deepEqual(loader.getDashboardGroups()[1].dashboards, [
{ id: 3, displayName: "Dashboard Accounting 1", status: Status.NotLoaded },
]);
});
QUnit.test("load spreadsheet data", async (assert) => {
const loader = await createDashboardLoader();
await loader.load();
const result = loader.getDashboard(3);
assert.strictEqual(result.status, Status.Loading);
await nextTick();
assert.strictEqual(result.status, Status.Loaded);
assert.ok(result.model);
});
QUnit.test("load spreadsheet data only once", async (assert) => {
const loader = await createDashboardLoader({
mockRPC: function (route, args) {
if (args.method === "read") {
assert.step(`spreadsheet ${args.args[0]} loaded`);
}
},
});
await loader.load();
let result = loader.getDashboard(3);
await nextTick();
assert.strictEqual(result.status, Status.Loaded);
assert.verifySteps(["spreadsheet 1,2,3 loaded", "spreadsheet 3 loaded"]);
result = loader.getDashboard(3);
await nextTick();
assert.strictEqual(result.status, Status.Loaded);
assert.verifySteps([]);
});
QUnit.test("don't return empty dashboard group", async (assert) => {
const loader = await createDashboardLoader({
mockRPC: async function (route, args) {
if (args.method === "search_read" && args.model === "spreadsheet.dashboard.group") {
return [
{
id: 45,
name: "Group A",
dashboard_ids: [1],
},
{
id: 46,
name: "Group B",
dashboard_ids: [],
},
];
}
},
});
await loader.load();
assert.deepEqual(loader.getDashboardGroups(), [
{
id: 45,
name: "Group A",
dashboards: [
{
id: 1,
displayName: "Dashboard CRM 1",
status: Status.NotLoaded,
},
],
},
]);
});
QUnit.test("load multiple spreadsheets", async (assert) => {
const loader = await createDashboardLoader({
mockRPC: function (route, args) {
if (args.method === "read") {
assert.step(`spreadsheet ${args.args[0]} loaded`);
}
},
});
await loader.load();
assert.verifySteps(["spreadsheet 1,2,3 loaded"]);
loader.getDashboard(1);
await nextTick();
assert.verifySteps(["spreadsheet 1 loaded"]);
loader.getDashboard(2);
await nextTick();
assert.verifySteps(["spreadsheet 2 loaded"]);
loader.getDashboard(1);
await nextTick();
assert.verifySteps([]);
});
QUnit.test("load spreadsheet data with error", async (assert) => {
const loader = await createDashboardLoader({
mockRPC: function (route, args) {
if (
args.method === "read" &&
args.model === "spreadsheet.dashboard" &&
args.args[1][0] === "raw"
) {
const error = new RPCError();
error.data = { message: "Bip" };
throw error;
}
},
});
await loader.load();
const result = loader.getDashboard(3);
assert.strictEqual(result.status, Status.Loading);
await result.promise.catch(() => assert.step("error"));
assert.strictEqual(result.status, Status.Error);
assert.strictEqual(result.error.data.message, "Bip");
assert.verifySteps(["error"], "error is thrown");
});
QUnit.test("async formulas are correctly evaluated", async (assert) => {
const spreadsheetData = {
sheets: [
{
id: "sheet1",
cells: {
A1: { content: `=ODOO.CURRENCY.RATE("EUR","USD")` }, // an async formula
},
},
],
};
const serverData = getDashboardServerData();
const dashboardId = 15;
serverData.models["spreadsheet.dashboard"].records = [
{
id: dashboardId,
raw: JSON.stringify(spreadsheetData),
json_data: JSON.stringify(spreadsheetData),
name: "Dashboard Accounting 1",
dashboard_group_id: 2,
},
];
serverData.models["spreadsheet.dashboard.group"].records = [
{ id: 1, name: "Container 1", dashboard_ids: [dashboardId] },
];
const loader = await createDashboardLoader({
serverData,
mockRPC: function (route, args) {
if (args.method === "get_rates_for_spreadsheet") {
const info = args.args[0][0];
return [{ ...info, rate: 0.9 }];
}
},
});
await loader.load();
loader.getDashboard(dashboardId);
await nextTick();
const { model } = loader.getDashboard(dashboardId);
await waitForDataSourcesLoaded(model);
assert.strictEqual(await getCellValue(model, "A1"), 0.9);
});
QUnit.test("Model is in dashboard mode", async (assert) => {
const loader = await createDashboardLoader();
await loader.load();
loader.getDashboard(3);
await nextTick();
const { model } = loader.getDashboard(3);
assert.strictEqual(model.config.mode, "dashboard");
});
QUnit.test("Model is in dashboard mode", async (assert) => {
patchWithCleanup(DashboardLoader.prototype, {
_activateFirstSheet: () => {
assert.step("activate sheet");
},
});
const loader = await createDashboardLoader();
await loader.load();
loader.getDashboard(3);
await nextTick();
assert.verifySteps(["activate sheet"]);
});

View file

@ -0,0 +1,55 @@
import { getFixture } from "@odoo/hoot";
import { animationFrame } from "@odoo/hoot-mock";
import { Spreadsheet } from "@odoo/o-spreadsheet";
import { makeSpreadsheetMockEnv } from "@spreadsheet/../tests/helpers/model";
import {
getService,
makeMockServer,
MockServer,
mountWithCleanup,
patchWithCleanup,
} from "@web/../tests/web_test_helpers";
import { loadBundle } from "@web/core/assets";
import { WebClient } from "@web/webclient/webclient";
/**
* @param {object} params
* @param {object} [params.serverData]
* @param {function} [params.mockRPC]
* @param {number} [params.spreadsheetId]
* @returns {Promise}
*/
export async function createSpreadsheetDashboard(params = {}) {
let model = undefined;
patchWithCleanup(Spreadsheet.prototype, {
setup() {
super.setup();
model = this.env.model;
},
});
await makeSpreadsheetMockEnv(params);
await loadBundle("web.chartjs_lib");
await mountWithCleanup(WebClient);
await getService("action").doAction({
type: "ir.actions.client",
tag: "action_spreadsheet_dashboard",
params: {
dashboard_id: params.spreadsheetId,
},
});
return { model, fixture: getFixture() };
}
export async function createDashboardActionWithData(data) {
if (!MockServer.env) {
await makeMockServer();
}
const json = JSON.stringify(data);
const [dashboard] = MockServer.env["spreadsheet.dashboard"];
dashboard.spreadsheet_data = json;
dashboard.json_data = json;
const { fixture, model } = await createSpreadsheetDashboard({ spreadsheetId: dashboard.id });
await animationFrame();
return { fixture, model };
}

View file

@ -0,0 +1,91 @@
import { SpreadsheetModels, defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
import { fields, models, onRpc } from "@web/../tests/web_test_helpers";
import { RPCError } from "@web/core/network/rpc";
export function getDashboardServerData() {
return {
models: {
"spreadsheet.dashboard": {},
"spreadsheet.dashboard.group": {},
},
views: {},
};
}
export class SpreadsheetDashboard extends models.Model {
_name = "spreadsheet.dashboard";
name = fields.Char({ string: "Name" });
spreadsheet_data = fields.Char({});
json_data = fields.Char({});
is_published = fields.Boolean({ string: "Is published" });
dashboard_group_id = fields.Many2one({ relation: "spreadsheet.dashboard.group" });
favorite_user_ids = fields.Many2many({ relation: "res.users", string: "Favorite Users" });
is_favorite = fields.Boolean({ compute: "_compute_is_favorite", string: "Is Favorite" });
_compute_is_favorite() {
for (const record of this) {
record.is_favorite = record.favorite_user_ids.includes(this.env.uid);
}
}
_records = [
{
id: 1,
spreadsheet_data: "{}",
json_data: "{}",
name: "Dashboard CRM 1",
dashboard_group_id: 1,
},
{
id: 2,
spreadsheet_data: "{}",
json_data: "{}",
name: "Dashboard CRM 2",
dashboard_group_id: 1,
},
{
id: 3,
spreadsheet_data: "{}",
json_data: "{}",
name: "Dashboard Accounting 1",
dashboard_group_id: 2,
},
];
}
export class SpreadsheetDashboardGroup extends models.Model {
_name = "spreadsheet.dashboard.group";
name = fields.Char({ string: "Name" });
published_dashboard_ids = fields.One2many({
relation: "spreadsheet.dashboard",
relation_field: "dashboard_group_id",
});
_records = [
{ id: 1, name: "Container 1", published_dashboard_ids: [1, 2] },
{ id: 2, name: "Container 2", published_dashboard_ids: [3] },
];
}
function mockDashboardDataController(_request, { res_id }) {
const [record] = this.env["spreadsheet.dashboard"].search_read([["id", "=", parseInt(res_id)]]);
if (!record) {
const error = new RPCError(`Dashboard ${res_id} does not exist`);
error.data = {};
throw error;
}
return {
snapshot: JSON.parse(record.spreadsheet_data),
revisions: [],
};
}
onRpc("/spreadsheet/dashboard/data/<int:res_id>", mockDashboardDataController);
export function defineSpreadsheetDashboardModels() {
const SpreadsheetDashboardModels = [SpreadsheetDashboard, SpreadsheetDashboardGroup];
Object.assign(SpreadsheetModels, SpreadsheetDashboardModels);
defineSpreadsheetModels();
}

View file

@ -1,113 +0,0 @@
/** @odoo-module */
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
import { registry } from "@web/core/registry";
import { actionService } from "@web/webclient/actions/action_service";
import { menuService } from "@web/webclient/menus/menu_service";
import { spreadsheetLinkMenuCellService } from "@spreadsheet/ir_ui_menu/index";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import { selectCell } from "@spreadsheet/../tests/utils/commands";
import { viewService } from "@web/views/view_service";
import { ormService } from "@web/core/orm_service";
import { getMenuServerData } from "@spreadsheet/../tests/links/menu_data_utils";
import { patchWithCleanup } from "@web/../tests/helpers/utils";
const { Model } = spreadsheet;
function beforeEach() {
registry
.category("services")
.add("menu", menuService)
.add("action", actionService)
.add("spreadsheetLinkMenuCell", spreadsheetLinkMenuCellService);
registry.category("services").add("view", viewService, { force: true }); // #action-serv-leg-compat-js-class
registry.category("services").add("orm", ormService, { force: true }); // #action-serv-leg-compat-js-class
}
QUnit.module("spreadsheet_dashboard > link", { beforeEach });
QUnit.test("click a web link", async (assert) => {
patchWithCleanup(window, {
open: (href) => {
assert.step(href.toString());
},
});
const env = await makeTestEnv();
const data = {
sheets: [
{
cells: { A1: { content: "[Odoo](https://odoo.com)" } },
},
],
};
const model = new Model(data, { mode: "dashboard", evalContext: { env } });
selectCell(model, "A1");
assert.verifySteps(["https://odoo.com"]);
});
QUnit.test("click a menu link", async (assert) => {
const fakeActionService = {
name: "action",
start() {
return {
doAction(action) {
assert.step(action);
},
};
},
};
registry.category("services").add("action", fakeActionService, { force: true });
const env = await makeTestEnv({ serverData: getMenuServerData() });
const data = {
sheets: [
{
cells: { A1: { content: "[label](odoo://ir_menu_xml_id/test_menu)" } },
},
],
};
const model = new Model(data, { mode: "dashboard", evalContext: { env } });
selectCell(model, "A1");
assert.verifySteps(["action1"]);
});
QUnit.test("click a menu link", async (assert) => {
const fakeActionService = {
name: "action",
start() {
return {
doAction(action) {
assert.step("do-action");
assert.deepEqual(action, {
context: undefined,
domain: undefined,
name: "an odoo view",
res_model: "partner",
target: "current",
type: "ir.actions.act_window",
views: [[false, "list"]],
});
},
};
},
};
registry.category("services").add("action", fakeActionService, { force: true });
const env = await makeTestEnv({ serverData: getMenuServerData() });
const view = {
name: "an odoo view",
viewType: "list",
action: {
modelName: "partner",
views: [[false, "list"]],
},
};
const data = {
sheets: [
{
cells: { A1: { content: `[a view](odoo://view/${JSON.stringify(view)})` } },
},
],
};
const model = new Model(data, { mode: "dashboard", evalContext: { env } });
selectCell(model, "A1");
assert.verifySteps(["do-action"]);
});

View file

@ -0,0 +1,198 @@
import { describe, expect, test } from "@odoo/hoot";
import { dblclick, queryAll, queryAllTexts } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { createSpreadsheetDashboard } from "@spreadsheet_dashboard/../tests/helpers/dashboard_action";
import {
defineSpreadsheetDashboardModels,
getDashboardServerData,
} from "@spreadsheet_dashboard/../tests/helpers/data";
import { contains } from "@web/../tests/web_test_helpers";
describe.current.tags("mobile");
defineSpreadsheetDashboardModels();
const TEST_LINE_CHART_DATA = {
type: "line",
dataSetsHaveTitle: false,
dataSets: [{ dataRange: "A1" }],
legendPosition: "top",
verticalAxisPosition: "left",
title: { text: "" },
};
const TEST_SCORECARD_CHART_DATA = {
type: "scorecard",
title: { text: "test" },
keyValue: "A1",
background: "#fff",
baselineMode: "absolute",
};
test("is empty with no figures", async () => {
await createSpreadsheetDashboard();
expect(".o_mobile_dashboard").toHaveCount(1);
expect(".o_mobile_dashboard").toHaveText(
"Only chart figures are displayed in small screens but this dashboard doesn't contain any"
);
});
test("with no available dashboard", async () => {
const serverData = getDashboardServerData();
serverData.models["spreadsheet.dashboard"].records = [];
serverData.models["spreadsheet.dashboard.group"].records = [];
await createSpreadsheetDashboard({ serverData });
expect(".o_mobile_dashboard").toHaveText("No available dashboard");
});
test("displays figures in first sheet", async () => {
const figure = {
tag: "chart",
height: 500,
width: 500,
col: 0,
row: 0,
offset: { x: 100, y: 100 },
data: TEST_LINE_CHART_DATA,
};
const spreadsheetData = {
sheets: [
{ id: "sheet1", figures: [{ ...figure, id: "figure1" }] },
{ id: "sheet2", figures: [{ ...figure, id: "figure2" }] },
],
};
const serverData = getDashboardServerData();
serverData.models["spreadsheet.dashboard.group"].records = [
{
published_dashboard_ids: [789],
id: 1,
name: "Chart",
},
];
serverData.models["spreadsheet.dashboard"].records = [
{
id: 789,
name: "Spreadsheet with chart figure",
json_data: JSON.stringify(spreadsheetData),
spreadsheet_data: JSON.stringify(spreadsheetData),
dashboard_group_id: 1,
},
];
await createSpreadsheetDashboard({ serverData });
expect(".o-chart-container").toHaveCount(1);
});
test("scorecards are placed two per row", async () => {
const figure = {
tag: "chart",
height: 500,
width: 500,
offset: { x: 100, y: 100 },
col: 0,
row: 0,
};
const spreadsheetData = {
sheets: [
{
id: "sheet1",
figures: [
{ ...figure, id: "figure1", data: TEST_SCORECARD_CHART_DATA },
{ ...figure, id: "figure2", data: TEST_SCORECARD_CHART_DATA },
{ ...figure, id: "figure3", data: TEST_SCORECARD_CHART_DATA },
{ ...figure, id: "figure4", data: TEST_LINE_CHART_DATA },
],
},
],
};
const serverData = getDashboardServerData();
serverData.models["spreadsheet.dashboard.group"].records = [
{ published_dashboard_ids: [789], id: 1, name: "Chart" },
];
serverData.models["spreadsheet.dashboard"].records = [
{
id: 789,
name: "Spreadsheet with chart figure",
json_data: JSON.stringify(spreadsheetData),
spreadsheet_data: JSON.stringify(spreadsheetData),
dashboard_group_id: 1,
},
];
await createSpreadsheetDashboard({ serverData });
const figureRows = queryAll(".o_figure_row");
expect(figureRows).toHaveLength(3);
expect(figureRows[0].querySelectorAll(".o-scorecard")).toHaveLength(2);
expect(figureRows[1].querySelectorAll(".o-scorecard")).toHaveLength(1);
expect(figureRows[1].querySelectorAll(".o_empty_figure")).toHaveLength(1);
expect(figureRows[2].querySelectorAll(".o-figure-canvas")).toHaveLength(1);
});
test("double clicking on a figure doesn't open the side panel", async () => {
const figure = {
tag: "chart",
height: 500,
width: 500,
col: 0,
row: 0,
offset: {
x: 100,
y: 100,
},
data: TEST_LINE_CHART_DATA,
};
const spreadsheetData = {
sheets: [
{
id: "sheet1",
figures: [{ ...figure, id: "figure1" }],
},
],
};
const serverData = getDashboardServerData();
serverData.models["spreadsheet.dashboard.group"].records = [
{
published_dashboard_ids: [789],
id: 1,
name: "Chart",
},
];
serverData.models["spreadsheet.dashboard"].records = [
{
id: 789,
name: "Spreadsheet with chart figure",
json_data: JSON.stringify(spreadsheetData),
spreadsheet_data: JSON.stringify(spreadsheetData),
dashboard_group_id: 1,
},
];
await createSpreadsheetDashboard({ serverData });
await contains(".o-chart-container").focus();
await dblclick(".o-chart-container");
await animationFrame();
expect(".o-chart-container").toHaveCount(1);
expect(".o-sidePanel").toHaveCount(0);
});
test("can switch dashboard", async () => {
await createSpreadsheetDashboard();
expect(".o_search_panel_current_selection").toHaveText("Dashboard CRM 1");
await contains(".o_search_panel_current_selection").click();
const dashboardElements = queryAll("section header.list-group-item", { root: document.body });
expect(dashboardElements[0]).toHaveClass("active");
expect(queryAllTexts(dashboardElements)).toEqual([
"Dashboard CRM 1",
"Dashboard CRM 2",
"Dashboard Accounting 1",
]);
await contains(dashboardElements[1]).click();
expect(".o_search_panel_current_selection").toHaveText("Dashboard CRM 2");
});
test("can go back from dashboard selection", async () => {
await createSpreadsheetDashboard();
expect(".o_mobile_dashboard").toHaveCount(1);
expect(".o_search_panel_current_selection").toHaveText("Dashboard CRM 1");
await contains(".o_search_panel_current_selection").click();
await contains(document.querySelector(".o_mobile_search_button")).click();
expect(".o_search_panel_current_selection").toHaveText("Dashboard CRM 1");
});

View file

@ -1,115 +0,0 @@
/** @odoo-module */
import { click, getFixture } from "@web/../tests/helpers/utils";
import { createSpreadsheetDashboard } from "../utils/dashboard_action";
import { getDashboardServerData } from "../utils/data";
QUnit.module("spreadsheet_dashboard > Mobile Dashboard action");
QUnit.test("is empty with no figures", async (assert) => {
await createSpreadsheetDashboard();
const fixture = getFixture();
assert.containsOnce(fixture, ".o_mobile_dashboard");
const content = fixture.querySelector(".o_mobile_dashboard");
assert.deepEqual(content.innerText.split("\n"), [
"Dashboard CRM 1",
"Only chart figures are displayed in small screens but this dashboard doesn't contain any",
]);
});
QUnit.test("with no available dashboard", async (assert) => {
const serverData = getDashboardServerData();
serverData.models["spreadsheet.dashboard"].records = [];
serverData.models["spreadsheet.dashboard.group"].records = [];
await createSpreadsheetDashboard({ serverData });
const fixture = getFixture();
const content = fixture.querySelector(".o_mobile_dashboard");
assert.deepEqual(content.innerText, "No available dashboard");
});
QUnit.test("displays figures in first sheet", async (assert) => {
const figure = {
tag: "chart",
height: 500,
width: 500,
x: 100,
y: 100,
data: {
type: "line",
dataSetsHaveTitle: false,
dataSets: ["A1"],
legendPosition: "top",
verticalAxisPosition: "left",
title: "",
},
};
const spreadsheetData = {
sheets: [
{
id: "sheet1",
figures: [{ ...figure, id: "figure1" }],
},
{
id: "sheet2",
figures: [{ ...figure, id: "figure2" }],
},
],
};
const serverData = getDashboardServerData();
serverData.models["spreadsheet.dashboard.group"].records = [
{
dashboard_ids: [789],
id: 1,
name: "Chart",
},
];
serverData.models["spreadsheet.dashboard"].records = [
{
id: 789,
name: "Spreadsheet with chart figure",
json_data: JSON.stringify(spreadsheetData),
raw: JSON.stringify(spreadsheetData),
dashboard_group_id: 1,
},
];
const fixture = getFixture();
await createSpreadsheetDashboard({ serverData });
assert.containsOnce(fixture, ".o-chart-container");
});
QUnit.test("can switch dashboard", async (assert) => {
await createSpreadsheetDashboard();
const fixture = getFixture();
assert.strictEqual(
fixture.querySelector(".o_search_panel_summary").innerText,
"Dashboard CRM 1"
);
await click(fixture, ".o_search_panel_current_selection");
const dashboardElements = [...document.querySelectorAll("section header.list-group-item")];
assert.strictEqual(dashboardElements[0].classList.contains("active"), true);
assert.deepEqual(
dashboardElements.map((el) => el.innerText),
["Dashboard CRM 1", "Dashboard CRM 2", "Dashboard Accounting 1"]
);
await click(dashboardElements[1]);
assert.strictEqual(
fixture.querySelector(".o_search_panel_summary").innerText,
"Dashboard CRM 2"
);
});
QUnit.test("can go back from dashboard selection", async (assert) => {
await createSpreadsheetDashboard();
const fixture = getFixture();
assert.containsOnce(fixture, ".o_mobile_dashboard");
assert.strictEqual(
fixture.querySelector(".o_search_panel_summary").innerText,
"Dashboard CRM 1"
);
await click(fixture, ".o_search_panel_current_selection");
await click(document, ".o_mobile_search_button");
assert.strictEqual(
fixture.querySelector(".o_search_panel_summary").innerText,
"Dashboard CRM 1"
);
});

View file

@ -1,25 +0,0 @@
/** @odoo-module */
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
import { getDashboardServerData } from "./data";
/**
* @param {object} params
* @param {object} [params.serverData]
* @param {function} [params.mockRPC]
* @param {number} [params.spreadsheetId]
* @returns {Promise}
*/
export async function createSpreadsheetDashboard(params = {}) {
const webClient = await createWebClient({
serverData: params.serverData || getDashboardServerData(),
mockRPC: params.mockRPC,
});
return await doAction(webClient, {
type: "ir.actions.client",
tag: "action_spreadsheet_dashboard",
params: {
dashboard_id: params.spreadsheetId,
},
});
}

View file

@ -1,57 +0,0 @@
/** @odoo-module */
export function getDashboardServerData() {
return {
models: {
"spreadsheet.dashboard": {
fields: {
json_data: { type: "char" },
raw: { type: "char " },
name: { type: "char" },
dashboard_group_id: {
type: "many2one",
relation: "spreadsheet.dashboard.group",
},
},
records: [
{
id: 1,
raw: "{}",
json_data: "{}",
name: "Dashboard CRM 1",
dashboard_group_id: 1,
},
{
id: 2,
raw: "{}",
json_data: "{}",
name: "Dashboard CRM 2",
dashboard_group_id: 1,
},
{
id: 3,
raw: "{}",
json_data: "{}",
name: "Dashboard Accounting 1",
dashboard_group_id: 2,
},
],
},
"spreadsheet.dashboard.group": {
fields: {
name: { type: "char" },
dashboard_ids: {
type: "one2many",
relation: "spreadsheet.dashboard",
relation_field: "dashboard_group_id",
},
},
records: [
{ id: 1, name: "Container 1", dashboard_ids: [1, 2] },
{ id: 2, name: "Container 2", dashboard_ids: [3] },
],
},
},
views: {},
};
}