Odoo18-Base/addons/web/static/tests/views/graph/graph_view.test.js
2025-01-06 10:57:38 +07:00

4036 lines
114 KiB
JavaScript

import { expect, test } from "@odoo/hoot";
import { queryAllTexts } from "@odoo/hoot-dom";
import { Deferred, animationFrame, mockDate } from "@odoo/hoot-mock";
import { onRendered } from "@odoo/owl";
import {
contains,
defineModels,
editFavoriteName,
fields,
getService,
makeMockServer,
mockService,
models,
mountView,
mountWithCleanup,
onRpc,
patchWithCleanup,
saveFavorite,
switchView,
toggleMenu,
toggleMenuItem,
toggleMenuItemOption,
toggleSaveFavorite,
toggleSearchBarMenu,
validateSearch,
} from "@web/../tests/web_test_helpers";
import {
checkDatasets,
checkLabels,
checkLegend,
checkModeIs,
checkTooltip,
checkYTicks,
clickOnDataset,
clickOnLegend,
clickSort,
getChart,
getGraphModel,
getGraphModelMetaData,
getGraphRenderer,
getModeButton,
getScaleY,
getYAxisLabel,
selectMode,
setupChartJsForTests,
} from "./graph_test_helpers";
import { DEFAULT_BG, getBorderWhite, getColors, lightenColor } from "@web/core/colors/colors";
import { Domain } from "@web/core/domain";
import { registry } from "@web/core/registry";
import { SampleServer } from "@web/model/sample_server";
import { GraphArchParser } from "@web/views/graph/graph_arch_parser";
import { GraphRenderer } from "@web/views/graph/graph_renderer";
import { graphView } from "@web/views/graph/graph_view";
import { WebClient } from "@web/webclient/webclient";
class Color extends models.Model {
name = fields.Char();
_records = [
{
id: 1,
name: "black",
},
{
id: 2,
name: "red",
},
];
}
class Product extends models.Model {
name = fields.Char();
_records = [
{
id: 100,
name: "xphone",
},
{
id: 200,
name: "xpad",
},
];
}
class Foo extends models.Model {
bar = fields.Boolean({ default: false });
color_id = fields.Many2one({ relation: "color" });
color_ids = fields.Many2many({ relation: "color" });
date = fields.Date();
foo = fields.Integer();
product_id = fields.Many2one({ relation: "product" });
revenue = fields.Float();
_records = [
{
id: 1,
foo: 3,
bar: true,
product_id: 100,
date: "2016-01-01",
revenue: 1,
color_ids: [2],
},
{
id: 2,
foo: 53,
bar: true,
product_id: 100,
color_id: 2,
date: "2016-01-03",
revenue: 2,
color_ids: [1],
},
{
id: 3,
foo: 2,
bar: true,
product_id: 100,
date: "2016-03-04",
revenue: 3,
color_ids: [1, 2],
},
{
id: 4,
foo: 24,
product_id: 100,
date: "2016-03-07",
revenue: 4,
color_ids: [2],
},
{
id: 5,
foo: 4,
product_id: 200,
date: "2016-05-01",
revenue: 5,
color_ids: [1, 2],
},
{
id: 6,
foo: 63,
product_id: 200,
},
{
id: 7,
foo: 42,
product_id: 200,
},
{
id: 8,
foo: 48,
product_id: 200,
date: "2016-04-01",
revenue: 8,
},
];
_views = {
graph: /* xml */ `<graph />`,
search: /* xml */ `
<search>
<filter name="false_domain" string="False Domain" domain="[(0, '=', 1)]" />
<filter name="filter_with_context"
string="Filter With Context"
domain="[]"
context="{ 'graph_measure': 'foo', 'graph_mode': 'line', 'graph_groupbys': ['color_id'] }"
/>
<filter name="group_by_color" string="Color" context="{ 'group_by': 'color_id' }" />
<filter name="group_by_product" string="Product" context="{ 'group_by': 'product_id' }" />
</search>
`,
};
}
defineModels([Foo, Color, Product]);
setupChartJsForTests();
test('graph view with "class" attribute', async () => {
await mountView({
type: "graph",
resModel: "foo",
arch: `<graph class="foobar-class"/>`,
});
expect(".o_graph_view").toHaveClass("foobar-class");
});
test("simple bar chart rendering", async () => {
const view = await mountView({ type: "graph", resModel: "foo" });
const { measure, mode, order, stacked } = getGraphModelMetaData(view);
expect(".o_graph_view").toHaveClass("o_view_controller");
expect(".o_graph_canvas_container canvas").toHaveCount(1);
expect(measure).toBe("__count", {
message: `the active measure should be "__count" by default`,
});
expect(mode).toBe("bar", { message: "should be in bar chart mode by default" });
expect(order).toBe(null, { message: "should not be ordered by default" });
expect(stacked).toBe(true, { message: "bar charts should be stacked by default" });
checkLabels(view, ["Total"]);
checkDatasets(view, ["backgroundColor", "borderColor", "data", "label", "stack"], {
backgroundColor: "#4EA7F2",
borderColor: undefined,
data: [8],
label: "Count",
stack: "",
});
checkLegend(view, "Count");
checkTooltip(view, { lines: [{ label: "Total", value: "8" }] }, 0);
});
test("simple bar chart rendering with no data", async () => {
Foo._records = [];
const view = await mountView({ type: "graph", resModel: "foo" });
expect(".o_graph_canvas_container canvas").toHaveCount(1);
expect(".o_nocontent_help").toHaveCount(0);
checkLabels(view, []);
checkDatasets(view, [], []);
});
test("simple bar chart rendering (one groupBy)", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="bar" />
</graph>
`,
});
expect(".o_graph_canvas_container canvas").toHaveCount(1);
checkLabels(view, ["false", "true"]);
checkDatasets(view, ["backgroundColor", "borderColor", "data", "label"], {
backgroundColor: "#4EA7F2",
borderColor: undefined,
data: [5, 3],
label: "Count",
});
checkLegend(view, "Count");
checkTooltip(view, { lines: [{ label: "false", value: "5" }] }, 0);
checkTooltip(view, { lines: [{ label: "true", value: "3" }] }, 1);
});
test("simple bar chart rendering (two groupBy)", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="bar" />
<field name="product_id" />
</graph>
`,
});
expect(".o_graph_canvas_container canvas").toHaveCount(1);
checkLabels(view, ["false", "true"]);
checkDatasets(
view,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: "#4EA7F2",
borderColor: undefined,
data: [1, 3],
label: "xphone",
},
{
backgroundColor: "#EA6175",
borderColor: undefined,
data: [4, 0],
label: "xpad",
},
{
backgroundColor: "#343a40",
borderColor: "rgba(0,0,0,.3)",
data: [5, 3],
label: "Sum",
},
]
);
checkLegend(view, ["xphone", "xpad", "Sum"]);
checkTooltip(view, { lines: [{ label: "false / xphone", value: "1" }] }, 0, 0);
checkTooltip(view, { lines: [{ label: "true / xphone", value: "3" }] }, 1, 0);
checkTooltip(view, { lines: [{ label: "false / xpad", value: "4" }] }, 0, 1);
checkTooltip(view, { lines: [{ label: "true / xpad", value: "0" }] }, 1, 1);
});
test("bar chart rendering (no groupBy, several domains)", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="revenue" type="measure" />
</graph>
`,
groupBy: [],
comparison: {
domains: [
{ arrayRepr: [["bar", "=", true]], description: "True group" },
{ arrayRepr: [["bar", "=", false]], description: "False group" },
],
},
});
checkLabels(view, ["Total"]);
checkDatasets(
view,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: "#4EA7F2",
borderColor: undefined,
data: [6],
label: "True group",
},
{
backgroundColor: "#EA6175",
borderColor: undefined,
data: [17],
label: "False group",
},
]
);
checkLegend(view, ["True group", "False group"]);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "Total / True group", value: "6" }],
},
0,
0
);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "Total / False group", value: "17" }],
},
0,
1
);
});
test("bar chart rendering (one groupBy, several domains)", async () => {
Foo._records = [
{ bar: true, foo: 1, revenue: 14 },
{ bar: true, foo: 2, revenue: 0 },
{ bar: false, foo: 1, revenue: 12 },
{ bar: false, foo: 2, revenue: -4 },
{ bar: false, foo: 3, revenue: 2 },
{ bar: false, foo: 4, revenue: 0 },
];
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="revenue" type="measure" />
<field name="foo" />
</graph>
`,
comparison: {
domains: [
{ arrayRepr: [["bar", "=", true]], description: "True group" },
{ arrayRepr: [["bar", "=", false]], description: "False group" },
],
},
});
checkLabels(view, ["1", "2", "3", "4"]);
checkDatasets(
view,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: "#4EA7F2",
borderColor: undefined,
data: [14, 0, 0, 0],
label: "True group",
},
{
backgroundColor: "#EA6175",
borderColor: undefined,
data: [12, -4, 2, 0],
label: "False group",
},
]
);
checkLegend(view, ["True group", "False group"]);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "1 / True group", value: "14" }],
},
0,
0
);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "1 / False group", value: "12" }],
},
0,
1
);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "2 / False group", value: "-4" }],
},
1,
1
);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "3 / False group", value: "2" }],
},
2,
1
);
});
test("bar chart many2many groupBy", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="revenue" type="measure" />
<field name="color_ids" />
</graph>
`,
});
expect(".o_graph_canvas_container canvas").toHaveCount(1);
checkLabels(view, ["black", "red", "None"]);
checkDatasets(view, ["backgroundColor", "borderColor", "data", "label"], {
backgroundColor: "#4EA7F2",
borderColor: undefined,
data: [10, 13, 8],
label: "Revenue",
});
checkLegend(view, "Revenue");
checkTooltip(view, { lines: [{ label: "black", value: "10" }], title: "Revenue" }, 0);
checkTooltip(view, { lines: [{ label: "red", value: "13" }], title: "Revenue" }, 1);
checkTooltip(view, { lines: [{ label: "None", value: "8" }], title: "Revenue" }, 2);
});
test("differentiate many2many values with same label", async () => {
Color._records.push({ id: 3, name: "red" });
Foo._records.push({ color_ids: [3], revenue: 14 });
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="revenue" type="measure" />
<field name="color_ids" />
</graph>
`,
});
expect(".o_graph_canvas_container canvas").toHaveCount(1);
checkLabels(view, ["black", "red", "red (2)", "None"]);
checkDatasets(view, ["backgroundColor", "borderColor", "data", "label"], {
backgroundColor: "#4EA7F2",
borderColor: undefined,
data: [10, 13, 14, 8],
label: "Revenue",
});
checkTooltip(view, { lines: [{ label: "black", value: "10" }], title: "Revenue" }, 0);
checkTooltip(view, { lines: [{ label: "red", value: "13" }], title: "Revenue" }, 1);
checkTooltip(view, { lines: [{ label: "red (2)", value: "14" }], title: "Revenue" }, 2);
checkTooltip(view, { lines: [{ label: "None", value: "8" }], title: "Revenue" }, 3);
});
test("bar chart rendering (one groupBy, several domains with date identification)", async () => {
Foo._records = [
{ date: "2021-01-04", revenue: 12 },
{ date: "2021-01-12", revenue: 5 },
{ date: "2021-01-19", revenue: 15 },
{ date: "2021-01-26", revenue: 2 },
{ date: "2021-02-04", revenue: 14 },
{ date: "2021-02-17", revenue: 0 },
{ date: false, revenue: 0 },
];
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="revenue" type="measure" />
<field name="date" interval="week" />
</graph>
`,
comparison: {
domains: [
{
arrayRepr: [
["date", ">=", "2021-02-01"],
["date", "<=", "2021-02-28"],
],
description: "February 2021",
},
{
arrayRepr: [
["date", ">=", "2021-01-01"],
["date", "<=", "2021-01-31"],
],
description: "January 2021",
},
],
fieldName: "date",
},
});
checkLabels(view, ["W05 2021", "W07 2021", "", ""]);
checkDatasets(
view,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: "#4EA7F2",
borderColor: undefined,
data: [14, 0],
label: "February 2021",
},
{
backgroundColor: "#EA6175",
borderColor: undefined,
data: [12, 5, 15, 2],
label: "January 2021",
},
]
);
checkLegend(view, ["February 2021", "January 2021"]);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "W05 2021 / February 2021", value: "14" }],
},
0,
0
);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "W01 2021 / January 2021", value: "12" }],
},
0,
1
);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "W02 2021 / January 2021", value: "5" }],
},
1,
1
);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "W03 2021 / January 2021", value: "15" }],
},
2,
1
);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "W04 2021 / January 2021", value: "2" }],
},
3,
1
);
});
test("bar chart rendering (two groupBy, several domains with no date identification)", async () => {
Foo._records = [
{ date: "2021-01-04", bar: false, revenue: 12 },
{ date: "2021-01-12", bar: true, revenue: 5 },
{ date: "2021-02-04", bar: false, revenue: 14 },
{ date: "2021-02-17", bar: true, revenue: 0 },
{ date: false, bar: false, revenue: 0 },
];
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="revenue" type="measure" />
<field name="bar" />
<field name="date" interval="week" />
</graph>
`,
comparison: {
domains: [
{
arrayRepr: [
["date", ">=", "2021-02-01"],
["date", "<=", "2021-02-28"],
],
description: "February 2021",
},
{
arrayRepr: [
["date", ">=", "2021-01-01"],
["date", "<=", "2021-01-31"],
],
description: "January 2021",
},
],
fieldName: "date",
},
});
checkLabels(view, ["false", "true"]);
checkDatasets(
view,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: "#4EA7F2",
borderColor: undefined,
data: [14, 0],
label: "February 2021 / W05 2021",
},
{
backgroundColor: "#EA6175",
borderColor: undefined,
data: [0, 0],
label: "February 2021 / W07 2021",
},
{
backgroundColor: "#43C5B1",
borderColor: undefined,
data: [12, 0],
label: "January 2021 / W01 2021",
},
{
backgroundColor: "#F4A261",
borderColor: undefined,
data: [0, 5],
label: "January 2021 / W02 2021",
},
]
);
checkLegend(view, [
"February 2021 / W05 2021",
"February 2021 / W07 2021",
"January 2021 / W01 2021",
"January 2021 / W02 2021",
]);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "false / February 2021 / W05 2021", value: "14" }],
},
0,
0
);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "false / January 2021 / W01 2021", value: "12" }],
},
0,
2
);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "true / January 2021 / W02 2021", value: "5" }],
},
1,
3
);
});
test("line chart rendering (no groupBy)", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `<graph type="line" />`,
});
expect(".o_graph_canvas_container canvas").toHaveCount(1);
expect(getGraphModelMetaData(view).mode).toBe("line");
checkLabels(view, ["", "Total", ""]);
checkDatasets(view, ["backgroundColor", "borderColor", "data", "label", "stack"], {
backgroundColor: "#a7d3f9",
borderColor: "#4EA7F2",
data: [undefined, 8],
label: "Count",
stack: undefined,
});
checkLegend(view, "Count");
checkTooltip(view, { lines: [{ label: "Total", value: "8" }] }, 1);
});
test("line chart rendering (one groupBy)", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="line">
<field name="bar" />
</graph>
`,
});
expect(".o_graph_canvas_container canvas").toHaveCount(1);
checkLabels(view, ["false", "true"]);
checkDatasets(view, ["backgroundColor", "borderColor", "data", "label"], {
backgroundColor: "#a7d3f9",
borderColor: "#4EA7F2",
data: [5, 3],
label: "Count",
});
checkLegend(view, "Count");
checkTooltip(view, { lines: [{ label: "false", value: "5" }] }, 0);
checkTooltip(view, { lines: [{ label: "true", value: "3" }] }, 1);
});
test("line chart rendering (two groupBy)", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="line" stacked="0">
<field name="bar" />
<field name="product_id" />
</graph>
`,
});
expect(".o_graph_canvas_container canvas").toHaveCount(1);
checkLabels(view, ["false", "true"]);
checkDatasets(
view,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: "#a7d3f9",
borderColor: "#4EA7F2",
data: [1, 3],
label: "xphone",
},
{
backgroundColor: "#f5b0ba",
borderColor: "#EA6175",
data: [4, 0],
label: "xpad",
},
]
);
checkLegend(view, ["xphone", "xpad"]);
checkTooltip(
view,
{
lines: [
{ label: "false / xpad", value: "4" },
{ label: "false / xphone", value: "1" },
],
},
0
);
checkTooltip(
view,
{
lines: [
{ label: "true / xphone", value: "3" },
{ label: "true / xpad", value: "0" },
],
},
1
);
});
test("line chart many2many groupBy", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="line">
<field name="revenue" type="measure" />
<field name="color_ids" />
</graph>
`,
});
expect(".o_graph_canvas_container canvas").toHaveCount(1);
checkLabels(view, ["black", "red"]);
checkDatasets(view, ["backgroundColor", "borderColor", "data", "label"], {
backgroundColor: "#a7d3f9",
borderColor: "#4EA7F2",
data: [10, 13],
label: "Revenue",
});
checkLegend(view, "Revenue");
checkTooltip(view, { lines: [{ label: "black", value: "10" }], title: "Revenue" }, 0);
checkTooltip(view, { lines: [{ label: "red", value: "13" }], title: "Revenue" }, 1);
});
test("Check if values in tooltip are correctly sorted when groupBy filter are applied", async () => {
Foo._records = [
{ product_id: 100, foo: 1, revenue: 12 },
{ product_id: 100, foo: 2, revenue: 5 },
{ product_id: 100, foo: 3, revenue: 1.45e2 },
{ product_id: 100, foo: 4, revenue: -9 },
{ product_id: 200, foo: 5, revenue: 0 },
{ product_id: 200, foo: 6, revenue: -1 },
{ product_id: 200, foo: 7, revenue: Math.PI },
{ product_id: 200, foo: 8, revenue: 80.67 },
];
const view = await mountView({
type: "graph",
resModel: "foo",
arch: `
<graph type="line" stacked="0">
<field name="revenue" type="measure"/>
<field name="product_id"/>
<field name="foo"/>
</graph>
`,
});
checkTooltip(
view,
{
lines: [
{ label: "xphone / 3", value: "145.00" },
{ label: "xphone / 1", value: "12.00" },
{ label: "xphone / 2", value: "5.00" },
{ label: "xphone / 5", value: "0.00" },
{ label: "xphone / 6", value: "0.00" },
{ label: "xphone / 7", value: "0.00" },
{ label: "xphone / 8", value: "0.00" },
{ label: "xphone / 4", value: "-9.00" },
],
title: "Revenue",
},
0
);
checkTooltip(
view,
{
lines: [
{ label: "xpad / 8", value: "80.67" },
{ label: "xpad / 7", value: "3.14" },
{ label: "xpad / 1", value: "0.00" },
{ label: "xpad / 2", value: "0.00" },
{ label: "xpad / 3", value: "0.00" },
{ label: "xpad / 4", value: "0.00" },
{ label: "xpad / 5", value: "0.00" },
{ label: "xpad / 6", value: "-1.00" },
],
title: "Revenue",
},
1
);
});
test("format total in hh:mm when measure is unit_amount", async () => {
Foo._fields.unit_amount = fields.Float({ string: "Unit Amount" });
Foo._records = [{ id: 1, unit_amount: 8 }];
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="unit_amount" type="measure" widget="float_time" />
</graph>
`,
});
const { measure, fieldAttrs } = getGraphModelMetaData(view);
expect(".o_graph_view").toHaveClass("o_view_controller");
expect("div.o_graph_canvas_container canvas").toHaveCount(1);
expect(measure).toBe("unit_amount", { message: `the measure should be "unit_amount"` });
checkLegend(view, "Unit Amount");
checkLabels(view, ["Total"]);
expect(fieldAttrs[measure].widget).toBe("float_time", {
message: "should be a float_time widget",
});
checkYTicks(view, [
"00:00",
"01:00",
"02:00",
"03:00",
"04:00",
"05:00",
"06:00",
"07:00",
"08:00",
]);
checkTooltip(view, { title: "Unit Amount", lines: [{ label: "Total", value: "08:00" }] }, 0);
});
test("Stacked button visible in the line chart", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="line">
<field name="bar" />
<field name="product_id" />
</graph>
`,
});
const model = getGraphModel(view);
await selectMode("line");
checkModeIs(view, "line");
expect(model.metaData.stacked).toBe(true, { message: "graph should be stacked." });
expect(getScaleY(view).stacked).toBe(true, {
message: "The y axes should have stacked property set to true",
});
expect(`button.o_graph_button[data-tooltip="Stacked"]`).toHaveCount(1);
await contains(`button.o_graph_button[data-tooltip="Stacked"]`).click();
expect(model.metaData.stacked).toBe(false, {
message: "graph should be a classic line chart.",
});
expect(getScaleY(view).stacked).toBe(undefined, {
message: "The y axes should have stacked property set to undefined",
});
});
test("Stacked line prop click false", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="line">
<field name="bar" />
<field name="product_id" />
</graph>
`,
});
await contains(`button.o_graph_button[data-tooltip="Stacked"]`).click();
expect(getGraphModel(view).metaData.stacked).toBe(false, {
message: "graph should be a classic line chart.",
});
expect(!!getScaleY(view).stacked).toBe(false, {
message:
"the y axes should have a stacked property set to false since the stacked property in line chart is false.",
});
expect(getGraphRenderer(view).getElementOptions().line.fill).toBe(false, {
message: "The fill property should be false since the stacked property is false.",
});
const expectedDatasets = [
{
backgroundColor: "#a7d3f9",
borderColor: "#4EA7F2",
originIndex: 0,
pointBackgroundColor: "#4EA7F2",
},
{
backgroundColor: "#f5b0ba",
borderColor: "#EA6175",
originIndex: 0,
pointBackgroundColor: "#EA6175",
},
];
const keysToEvaluate = [
"backgroundColor",
"borderColor",
"originIndex",
"pointBackgroundColor",
];
checkDatasets(view, keysToEvaluate, expectedDatasets);
});
test("Stacked prop and default line chart", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="line">
<field name="bar" />
<field name="product_id" />
</graph>
`,
});
expect(getGraphModel(view).metaData.mode).toBe("line", {
message: "should be in line chart mode.",
});
expect(getGraphModel(view).metaData.stacked).toBe(true, {
message: "should be stacked by default.",
});
expect(getScaleY(view).stacked).toBe(true, {
message:
"the stacked property in y axes should be true when the stacked is enabled in line chart",
});
expect(getGraphRenderer(view).getElementOptions().line.fill).toBe(true, {
message: "The fill property should be true to add backgroundColor in line chart.",
});
const expectedDatasets = [];
const keysToEvaluate = [
"backgroundColor",
"borderColor",
"originIndex",
"pointBackgroundColor",
];
const datasets = getChart(view).data.datasets;
const colors = getColors(undefined, "sm");
for (let i = 0; i < datasets.length; i++) {
const expectedColor = colors[i];
expectedDatasets.push({
backgroundColor: lightenColor(expectedColor, 0.5),
borderColor: expectedColor,
originIndex: 0,
pointBackgroundColor: expectedColor,
});
}
checkDatasets(view, keysToEvaluate, expectedDatasets);
});
test("Cumulative prop and default line chart", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="line" stacked="0">
<field name="bar" />
<field name="product_id" />
</graph>
`,
});
expect(getGraphModel(view).metaData.mode).toBe("line", {
message: "should be in line chart mode.",
});
expect(getGraphModel(view).metaData.cumulated).toBe(false, {
message: "should not be cumulative by default.",
});
await contains('[data-tooltip="Cumulative"]').click();
expect(getGraphModel(view).metaData.cumulated).toBe(true, {
message: "should be in cumulative",
});
const expectedDatasets = [
{
data: [1, 4],
},
{
data: [4, 4],
},
];
checkDatasets(view, ["data"], expectedDatasets);
});
test("Default cumulative prop", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="line" stacked="0" cumulated="1">
<field name="bar" />
<field name="product_id" />
</graph>
`,
});
expect(getGraphModel(view).metaData.mode).toBe("line", {
message: "should be in line chart mode.",
});
expect(getGraphModel(view).metaData.cumulated).toBe(true, {
message: "should be in cumulative",
});
expect(getGraphModel(view).metaData.cumulatedStart).toBe(false, {
message: "should have cumulated start opted-out",
});
});
test("Cumulative prop and cumulated start", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="line" stacked="0" cumulated="1" cumulated_start="1">
<field name="date" />
<field name="product_id" />
</graph>
`,
searchViewArch: /* xml */ `
<search>
<filter name="filter_after_march"
string="After March 2016"
domain="[['date', '>=', '2016-03-01']]"
/>
</search>
`,
context: {
search_default_filter_after_march: 1,
},
});
expect(getGraphModel(view).metaData.mode).toBe("line", {
message: "should be in line chart mode",
});
expect(getGraphModel(view).metaData.cumulated).toBe(true, {
message: "should be in cumulative",
});
expect(getGraphModel(view).metaData.cumulatedStart).toBe(true, {
message: "should have cumulated start opted-in",
});
const expectedDatasets = [
{
data: [4, 4, 4],
},
{
data: [0, 1, 2],
},
];
checkDatasets(view, ["data"], expectedDatasets);
});
test("line chart rendering (no groupBy, several domains)", async () => {
const view = await mountView({
resModel: "foo",
type: "graph",
arch: /* xml */ `
<graph type="line" stacked="0">
<field name="revenue" type="measure" />
</graph>
`,
comparison: {
domains: [
{ arrayRepr: [["bar", "=", true]], description: "True group" },
{ arrayRepr: [["bar", "=", false]], description: "False group" },
],
},
});
checkLabels(view, ["", "Total", ""]);
checkDatasets(
view,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: "#a7d3f9",
borderColor: "#4EA7F2",
data: [undefined, 6],
label: "True group",
},
{
backgroundColor: "#f5b0ba",
borderColor: "#EA6175",
data: [undefined, 17],
label: "False group",
},
]
);
checkLegend(view, ["True group", "False group"]);
checkTooltip(
view,
{
title: "Revenue",
lines: [
{ label: "Total / False group", value: "17" },
{ label: "Total / True group", value: "6" },
],
},
1
);
});
test("line chart rendering (one groupBy, several domains)", async () => {
Foo._records = [
{ bar: true, foo: 1, revenue: 14 },
{ bar: true, foo: 2, revenue: 0 },
{ bar: false, foo: 1, revenue: 12 },
{ bar: false, foo: 2, revenue: -4 },
{ bar: false, foo: 3, revenue: 2 },
{ bar: false, foo: 4, revenue: 0 },
];
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="line" stacked="0">
<field name="revenue" type="measure" />
<field name="foo" />
</graph>
`,
comparison: {
domains: [
{ arrayRepr: [["bar", "=", true]], description: "True group" },
{ arrayRepr: [["bar", "=", false]], description: "False group" },
],
},
});
checkLabels(view, ["1", "2", "3", "4"]);
checkDatasets(
view,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: "#a7d3f9",
borderColor: "#4EA7F2",
data: [14, 0, 0, 0],
label: "True group",
},
{
backgroundColor: "#f5b0ba",
borderColor: "#EA6175",
data: [12, -4, 2, 0],
label: "False group",
},
]
);
checkLegend(view, ["True group", "False group"]);
checkTooltip(
view,
{
title: "Revenue",
lines: [
{ label: "1 / True group", value: "14" },
{ label: "1 / False group", value: "12" },
],
},
0
);
checkTooltip(
view,
{
title: "Revenue",
lines: [
{ label: "2 / True group", value: "0" },
{ label: "2 / False group", value: "-4" },
],
},
1
);
checkTooltip(
view,
{
title: "Revenue",
lines: [
{ label: "3 / False group", value: "2" },
{ label: "3 / True group", value: "0" },
],
},
2
);
checkTooltip(
view,
{
title: "Revenue",
lines: [
{ label: "4 / True group", value: "0" },
{ label: "4 / False group", value: "0" },
],
},
3
);
});
test("line chart rendering (one groupBy, several domains with date identification)", async () => {
Foo._records = [
{ date: "2021-01-04", revenue: 12 },
{ date: "2021-01-12", revenue: 5 },
{ date: "2021-01-19", revenue: 15 },
{ date: "2021-01-26", revenue: 2 },
{ date: "2021-02-04", revenue: 14 },
{ date: "2021-02-17", revenue: 0 },
{ date: false, revenue: 0 },
];
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="line" stacked="0">
<field name="revenue" type="measure" />
<field name="date" interval="week" />
</graph>
`,
comparison: {
domains: [
{
arrayRepr: [
["date", ">=", "2021-02-01"],
["date", "<=", "2021-02-28"],
],
description: "February 2021",
},
{
arrayRepr: [
["date", ">=", "2021-01-01"],
["date", "<=", "2021-01-31"],
],
description: "January 2021",
},
],
fieldName: "date",
},
});
checkLabels(view, ["W05 2021", "W07 2021", "", ""]);
checkDatasets(
view,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: "#a7d3f9",
borderColor: "#4EA7F2",
data: [14, 0],
label: "February 2021",
},
{
backgroundColor: "#f5b0ba",
borderColor: "#EA6175",
data: [12, 5, 15, 2],
label: "January 2021",
},
]
);
checkLegend(view, ["February 2021", "January 2021"]);
checkTooltip(
view,
{
title: "Revenue",
lines: [
{ label: "W05 2021 / February 2021", value: "14" },
{ label: "W01 2021 / January 2021", value: "12" },
],
},
0
);
checkTooltip(
view,
{
title: "Revenue",
lines: [
{ label: "W02 2021 / January 2021", value: "5" },
{ label: "W07 2021 / February 2021", value: "0" },
],
},
1
);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "W03 2021 / January 2021", value: "15" }],
},
2
);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "W04 2021 / January 2021", value: "2" }],
},
3
);
});
test("line chart rendering (one groupBy, several domains with date identification) without stacked attribute", async () => {
Foo._records = [
{ date: "2021-01-04", revenue: 12 },
{ date: "2021-01-12", revenue: 5 },
{ date: "2021-01-19", revenue: 15 },
{ date: "2021-01-26", revenue: 2 },
{ date: "2021-02-04", revenue: 14 },
{ date: "2021-02-17", revenue: 0 },
{ date: false, revenue: 0 },
];
await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="line">
<field name="revenue" type="measure" />
<field name="date" interval="week" />
</graph>
`,
comparison: {
domains: [
{
arrayRepr: [
["date", ">=", "2021-02-01"],
["date", "<=", "2021-02-28"],
],
description: "February 2021",
},
{
arrayRepr: [
["date", ">=", "2021-01-01"],
["date", "<=", "2021-01-31"],
],
description: "January 2021",
},
],
fieldName: "date",
},
});
expect(".o_graph_button[data-tooltip=Stacked]").not.toHaveClass("active", {
message: "The stacked mode should be disabled",
});
});
test("line chart rendering (two groupBy, several domains with no date identification)", async () => {
Foo._records = [
{ date: "2021-01-04", bar: false, revenue: 12 },
{ date: "2021-01-12", bar: true, revenue: 5 },
{ date: "2021-02-04", bar: false, revenue: 14 },
{ date: "2021-02-17", bar: true, revenue: 0 },
{ date: false, bar: false, revenue: 0 },
];
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="line" stacked="0">
<field name="revenue" type="measure" />
<field name="bar" />
<field name="date" interval="week" />
</graph>
`,
comparison: {
domains: [
{
arrayRepr: [
["date", ">=", "2021-02-01"],
["date", "<=", "2021-02-28"],
],
description: "February 2021",
},
{
arrayRepr: [
["date", ">=", "2021-01-01"],
["date", "<=", "2021-01-31"],
],
description: "January 2021",
},
],
fieldName: "date",
},
});
checkLabels(view, ["false", "true"]);
checkDatasets(
view,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: "#a7d3f9",
borderColor: "#4EA7F2",
data: [14, 0],
label: "February 2021 / W05 2021",
},
{
backgroundColor: "#f5b0ba",
borderColor: "#EA6175",
data: [0, 0],
label: "February 2021 / W07 2021",
},
{
backgroundColor: "#a1e2d8",
borderColor: "#43C5B1",
data: [12, 0],
label: "January 2021 / W01 2021",
},
{
backgroundColor: "#fad1b0",
borderColor: "#F4A261",
data: [0, 5],
label: "January 2021 / W02 2021",
},
]
);
checkLegend(view, [
"February 2021 / W05 2021",
"February 2021 / W07 2021",
"January 2021 / W01 2021",
"January 2021 / W02 2021",
]);
checkTooltip(
view,
{
title: "Revenue",
lines: [
{ label: "false / February 2021 / W05 2021", value: "14" },
{ label: "false / January 2021 / W01 2021", value: "12" },
{ label: "false / February 2021 / W07 2021", value: "0" },
{ label: "false / January 2021 / W02 2021", value: "0" },
],
},
0
);
checkTooltip(
view,
{
title: "Revenue",
lines: [
{ label: "true / January 2021 / W02 2021", value: "5" },
{ label: "true / February 2021 / W05 2021", value: "0" },
{ label: "true / February 2021 / W07 2021", value: "0" },
{ label: "true / January 2021 / W01 2021", value: "0" },
],
},
1
);
});
test("displaying line chart with only 1 data point", async () => {
// this test makes sure the line chart does not crash when only one data
// point is displayed.
Foo._records = Foo._records.filter((id) => id === 1);
await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `<graph type="line" stacked="0" />`,
});
expect("canvas").toHaveCount(1);
});
test("pie chart rendering (no groupBy)", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `<graph type="pie" />`,
});
expect(".o_graph_canvas_container canvas").toHaveCount(1);
expect(getGraphModelMetaData(view).mode).toBe("pie");
checkLabels(view, ["Total"]);
checkDatasets(view, ["backgroundColor", "borderColor", "data", "label", "stack"], {
backgroundColor: ["#4EA7F2"],
borderColor: getBorderWhite(),
data: [8],
label: "",
stack: undefined,
});
checkLegend(view, "Total");
checkTooltip(view, { lines: [{ label: "Total", value: "8 (100.00%)" }] }, 0);
});
test("pie chart rendering (one groupBy)", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="pie">
<field name="bar" />
</graph>
`,
});
expect(".o_graph_canvas_container canvas").toHaveCount(1);
checkLabels(view, ["false", "true"]);
checkDatasets(view, ["backgroundColor", "borderColor", "data"], {
backgroundColor: ["#4EA7F2", "#EA6175"],
borderColor: getBorderWhite(),
data: [5, 3],
});
checkLegend(view, ["false", "true"]);
checkTooltip(view, { lines: [{ label: "false", value: "5 (62.50%)" }] }, 0);
checkTooltip(view, { lines: [{ label: "true", value: "3 (37.50%)" }] }, 1);
});
test("pie chart many2many groupby", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="pie">
<field name="revenue" type="measure" />
<field name="color_ids" />
</graph>
`,
});
expect(".o_graph_canvas_container canvas").toHaveCount(1);
checkLabels(view, ["black", "red", "None"]);
checkDatasets(view, ["backgroundColor", "borderColor", "data"], {
backgroundColor: ["#4EA7F2", "#EA6175", "#43C5B1"],
borderColor: getBorderWhite(),
data: [10, 13, 8],
});
checkLegend(view, ["black", "red", "None"]);
checkTooltip(view, { lines: [{ label: "black", value: "10 (32.26%)" }], title: "Revenue" }, 0);
checkTooltip(view, { lines: [{ label: "red", value: "13 (41.94%)" }], title: "Revenue" }, 1);
checkTooltip(view, { lines: [{ label: "None", value: "8 (25.81%)" }], title: "Revenue" }, 2);
});
test("pie chart rendering (two groupBy)", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="pie">
<field name="bar" />
<field name="product_id" />
</graph>
`,
});
expect(".o_graph_canvas_container canvas").toHaveCount(1);
checkLabels(view, ["false / xphone", "false / xpad", "true / xphone"]);
checkDatasets(view, ["backgroundColor", "borderColor", "data", "label"], {
backgroundColor: ["#4EA7F2", "#EA6175", "#43C5B1"],
borderColor: getBorderWhite(),
data: [1, 4, 3],
label: "",
});
checkLegend(view, ["false / xphone", "false / xpad", "true / xphone"]);
checkTooltip(view, { lines: [{ label: "false / xphone", value: "1 (12.50%)" }] }, 0);
checkTooltip(view, { lines: [{ label: "false / xpad", value: "4 (50.00%)" }] }, 1);
checkTooltip(view, { lines: [{ label: "true / xphone", value: "3 (37.50%)" }] }, 2);
});
test("pie chart rendering (no groupBy, several domains)", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="pie">
<field name="revenue" type="measure" />
</graph>
`,
comparison: {
domains: [
{ arrayRepr: [["bar", "=", true]], description: "True group" },
{ arrayRepr: [["bar", "=", false]], description: "False group" },
],
},
});
checkLabels(view, ["Total"]);
checkDatasets(
view,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: ["#4EA7F2"],
borderColor: getBorderWhite(),
data: [6],
label: "True group",
},
{
backgroundColor: ["#4EA7F2"],
borderColor: getBorderWhite(),
data: [17],
label: "False group",
},
]
);
checkLegend(view, ["Total"]);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "True group / Total", value: "6 (100.00%)" }],
},
0,
0
);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "False group / Total", value: "17 (100.00%)" }],
},
0,
1
);
});
test("pie chart rendering (one groupBy, several domains)", async () => {
Foo._records = [
{ bar: true, foo: 1, revenue: 14 },
{ bar: true, foo: 2, revenue: 0 },
{ bar: false, foo: 1, revenue: 12 },
{ bar: false, foo: 2, revenue: 5 },
{ bar: false, foo: 3, revenue: 0 },
{ bar: false, foo: 4, revenue: 2 },
];
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="pie">
<field name="revenue" type="measure" />
<field name="foo" />
</graph>
`,
comparison: {
domains: [
{ arrayRepr: [["bar", "=", true]], description: "True group" },
{ arrayRepr: [["bar", "=", false]], description: "False group" },
],
},
});
checkLabels(view, ["1", "2", "4"]);
checkDatasets(
view,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: ["#4EA7F2", "#EA6175", "#43C5B1"],
borderColor: getBorderWhite(),
data: [14, 0, 0],
label: "True group",
},
{
backgroundColor: ["#4EA7F2", "#EA6175", "#43C5B1"],
borderColor: getBorderWhite(),
data: [12, 5, 2],
label: "False group",
},
]
);
checkLegend(view, ["1", "2", "4"]);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "True group / 1", value: "14 (100.00%)" }],
},
0,
0
);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "False group / 1", value: "12 (63.16%)" }],
},
0,
1
);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "False group / 2", value: "5 (26.32%)" }],
},
1,
1
);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "False group / 4", value: "2 (10.53%)" }],
},
2,
1
);
});
test("pie chart rendering (one groupBy, several domains with date identification)", async () => {
Foo._records = [
{ date: "2021-01-04" },
{ date: "2021-01-12" },
{ date: "2021-01-19" },
{ date: "2021-01-26" },
{ date: "2021-02-04" },
{ date: "2021-02-17" },
{ date: false },
];
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="pie">
<field name="date" interval="week" />
</graph>
`,
comparison: {
domains: [
{
arrayRepr: [
["date", ">=", "2021-02-01"],
["date", "<=", "2021-02-28"],
],
description: "February 2021",
},
{
arrayRepr: [
["date", ">=", "2021-01-01"],
["date", "<=", "2021-01-31"],
],
description: "January 2021",
},
],
fieldName: "date",
},
});
checkLabels(view, ["W05 2021, W01 2021", "W07 2021, W02 2021", "W03 2021", "W04 2021"]);
checkDatasets(
view,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: ["#4EA7F2", "#EA6175", "#43C5B1", "#F4A261"],
borderColor: getBorderWhite(),
data: [1, 1, 0, 0],
label: "February 2021",
},
{
backgroundColor: ["#4EA7F2", "#EA6175", "#43C5B1", "#F4A261"],
borderColor: getBorderWhite(),
data: [1, 1, 1, 1],
label: "January 2021",
},
]
);
checkLegend(view, ["W05 2021, W01 2021", "W07 2021, W02 2021", "W03 2021", "W04 2021"]);
checkTooltip(
view,
{
lines: [{ label: "February 2021 / W05 2021", value: "1 (50.00%)" }],
},
0,
0
);
checkTooltip(
view,
{
lines: [{ label: "January 2021 / W01 2021", value: "1 (25.00%)" }],
},
0,
1
);
checkTooltip(
view,
{
lines: [{ label: "February 2021 / W07 2021", value: "1 (50.00%)" }],
},
1,
0
);
checkTooltip(
view,
{
lines: [{ label: "January 2021 / W02 2021", value: "1 (25.00%)" }],
},
1,
1
);
checkTooltip(
view,
{
lines: [{ label: "January 2021 / W03 2021", value: "1 (25.00%)" }],
},
2,
1
);
checkTooltip(
view,
{
lines: [{ label: "January 2021 / W04 2021", value: "1 (25.00%)" }],
},
3,
1
);
});
test("pie chart rendering (two groupBy, several domains with no date identification)", async () => {
Foo._records = [
{ date: "2021-01-04", bar: false, revenue: 12 },
{ date: "2021-01-12", bar: true, revenue: 5 },
{ date: "2021-02-04", bar: false, revenue: 14 },
{ date: "2021-02-17", bar: true, revenue: 0 },
{ date: false, bar: false, revenue: 0 },
];
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="pie">
<field name="revenue" type="measure" />
<field name="bar" />
<field name="date" interval="week" />
</graph>
`,
comparison: {
domains: [
{
arrayRepr: [
["date", ">=", "2021-02-01"],
["date", "<=", "2021-02-28"],
],
description: "February 2021",
},
{
arrayRepr: [
["date", ">=", "2021-01-01"],
["date", "<=", "2021-01-31"],
],
description: "January 2021",
},
],
fieldName: "date",
},
});
checkLabels(view, ["false / W05 2021", "false / W01 2021", "true / W02 2021"]);
checkDatasets(
view,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: ["#4EA7F2", "#EA6175", "#43C5B1"],
borderColor: getBorderWhite(),
data: [14, 0, 0],
label: "February 2021",
},
{
backgroundColor: ["#4EA7F2", "#EA6175", "#43C5B1"],
borderColor: getBorderWhite(),
data: [0, 12, 5],
label: "January 2021",
},
]
);
checkLegend(view, ["false / W05 2021", "false / W01 2021", "true / W02 2021"]);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "February 2021 / false / W05 2021", value: "14 (100.00%)" }],
},
0,
0
);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "January 2021 / false / W01 2021", value: "12 (70.59%)" }],
},
1,
1
);
checkTooltip(
view,
{
title: "Revenue",
lines: [{ label: "January 2021 / true / W02 2021", value: "5 (29.41%)" }],
},
2,
1
);
});
test("pie chart rendering (no data)", async () => {
Foo._records = [];
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `<graph type="pie" />`,
});
checkLabels(view, ["No data"]);
checkDatasets(
view,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: [DEFAULT_BG],
borderColor: getBorderWhite(),
data: [1],
label: null,
},
]
);
checkLegend(view, ["No data"]);
checkTooltip(view, { lines: [{ label: "No data", value: "0 (100.00%)" }] }, 0);
});
test("pie chart rendering (no data, several domains)", async () => {
Foo._records = [{ product_id: 100, bar: true }];
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="pie">
<field name="product_id" />
</graph>
`,
comparison: {
domains: [
{ arrayRepr: [["bar", "=", true]], description: "True group" },
{ arrayRepr: [["bar", "=", false]], description: "False group" },
],
},
});
checkLabels(view, ["xphone", "No data"]);
checkDatasets(
view,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: ["#4EA7F2"],
borderColor: getBorderWhite(),
data: [1],
label: "True group",
},
{
backgroundColor: ["#4EA7F2", DEFAULT_BG],
borderColor: getBorderWhite(),
data: [undefined, 1],
label: "False group",
},
]
);
checkLegend(view, ["xphone", "No data"]);
checkTooltip(view, { lines: [{ label: "True group / xphone", value: "1 (100.00%)" }] }, 0, 0);
checkTooltip(view, { lines: [{ label: "False group / No data", value: "0 (100.00%)" }] }, 1, 1);
});
test("pie chart rendering (mix of positive and negative values)", async () => {
Foo._records = [
{ bar: true, revenue: 2 },
{ bar: false, revenue: -3 },
];
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="pie">
<field name="revenue" type="measure" />
<field name="bar" />
</graph>
`,
});
expect(".o_view_nocontent").toHaveCount(0);
expect(".o_graph_canvas_container").toHaveCount(1);
checkDatasets(view, ["backgroundColor", "borderColor", "data", "label", "stack"], {
backgroundColor: ["#4EA7F2"],
borderColor: getBorderWhite(),
data: [2],
label: "",
stack: undefined,
});
});
test("pie chart toggling dataset hides label", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: `<graph type="pie"/>`,
});
checkLabels(view, ["Total"]);
await clickOnLegend(view, "Total");
expect(getChart(view).legend.legendItems[0].hidden).toBe(true);
});
test("mode props", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `<graph type="pie" />`,
});
expect(getGraphModelMetaData(view).mode).toBe("pie", {
message: "should be in pie chart mode",
});
expect(getChart(view).config.type).toBe("pie");
});
test("field id not in groupBy", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="id" />
</graph>
`,
});
checkLabels(view, ["Total"]);
checkDatasets(view, ["backgroundColor", "data", "label", "originIndex", "stack"], {
backgroundColor: "#4EA7F2",
data: [8],
label: "Count",
originIndex: 0,
stack: "",
});
checkLegend(view, "Count");
});
test("props modifications", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="bar" />
</graph>
`,
searchViewArch: /* xml */ `
<search>
<filter name="group_by_color" string="Color" context="{ 'group_by': 'color_id' }" />
</search>
`,
});
checkModeIs(view, "bar");
expect(getYAxisLabel(view)).toBe("Count");
await selectMode("line");
checkModeIs(view, "line");
await toggleMenu("Measures");
await toggleMenuItem("Revenue");
expect(getYAxisLabel(view)).toBe("Revenue");
await toggleSearchBarMenu();
await toggleMenuItem("Color");
checkModeIs(view, "line");
expect(getYAxisLabel(view)).toBe("Revenue");
});
test("switching mode", async () => {
const view = await mountView({ type: "graph", resModel: "foo" });
checkModeIs(view, "bar");
await selectMode("bar"); // click on the active mode does not change anything
checkModeIs(view, "bar");
await selectMode("line");
checkModeIs(view, "line");
await selectMode("pie");
checkModeIs(view, "pie");
});
test("switching measure", async () => {
const checkMeasure = (measure) => {
const yAxe = getChart(view).config.options.scales.y;
expect(yAxe.title.text).toBe(measure);
expect(`.o_menu_item:contains(${measure})`).toHaveClass("selected");
};
const view = await mountView({ type: "graph", resModel: "foo" });
await toggleMenu("Measures");
checkMeasure("Count");
checkLegend(view, "Count");
await toggleMenuItem("Foo");
checkMeasure("Foo");
checkLegend(view, "Foo");
});
test("process default view description", async () => {
expect(new GraphArchParser().parse()).toEqual({
fields: {},
fieldAttrs: {},
groupBy: [],
measures: [],
});
});
test("process simple arch (no field tag)", async () => {
const { env } = await makeMockServer();
const fooFields = env["foo"]._fields;
const arch1 = /* xml */ `
<graph order="ASC" disable_linking="1" type="line" />
`;
expect(new GraphArchParser().parse(arch1, fooFields)).toEqual({
disableLinking: true,
fields: fooFields,
fieldAttrs: {},
groupBy: [],
measures: [],
mode: "line",
order: "ASC",
});
const arch2 = /* xml */ `
<graph disable_linking="0" string="Title" stacked="False" />
`;
expect(new GraphArchParser().parse(arch2, fooFields)).toEqual({
disableLinking: false,
fields: fooFields,
fieldAttrs: {},
groupBy: [],
measures: [],
stacked: false,
title: "Title",
});
});
test("process arch with field tags", async () => {
Foo._fields.fighters = fields.Text();
const { env } = await makeMockServer();
const fooFields = env["foo"]._fields;
const arch = /* xml */ `
<graph type="pie">
<field name="revenue" type="measure" />
<field name="date" interval="day" />
<field name="foo" invisible="False" />
<field name="bar" invisible="True" string="My invisible field" />
<field name="id" />
<field name="fighters" string="FooFighters" />
</graph>
`;
expect(new GraphArchParser().parse(arch, fooFields)).toEqual({
fields: fooFields,
fieldAttrs: {
bar: { isInvisible: true, string: "My invisible field" },
fighters: { string: "FooFighters" },
},
measure: "revenue",
measures: ["revenue"],
groupBy: ["date:day", "foo"],
mode: "pie",
});
});
test("process arch with non stored field tags of type measure", async () => {
Foo._fields.revenue.store = false;
const { env } = await makeMockServer();
const fooFields = env["foo"]._fields;
const arch = `
<graph>
<field name="product_id"/>
<field name="revenue" type="measure"/>
<field name="foo" type="measure"/>
</graph>
`;
expect(new GraphArchParser().parse(arch, fooFields)).toEqual({
fields: fooFields,
fieldAttrs: {},
measure: "foo",
measures: ["revenue", "foo"],
groupBy: ["product_id"],
});
});
test("displaying chart data with three groupbys", async () => {
// this test makes sure the line chart shows all data labels (X axis) when
// it is grouped by several fields
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="bar">
<field name="product_id" />
<field name="bar" />
<field name="color_id" />
</graph>
`,
});
checkLabels(view, ["xphone", "xpad"]);
checkLegend(view, ["false / None", "true / red", "true / None", "Sum"]);
await selectMode("line");
checkLabels(view, ["xphone", "xpad"]);
checkLegend(view, ["false / None", "true / red", "true / None"]);
await selectMode("pie");
checkLabels(view, [
"xphone / false / None",
"xphone / true / red",
"xphone / true / None",
"xpad / false / None",
]);
checkLegend(view, [
"xphone / false / None",
"xphone / true / red",
"xphone / true / None",
"xpad / false / None",
]);
});
test("no content helper", async () => {
Foo._records = [];
await mountView({
type: "graph",
resModel: "foo",
noContentHelp: /* xml */ `
<p class="abc">This helper should not be displayed in graph views</p>
`,
});
expect(".o_graph_canvas_container canvas").toHaveCount(1);
expect(".o_view_nocontent").toHaveCount(0);
expect(".abc").toHaveCount(0);
});
test("no content helper after update", async () => {
await mountView({
type: "graph",
resModel: "foo",
noContentHelp: /* xml */ `
<p class="abc">This helper should not be displayed in graph views</p>
`,
config: {
views: [[false, "search"]],
},
});
expect(".o_graph_canvas_container canvas").toHaveCount(1);
expect(".o_view_nocontent").toHaveCount(0);
expect(".abc").toHaveCount(0);
await toggleSearchBarMenu();
await toggleMenuItem("False Domain");
expect(".o_graph_canvas_container canvas").toHaveCount(1);
expect(".o_view_nocontent").toHaveCount(0);
expect(".abc").toHaveCount(0);
});
test("can reload with other group by", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="product_id" />
</graph>
`,
searchViewArch: /* xml */ `
<search>
<filter name="group_by_color" string="Color" context="{ 'group_by': 'color_id' }" />
</search>
`,
});
checkLabels(view, ["xphone", "xpad"]);
await toggleSearchBarMenu();
await toggleMenuItem("Color");
checkLabels(view, ["red", "None"]);
});
test("save params succeeds", async () => {
expect.assertions(4);
const expectedContexts = [
{
graph_mode: "bar",
graph_measure: "__count",
graph_groupbys: ["product_id"],
graph_order: null,
graph_stacked: true,
group_by: [],
},
{
graph_mode: "bar",
graph_measure: "foo",
graph_groupbys: ["product_id"],
graph_order: null,
graph_stacked: true,
group_by: [],
},
{
graph_mode: "line",
graph_measure: "foo",
graph_cumulated: false,
graph_groupbys: ["product_id"],
graph_order: null,
graph_stacked: true,
group_by: [],
},
{
graph_mode: "line",
graph_measure: "foo",
graph_cumulated: false,
graph_groupbys: ["product_id", "color_id"],
graph_order: null,
graph_stacked: true,
group_by: ["product_id", "color_id"],
},
];
let serverId = 1;
onRpc("create_or_replace", ({ args }) => {
expect(args[0].context).toEqual(expectedContexts.shift());
return serverId++;
});
await mountView({
resModel: "foo",
type: "graph",
arch: /* xml */ `
<graph>
<field name="product_id" />
</graph>
`,
searchViewId: false,
searchViewArch: /* xml */ `
<search>
<filter name="false_domain" string="False Domain" domain="[(0, '=', 1)]" />
<filter name="filter_with_context"
string="Filter With Context"
domain="[]"
context="{ 'graph_measure': 'foo', 'graph_mode': 'line', 'graph_groupbys': ['color_id'] }"
/>
<filter name="group_by_color" string="Color" context="{ 'group_by': 'color_id' }" />
<filter name="group_by_product" string="Product" context="{ 'group_by': 'product_id' }" />
</search>
`,
});
await toggleSaveFavorite();
await editFavoriteName("First Favorite");
await saveFavorite();
await toggleMenu("Measures");
await toggleMenuItem("Foo");
await toggleSaveFavorite();
await editFavoriteName("Second Favorite");
await saveFavorite();
await selectMode("line");
await toggleSaveFavorite();
await editFavoriteName("Third Favorite");
await saveFavorite();
await toggleMenuItem("Product");
await toggleMenuItem("Color");
await editFavoriteName("Fourth Favorite");
await saveFavorite();
});
test("correctly uses graph_ keys from the context", async () => {
Foo._records.at(-1).color_id = 1;
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="product_id" />
</graph>
`,
context: {
graph_measure: "foo",
graph_mode: "line",
graph_groupbys: ["color_id"],
},
});
checkLabels(view, ["black", "red"]);
checkLegend(view, "Foo");
checkModeIs(view, "line");
expect(getYAxisLabel(view)).toBe("Foo");
expect(getGraphModelMetaData(view).mode).toBe("line");
});
test("correctly uses graph_ keys from the context (at reload)", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="product_id" />
</graph>
`,
searchViewArch: /* xml */ `
<search>
<filter name="context" domain="[]" string="Context" context="{ 'graph_measure': 'foo', 'graph_mode': 'line' }" />
</search>
`,
});
checkLegend(view, "Count");
expect(getYAxisLabel(view)).toBe("Count");
checkModeIs(view, "bar");
await toggleSearchBarMenu();
await toggleMenuItem("Context");
checkLegend(view, "Foo");
expect(getYAxisLabel(view)).toBe("Foo");
checkModeIs(view, "line");
});
test("correctly use group_by key from the context", async () => {
Foo._records.at(-1).color_id = 1;
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="product_id" />
</graph>
`,
searchViewArch: /* xml */ `
<search>
<filter name="filter_with_context"
string="Filter With Context"
domain="[]"
context="{ 'graph_measure': 'foo', 'graph_mode': 'line', 'graph_groupbys': ['color_id'] }"
/>
</search>
`,
context: {
search_default_filter_with_context: 1,
},
});
checkLabels(view, ["black", "red"]);
checkLegend(view, "Foo");
checkModeIs(view, "line");
expect(getYAxisLabel(view)).toBe("Foo");
expect(getGraphModelMetaData(view).mode).toBe("line");
});
test("an invisible field should not be used as groupBy", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="foo" invisible="1" />
</graph>
`,
});
checkLabels(view, ["Total"]);
});
test("format values as float in case at least one value is not an number", async () => {
Foo._records = [
{ bar: false, revenue: 1.5 },
{ bar: true, revenue: 2 },
];
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="revenue" type="measure" />
<field name="bar" />
</graph>
`,
});
checkDatasets(view, "data", { data: [1.5, 2] });
checkLabels(view, ["false", "true"]);
checkTooltip(view, { title: "Revenue", lines: [{ label: "false", value: "1.50" }] }, 0);
checkTooltip(view, { title: "Revenue", lines: [{ label: "true", value: "2.00" }] }, 1);
});
test("the active measure description is the arch string attribute in priority", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="revenue" type="measure" string="Nirvana" />
<field name="foo" type="measure" string="FooFighters" />
</graph>
`,
});
checkTooltip(view, { title: "FooFighters", lines: [{ label: "Total", value: "239" }] }, 0);
await toggleMenu("Measures");
await toggleMenuItem("Nirvana");
checkTooltip(view, { title: "Nirvana", lines: [{ label: "Total", value: "23" }] }, 0);
});
test("reload graph with correct fields", async () => {
expect.assertions(2);
onRpc("web_read_group", ({ kwargs }) => {
expect(kwargs.fields).toEqual(["__count", "foo:sum"]);
});
await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="foo" type="measure" />
</graph>
`,
searchViewArch: /* xml */ `
<search>
<filter name="false_domain" string="False Domain" domain="[(0, '=', 1)]" />
</search>
`,
});
await toggleSearchBarMenu();
await toggleMenuItem("False Domain");
});
test("initial groupby is kept when reloading", async () => {
expect.assertions(7);
onRpc("web_read_group", ({ kwargs }) => {
expect(kwargs.groupby).toEqual(["product_id"]);
});
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="product_id" />
<field name="foo" type="measure" />
</graph>
`,
searchViewArch: /* xml */ `
<search>
<filter name="false_domain" string="False Domain" domain="[(0, '=', 1)]" />
</search>
`,
});
checkLabels(view, ["xphone", "xpad"]);
checkLegend(view, "Foo");
checkDatasets(view, "data", { data: [82, 157] });
expect(getYAxisLabel(view)).toBe("Foo");
await toggleSearchBarMenu();
await toggleMenuItem("False Domain");
expect(".o_graph_canvas_container").toHaveCount(0);
});
test("use a many2one as a measure should work (without groupBy)", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="product_id" type="measure" />
</graph>
`,
});
checkLabels(view, ["Total"]);
checkLegend(view, "Product");
checkDatasets(view, "data", { data: [2] });
expect(getYAxisLabel(view)).toBe("Product");
});
test("use a many2one as a measure should work (with groupBy)", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="bar" />
<field name="product_id" type="measure" />
</graph>
`,
});
checkLabels(view, ["false", "true"]);
checkLegend(view, "Product");
checkDatasets(view, "data", { data: [2, 1] });
});
test("use a many2one as a measure and as a groupby should work", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="product_id" type="measure" />
<field name="product_id" />
</graph>
`,
});
checkLabels(view, ["xphone", "xpad"]);
checkLegend(view, "Product");
checkDatasets(view, "data", { data: [1, 1] });
expect(getYAxisLabel(view)).toBe("Product");
});
test("differentiate many2one values with same label", async () => {
Product._records.push({ id: 300, name: "xphone" });
Foo._records.push({ product_id: 300 });
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="product_id" />
</graph>
`,
});
checkLabels(view, ["xphone", "xpad", "xphone (2)"]);
});
test("not use a many2one as a measure by default", async () => {
await mountView({
type: "graph",
resModel: "foo",
viewId: false,
});
await toggleMenu("Measures");
expect(queryAllTexts(".o-dropdown--menu .o_menu_item")).toEqual(["Foo", "Revenue", "Count"]);
});
test.tags("desktop");
test("graph view crash when moving from search view using Down key", async () => {
await mountView({ type: "graph", resModel: "foo" });
await contains(".o_searchview input").press("ArrowDown");
expect(".o_graph_view").toHaveCount(1);
});
test("graph measures should be alphabetically sorted (exception: 'Count' is last)", async () => {
Foo._fields.bouh = fields.Integer();
await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="foo" type="measure" />
<field name="bouh" type="measure" />
</graph>
`,
});
await toggleMenu("Measures");
expect(queryAllTexts(".o-dropdown--menu .o_menu_item")).toEqual([
"Bouh",
"Foo",
"Revenue",
"Count",
]);
});
test("a many2one field can be added as measure in arch", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="product_id" type="measure" />
</graph>
`,
});
checkLegend(view, "Product");
expect(getYAxisLabel(view)).toBe("Product");
});
test("non store fields defined on the arch are present in the measures", async () => {
Foo._fields.revenue.store = false;
await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="product_id"/>
<field name="revenue" type="measure"/>
<field name="foo" type="measure"/>
</graph>
`,
});
await toggleMenu("Measures");
expect(queryAllTexts(`.o_menu_item`)).toEqual(["Foo", "Revenue", "Count"]);
});
test("graph view `graph_measure` field in context", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
viewId: false,
context: {
graph_measure: "product_id",
},
});
expect(getYAxisLabel(view)).toBe("Product");
checkLegend(view, "Product");
checkTooltip(view, { title: "Product", lines: [{ label: "Total", value: "2" }] }, 0);
});
test("`graph_measure` in context is prefered to measure in arch", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="revenue" type="measure" />
</graph>
`,
context: {
graph_measure: "product_id",
},
});
expect(getYAxisLabel(view)).toBe("Product");
checkLegend(view, "Product");
checkTooltip(view, { title: "Product", lines: [{ label: "Total", value: "2" }] }, 0);
});
test("None should appear in bar, pie graph but not in line graph with multiple groupbys", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="line">
<field name="date" />
<field name="color_id" />
</graph>
`,
});
const someNone = () => getChart(view).data.labels.some((l) => /none/i.test(l));
expect(someNone()).toBe(false);
await selectMode("bar");
expect(someNone()).toBe(true);
await selectMode("pie");
expect(someNone()).toBe(true);
// None should not appear after switching back to line chart
await selectMode("line");
expect(someNone()).toBe(false);
});
test("an invisible field can not be found in the 'Measures' menu", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="revenue" invisible="1" />
</graph>
`,
});
checkTooltip(view, { lines: [{ label: "Total", value: "8" }] }, 0);
await toggleMenu("Measures");
expect(".o_menu_item:contains(Revenue)").toHaveCount(0, {
message: `"Revenue" can not be found in the "Measures" menu`,
});
});
test("graph view only keeps finer groupby filter option for a given groupby", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
groupBy: ["date:year", "product_id", "date", "date:quarter"],
arch: /* xml */ `<graph type="line" />`,
});
checkLabels(view, ["January 2016", "March 2016", "April 2016", "May 2016"]);
checkLegend(view, ["xphone", "xpad"]);
checkDatasets(
view,
["label", "data"],
[
{
label: "xphone",
data: [2, 2, 0, 0],
},
{
label: "xpad",
data: [0, 0, 1, 1],
},
]
);
});
test("action name is displayed in breadcrumbs", async () => {
await mountWithCleanup(WebClient);
await getService("action").doAction({
name: "Glou glou",
res_model: "foo",
type: "ir.actions.act_window",
views: [[false, "graph"]],
});
expect(".o_breadcrumb .active:first").toHaveText("Glou glou");
});
test("clicking on bar charts triggers a do_action", async () => {
expect.assertions(6);
mockService("action", {
doAction(actionRequest, options) {
expect(actionRequest).toEqual({
context: { allowed_company_ids: [1], lang: "en", tz: "taht", uid: 7 },
domain: [["bar", "=", false]],
name: "Foo Analysis",
res_model: "foo",
target: "current",
type: "ir.actions.act_window",
views: [
[false, "list"],
[false, "form"],
],
});
expect(options).toEqual({ viewType: "list" });
},
});
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph string="Foo Analysis">
<field name="bar" />
</graph>
`,
});
checkModeIs(view, "bar");
checkDatasets(view, ["domains"], {
domains: [[["bar", "=", false]], [["bar", "=", true]]],
});
await clickOnDataset(view);
});
test("Clicking on bar charts removes group_by and search_default_* context keys", async () => {
expect.assertions(2);
mockService("action", {
doAction(actionRequest, options) {
expect(actionRequest).toEqual({
context: { allowed_company_ids: [1], lang: "en", tz: "taht", uid: 7 },
domain: [["bar", "=", false]],
name: "Foo Analysis",
res_model: "foo",
target: "current",
type: "ir.actions.act_window",
views: [
[false, "list"],
[false, "form"],
],
});
expect(options).toEqual({ viewType: "list" });
},
});
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph string="Foo Analysis">
<field name="bar" />
</graph>
`,
context: {
search_default_user: 1,
group_by: "bar",
},
});
await clickOnDataset(view);
});
test("clicking on a pie chart trigger a do_action with correct views", async () => {
expect.assertions(6);
Foo._views[["list", 364]] = /* xml */ `<list />`;
Foo._views[["form", 29]] = /* xml */ `<form />`;
mockService("action", {
doAction(actionRequest, options) {
expect(actionRequest).toEqual({
context: { allowed_company_ids: [1], lang: "en", tz: "taht", uid: 7 },
domain: [["bar", "=", false]],
name: "Foo Analysis",
res_model: "foo",
target: "current",
type: "ir.actions.act_window",
views: [
[364, "list"],
[29, "form"],
],
});
expect(options).toEqual({ viewType: "list" });
},
});
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph string="Foo Analysis" type="pie">
<field name="bar" />
</graph>
`,
config: {
views: [
[364, "list"],
[29, "form"],
],
},
});
checkModeIs(view, "pie");
checkDatasets(view, ["domains"], {
domains: [[["bar", "=", false]], [["bar", "=", true]]],
});
await clickOnDataset(view);
});
test('graph view with attribute disable_linking="1"', async () => {
mockService("action", {
doAction() {
throw new Error("should not perform a `doAction`");
},
});
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph disable_linking="1">
<field name="bar" />
</graph>
`,
});
checkModeIs(view, "bar");
checkDatasets(view, ["domains"], {
domains: [[["bar", "=", false]], [["bar", "=", true]]],
});
await clickOnDataset(view);
});
test("graph view without invisible attribute on field", async () => {
await mountView({
type: "graph",
resModel: "foo",
});
await toggleMenu("Measures");
expect(".o_menu_item").toHaveCount(3, {
message:
"there should be three menu items in the measures dropdown (count, revenue and foo)",
});
expect(".o_menu_item:contains(Revenue)").toHaveCount(1);
expect(".o_menu_item:contains(Foo)").toHaveCount(1);
expect(".o_menu_item:contains(Count)").toHaveCount(1);
});
test("graph view with invisible attribute on field", async () => {
await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="revenue" invisible="1" />
</graph>
`,
});
await toggleMenu("Measures");
expect(".o_menu_item").toHaveCount(2, {
message: "there should be only two menu items in the measures dropdown (count and foo)",
});
expect(".o_menu_item:contains(Revenue)").toHaveCount(0);
});
test("graph view sort by measure", async () => {
// change last record from foo as there are 4 records count for each product
Product._records.push({ id: 150, name: "zphone" });
Foo._records.at(-1).product_id = 150;
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph order="DESC">
<field name="product_id" />
</graph>
`,
});
expect(".fa-sort-amount-asc").toHaveCount(1);
expect(".fa-sort-amount-desc").toHaveCount(1);
checkLegend(view, "Count", "measure should be by count");
expect(".fa-sort-amount-desc").toHaveClass("active");
checkDatasets(view, "data", { data: [4, 3, 1] });
await clickSort("asc");
expect(".fa-sort-amount-asc").toHaveClass("active");
checkDatasets(view, "data", { data: [1, 3, 4] });
await clickSort("desc");
expect(".fa-sort-amount-desc").toHaveClass("active");
checkDatasets(view, "data", { data: [4, 3, 1] });
// again click on descending button to deactivate order
await clickSort("desc");
expect(".fa-sort-amount-desc").not.toHaveClass("active");
checkDatasets(view, "data", { data: [4, 1, 3] });
// set line mode
await selectMode("line");
expect(".fa-sort-amount-asc").toHaveCount(1);
expect(".fa-sort-amount-desc").toHaveCount(1);
checkLegend(view, "Count", "measure should be by count");
expect(".fa-sort-amount-desc").not.toHaveClass("active");
checkDatasets(view, "data", { data: [4, 1, 3] });
await clickSort("asc");
expect(".fa-sort-amount-asc").toHaveClass("active");
checkDatasets(view, "data", { data: [1, 3, 4] });
await clickSort("desc");
expect(".fa-sort-amount-desc").toHaveClass("active");
checkDatasets(view, "data", { data: [4, 3, 1] });
});
test("graph view sort by measure for grouped data", async () => {
// change last record from foo as there are 4 records count for each product
Product._records.push({ id: 150, name: "zphone" });
Foo._records.at(-1).product_id = 150;
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="product_id" />
<field name="bar" />
</graph>
`,
});
checkLegend(view, ["false", "true", "Sum"], "measure should be by count");
checkDatasets(view, "data", [{ data: [1, 1, 3] }, { data: [3, 0, 0] }, { data: [4, 1, 3] }]);
await clickSort("asc");
expect(".fa-sort-amount-asc").toHaveClass("active");
checkDatasets(view, "data", [{ data: [1, 3, 1] }, { data: [0, 0, 3] }, { data: [1, 3, 4] }]);
await clickSort("desc");
expect(".fa-sort-amount-desc").toHaveClass("active");
checkDatasets(view, "data", [{ data: [1, 3, 1] }, { data: [3, 0, 0] }, { data: [4, 3, 1] }]);
// again click on descending button to deactivate order
await clickSort("desc");
expect(".fa-sort-amount-desc").not.toHaveClass("active");
checkDatasets(view, "data", [{ data: [1, 1, 3] }, { data: [3, 0, 0] }, { data: [4, 1, 3] }]);
});
test("graph view sort by measure for multiple grouped data", async () => {
// change last record from foo as there are 4 records count for each product
Product._records.push({ id: 150, name: "zphone" });
Foo._records.at(-1).product_id = 150;
Foo._records.splice(
0,
4,
{ id: 1, foo: 48, bar: false, product_id: 200, date: "2016-04-01" },
{ id: 2, foo: 49, bar: false, product_id: 200, date: "2016-04-01" },
{ id: 3, foo: 50, bar: true, product_id: 100, date: "2016-01-03" },
{ id: 4, foo: 50, bar: true, product_id: 200, date: "2016-01-03" }
);
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="date" />
<field name="product_id" />
</graph>
`,
});
checkLegend(view, ["xphone", "xpad", "zphone", "Sum"], "measure should be by count");
checkDatasets(view, "data", [
{ data: [1, 0, 0, 0] },
{ data: [1, 2, 1, 2] },
{ data: [0, 1, 0, 0] },
{ data: [2, 3, 1, 2] },
]);
await clickSort("asc");
expect(".fa-sort-amount-asc").toHaveClass("active");
checkDatasets(view, "data", [
{ data: [1, 1, 2, 2] },
{ data: [0, 1, 0, 0] },
{ data: [0, 0, 0, 1] },
{ data: [1, 2, 2, 3] },
]);
await clickSort("desc");
expect(".fa-sort-amount-desc").toHaveClass("active");
checkDatasets(view, "data", [
{ data: [1, 0, 0, 0] },
{ data: [2, 1, 2, 1] },
{ data: [0, 1, 0, 0] },
{ data: [3, 2, 2, 1] },
]);
// again click on descending button to deactivate order
await clickSort("desc");
expect(".fa-sort-amount-desc").not.toHaveClass("active");
checkDatasets(view, "data", [
{ data: [1, 0, 0, 0] },
{ data: [1, 2, 1, 2] },
{ data: [0, 1, 0, 0] },
{ data: [2, 3, 1, 2] },
]);
});
test("empty graph view with sample data", async () => {
await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph sample="1">
<field name="product_id" />
<field name="date" />
</graph>
`,
context: {
search_default_false_domain: 1,
},
searchViewArch: /* xml */ `
<search>
<filter name="false_domain" string="False Domain" domain="[(0, '=', 1)]" />
</search>
`,
noContentHelp: /* xml */ `<p class="abc">click to add a foo</p>`,
});
expect(".o_graph_view .o_content").toHaveClass("o_view_sample_data");
expect(".o_view_nocontent").toHaveCount(1);
expect(".o_graph_canvas_container canvas").toHaveCount(1);
await toggleSearchBarMenu();
await toggleMenuItem("False Domain");
expect(".o_graph_view .o_content").not.toHaveClass("o_view_sample_data");
expect(".o_view_nocontent").toHaveCount(0);
expect(".o_graph_canvas_container canvas").toHaveCount(1);
});
test("non empty graph view with sample data", async () => {
await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph sample="1">
<field name="product_id" />
<field name="date" />
</graph>
`,
searchViewArch: /* xml */ `
<search>
<filter name="false_domain" string="False Domain" domain="[(0, '=', 1)]" />
</search>
`,
noContentHelp: /* xml */ `<p class="abc">click to add a foo</p>`,
});
expect(".o_content").not.toHaveClass("o_view_sample_data");
expect(".o_view_nocontent").toHaveCount(0);
expect(".o_graph_canvas_container canvas").toHaveCount(1);
await toggleSearchBarMenu();
await toggleMenuItem("False Domain");
expect(".o_content").not.toHaveClass("o_view_sample_data");
expect(".o_graph_canvas_container canvas").toHaveCount(0);
expect(".o_view_nocontent").toHaveCount(1);
});
test("empty graph view without sample data after filter", async () => {
await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="date" />
</graph>
`,
domain: Domain.FALSE.toList(),
noContentHelp: /* xml */ `<p class="abc">click to add a foo</p>`,
});
expect(".o_graph_canvas_container canvas").toHaveCount(0);
expect(".o_view_nocontent").toHaveCount(1);
});
test.tags("desktop");
test("reload chart with switchView button keep internal state", async () => {
Foo._views.list = /* xml */ `<list />`;
await mountWithCleanup(WebClient);
await getService("action").doAction({
name: "Foo Action 1",
res_model: "foo",
type: "ir.actions.act_window",
views: [
[false, "graph"],
[false, "list"],
],
});
expect(getModeButton("bar")).toHaveClass("active");
await selectMode("line");
expect(getModeButton("line")).toHaveClass("active");
await switchView("graph");
expect(getModeButton("line")).toHaveClass("active");
});
test("fallback on initial groupby when the groupby from control panel has 0 length", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="line">
<field name="product_id" />
</graph>
`,
searchViewArch: /* xml */ `
<search>
<filter name="group_by_foo" string="Foo" domain="[]" context="{ 'group_by': 'foo'}" />
</search>
`,
context: {
search_default_group_by_foo: 1,
},
});
checkLabels(view, ["2", "3", "4", "24", "42", "48", "53", "63"]);
await toggleSearchBarMenu();
await toggleMenuItem("Foo");
checkLabels(view, ["xphone", "xpad"]);
});
test("change mode, stacked, or order via the graph buttons does not reload datapoints, change measure does", async () => {
onRpc("web_read_group", ({ kwargs }) => {
expect.step(kwargs.fields);
});
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="line">
<field name="product_id" />
</graph>
`,
});
checkModeIs(view, "line");
await selectMode("bar");
checkModeIs(view, "bar");
expect(`[data-tooltip="Stacked"]`).toHaveClass("active");
await contains(`[data-tooltip="Stacked"]`).click();
expect(`[data-tooltip="Stacked"]`).not.toHaveClass("active");
expect(`[data-tooltip="Ascending"]`).not.toHaveClass("active");
await contains(`[data-tooltip="Ascending"]`).click();
expect(`[data-tooltip="Ascending"]`).toHaveClass("active");
await toggleMenu("Measures");
await toggleMenuItem("Foo");
expect.verifySteps([
["__count"], // first load
["__count", "foo:sum"], // reload due to change in measure
]);
});
test("concurrent reloads: add a filter, and directly toggle a measure", async () => {
let def;
onRpc("web_read_group", () => def);
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="line">
<field name="product_id" />
</graph>
`,
searchViewArch: /* xml */ `
<search>
<filter name="my_filter" string="My Filter" domain="[('id', '&lt;', 6)]" />
</search>
`,
});
checkDatasets(view, ["data", "label"], {
data: [4, 4],
label: "Count",
});
// Set a domain (this reload is delayed)
def = new Deferred();
await toggleSearchBarMenu();
await toggleMenuItem("My Filter");
checkDatasets(view, ["data", "label"], {
data: [4, 4],
label: "Count",
});
// Toggle a measure
await toggleMenu("Measures");
await toggleMenuItem("Foo");
checkDatasets(view, ["data", "label"], {
data: [4, 4],
label: "Count",
});
def.resolve();
await animationFrame();
checkDatasets(view, ["data", "label"], {
data: [82, 4],
label: "Foo",
});
});
test("change graph mode while loading a filter", async () => {
let def;
onRpc("web_read_group", () => def);
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph type="line">
<field name="product_id" />
</graph>
`,
searchViewArch: /* xml */ `
<search>
<filter name="my_filter" string="My Filter" domain="[('id', '&lt;', 2)]" />
</search>
`,
});
checkDatasets(view, ["data", "label"], {
data: [4, 4],
label: "Count",
});
checkModeIs(view, "line");
// Set a domain (this reload is delayed)
def = new Deferred();
await toggleSearchBarMenu();
await toggleMenuItem("My Filter");
checkDatasets(view, ["data", "label"], {
data: [4, 4],
label: "Count",
});
checkModeIs(view, "line");
// Change graph mode
await selectMode("bar");
checkDatasets(view, ["data", "label"], {
data: [4, 4],
label: "Count",
});
checkModeIs(view, "line");
def.resolve();
await animationFrame();
checkDatasets(view, ["data", "label"], {
data: [1],
label: "Count",
});
checkModeIs(view, "bar");
});
test("only process most recent data for concurrent groupby", async () => {
let def;
onRpc(() => def);
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph>
<field name="product_id" type="row" />
<field name="foo" type="measure" />
</graph>
`,
searchViewArch: /* xml */ `
<search>
<filter name="group_by_color" string="Color" context="{ 'group_by': 'color_id' }" />
<filter name="group_by_date" string="Date" context="{ 'group_by': 'date' }" />
</search>
`,
});
checkLabels(view, ["xphone", "xpad"]);
checkDatasets(view, "data", { data: [82, 157] });
def = new Deferred();
await toggleSearchBarMenu();
await toggleMenuItem("Color");
await toggleMenuItem("Color");
await toggleMenuItem("Date");
await toggleMenuItemOption("Date", "Month");
checkLabels(view, ["xphone", "xpad"]);
checkDatasets(view, "data", { data: [82, 157] });
def.resolve();
await animationFrame();
checkLabels(view, ["January 2016", "March 2016", "April 2016", "May 2016", "None"]);
checkDatasets(view, "data", { data: [56, 26, 48, 4, 105] });
});
test("fill_temporal is true by default", async () => {
expect.assertions(1);
onRpc("web_read_group", ({ kwargs }) => {
expect(kwargs.context.fill_temporal).toBe(true, {
message: "The observable state of fill_temporal should be true",
});
});
await mountView({ type: "graph", resModel: "foo" });
});
test("fill_temporal can be changed throught the context", async () => {
expect.assertions(1);
onRpc("web_read_group", ({ kwargs }) => {
expect(kwargs.context.fill_temporal).toBe(false, {
message: "The observable state of fill_temporal should be false",
});
});
await mountView({
type: "graph",
resModel: "foo",
context: {
fill_temporal: false,
},
});
});
test("fake data in line chart", async () => {
mockDate("2020-05-19 01:00:00");
Foo._records = [];
await mountView({
type: "graph",
resModel: "foo",
context: {
search_default_date_filter: 1,
},
arch: /* xml */ `
<graph type="line">
<field name="date" />
</graph>
`,
searchViewArch: /* xml */ `
<search>
<filter name="date_filter" domain="[]" date="date" default_period="third_quarter" />
</search>
`,
});
await toggleSearchBarMenu();
await toggleMenuItem("Date: Previous period");
expect(".o_graph_canvas_container").toHaveCount(0);
});
test("no filling color for period of comparison", async () => {
mockDate("2020-05-19 01:00:00");
for (const record of Foo._records) {
record.date = record.date?.replace(/^\d{4}/, "2019");
}
const view = await mountView({
type: "graph",
resModel: "foo",
context: {
search_default_date_filter: 1,
},
arch: /* xml */ `
<graph type="line" stacked="0">
<field name="product_id" />
</graph>
`,
searchViewArch: /* xml */ `
<search>
<filter name="date_filter" domain="[]" date="date" default_period="year" />
</search>
`,
});
await toggleSearchBarMenu();
await toggleMenuItem("Date: Previous period");
checkDatasets(view, "backgroundColor", { backgroundColor: "#a7d3f9" });
});
test("group by a non stored, sortable field", async () => {
// When a field is non-stored but sortable it's inherited
// from a stored field, so it can be sortable
Foo._fields.date.store = false;
const view = await mountView({
type: "graph",
resModel: "foo",
groupBy: ["date:month"],
arch: /* xml */ `<graph type="line" />`,
});
checkLabels(view, ["January 2016", "March 2016", "April 2016", "May 2016"]);
});
test("graph_groupbys should be also used after first load", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
viewId: false,
groupBy: ["date:quarter"],
irFilters: [
{
user_id: [2, "Mitchell Admin"],
name: "Favorite",
id: 1,
context: JSON.stringify({
group_by: [],
graph_measure: "revenue",
graph_mode: "bar",
graph_groupbys: ["color_id"],
}),
sort: "[]",
domain: "",
is_default: false,
model_id: "foo",
action_id: false,
},
],
});
checkModeIs(view, "bar");
checkLabels(view, ["Q1 2016", "Q2 2016", "None"]);
checkLegend(view, "Count");
await toggleSearchBarMenu();
await toggleMenuItem("Favorite");
checkModeIs(view, "bar");
checkLabels(view, ["red", "None"]);
checkLegend(view, "Revenue");
});
test("order='desc' on arch", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph order="desc">
<field name="date" />
</graph>
`,
});
checkDatasets(view, ["data", "label"], {
data: [2, 2, 2, 1, 1],
label: "Count",
});
});
test("order='asc' on arch", async () => {
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph order="asc">
<field name="date" />
</graph>
`,
});
checkDatasets(view, ["data", "label"], {
data: [1, 1, 2, 2, 2],
label: "Count",
});
});
test("In the middle of a year, a graph view grouped by a date field with granularity 'year' should have a single group of SampleServer.MAIN_RECORDSET_SIZE records", async () => {
mockDate("2023-06-15 08:00:00");
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph sample="1">
<field name="date" interval="year" />
</graph>
`,
domain: Domain.FALSE.toList(),
});
checkDatasets(view, ["data"], { data: [SampleServer.MAIN_RECORDSET_SIZE] });
});
test("no class 'o_view_sample_data' when real data are presented", async () => {
Foo._records = [];
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph sample="1">
<field name="date" />
</graph>
`,
});
expect(".o_graph_view .o_view_sample_data").toHaveCount(1);
expect(getChart(view).data.datasets.length).toBeGreaterThan(0);
await selectMode("line");
expect(".o_graph_view .o_view_sample_data").toHaveCount(1);
expect(getChart(view).data.datasets.length).toBeGreaterThan(0);
await toggleMenu("Measures");
await toggleMenuItem("Revenue");
expect(".o_graph_view .o_view_sample_data").toHaveCount(0);
expect(".o_graph_canvas_container").toHaveCount(0);
});
test("single chart rendering on search", async () => {
patchWithCleanup(GraphRenderer.prototype, {
setup() {
super.setup();
onRendered(() => expect.step("rendering"));
},
});
await mountView({
type: "graph",
resModel: "foo",
});
expect.verifySteps(["rendering"]);
await validateSearch();
expect.verifySteps(["rendering"]);
});
test("apply default filter label", async () => {
class CustomGraphModel extends graphView.Model {
_getDefaultFilterLabel(fields) {
return "None";
}
}
registry.category("views").add("custom_graph", {
...graphView,
Model: CustomGraphModel,
});
const view = await mountView({
type: "graph",
resModel: "foo",
arch: /* xml */ `
<graph js_class="custom_graph">
<field name="product_id" />
<field name="color_id" />
</graph>
`,
});
checkLabels(view, ["xphone", "xpad"]);
checkLegend(view, ["red", "None", "Sum"]);
await selectMode("line");
checkLabels(view, ["xphone", "xpad"]);
checkLegend(view, ["red", "None"]);
await selectMode("pie");
checkLabels(view, ["xphone / red", "xphone / None", "xpad / None"]);
checkLegend(view, ["xphone / red", "xphone / None", "xpad / None"]);
});
test("missing property field definition is fetched", async function () {
Foo._fields.properties_definition = fields.PropertiesDefinition();
Foo._fields.parent_id = fields.Many2one({ relation: "foo" });
Foo._fields.properties = fields.Properties({
definition_record: "parent_id",
definition_record_field: "properties_definition",
});
onRpc(({ method, kwargs }) => {
if (method === "web_read_group" && kwargs.groupby?.includes("properties.my_char")) {
expect.step(JSON.stringify(kwargs.groupby));
return {
groups: [
{
"properties.my_char": false,
__domain: [["properties.my_char", "=", false]],
__count: 2,
},
{
"properties.my_char": "aaa",
__domain: [["properties.my_char", "=", "aaa"]],
__count: 1,
},
],
length: 2,
};
} else if (method === "get_property_definition") {
return {
name: "my_char",
type: "char",
};
}
});
const view = await mountView({
type: "graph",
resModel: "foo",
arch: `<graph/>`,
irFilters: [
{
user_id: [2, "Mitchell Admin"],
name: "My Filter",
id: 5,
context: `{"group_by": ['properties.my_char']}`,
sort: "[]",
domain: "[]",
is_default: true,
model_id: "foo",
action_id: false,
},
],
});
expect.verifySteps([`["properties.my_char"]`]);
checkLabels(view, ["None", "aaa"]);
checkDatasets(
view,
["data", "label"],
[
{
data: [2, 1],
label: "Count",
},
]
);
});
test("missing deleted property field definition is created", async function () {
Foo._fields.properties_definition = fields.PropertiesDefinition();
Foo._fields.parent_id = fields.Many2one({ relation: "foo" });
Foo._fields.properties = fields.Properties({
definition_record: "parent_id",
definition_record_field: "properties_definition",
});
onRpc(({ method, kwargs }) => {
if (method === "web_read_group" && kwargs.groupby?.includes("properties.my_char")) {
expect.step(JSON.stringify(kwargs.groupby));
return {
groups: [
{
"properties.my_char": false,
__domain: [["properties.my_char", "=", false]],
__count: 2,
},
{
"properties.my_char": "aaa",
__domain: [["properties.my_char", "=", "aaa"]],
__count: 1,
},
],
length: 2,
};
} else if (method === "get_property_definition") {
return {};
}
});
const view = await mountView({
type: "graph",
resModel: "foo",
arch: `<graph/>`,
irFilters: [
{
user_id: [2, "Mitchell Admin"],
name: "My Filter",
id: 5,
context: `{"group_by": ['properties.my_char']}`,
sort: "[]",
domain: "[]",
is_default: true,
model_id: "foo",
action_id: false,
},
],
});
expect.verifySteps([`["properties.my_char"]`]);
checkLabels(view, ["None", "aaa"]);
checkDatasets(
view,
["data", "label"],
[
{
data: [2, 1],
label: "Count",
},
]
);
});