Odoo18-Base/addons/web/static/tests/views/graph_view_tests.js
2025-03-10 10:52:11 +07:00

4769 lines
163 KiB
JavaScript

/** @odoo-module **/
import {
click,
getFixture,
makeDeferred,
nextTick,
patchDate,
triggerEvent,
findChildren,
} from "@web/../tests/helpers/utils";
import {
editFavoriteName,
saveFavorite,
switchView,
toggleMenu,
toggleMenuItem,
toggleMenuItemOption,
toggleSaveFavorite,
toggleSearchBarMenu,
validateSearch,
} from "@web/../tests/search/helpers";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { getBorderWhite, DEFAULT_BG, getColors, hexToRGBA } from "@web/core/colors/colors";
import { GraphArchParser } from "@web/views/graph/graph_arch_parser";
import { GraphRenderer } from "@web/views/graph/graph_renderer";
import { onRendered } from "@odoo/owl";
import { patchWithCleanup } from "../helpers/utils";
import { Domain } from "@web/core/domain";
import { SampleServer } from "@web/model/sample_server";
import { GraphModel } from "@web/views/graph/graph_model";
const serviceRegistry = registry.category("services");
function getGraphModelMetaData(graph) {
return graph.model.metaData;
}
export function getGraphRenderer(graph) {
const layoutNode = findChildren(graph);
return Object.values(layoutNode.children)
.map((c) => c.component)
.find((c) => c.chart);
}
function getChart(graph) {
return getGraphRenderer(graph).chart;
}
function checkDatasets(assert, graph, keys, expectedDatasets) {
keys = keys instanceof Array ? keys : [keys];
expectedDatasets = expectedDatasets instanceof Array ? expectedDatasets : [expectedDatasets];
const datasets = getChart(graph).data.datasets;
const actualValues = [];
for (const dataset of datasets) {
const partialDataset = {};
for (const key of keys) {
partialDataset[key] = dataset[key];
}
actualValues.push(partialDataset);
}
assert.deepEqual(actualValues, expectedDatasets);
}
export function checkLabels(assert, graph, expectedLabels) {
const labels = getChart(graph).data.labels.map((l) => l.toString());
assert.deepEqual(labels, expectedLabels);
}
export function checkYTicks(assert, graph, expectedLabels) {
const labels = getChart(graph).scales.y.ticks.map((l) => l.label);
assert.deepEqual(labels, expectedLabels);
}
export function checkLegend(assert, graph, expectedLegendLabels) {
expectedLegendLabels =
expectedLegendLabels instanceof Array ? expectedLegendLabels : [expectedLegendLabels];
const chart = getChart(graph);
const actualLegendLabels = chart.config.options.plugins.legend.labels
.generateLabels(chart)
.map((o) => o.text);
assert.deepEqual(actualLegendLabels, expectedLegendLabels);
}
function checkTooltip(assert, graph, expectedTooltipContent, index, datasetIndex) {
// If the tooltip options are changed, this helper should change: we construct the dataPoints
// similarly to Chart.js according to the values set for the tooltips options 'mode' and 'intersect'.
const { datasets } = getChart(graph).data;
const dataPoints = [];
for (let i = 0; i < datasets.length; i++) {
const dataset = datasets[i];
const raw = dataset.data[index];
if (raw !== undefined && (datasetIndex === undefined || datasetIndex === i)) {
dataPoints.push({
datasetIndex: i,
dataIndex: index,
raw,
});
}
}
const tooltipModel = { opacity: 1, x: 1, y: 1, dataPoints };
getChart(graph).config.options.plugins.tooltip.external({ tooltip: tooltipModel });
const { title, lines } = expectedTooltipContent;
const lineLabels = [];
const lineValues = [];
for (const line of lines) {
lineLabels.push(line.label);
lineValues.push(`${line.value}`);
}
assert.containsOnce(target, "div.o_graph_custom_tooltip");
const tooltipTitle = target.querySelector("table thead tr th.o_measure");
assert.strictEqual(tooltipTitle.innerText, title || "Count", `Tooltip title`);
assert.deepEqual(
[...target.querySelectorAll("table tbody tr td span.o_label")].map((td) => td.innerText),
lineLabels,
`Tooltip line labels`
);
assert.deepEqual(
[...target.querySelectorAll("table tbody tr td.o_value")].map((td) => td.innerText),
lineValues,
`Tooltip line values`
);
}
function getModeButton(el, mode) {
return el.querySelector(`.o_graph_button[data-mode="${mode}"`);
}
export async function selectMode(el, mode) {
await click(getModeButton(el, mode));
}
function checkModeIs(assert, graph, mode) {
assert.strictEqual(getGraphModelMetaData(graph).mode, mode);
assert.strictEqual(getChart(graph).config.type, mode);
assert.hasClass(getModeButton(target, mode), "active");
}
function getScaleY(graph) {
return getChart(graph).config.options.scales.y;
}
function getXAxeLabel(graph) {
return getChart(graph).config.options.scales.x.title.text;
}
function getYAxeLabel(graph) {
return getChart(graph).config.options.scales.y.title.text;
}
export async function clickOnDataset(graph) {
const chart = getChart(graph);
const meta = chart.getDatasetMeta(0);
const rectangle = chart.canvas.getBoundingClientRect();
const point = meta.data[0].getCenterPoint();
await triggerEvent(chart.canvas, null, "click", {
pageX: rectangle.left + point.x,
pageY: rectangle.top + point.y,
});
}
export async function clickOnLegend(graph, text) {
const chart = getChart(graph);
const index = chart.legend.legendItems.findIndex((e) => e.text === text);
const { left, top, width, height } = chart.legend.legendHitBoxes[index];
const rectangle = chart.canvas.getBoundingClientRect();
const middle = {
x: left + width / 2,
y: top + height / 2,
};
await triggerEvent(chart.canvas, null, "click", {
pageX: rectangle.left + middle.x,
pageY: rectangle.top + middle.y,
});
}
let serverData;
let target;
QUnit.module("Views", (hooks) => {
hooks.beforeEach(async () => {
serverData = {
models: {
foo: {
fields: {
id: { string: "Id", type: "integer" },
foo: {
string: "Foo",
type: "integer",
store: true,
group_operator: "sum",
sortable: true,
},
bar: { string: "bar", type: "boolean", store: true, sortable: true },
product_id: {
string: "Product",
type: "many2one",
relation: "product",
store: true,
sortable: true,
},
color_id: {
string: "Color",
type: "many2one",
relation: "color",
store: true,
sortable: true,
},
date: { string: "Date", type: "date", store: true, sortable: true },
revenue: {
string: "Revenue",
type: "float",
store: true,
group_operator: "sum",
sortable: true,
},
color_ids: {
string: "Colors",
type: "many2many",
relation: "color",
store: true,
},
},
records: [
{
id: 1,
foo: 3,
bar: true,
product_id: 37,
date: "2016-01-01",
revenue: 1,
color_ids: [7],
},
{
id: 2,
foo: 53,
bar: true,
product_id: 37,
color_id: 7,
date: "2016-01-03",
revenue: 2,
color_ids: [14],
},
{
id: 3,
foo: 2,
bar: true,
product_id: 37,
date: "2016-03-04",
revenue: 3,
color_ids: [7, 14],
},
{
id: 4,
foo: 24,
bar: false,
product_id: 37,
date: "2016-03-07",
revenue: 4,
color_ids: [7],
},
{
id: 5,
foo: 4,
bar: false,
product_id: 41,
date: "2016-05-01",
revenue: 5,
color_ids: [7, 14],
},
{ id: 6, foo: 63, bar: false, product_id: 41 },
{ id: 7, foo: 42, bar: false, product_id: 41 },
{
id: 8,
foo: 48,
bar: false,
product_id: 41,
date: "2016-04-01",
revenue: 8,
},
],
},
product: {
fields: {
id: { string: "Id", type: "integer" },
name: { string: "Product Name", type: "char" },
},
records: [
{
id: 37,
display_name: "xphone",
},
{
id: 41,
display_name: "xpad",
},
],
},
color: {
fields: {
id: { string: "Id", type: "integer" },
name: { string: "Color", type: "char" },
},
records: [
{
id: 7,
display_name: "red",
},
{
id: 14,
display_name: "black",
},
],
},
},
views: {
"foo,false,graph": `<graph/>`,
"foo,false,search": `
<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>
`,
},
};
setupViewRegistries();
patchWithCleanup(browser, { setTimeout: (fn) => fn() });
target = getFixture();
});
QUnit.module("GraphView");
QUnit.test("simple bar chart rendering", async function (assert) {
const graph = await makeView({ serverData, type: "graph", resModel: "foo" });
const { measure, mode, order, stacked } = getGraphModelMetaData(graph);
assert.hasClass(target.querySelector(".o_graph_view"), "o_view_controller");
assert.containsOnce(target, "div.o_graph_canvas_container canvas");
assert.strictEqual(measure, "__count", `the active measure should be "__count" by default`);
assert.strictEqual(mode, "bar", "should be in bar chart mode by default");
assert.strictEqual(order, null, "should not be ordered by default");
assert.strictEqual(stacked, true, "bar charts should be stacked by default");
checkLabels(assert, graph, ["Total"]);
checkDatasets(assert, graph, ["backgroundColor", "borderColor", "data", "label", "stack"], {
backgroundColor: "#1f77b4",
borderColor: undefined,
data: [8],
label: "Count",
stack: "",
});
checkLegend(assert, graph, "Count");
checkTooltip(assert, graph, { lines: [{ label: "Total", value: "8" }] }, 0);
});
QUnit.test("simple bar chart rendering with no data", async function (assert) {
assert.expect(4);
serverData.models.foo.records = [];
const graph = await makeView({ serverData, type: "graph", resModel: "foo" });
assert.containsOnce(target, "div.o_graph_canvas_container canvas");
assert.containsNone(target, ".o_nocontent_help");
checkLabels(assert, graph, []);
checkDatasets(assert, graph, [], []);
});
QUnit.test("simple bar chart rendering (one groupBy)", async function (assert) {
assert.expect(12);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `<graph><field name="bar"/></graph>`,
});
assert.containsOnce(target, "div.o_graph_canvas_container canvas");
checkLabels(assert, graph, ["false", "true"]);
checkDatasets(assert, graph, ["backgroundColor", "borderColor", "data", "label"], {
backgroundColor: "#1f77b4",
borderColor: undefined,
data: [5, 3],
label: "Count",
});
checkLegend(assert, graph, "Count");
checkTooltip(assert, graph, { lines: [{ label: "false", value: "5" }] }, 0);
checkTooltip(assert, graph, { lines: [{ label: "true", value: "3" }] }, 1);
});
QUnit.test("simple bar chart rendering (two groupBy)", async function (assert) {
assert.expect(20);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="bar"/>
<field name="product_id"/>
</graph>
`,
});
assert.containsOnce(target, "div.o_graph_canvas_container canvas");
checkLabels(assert, graph, ["false", "true"]);
checkDatasets(
assert,
graph,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: "#1f77b4",
borderColor: undefined,
data: [1, 3],
label: "xphone",
},
{
backgroundColor: "#ff7f0e",
borderColor: undefined,
data: [4, 0],
label: "xpad",
},
{
backgroundColor: "rgba(0,0,0,0.4)",
borderColor: "rgba(0,0,0,0.4)",
data: [5, 3],
label: "Sum",
},
]
);
checkLegend(assert, graph, ["xphone", "xpad", "Sum"]);
checkTooltip(assert, graph, { lines: [{ label: "false / xphone", value: "1" }] }, 0, 0);
checkTooltip(assert, graph, { lines: [{ label: "true / xphone", value: "3" }] }, 1, 0);
checkTooltip(assert, graph, { lines: [{ label: "false / xpad", value: "4" }] }, 0, 1);
checkTooltip(assert, graph, { lines: [{ label: "true / xpad", value: "0" }] }, 1, 1);
});
QUnit.test("bar chart rendering (no groupBy, several domains)", async function (assert) {
assert.expect(11);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="revenue" type="measure"/>
</graph>
`,
groupBy: [],
comparison: {
domains: [
{ arrayRepr: [["bar", "=", true]], description: "True group" },
{ arrayRepr: [["bar", "=", false]], description: "False group" },
],
},
});
checkLabels(assert, graph, ["Total"]);
checkDatasets(
assert,
graph,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: "#1f77b4",
borderColor: undefined,
data: [6],
label: "True group",
},
{
backgroundColor: "#ff7f0e",
borderColor: undefined,
data: [17],
label: "False group",
},
]
);
checkLegend(assert, graph, ["True group", "False group"]);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "Total / True group", value: "6" }],
},
0,
0
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "Total / False group", value: "17" }],
},
0,
1
);
});
QUnit.test("bar chart rendering (one groupBy, several domains)", async function (assert) {
assert.expect(19);
serverData.models.foo.records = [
{ bar: true, foo: 1, revenue: 14 },
{ bar: true, foo: 2, revenue: false },
{ 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 graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<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(assert, graph, ["1", "2", "3", "4"]);
checkDatasets(
assert,
graph,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: "#1f77b4",
borderColor: undefined,
data: [14, 0, 0, 0],
label: "True group",
},
{
backgroundColor: "#ff7f0e",
borderColor: undefined,
data: [12, -4, 2, 0],
label: "False group",
},
]
);
checkLegend(assert, graph, ["True group", "False group"]);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "1 / True group", value: "14" }],
},
0,
0
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "1 / False group", value: "12" }],
},
0,
1
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "2 / False group", value: "-4" }],
},
1,
1
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "3 / False group", value: "2" }],
},
2,
1
);
});
QUnit.test("bar chart many2many groupBy", async function (assert) {
assert.expect(16);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="revenue" type="measure"/>
<field name="color_ids"/>
</graph>`,
});
assert.containsOnce(target, "div.o_graph_canvas_container canvas");
checkLabels(assert, graph, ["None", "black", "red"]);
checkDatasets(assert, graph, ["backgroundColor", "borderColor", "data", "label"], {
backgroundColor: "#1f77b4",
borderColor: undefined,
data: [8, 10, 13],
label: "Revenue",
});
checkLegend(assert, graph, "Revenue");
checkTooltip(
assert,
graph,
{ lines: [{ label: "None", value: "8" }], title: "Revenue" },
0
);
checkTooltip(
assert,
graph,
{ lines: [{ label: "black", value: "10" }], title: "Revenue" },
1
);
checkTooltip(
assert,
graph,
{ lines: [{ label: "red", value: "13" }], title: "Revenue" },
2
);
});
QUnit.test("differentiate many2many values with same label", async function (assert) {
assert.expect(19);
serverData.models.color.records.push({ id: 21, display_name: "red" });
serverData.models.foo.records.push({ id: 30, color_ids: [21], revenue: 14 });
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="revenue" type="measure"/>
<field name="color_ids"/>
</graph>`,
});
assert.containsOnce(target, "div.o_graph_canvas_container canvas");
checkLabels(assert, graph, ["None", "black", "red", "red (2)"]);
checkDatasets(assert, graph, ["backgroundColor", "borderColor", "data", "label"], {
backgroundColor: "#1f77b4",
borderColor: undefined,
data: [8, 10, 14, 13],
label: "Revenue",
});
checkTooltip(
assert,
graph,
{ lines: [{ label: "None", value: "8" }], title: "Revenue" },
0
);
checkTooltip(
assert,
graph,
{ lines: [{ label: "black", value: "10" }], title: "Revenue" },
1
);
checkTooltip(
assert,
graph,
{ lines: [{ label: "red", value: "14" }], title: "Revenue" },
2
);
checkTooltip(
assert,
graph,
{ lines: [{ label: "red (2)", value: "13" }], title: "Revenue" },
3
);
});
QUnit.test(
"bar chart rendering (one groupBy, several domains with date identification)",
async function (assert) {
assert.expect(23);
serverData.models.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: false },
{ date: false, revenue: 0 },
];
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<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(assert, graph, ["W05 2021", "W07 2021", "", ""]);
checkDatasets(
assert,
graph,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: "#1f77b4",
borderColor: undefined,
data: [14, 0],
label: "February 2021",
},
{
backgroundColor: "#ff7f0e",
borderColor: undefined,
data: [12, 5, 15, 2],
label: "January 2021",
},
]
);
checkLegend(assert, graph, ["February 2021", "January 2021"]);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "W05 2021 / February 2021", value: "14" }],
},
0,
0
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "W01 2021 / January 2021", value: "12" }],
},
0,
1
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "W02 2021 / January 2021", value: "5" }],
},
1,
1
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "W03 2021 / January 2021", value: "15" }],
},
2,
1
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "W04 2021 / January 2021", value: "2" }],
},
3,
1
);
}
);
QUnit.test(
"bar chart rendering (two groupBy, several domains with no date identification)",
async function (assert) {
assert.expect(15);
serverData.models.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: false },
{ date: false, bar: false, revenue: 0 },
];
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<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(assert, graph, ["false", "true"]);
checkDatasets(
assert,
graph,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: "#1f77b4",
borderColor: undefined,
data: [14, 0],
label: "February 2021 / W05 2021",
},
{
backgroundColor: "#ff7f0e",
borderColor: undefined,
data: [0, 0],
label: "February 2021 / W07 2021",
},
{
backgroundColor: "#aec7e8",
borderColor: undefined,
data: [12, 0],
label: "January 2021 / W01 2021",
},
{
backgroundColor: "#ffbb78",
borderColor: undefined,
data: [0, 5],
label: "January 2021 / W02 2021",
},
]
);
checkLegend(assert, graph, [
"February 2021 / W05 2021",
"February 2021 / W07 2021",
"January 2021 / W01 2021",
"January 2021 / W02 2021",
]);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "false / February 2021 / W05 2021", value: "14" }],
},
0,
0
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "false / January 2021 / W01 2021", value: "12" }],
},
0,
2
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "true / January 2021 / W02 2021", value: "5" }],
},
1,
3
);
}
);
QUnit.test("line chart rendering (no groupBy)", async function (assert) {
assert.expect(9);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `<graph type="line"/>`,
});
assert.containsOnce(target, "div.o_graph_canvas_container canvas");
const { mode } = getGraphModelMetaData(graph);
assert.strictEqual(mode, "line");
checkLabels(assert, graph, ["", "Total", ""]);
checkDatasets(assert, graph, ["backgroundColor", "borderColor", "data", "label", "stack"], {
backgroundColor: "rgba(31,119,180,0.4)",
borderColor: "#1f77b4",
data: [undefined, 8],
label: "Count",
stack: undefined,
});
checkLegend(assert, graph, "Count");
checkTooltip(assert, graph, { lines: [{ label: "Total", value: "8" }] }, 1);
});
QUnit.test("line chart rendering (one groupBy)", async function (assert) {
assert.expect(12);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="line">
<field name="bar"/>
</graph>
`,
});
assert.containsOnce(target, "div.o_graph_canvas_container canvas");
checkLabels(assert, graph, ["false", "true"]);
checkDatasets(assert, graph, ["backgroundColor", "borderColor", "data", "label"], {
backgroundColor: "rgba(31,119,180,0.4)",
borderColor: "#1f77b4",
data: [5, 3],
label: "Count",
});
checkLegend(assert, graph, "Count");
checkTooltip(assert, graph, { lines: [{ label: "false", value: "5" }] }, 0);
checkTooltip(assert, graph, { lines: [{ label: "true", value: "3" }] }, 1);
});
QUnit.test("line chart rendering (two groupBy)", async function (assert) {
assert.expect(12);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="line" stacked="0">
<field name="bar"/>
<field name="product_id"/>
</graph>
`,
});
assert.containsOnce(target, "div.o_graph_canvas_container canvas");
checkLabels(assert, graph, ["false", "true"]);
checkDatasets(
assert,
graph,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: undefined,
borderColor: "#1f77b4",
data: [1, 3],
label: "xphone",
},
{
backgroundColor: undefined,
borderColor: "#ff7f0e",
data: [4, 0],
label: "xpad",
},
]
);
checkLegend(assert, graph, ["xphone", "xpad"]);
checkTooltip(
assert,
graph,
{
lines: [
{ label: "false / xpad", value: "4" },
{ label: "false / xphone", value: "1" },
],
},
0
);
checkTooltip(
assert,
graph,
{
lines: [
{ label: "true / xphone", value: "3" },
{ label: "true / xpad", value: "0" },
],
},
1
);
});
QUnit.test("line chart many2many groupBy", async function (assert) {
assert.expect(12);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="line">
<field name="revenue" type="measure"/>
<field name="color_ids"/>
</graph>
`,
});
assert.containsOnce(target, "div.o_graph_canvas_container canvas");
checkLabels(assert, graph, ["black", "red"]);
checkDatasets(assert, graph, ["backgroundColor", "borderColor", "data", "label"], {
backgroundColor: "rgba(31,119,180,0.4)",
borderColor: "#1f77b4",
data: [10, 13],
label: "Revenue",
});
checkLegend(assert, graph, "Revenue");
checkTooltip(
assert,
graph,
{ lines: [{ label: "black", value: "10" }], title: "Revenue" },
0
);
checkTooltip(
assert,
graph,
{ lines: [{ label: "red", value: "13" }], title: "Revenue" },
1
);
});
QUnit.test(
"Check if values in tooltip are correctly sorted when groupBy filter are applied",
async function (assert) {
serverData.models.foo.records = [
{ product_id: 37, foo: 1, revenue: 12 },
{ product_id: 37, foo: 2, revenue: 5 },
{ product_id: 37, foo: 3, revenue: 1.45e2 },
{ product_id: 37, foo: 4, revenue: -9 },
{ product_id: 41, foo: 5, revenue: 0 },
{ product_id: 41, foo: 6, revenue: -1 },
{ product_id: 41, foo: 7, revenue: Math.PI },
{ product_id: 41, foo: 8, revenue: 80.67 },
];
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="line" stacked="0">
<field name="revenue" type="measure"/>
<field name="product_id"/>
<field name="foo"/>
</graph>
`,
});
checkTooltip(
assert,
graph,
{
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(
assert,
graph,
{
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
);
}
);
QUnit.test("format total in hh:mm when measure is unit_amount", async function (assert) {
assert.expect(11);
serverData.models["account.analytic.line"] = {
fields: {
unit_amount: {
string: "Unit Amount",
type: "float",
group_operator: "sum",
store: true,
},
project_id: {
string: "Project",
type: "many2one",
relation: "project.project",
store: true,
sortable: true,
},
},
records: [{ id: 1, unit_amount: 8, project_id: false }],
};
const graph = await makeView({
serverData,
resModel: "account.analytic.line",
type: "graph",
arch: `
<graph>
<field name="unit_amount"/>
<field name="unit_amount" type="measure" widget="float_time"/>
</graph>`,
});
const { measure, fieldAttrs } = getGraphModelMetaData(graph);
assert.hasClass(target.querySelector(".o_graph_view"), "o_view_controller");
assert.containsOnce(target, "div.o_graph_canvas_container canvas");
assert.strictEqual(measure, "unit_amount", `the measure should be "unit_amount"`);
checkLegend(assert, graph, "Unit Amount");
checkLabels(assert, graph, ["Total"]);
assert.strictEqual(
fieldAttrs[measure].widget,
"float_time",
"should be a float_time widget"
);
checkYTicks(assert, graph, ["00:00", "02:00", "04:00", "06:00", "08:00"]);
checkTooltip(
assert,
graph,
{ title: "Unit Amount", lines: [{ label: "Total", value: "08:00" }] },
0
);
});
QUnit.test("Stacked button visible in the line chart", async function (assert) {
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="line">
<field name="bar"/>
<field name="product_id"/>
</graph>
`,
});
await selectMode(target, "line");
checkModeIs(assert, graph, "line");
assert.strictEqual(graph.model.metaData.stacked, true, "graph should be stacked.");
assert.strictEqual(
getScaleY(graph).stacked,
true,
"The y axes should have stacked property set to true"
);
assert.containsOnce(target, `button.o_graph_button[data-tooltip="Stacked"]`);
const stackButton = target.querySelector(`button.o_graph_button[data-tooltip="Stacked"]`);
await click(stackButton);
assert.strictEqual(
graph.model.metaData.stacked,
false,
"graph should be a classic line chart."
);
assert.strictEqual(
getScaleY(graph).stacked == undefined,
true,
"The y axes should have stacked property set to undefined"
);
});
QUnit.test("Stacked line prop click false", async function (assert) {
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="line">
<field name="bar"/>
<field name="product_id"/>
</graph>
`,
});
const stackButton = target.querySelector(`button.o_graph_button[data-tooltip="Stacked"]`);
await click(stackButton);
assert.strictEqual(
graph.model.metaData.stacked,
false,
"graph should be a classic line chart."
);
assert.strictEqual(
!!getScaleY(graph).stacked,
false,
"the y axes should have a stacked property set to false since the stacked property in line chart is false."
);
assert.strictEqual(
getGraphRenderer(graph).getElementOptions().line.fill,
false,
"The fill property should be false since the stacked property is false."
);
const expectedDatasets = [
{
backgroundColor: undefined,
borderColor: "#1f77b4",
originIndex: 0,
pointBackgroundColor: "#1f77b4",
},
{
backgroundColor: undefined,
borderColor: "#ff7f0e",
originIndex: 0,
pointBackgroundColor: "#ff7f0e",
},
];
const keysToEvaluate = [
"backgroundColor",
"borderColor",
"originIndex",
"pointBackgroundColor",
];
checkDatasets(assert, graph, keysToEvaluate, expectedDatasets);
});
QUnit.test("Stacked prop and default line chart", async function (assert) {
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="line">
<field name="bar"/>
<field name="product_id"/>
</graph>
`,
});
assert.strictEqual(graph.model.metaData.mode, "line", "should be in line chart mode.");
assert.strictEqual(graph.model.metaData.stacked, true, "should be stacked by default.");
assert.strictEqual(
getScaleY(graph).stacked,
true,
"the stacked property in y axes should be true when the stacked is enabled in line chart"
);
assert.strictEqual(
getGraphRenderer(graph).getElementOptions().line.fill,
true,
"The fill property should be true to add backgroundColor in line chart."
);
const expectedDatasets = [];
const keysToEvaluate = [
"backgroundColor",
"borderColor",
"originIndex",
"pointBackgroundColor",
];
const datasets = getChart(graph).data.datasets;
const colors = getColors();
for (let i = 0; i < datasets.length; i++) {
const expectedColor = colors[i];
expectedDatasets.push({
backgroundColor: hexToRGBA(expectedColor, 0.4),
borderColor: expectedColor,
originIndex: 0,
pointBackgroundColor: expectedColor,
});
}
checkDatasets(assert, graph, keysToEvaluate, expectedDatasets);
});
QUnit.test("Cumulative prop and default line chart", async function (assert) {
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="line" stacked="0">
<field name="bar"/>
<field name="product_id"/>
</graph>
`,
});
assert.strictEqual(graph.model.metaData.mode, "line", "should be in line chart mode.");
assert.strictEqual(
graph.model.metaData.cumulated,
false,
"should not be cumulative by default."
);
await click(target, '[data-tooltip="Cumulative"]');
assert.strictEqual(graph.model.metaData.cumulated, true, "should be in cumulative");
const expectedDatasets = [
{
data: [1, 4],
},
{
data: [4, 4],
},
];
checkDatasets(assert, graph, ["data"], expectedDatasets);
});
QUnit.test("Default cumulative prop", async function (assert) {
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="line" stacked="0" cumulated="1">
<field name="bar"/>
<field name="product_id"/>
</graph>
`,
});
assert.strictEqual(graph.model.metaData.mode, "line", "should be in line chart mode.");
assert.strictEqual(graph.model.metaData.cumulated, true, "should be in cumulative");
assert.strictEqual(
graph.model.metaData.cumulatedStart,
false,
"should have cumulated start opted-out"
);
});
QUnit.test("Cumulative prop and cumulated start", async function (assert) {
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="line" stacked="0" cumulated="1" cumulated_start="1">
<field name="date"/>
<field name="product_id"/>
</graph>
`,
searchViewArch: `
<search>
<filter name="filter_after_march"
string="After March 2016"
domain="[['date', '>=', '2016-03-01']]"
/>
</search>
`,
context: {
search_default_filter_after_march: 1,
},
});
assert.strictEqual(graph.model.metaData.mode, "line", "should be in line chart mode.");
assert.strictEqual(graph.model.metaData.cumulated, true, "should be in cumulative");
assert.strictEqual(
graph.model.metaData.cumulatedStart,
true,
"should have cumulated start opted-in"
);
const expectedDatasets = [
{
data: [4, 4, 4],
},
{
data: [0, 1, 2],
},
];
checkDatasets(assert, graph, ["data"], expectedDatasets);
});
QUnit.test("line chart rendering (no groupBy, several domains)", async function (assert) {
assert.expect(7);
const graph = await makeView({
serverData,
resModel: "foo",
type: "graph",
arch: `
<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(assert, graph, ["", "Total", ""]);
checkDatasets(
assert,
graph,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: "rgba(31,119,180,0.4)",
borderColor: "#1f77b4",
data: [undefined, 6],
label: "True group",
},
{
backgroundColor: undefined,
borderColor: "#ff7f0e",
data: [undefined, 17],
label: "False group",
},
]
);
checkLegend(assert, graph, ["True group", "False group"]);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [
{ label: "Total / False group", value: "17" },
{ label: "Total / True group", value: "6" },
],
},
1
);
});
QUnit.test("line chart rendering (one groupBy, several domains)", async function (assert) {
assert.expect(19);
serverData.models.foo.records = [
{ bar: true, foo: 1, revenue: 14 },
{ bar: true, foo: 2, revenue: false },
{ 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 graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<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(assert, graph, ["1", "2", "3", "4"]);
checkDatasets(
assert,
graph,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: "rgba(31,119,180,0.4)",
borderColor: "#1f77b4",
data: [14, 0, 0, 0],
label: "True group",
},
{
backgroundColor: undefined,
borderColor: "#ff7f0e",
data: [12, -4, 2, 0],
label: "False group",
},
]
);
checkLegend(assert, graph, ["True group", "False group"]);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [
{ label: "1 / True group", value: "14" },
{ label: "1 / False group", value: "12" },
],
},
0
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [
{ label: "2 / True group", value: "0" },
{ label: "2 / False group", value: "-4" },
],
},
1
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [
{ label: "3 / False group", value: "2" },
{ label: "3 / True group", value: "0" },
],
},
2
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [
{ label: "4 / True group", value: "0" },
{ label: "4 / False group", value: "0" },
],
},
3
);
});
QUnit.test(
"line chart rendering (one groupBy, several domains with date identification) without stacked attribute",
async function (assert) {
serverData.models.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: false },
{ date: false, revenue: 0 },
];
await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<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",
},
});
assert.doesNotHaveClass(
target.querySelector(".o_graph_button[data-tooltip=Stacked]"),
"active",
"The stacked mode should be disabled"
);
}
);
QUnit.test(
"line chart rendering (one groupBy, several domains with date identification)",
async function (assert) {
assert.expect(19);
serverData.models.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: false },
{ date: false, revenue: 0 },
];
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<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(assert, graph, ["W05 2021", "W07 2021", "", ""]);
checkDatasets(
assert,
graph,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: "rgba(31,119,180,0.4)",
borderColor: "#1f77b4",
data: [14, 0],
label: "February 2021",
},
{
backgroundColor: undefined,
borderColor: "#ff7f0e",
data: [12, 5, 15, 2],
label: "January 2021",
},
]
);
checkLegend(assert, graph, ["February 2021", "January 2021"]);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [
{ label: "W05 2021 / February 2021", value: "14" },
{ label: "W01 2021 / January 2021", value: "12" },
],
},
0
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [
{ label: "W02 2021 / January 2021", value: "5" },
{ label: "W07 2021 / February 2021", value: "0" },
],
},
1
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "W03 2021 / January 2021", value: "15" }],
},
2
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "W04 2021 / January 2021", value: "2" }],
},
3
);
}
);
QUnit.test(
"line chart rendering (two groupBy, several domains with no date identification)",
async function (assert) {
assert.expect(11);
serverData.models.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: false },
{ date: false, bar: false, revenue: 0 },
];
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<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(assert, graph, ["false", "true"]);
checkDatasets(
assert,
graph,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: undefined,
borderColor: "#1f77b4",
data: [14, 0],
label: "February 2021 / W05 2021",
},
{
backgroundColor: undefined,
borderColor: "#ff7f0e",
data: [0, 0],
label: "February 2021 / W07 2021",
},
{
backgroundColor: undefined,
borderColor: "#aec7e8",
data: [12, 0],
label: "January 2021 / W01 2021",
},
{
backgroundColor: undefined,
borderColor: "#ffbb78",
data: [0, 5],
label: "January 2021 / W02 2021",
},
]
);
checkLegend(assert, graph, [
"February 2021 / W05 2021",
"February 2021 / W07 2021",
"January 2021 / W01 2021",
"January 2021 / W02 2021",
]);
checkTooltip(
assert,
graph,
{
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(
assert,
graph,
{
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
);
}
);
QUnit.test("displaying line chart with only 1 data point", async function (assert) {
assert.expect(1);
// this test makes sure the line chart does not crash when only one data
// point is displayed.
serverData.models.foo.records = serverData.models.foo.records.slice(0, 1);
await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `<graph type="line" stacked="0"/>`,
});
assert.containsOnce(target, "canvas", "should have a canvas");
});
QUnit.test("pie chart rendering (no groupBy)", async function (assert) {
assert.expect(9);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `<graph type="pie"/>`,
});
assert.containsOnce(target, "div.o_graph_canvas_container canvas");
const { mode } = getGraphModelMetaData(graph);
assert.strictEqual(mode, "pie");
checkLabels(assert, graph, ["Total"]);
checkDatasets(assert, graph, ["backgroundColor", "borderColor", "data", "label", "stack"], {
backgroundColor: ["#1f77b4"],
borderColor: getBorderWhite(),
data: [8],
label: "",
stack: undefined,
});
checkLegend(assert, graph, "Total");
checkTooltip(assert, graph, { lines: [{ label: "Total", value: "8 (100.00%)" }] }, 0);
});
QUnit.test("pie chart rendering (one groupBy)", async function (assert) {
assert.expect(12);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="pie">
<field name="bar"/>
</graph>
`,
});
assert.containsOnce(target, "div.o_graph_canvas_container canvas");
checkLabels(assert, graph, ["false", "true"]);
checkDatasets(assert, graph, ["backgroundColor", "borderColor", "data"], {
backgroundColor: ["#1f77b4", "#ff7f0e"],
borderColor: getBorderWhite(),
data: [5, 3],
});
checkLegend(assert, graph, ["false", "true"]);
checkTooltip(assert, graph, { lines: [{ label: "false", value: "5 (62.50%)" }] }, 0);
checkTooltip(assert, graph, { lines: [{ label: "true", value: "3 (37.50%)" }] }, 1);
});
QUnit.test("pie chart many2many groupby", async function (assert) {
assert.expect(16);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="pie">
<field name="revenue" type="measure"/>
<field name="color_ids"/>
</graph>
`,
});
assert.containsOnce(target, "div.o_graph_canvas_container canvas");
checkLabels(assert, graph, ["None", "black", "red"]);
checkDatasets(assert, graph, ["backgroundColor", "borderColor", "data"], {
backgroundColor: ["#1f77b4", "#ff7f0e", "#aec7e8"],
borderColor: getBorderWhite(),
data: [8, 10, 13],
});
checkLegend(assert, graph, ["None", "black", "red"]);
checkTooltip(
assert,
graph,
{ lines: [{ label: "None", value: "8 (25.81%)" }], title: "Revenue" },
0
);
checkTooltip(
assert,
graph,
{ lines: [{ label: "black", value: "10 (32.26%)" }], title: "Revenue" },
1
);
checkTooltip(
assert,
graph,
{ lines: [{ label: "red", value: "13 (41.94%)" }], title: "Revenue" },
2
);
});
QUnit.test("pie chart rendering (two groupBy)", async function (assert) {
assert.expect(16);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="pie">
<field name="bar"/>
<field name="product_id"/>
</graph>
`,
});
assert.containsOnce(target, "div.o_graph_canvas_container canvas");
checkLabels(assert, graph, ["false / xphone", "false / xpad", "true / xphone"]);
checkDatasets(assert, graph, ["backgroundColor", "borderColor", "data", "label"], {
backgroundColor: ["#1f77b4", "#ff7f0e", "#aec7e8"],
borderColor: getBorderWhite(),
data: [1, 4, 3],
label: "",
});
checkLegend(assert, graph, ["false / xphone", "false / xpad", "true / xphone"]);
checkTooltip(
assert,
graph,
{ lines: [{ label: "false / xphone", value: "1 (12.50%)" }] },
0
);
checkTooltip(assert, graph, { lines: [{ label: "false / xpad", value: "4 (50.00%)" }] }, 1);
checkTooltip(
assert,
graph,
{ lines: [{ label: "true / xphone", value: "3 (37.50%)" }] },
2
);
});
QUnit.test("pie chart rendering (no groupBy, several domains)", async function (assert) {
assert.expect(11);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="pie">
<field name="revenue" type="measure"/>
</graph>
`,
comparison: {
domains: [
{ arrayRepr: [["bar", "=", true]], description: "True group" },
{ arrayRepr: [["bar", "=", false]], description: "False group" },
],
},
});
checkLabels(assert, graph, ["Total"]);
checkDatasets(
assert,
graph,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: ["#1f77b4"],
borderColor: getBorderWhite(),
data: [6],
label: "True group",
},
{
backgroundColor: ["#1f77b4"],
borderColor: getBorderWhite(),
data: [17],
label: "False group",
},
]
);
checkLegend(assert, graph, ["Total"]);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "True group / Total", value: "6 (100.00%)" }],
},
0,
0
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "False group / Total", value: "17 (100.00%)" }],
},
0,
1
);
});
QUnit.test("pie chart rendering (one groupBy, several domains)", async function (assert) {
assert.expect(19);
serverData.models.foo.records = [
{ bar: true, foo: 1, revenue: 14 },
{ bar: true, foo: 2, revenue: false },
{ 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 graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<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(assert, graph, ["1", "2", "4"]);
checkDatasets(
assert,
graph,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: ["#1f77b4", "#ff7f0e", "#aec7e8"],
borderColor: getBorderWhite(),
data: [14, 0, 0],
label: "True group",
},
{
backgroundColor: ["#1f77b4", "#ff7f0e", "#aec7e8"],
borderColor: getBorderWhite(),
data: [12, 5, 2],
label: "False group",
},
]
);
checkLegend(assert, graph, ["1", "2", "4"]);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "True group / 1", value: "14 (100.00%)" }],
},
0,
0
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "False group / 1", value: "12 (63.16%)" }],
},
0,
1
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "False group / 2", value: "5 (26.32%)" }],
},
1,
1
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "False group / 4", value: "2 (10.53%)" }],
},
2,
1
);
});
QUnit.test(
"pie chart rendering (one groupBy, several domains with date identification)",
async function (assert) {
assert.expect(27);
serverData.models.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 graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<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(assert, graph, [
"W05 2021, W01 2021",
"W07 2021, W02 2021",
"W03 2021",
"W04 2021",
]);
checkDatasets(
assert,
graph,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: ["#1f77b4", "#ff7f0e", "#aec7e8", "#ffbb78"],
borderColor: getBorderWhite(),
data: [1, 1, 0, 0],
label: "February 2021",
},
{
backgroundColor: ["#1f77b4", "#ff7f0e", "#aec7e8", "#ffbb78"],
borderColor: getBorderWhite(),
data: [1, 1, 1, 1],
label: "January 2021",
},
]
);
checkLegend(assert, graph, [
"W05 2021, W01 2021",
"W07 2021, W02 2021",
"W03 2021",
"W04 2021",
]);
checkTooltip(
assert,
graph,
{
lines: [{ label: "February 2021 / W05 2021", value: "1 (50.00%)" }],
},
0,
0
);
checkTooltip(
assert,
graph,
{
lines: [{ label: "January 2021 / W01 2021", value: "1 (25.00%)" }],
},
0,
1
);
checkTooltip(
assert,
graph,
{
lines: [{ label: "February 2021 / W07 2021", value: "1 (50.00%)" }],
},
1,
0
);
checkTooltip(
assert,
graph,
{
lines: [{ label: "January 2021 / W02 2021", value: "1 (25.00%)" }],
},
1,
1
);
checkTooltip(
assert,
graph,
{
lines: [{ label: "January 2021 / W03 2021", value: "1 (25.00%)" }],
},
2,
1
);
checkTooltip(
assert,
graph,
{
lines: [{ label: "January 2021 / W04 2021", value: "1 (25.00%)" }],
},
3,
1
);
}
);
QUnit.test(
"pie chart rendering (two groupBy, several domains with no date identification)",
async function (assert) {
assert.expect(15);
serverData.models.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: false },
{ date: false, bar: false, revenue: 0 },
];
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<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(assert, graph, ["false / W05 2021", "false / W01 2021", "true / W02 2021"]);
checkDatasets(
assert,
graph,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: ["#1f77b4", "#ff7f0e", "#aec7e8"],
borderColor: getBorderWhite(),
data: [14, 0, 0],
label: "February 2021",
},
{
backgroundColor: ["#1f77b4", "#ff7f0e", "#aec7e8"],
borderColor: getBorderWhite(),
data: [0, 12, 5],
label: "January 2021",
},
]
);
checkLegend(assert, graph, ["false / W05 2021", "false / W01 2021", "true / W02 2021"]);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "February 2021 / false / W05 2021", value: "14 (100.00%)" }],
},
0,
0
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "January 2021 / false / W01 2021", value: "12 (70.59%)" }],
},
1,
1
);
checkTooltip(
assert,
graph,
{
title: "Revenue",
lines: [{ label: "January 2021 / true / W02 2021", value: "5 (29.41%)" }],
},
2,
1
);
}
);
QUnit.test("pie chart rendering (no data)", async function (assert) {
assert.expect(7);
serverData.models.foo.records = [];
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `<graph type="pie"/>`,
});
checkLabels(assert, graph, ["No data"]);
checkDatasets(
assert,
graph,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: [DEFAULT_BG],
borderColor: getBorderWhite(),
data: [1],
label: null,
},
]
);
checkLegend(assert, graph, ["No data"]);
checkTooltip(assert, graph, { lines: [{ label: "No data", value: "0 (100.00%)" }] }, 0);
});
QUnit.test("pie chart rendering (no data, several domains)", async function (assert) {
assert.expect(11);
serverData.models.foo.records = [{ product_id: 37, bar: true }];
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="pie">
<field name="product_id"/>
</graph>
`,
comparison: {
domains: [
{ arrayRepr: [["bar", "=", true]], description: "True group" },
{ arrayRepr: [["bar", "=", false]], description: "False group" },
],
},
});
checkLabels(assert, graph, ["xphone", "No data"]);
checkDatasets(
assert,
graph,
["backgroundColor", "borderColor", "data", "label"],
[
{
backgroundColor: ["#1f77b4"],
borderColor: getBorderWhite(),
data: [1],
label: "True group",
},
{
backgroundColor: ["#1f77b4", DEFAULT_BG],
borderColor: getBorderWhite(),
data: [undefined, 1],
label: "False group",
},
]
);
checkLegend(assert, graph, ["xphone", "No data"]);
checkTooltip(
assert,
graph,
{ lines: [{ label: "True group / xphone", value: "1 (100.00%)" }] },
0,
0
);
checkTooltip(
assert,
graph,
{ lines: [{ label: "False group / No data", value: "0 (100.00%)" }] },
1,
1
);
});
QUnit.test(
"pie chart rendering (mix of positive and negative values)",
async function (assert) {
assert.expect(3);
serverData.models.foo.records = [
{ bar: true, revenue: 2 },
{ bar: false, revenue: -3 },
];
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="pie">
<field name="revenue" type="measure"/>
<field name="bar"/>
</graph>
`,
});
assert.containsNone(target, ".o_view_nocontent");
assert.containsOnce(target, ".o_graph_canvas_container");
checkDatasets(
assert,
graph,
["backgroundColor", "borderColor", "data", "label", "stack"],
{
backgroundColor: ["#1f77b4"],
borderColor: getBorderWhite(),
data: [2],
label: "",
stack: undefined,
}
);
}
);
QUnit.test("pie chart toggling dataset hides label", async function (assert) {
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `<graph type="pie"/>`,
});
checkLabels(assert, graph, ["Total"]);
await clickOnLegend(graph, "Total");
assert.ok(getChart(graph).legend.legendItems[0].hidden);
});
QUnit.test("mode props", async function (assert) {
assert.expect(2);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `<graph type="pie"/>`,
});
assert.strictEqual(getGraphModelMetaData(graph).mode, "pie", "should be in pie chart mode");
assert.strictEqual(getChart(graph).config.type, "pie");
});
QUnit.test("field id not in groupBy", async function (assert) {
assert.expect(3);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="id"/>
</graph>
`,
});
checkLabels(assert, graph, ["Total"]);
checkDatasets(assert, graph, ["backgroundColor", "data", "label", "originIndex", "stack"], {
backgroundColor: "#1f77b4",
data: [8],
label: "Count",
originIndex: 0,
stack: "",
});
checkLegend(assert, graph, "Count");
});
QUnit.test("props modifications", async function (assert) {
assert.expect(16);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="bar"/>
</graph>
`,
searchViewArch: `
<search>
<filter name="group_by_color" string="Color" context="{ 'group_by': 'color_id' }"/>
</search>
`,
});
checkModeIs(assert, graph, "bar");
assert.strictEqual(getXAxeLabel(graph), "bar");
assert.strictEqual(getYAxeLabel(graph), "Count");
await selectMode(target, "line");
checkModeIs(assert, graph, "line");
assert.strictEqual(getXAxeLabel(graph), "bar");
await toggleMenu(target, "Measures");
await toggleMenuItem(target, "Revenue");
assert.strictEqual(getYAxeLabel(graph), "Revenue");
assert.ok(true, "Message");
await toggleSearchBarMenu(target);
await toggleMenuItem(target, "Color");
checkModeIs(assert, graph, "line");
assert.strictEqual(getXAxeLabel(graph), "Color");
assert.strictEqual(getYAxeLabel(graph), "Revenue");
});
QUnit.test("switching mode", async function (assert) {
assert.expect(12);
const graph = await makeView({ serverData, type: "graph", resModel: "foo" });
checkModeIs(assert, graph, "bar");
await selectMode(target, "bar"); // click on the active mode does not change anything
checkModeIs(assert, graph, "bar");
await selectMode(target, "line");
checkModeIs(assert, graph, "line");
await selectMode(target, "pie");
checkModeIs(assert, graph, "pie");
});
QUnit.test("switching measure", async function (assert) {
assert.expect(6);
const graph = await makeView({ serverData, type: "graph", resModel: "foo" });
function checkMeasure(measure) {
const yAxe = getChart(graph).config.options.scales.y;
assert.strictEqual(yAxe.title.text, measure);
const item = [...target.querySelectorAll(".o_menu_item")].find(
(el) => el.innerText === measure
);
assert.hasClass(item, "selected");
}
await toggleMenu(target, "Measures");
checkMeasure("Count");
checkLegend(assert, graph, "Count");
await toggleMenuItem(target, "Foo");
checkMeasure("Foo");
checkLegend(assert, graph, "Foo");
});
QUnit.test("process default view description", async function (assert) {
assert.expect(1);
const propsFromArch = new GraphArchParser().parse();
assert.deepEqual(propsFromArch, { fields: {}, fieldAttrs: {}, groupBy: [], measures: [] });
});
QUnit.test("process simple arch (no field tag)", async function (assert) {
assert.expect(2);
const fields = serverData.models.foo.fields;
const arch1 = `<graph order="ASC" disable_linking="1" type="line"/>`;
let propsFromArch = new GraphArchParser().parse(arch1, fields);
assert.deepEqual(propsFromArch, {
disableLinking: true,
fields,
fieldAttrs: {},
groupBy: [],
measures: [],
mode: "line",
order: "ASC",
});
const arch2 = `<graph disable_linking="0" string="Title" stacked="False"/>`;
propsFromArch = new GraphArchParser().parse(arch2, fields);
assert.deepEqual(propsFromArch, {
disableLinking: false,
fields,
fieldAttrs: {},
groupBy: [],
measures: [],
stacked: false,
title: "Title",
});
});
QUnit.test("process arch with field tags", async function (assert) {
assert.expect(1);
const fields = serverData.models.foo.fields;
fields.fighters = { type: "text", string: "Fighters" };
const arch = `
<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>
`;
const propsFromArch = new GraphArchParser().parse(arch, fields);
assert.deepEqual(propsFromArch, {
fields,
fieldAttrs: {
bar: { isInvisible: true, string: "My invisible field" },
fighters: { string: "FooFighters" },
},
measure: "revenue",
measures: ["revenue"],
groupBy: ["date:day", "foo"],
mode: "pie",
});
});
QUnit.test("process arch with non stored field tags of type measure", async function (assert) {
assert.expect(1);
const fields = serverData.models.foo.fields;
fields.revenue.store = false;
const arch = `
<graph>
<field name="product_id"/>
<field name="revenue" type="measure"/>
<field name="foo" type="measure"/>
</graph>
`;
const propsFromArch = new GraphArchParser().parse(arch, fields);
assert.deepEqual(propsFromArch, {
fields,
fieldAttrs: {},
measure: "foo",
measures: ["revenue", "foo"],
groupBy: ["product_id"],
});
});
QUnit.test("displaying chart data with three groupbys", async function (assert) {
// this test makes sure the line chart shows all data labels (X axis) when
// it is grouped by several fields
assert.expect(6);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="bar">
<field name="product_id"/>
<field name="bar"/>
<field name="color_id"/>
</graph>
`,
});
checkLabels(assert, graph, ["xphone", "xpad"]);
checkLegend(assert, graph, ["false / None", "true / None", "true / red", "Sum"]);
await selectMode(target, "line");
checkLabels(assert, graph, ["xphone", "xpad"]);
checkLegend(assert, graph, ["false / None", "true / None", "true / red"]);
await selectMode(target, "pie");
checkLabels(assert, graph, [
"xphone / false / None",
"xphone / true / None",
"xphone / true / red",
"xpad / false / None",
]);
checkLegend(assert, graph, [
"xphone / false / None",
"xphone / true / None",
"xphone / true / red",
"xpad / false / None",
]);
});
QUnit.test("no content helper", async function (assert) {
assert.expect(3);
serverData.models.foo.records = [];
await makeView({
serverData,
type: "graph",
resModel: "foo",
noContentHelp: '<p class="abc">This helper should not be displayed in graph views</p>',
});
assert.containsOnce(target, "div.o_graph_canvas_container canvas");
assert.containsNone(target, "div.o_view_nocontent");
assert.containsNone(target, ".abc");
});
QUnit.test("no content helper after update", async function (assert) {
assert.expect(6);
await makeView({
serverData,
type: "graph",
resModel: "foo",
noContentHelp: '<p class="abc">This helper should not be displayed in graph views</p>',
config: {
views: [[false, "search"]],
},
});
assert.containsOnce(target, "div.o_graph_canvas_container canvas");
assert.containsNone(target, "div.o_view_nocontent");
assert.containsNone(target, ".abc");
await toggleSearchBarMenu(target);
await toggleMenuItem(target, "False Domain");
assert.containsOnce(target, "div.o_graph_canvas_container canvas");
assert.containsNone(target, "div.o_view_nocontent");
assert.containsNone(target, ".abc");
});
QUnit.test("can reload with other group by", async function (assert) {
assert.expect(2);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="product_id"/>
</graph>
`,
searchViewArch: `
<search>
<filter name="group_by_color" string="Color" context="{ 'group_by': 'color_id' }"/>
</search>
`,
});
checkLabels(assert, graph, ["xphone", "xpad"]);
await toggleSearchBarMenu(target);
await toggleMenuItem(target, "Color");
checkLabels(assert, graph, ["None", "red"]);
});
QUnit.test("save params succeeds", async function (assert) {
assert.expect(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;
await makeView({
mockRPC: function (_, args) {
if (args.method === "create_or_replace") {
const favorite = args.args[0];
assert.deepEqual(favorite.context, expectedContexts.shift());
return serverId++;
}
},
serverData,
resModel: "foo",
type: "graph",
arch: `
<graph>
<field name="product_id"/>
</graph>
`,
searchViewId: false,
searchViewArch: `
<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 toggleSearchBarMenu(target);
await toggleSaveFavorite(target);
await editFavoriteName(target, "First Favorite");
await saveFavorite(target);
await toggleMenu(target, "Measures");
await toggleMenuItem(target, "Foo");
await toggleSearchBarMenu(target);
await toggleSaveFavorite(target);
await editFavoriteName(target, "Second Favorite");
await saveFavorite(target);
await selectMode(target, "line");
await toggleSearchBarMenu(target);
await toggleSaveFavorite(target);
await editFavoriteName(target, "Third Favorite");
await saveFavorite(target);
await toggleMenuItem(target, "Product");
await toggleMenuItem(target, "Color");
await editFavoriteName(target, "Fourth Favorite");
await saveFavorite(target);
});
QUnit.test("correctly uses graph_ keys from the context", async function (assert) {
assert.expect(8);
const recs = serverData.models.foo.records;
const lastOne = recs[recs.length - 1];
lastOne.color_id = 14;
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: '<graph><field name="product_id"/></graph>',
context: {
graph_measure: "foo",
graph_mode: "line",
graph_groupbys: ["color_id"],
},
});
checkLabels(assert, graph, ["red", "black"]);
checkLegend(assert, graph, "Foo");
checkModeIs(assert, graph, "line");
assert.strictEqual(getXAxeLabel(graph), "Color");
assert.strictEqual(getYAxeLabel(graph), "Foo");
const { mode } = getGraphModelMetaData(graph);
assert.strictEqual(mode, "line");
});
QUnit.test("correctly use group_by key from the context", async function (assert) {
assert.expect(8);
const recs = serverData.models.foo.records;
const lastOne = recs[recs.length - 1];
lastOne.color_id = 14;
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="product_id"/>
</graph>
`,
searchViewArch: `
<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(assert, graph, ["red", "black"]);
checkLegend(assert, graph, "Foo");
checkModeIs(assert, graph, "line");
assert.strictEqual(getXAxeLabel(graph), "Color");
assert.strictEqual(getYAxeLabel(graph), "Foo");
const mode = getGraphModelMetaData(graph).mode;
assert.strictEqual(mode, "line");
});
QUnit.test("an invisible field should not be used as groupBy", async function (assert) {
assert.expect(1);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="foo" invisible="1"/>
</graph>
`,
});
checkLabels(assert, graph, ["Total"]);
});
QUnit.test(
"format values as float in case at least one value is not an integer",
async function (assert) {
assert.expect(10);
serverData.models.foo.records = [
{ id: 1, bar: false, revenue: 1.5 },
{ id: 2, bar: true, revenue: 2 },
];
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="revenue" type="measure"/>
<field name="bar"/>
</graph>
`,
});
checkDatasets(assert, graph, "data", { data: [1.5, 2] });
checkLabels(assert, graph, ["false", "true"]);
checkTooltip(
assert,
graph,
{ title: "Revenue", lines: [{ label: "false", value: "1.50" }] },
0
);
checkTooltip(
assert,
graph,
{ title: "Revenue", lines: [{ label: "true", value: "2.00" }] },
1
);
}
);
QUnit.test(
"the active measure description is the arch string attribute in priority",
async function (assert) {
assert.expect(8);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="revenue" type="measure" string="Nirvana"/>
<field name="foo" type="measure" string="FooFighters"/>
</graph>
`,
});
checkTooltip(
assert,
graph,
{ title: "FooFighters", lines: [{ label: "Total", value: "239" }] },
0
);
await toggleMenu(target, "Measures");
await toggleMenuItem(target, "Nirvana");
checkTooltip(
assert,
graph,
{ title: "Nirvana", lines: [{ label: "Total", value: "23" }] },
0
);
}
);
QUnit.test("correctly uses graph_ keys from the context (at reload)", async function (assert) {
assert.expect(10);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: '<graph><field name="product_id"/></graph>',
searchViewArch: `
<search>
<filter name="context" domain="[]" string="Context" context="{ 'graph_measure': 'foo', 'graph_mode': 'line' }"/>
</search>
`,
});
checkLegend(assert, graph, "Count");
assert.strictEqual(getYAxeLabel(graph), "Count");
checkModeIs(assert, graph, "bar");
await toggleSearchBarMenu(target);
await toggleMenuItem(target, "Context");
checkLegend(assert, graph, "Foo");
assert.strictEqual(getYAxeLabel(graph), "Foo");
checkModeIs(assert, graph, "line");
});
QUnit.test("reload graph with correct fields", async function (assert) {
assert.expect(2);
await makeView({
serverData,
mockRPC: function (_, args) {
if (args.method === "web_read_group") {
assert.deepEqual(args.kwargs.fields, ["__count", "foo:sum"]);
}
},
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="foo" type="measure"/>
</graph>
`,
searchViewArch: `
<search>
<filter name="false_domain" string="False Domain" domain="[(0, '=', 1)]"/>
</search>
`,
});
await toggleSearchBarMenu(target);
await toggleMenuItem(target, "False Domain");
});
QUnit.test("initial groupby is kept when reloading", async function (assert) {
assert.expect(12);
const graph = await makeView({
serverData,
mockRPC: function (_, args) {
if (args.method === "web_read_group") {
assert.deepEqual(args.kwargs.groupby, ["product_id"]);
}
},
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="product_id"/>
<field name="foo" type="measure"/>
</graph>
`,
searchViewArch: `
<search>
<filter name="false_domain" string="False Domain" domain="[(0, '=', 1)]"/>
</search>
`,
});
checkLabels(assert, graph, ["xphone", "xpad"]);
checkLegend(assert, graph, "Foo");
checkDatasets(assert, graph, "data", { data: [82, 157] });
assert.strictEqual(getXAxeLabel(graph), "Product");
assert.strictEqual(getYAxeLabel(graph), "Foo");
await toggleSearchBarMenu(target);
await toggleMenuItem(target, "False Domain");
checkLabels(assert, graph, []);
checkLegend(assert, graph, []);
checkDatasets(assert, graph, "data", []);
assert.strictEqual(getXAxeLabel(graph), "Product");
assert.strictEqual(getYAxeLabel(graph), "Foo");
});
QUnit.test(
"use a many2one as a measure should work (without groupBy)",
async function (assert) {
assert.expect(5);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="product_id" type="measure"/>
</graph>
`,
});
checkLabels(assert, graph, ["Total"]);
checkLegend(assert, graph, "Product");
checkDatasets(assert, graph, "data", { data: [2] });
assert.strictEqual(getXAxeLabel(graph), "");
assert.strictEqual(getYAxeLabel(graph), "Product");
}
);
QUnit.test("use a many2one as a measure should work (with groupBy)", async function (assert) {
assert.expect(3);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="bar"/>
<field name="product_id" type="measure"/>
</graph>
`,
});
checkLabels(assert, graph, ["false", "true"]);
checkLegend(assert, graph, "Product");
checkDatasets(assert, graph, "data", { data: [2, 1] });
});
QUnit.test("use a many2one as a measure and as a groupby should work", async function (assert) {
assert.expect(5);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="product_id" type="measure"/>
<field name="product_id"/>
</graph>
`,
});
checkLabels(assert, graph, ["xphone", "xpad"]);
checkLegend(assert, graph, "Product");
checkDatasets(assert, graph, "data", { data: [1, 1] });
assert.strictEqual(getXAxeLabel(graph), "Product");
assert.strictEqual(getYAxeLabel(graph), "Product");
});
QUnit.test("differentiate many2one values with same label", async function (assert) {
assert.expect(1);
serverData.models.product.records.push({ id: 39, display_name: "xphone" });
serverData.models.foo.records.push({ id: 18, product_id: 39 });
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="product_id"/>
</graph>
`,
});
checkLabels(assert, graph, ["xphone", "xphone (2)", "xpad"]);
});
QUnit.test("not use a many2one as a measure by default", async function (assert) {
assert.expect(1);
await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: "<graph/>",
});
await toggleMenu(target, "Measures");
assert.deepEqual(
[...target.querySelectorAll(".o-dropdown .o_menu_item")].map((el) => el.innerText),
["Foo", "Revenue", "Count"]
);
});
QUnit.test(
"graph view crash when moving from search view using Down key",
async function (assert) {
assert.expect(1);
await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `<graph/>`,
});
await triggerEvent(target, ".o_searchview input", "keydown", { key: "ArrowDown" });
assert.ok(true, "should not generate any error");
}
);
QUnit.test(
"graph measures should be alphabetically sorted (exception: 'Count' is last)",
async function (assert) {
assert.expect(1);
serverData.models.foo.fields.bouh = {
string: "Bouh",
type: "integer",
store: true,
group_operator: "sum",
};
await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="foo" type="measure"/>
<field name="bouh" type="measure"/>
</graph>
`,
});
await toggleMenu(target, "Measures");
assert.deepEqual(
[...target.querySelectorAll(".o-dropdown .o_menu_item")].map((el) => el.innerText),
["Bouh", "Foo", "Revenue", "Count"]
);
}
);
QUnit.test("a many2one field can be added as measure in arch", async function (assert) {
assert.expect(2);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="product_id" type="measure"/>
</graph>
`,
});
checkLegend(assert, graph, "Product");
assert.strictEqual(getYAxeLabel(graph), "Product");
});
QUnit.test(
"non store fields defined on the arch are present in the measures",
async function (assert) {
serverData.models.foo.fields.revenue.store = false;
await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `<graph>
<field name="product_id"/>
<field name="revenue" type="measure"/>
<field name="foo" type="measure"/>
</graph>`,
});
await toggleMenu(target, "Measures");
assert.deepEqual(
Array.from(target.querySelectorAll(".o_menu_item")).map((e) => e.innerText.trim()),
["Foo", "Revenue", "Count"]
);
}
);
QUnit.test('graph view "graph_measure" field in context', async function (assert) {
assert.expect(6);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: "<graph/>",
context: {
graph_measure: "product_id",
},
});
checkLegend(assert, graph, "Product");
assert.strictEqual(getYAxeLabel(graph), "Product");
checkTooltip(
assert,
graph,
{ title: "Product", lines: [{ label: "Total", value: "2" }] },
0
);
});
QUnit.test(
'"graph_measure" in context is prefered to measure in arch',
async function (assert) {
assert.expect(6);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: '<graph><field name="revenue" type="measure"/></graph>',
context: {
graph_measure: "product_id",
},
});
checkLegend(assert, graph, "Product");
assert.strictEqual(getYAxeLabel(graph), "Product");
checkTooltip(
assert,
graph,
{ title: "Product", lines: [{ label: "Total", value: "2" }] },
0
);
}
);
QUnit.test(
"None should appear in bar, pie graph but not in line graph with multiple groupbys",
async function (assert) {
assert.expect(4);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="line">
<field name="date"/>
<field name="color_id"/>
</graph>
`,
});
function someNone() {
return getChart(graph).data.labels.some((l) => /None/.test(l));
}
assert.notOk(someNone());
await selectMode(target, "bar");
assert.ok(someNone());
await selectMode(target, "pie");
assert.ok(someNone());
// None should not appear after switching back to line chart
await selectMode(target, "line");
assert.notOk(someNone());
}
);
QUnit.test(
"an invisible field can not be found in the 'Measures' menu",
async function (assert) {
assert.expect(5);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="revenue" invisible="1"/>
</graph>
`,
});
checkTooltip(assert, graph, { lines: [{ label: "Total", value: "8" }] }, 0);
await toggleMenu(target, "Measures");
assert.notOk(
[...target.querySelectorAll(".o_menu_item")].find(
(el) => el.innerText.trim() === "Revenue"
),
`"Revenue" can not be found in the "Measures" menu`
);
}
);
QUnit.test(
"graph view only keeps finer groupby filter option for a given groupby",
async function (assert) {
assert.expect(3);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
groupBy: ["date:year", "product_id", "date", "date:quarter"],
arch: `<graph type="line"/>`,
config: {
views: [[false, "search"]],
},
});
checkLabels(assert, graph, ["January 2016", "March 2016", "May 2016", "April 2016"]);
// mockReadGroup does not always sort groups -> May 2016 is before April 2016 for that reason.
checkLegend(assert, graph, ["xphone", "xpad"]);
checkDatasets(
assert,
graph,
["label", "data"],
[
{
label: "xphone",
data: [2, 2, 0, 0],
},
{
label: "xpad",
data: [0, 0, 1, 1],
},
]
);
}
);
QUnit.test("action name is displayed in breadcrumbs", async function (assert) {
const target = getFixture();
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
name: "Glou glou",
res_model: "foo",
type: "ir.actions.act_window",
views: [[false, "graph"]],
});
assert.strictEqual(
target.querySelector(".o_control_panel .o_breadcrumb .active:first-child").innerText,
"Glou glou"
);
});
QUnit.test("clicking on bar charts triggers a do_action", async function (assert) {
assert.expect(6);
serviceRegistry.add(
"action",
{
start() {
return {
doAction(actionRequest, options) {
assert.deepEqual(actionRequest, {
context: {
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"],
],
});
assert.deepEqual(options, { viewType: "list" });
},
};
},
},
{ force: true }
);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph string="Foo Analysis">
<field name="bar"/>
</graph>
`,
});
checkModeIs(assert, graph, "bar");
checkDatasets(assert, graph, ["domains"], {
domains: [[["bar", "=", false]], [["bar", "=", true]]],
});
await clickOnDataset(graph);
});
QUnit.test(
"Clicking on bar charts removes group_by and search_default_* context keys",
async function (assert) {
assert.expect(2);
serviceRegistry.add(
"action",
{
start() {
return {
doAction(actionRequest, options) {
assert.deepEqual(actionRequest, {
context: {
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"],
],
});
assert.deepEqual(options, { viewType: "list" });
},
};
},
},
{ force: true }
);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph string="Foo Analysis">
<field name="bar"/>
</graph>
`,
context: {
search_default_user: 1,
group_by: "bar",
},
});
await clickOnDataset(graph);
}
);
QUnit.test(
"clicking on a pie chart trigger a do_action with correct views",
async function (assert) {
assert.expect(6);
serverData.views["foo,364,list"] = `<list/>`;
serverData.views["foo,29,form"] = `<form/>`;
serviceRegistry.add(
"action",
{
start() {
return {
doAction(actionRequest, options) {
assert.deepEqual(actionRequest, {
context: {
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"],
],
});
assert.deepEqual(options, { viewType: "list" });
},
};
},
},
{ force: true }
);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph string="Foo Analysis" type="pie">
<field name="bar"/>
</graph>
`,
config: {
views: [
[364, "list"],
[29, "form"],
],
},
});
checkModeIs(assert, graph, "pie");
checkDatasets(assert, graph, ["domains"], {
domains: [[["bar", "=", false]], [["bar", "=", true]]],
});
await clickOnDataset(graph);
}
);
QUnit.test('graph view with attribute disable_linking="1"', async function (assert) {
assert.expect(4);
serviceRegistry.add(
"action",
{
start() {
return {
doAction() {
throw new Error("Should not perform a do_action");
},
};
},
},
{ force: true }
);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph disable_linking="1">
<field name="bar"/>
</graph>
`,
});
checkModeIs(assert, graph, "bar");
checkDatasets(assert, graph, ["domains"], {
domains: [[["bar", "=", false]], [["bar", "=", true]]],
});
await clickOnDataset(graph);
});
QUnit.test("graph view without invisible attribute on field", async function (assert) {
assert.expect(4);
await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `<graph/>`,
});
await toggleMenu(target, "Measures");
assert.containsN(
target,
".o_menu_item",
3,
"there should be three menu item in the measures dropdown (count, revenue and foo)"
);
assert.containsOnce(target, '.o_menu_item:contains("Revenue")');
assert.containsOnce(target, '.o_menu_item:contains("Foo")');
assert.containsOnce(target, '.o_menu_item:contains("Count")');
});
QUnit.test("graph view with invisible attribute on field", async function (assert) {
assert.expect(2);
await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="revenue" invisible="1"/>
</graph>
`,
});
await toggleMenu(target, "Measures");
assert.containsN(
target,
".o_menu_item",
2,
"there should be only two menu item in the measures dropdown (count and foo)"
);
assert.containsNone(target, '.o_menu_item:contains("Revenue")');
});
QUnit.test("graph view sort by measure", async function (assert) {
assert.expect(20);
// change first record from foo as there are 4 records count for each product
serverData.models.product.records.push({ id: 38, display_name: "zphone" });
serverData.models.foo.records[7].product_id = 38;
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph order="DESC">
<field name="product_id"/>
</graph>
`,
});
assert.containsOnce(target, "button.fa-sort-amount-asc");
assert.containsOnce(target, "button.fa-sort-amount-desc");
checkLegend(assert, graph, "Count", "measure should be by count");
assert.hasClass(
target.querySelector("button.fa-sort-amount-desc"),
"active",
'sorting should be applie on descending order by default when sorting="desc"'
);
checkDatasets(assert, graph, "data", { data: [4, 3, 1] });
await click(target, "button.fa-sort-amount-asc");
assert.hasClass(
target.querySelector("button.fa-sort-amount-asc"),
"active",
"ascending order should be applied"
);
checkDatasets(assert, graph, "data", { data: [1, 3, 4] });
await click(target, "button.fa-sort-amount-desc");
assert.hasClass(
target.querySelector("button.fa-sort-amount-desc"),
"active",
"descending order button should be active"
);
checkDatasets(assert, graph, "data", { data: [4, 3, 1] });
// again click on descending button to deactivate order button
await click(target, "button.fa-sort-amount-desc");
assert.doesNotHaveClass(
target.querySelector("button.fa-sort-amount-desc"),
"active",
"descending order button should not be active"
);
checkDatasets(assert, graph, "data", { data: [4, 1, 3] });
// set line mode
await selectMode(target, "line");
assert.containsOnce(target, "button.fa-sort-amount-asc");
assert.containsOnce(target, "button.fa-sort-amount-desc");
checkLegend(assert, graph, "Count", "measure should be by count");
assert.doesNotHaveClass(
target.querySelector("button.fa-sort-amount-desc"),
"active",
"descending order should be applied"
);
checkDatasets(assert, graph, "data", { data: [4, 1, 3] });
await click(target, "button.fa-sort-amount-asc");
assert.hasClass(
target.querySelector("button.fa-sort-amount-asc"),
"active",
"ascending order button should be active"
);
checkDatasets(assert, graph, "data", { data: [1, 3, 4] });
await click(target, "button.fa-sort-amount-desc");
assert.hasClass(
target.querySelector("button.fa-sort-amount-desc"),
"active",
"descending order button should be active"
);
checkDatasets(assert, graph, "data", { data: [4, 3, 1] });
});
QUnit.test("graph view sort by measure for grouped data", async function (assert) {
assert.expect(8);
// change first record from foo as there are 4 records count for each product
serverData.models.product.records.push({ id: 38, display_name: "zphone" });
serverData.models.foo.records[7].product_id = 38;
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="product_id"/>
<field name="bar"/>
</graph>
`,
});
checkLegend(assert, graph, ["false", "true", "Sum"], "measure should be by count");
checkDatasets(assert, graph, "data", [
{ data: [1, 1, 3] },
{ data: [3, 0, 0] },
{ data: [4, 1, 3] },
]);
await click(target, "button.fa-sort-amount-asc");
assert.hasClass(
target.querySelector("button.fa-sort-amount-asc"),
"active",
"ascending order should be applied by default"
);
checkDatasets(assert, graph, "data", [
{ data: [1, 3, 1] },
{ data: [0, 0, 3] },
{ data: [1, 3, 4] },
]);
await click(target, "button.fa-sort-amount-desc");
assert.hasClass(
target.querySelector("button.fa-sort-amount-desc"),
"active",
"ascending order button should be active"
);
checkDatasets(assert, graph, "data", [
{ data: [1, 3, 1] },
{ data: [3, 0, 0] },
{ data: [4, 3, 1] },
]);
// again click on descending button to deactivate order button
await click(target, "button.fa-sort-amount-desc");
assert.doesNotHaveClass(
target.querySelector("button.fa-sort-amount-desc"),
"active",
"descending order button should not be active"
);
checkDatasets(assert, graph, "data", [
{ data: [1, 1, 3] },
{ data: [3, 0, 0] },
{ data: [4, 1, 3] },
]);
});
QUnit.test("graph view sort by measure for multiple grouped data", async function (assert) {
assert.expect(8);
// change first record from foo as there are 4 records count for each product
serverData.models.product.records.push({ id: 38, display_name: "zphone" });
serverData.models.foo.records[7].product_id = 38;
serverData.models.foo.records.splice(
0,
4,
{ id: 9, foo: 48, bar: false, product_id: 41, date: "2016-04-01" },
{ id: 10, foo: 49, bar: false, product_id: 41, date: "2016-04-01" },
{ id: 11, foo: 50, bar: true, product_id: 37, date: "2016-01-03" },
{ id: 12, foo: 50, bar: true, product_id: 41, date: "2016-01-03" }
);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="date"/>
<field name="product_id"/>
</graph>
`,
});
checkLegend(
assert,
graph,
["xphone", "xpad", "zphone", "Sum"],
"measure should be by count"
);
checkDatasets(assert, graph, "data", [
{ data: [1, 0, 0, 0] },
{ data: [1, 2, 1, 2] },
{ data: [0, 1, 0, 0] },
{ data: [2, 3, 1, 2] },
]);
await click(target, "button.fa-sort-amount-asc");
assert.hasClass(
target.querySelector("button.fa-sort-amount-asc"),
"active",
"ascending order should be applied by default"
);
checkDatasets(assert, graph, "data", [
{ data: [1, 1, 2, 2] },
{ data: [0, 1, 0, 0] },
{ data: [0, 0, 0, 1] },
{ data: [1, 2, 2, 3] },
]);
await click(target, "button.fa-sort-amount-desc");
assert.hasClass(
target.querySelector("button.fa-sort-amount-desc"),
"active",
"descending order button should be active"
);
checkDatasets(assert, graph, "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 button
await click(target, "button.fa-sort-amount-desc");
assert.doesNotHaveClass(
target.querySelector("button.fa-sort-amount-desc"),
"active",
"descending order button should not be active"
);
checkDatasets(assert, graph, "data", [
{ data: [1, 0, 0, 0] },
{ data: [1, 2, 1, 2] },
{ data: [0, 1, 0, 0] },
{ data: [2, 3, 1, 2] },
]);
});
QUnit.test("empty graph view with sample data", async function (assert) {
await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph sample="1">
<field name="product_id"/>
<field name="date"/>
</graph>
`,
context: { search_default_false_domain: 1 },
searchViewArch: `
<search>
<filter name="false_domain" string="False Domain" domain="[(0, '=', 1)]"/>
</search>
`,
noContentHelp: '<p class="abc">click to add a foo</p>',
});
assert.hasClass(target.querySelector(".o_graph_view .o_content"), "o_view_sample_data");
assert.containsOnce(target, ".o_view_nocontent");
assert.containsOnce(target, ".o_graph_canvas_container canvas");
await toggleSearchBarMenu(target);
await toggleMenuItem(target, "False Domain");
assert.doesNotHaveClass(
target.querySelector(".o_graph_view .o_content"),
"o_view_sample_data"
);
assert.containsNone(target, ".o_view_nocontent");
assert.containsOnce(target, ".o_graph_canvas_container canvas");
});
QUnit.test("non empty graph view with sample data", async function (assert) {
await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph sample="1">
<field name="product_id"/>
<field name="date"/>
</graph>
`,
searchViewArch: `
<search>
<filter name="false_domain" string="False Domain" domain="[(0, '=', 1)]"/>
</search>
`,
noContentHelp: '<p class="abc">click to add a foo</p>',
});
assert.doesNotHaveClass(target, "o_view_sample_data");
assert.containsNone(target, ".o_view_nocontent");
assert.containsOnce(target, ".o_graph_canvas_container canvas");
await toggleSearchBarMenu(target);
await toggleMenuItem(target, "False Domain");
assert.doesNotHaveClass(target, "o_view_sample_data");
assert.containsOnce(target, ".o_graph_canvas_container canvas");
assert.containsOnce(target, ".o_view_nocontent");
});
QUnit.test("empty graph view without sample data after filter", async function (assert) {
await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="date"/>
</graph>
`,
domain: Domain.FALSE.toList(),
noContentHelp: '<p class="abc">click to add a foo</p>',
});
assert.containsOnce(target, ".o_graph_canvas_container canvas");
assert.containsOnce(target, ".o_view_nocontent");
});
QUnit.test("reload chart with switchView button keep internal state", async function (assert) {
assert.expect(3);
serverData.views["foo,false,list"] = `<list/>`;
const target = getFixture();
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
name: "Foo Action 1",
res_model: "foo",
type: "ir.actions.act_window",
views: [
[false, "graph"],
[false, "list"],
],
});
assert.hasClass(getModeButton(target, "bar"), "active");
await selectMode(target, "line");
assert.hasClass(getModeButton(target, "line"), "active");
await switchView(target, "graph");
assert.hasClass(getModeButton(target, "line"), "active");
});
QUnit.test(
"fallback on initial groupby when the groupby from control panel has 0 length",
async function (assert) {
assert.expect(2);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="line">
<field name="product_id"/>
</graph>
`,
searchViewArch: `
<search>
<filter name="group_by_foo" string="Foo" domain="[]" context="{ 'group_by': 'foo'}"/>
</search>
`,
context: {
search_default_group_by_foo: 1,
},
});
checkLabels(assert, graph, ["2", "3", "4", "24", "42", "48", "53", "63"]);
await toggleSearchBarMenu(target);
await toggleMenuItem(target, "Foo");
checkLabels(assert, graph, ["xphone", "xpad"]);
}
);
QUnit.test(
"change mode, stacked, or order via the graph buttons does not reload datapoints, change measure does",
async function (assert) {
assert.expect(13);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="line">
<field name="product_id"/>
</graph>
`,
mockRPC: function (_, args) {
if (args.method === "web_read_group") {
assert.step(JSON.stringify(args.kwargs.fields));
}
},
});
checkModeIs(assert, graph, "line");
await selectMode(target, "bar");
checkModeIs(assert, graph, "bar");
assert.hasClass(target.querySelector(`[data-tooltip="Stacked"]`), "active");
await click(target.querySelector(`[data-tooltip="Stacked"]`));
assert.doesNotHaveClass(target.querySelector(`[data-tooltip="Stacked"]`), "active");
assert.doesNotHaveClass(target.querySelector(`[data-tooltip="Ascending"]`), "active");
await click(target.querySelector(`[data-tooltip="Ascending"]`));
assert.hasClass(target.querySelector(`[data-tooltip="Ascending"]`), "active");
await toggleMenu(target, "Measures");
await toggleMenuItem(target, "Foo");
assert.verifySteps([
`["__count"]`, // first load
`["__count","foo:sum"]`, // reload due to change in measure
]);
}
);
QUnit.test(
"concurrent reloads: add a filter, and directly toggle a measure",
async function (assert) {
let def;
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="line">
<field name="product_id"/>
</graph>`,
searchViewArch: `
<search>
<filter name="my_filter" string="My Filter" domain="[('id', '&lt;', 6)]"/>
</search>`,
mockRPC: function (route, args) {
if (args.method === "web_read_group") {
return Promise.resolve(def);
}
},
});
checkDatasets(assert, graph, ["data", "label"], {
data: [4, 4],
label: "Count",
});
// Set a domain (this reload is delayed)
def = makeDeferred();
await toggleSearchBarMenu(target);
await toggleMenuItem(target, "My Filter");
checkDatasets(assert, graph, ["data", "label"], {
data: [4, 4],
label: "Count",
});
// Toggle a measure
await toggleMenu(target, "Measures");
await toggleMenuItem(target, "Foo");
checkDatasets(assert, graph, ["data", "label"], {
data: [4, 4],
label: "Count",
});
def.resolve();
await nextTick();
checkDatasets(assert, graph, ["data", "label"], {
data: [82, 4],
label: "Foo",
});
}
);
QUnit.test("change graph mode while loading a filter", async function (assert) {
let def;
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph type="line">
<field name="product_id"/>
</graph>`,
searchViewArch: `
<search>
<filter name="my_filter" string="My Filter" domain="[('id', '&lt;', 2)]"/>
</search>`,
mockRPC: function (route, args) {
if (args.method === "web_read_group") {
return Promise.resolve(def);
}
},
});
checkDatasets(assert, graph, ["data", "label"], {
data: [4, 4],
label: "Count",
});
checkModeIs(assert, graph, "line");
// Set a domain (this reload is delayed)
def = makeDeferred();
await toggleSearchBarMenu(target);
await toggleMenuItem(target, "My Filter");
checkDatasets(assert, graph, ["data", "label"], {
data: [4, 4],
label: "Count",
});
checkModeIs(assert, graph, "line");
// Change graph mode
await selectMode(target, "bar");
checkDatasets(assert, graph, ["data", "label"], {
data: [4, 4],
label: "Count",
});
checkModeIs(assert, graph, "line");
def.resolve();
await nextTick();
checkDatasets(assert, graph, ["data", "label"], {
data: [1],
label: "Count",
});
checkModeIs(assert, graph, "bar");
});
QUnit.test("only process most recent data for concurrent groupby", async function (assert) {
assert.expect(6);
let def;
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph>
<field name="product_id" type="row"/>
<field name="foo" type="measure"/>
</graph>
`,
searchViewArch: `
<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>
`,
mockRPC() {
return Promise.resolve(def);
},
});
checkLabels(assert, graph, ["xphone", "xpad"]);
checkDatasets(assert, graph, "data", { data: [82, 157] });
def = makeDeferred();
await toggleSearchBarMenu(target);
await toggleMenuItem(target, "Color");
await toggleMenuItem(target, "Color");
await toggleMenuItem(target, "Date");
await toggleMenuItemOption(target, "Date", "Month");
checkLabels(assert, graph, ["xphone", "xpad"]);
checkDatasets(assert, graph, "data", { data: [82, 157] });
def.resolve();
await nextTick();
checkLabels(assert, graph, [
"January 2016",
"March 2016",
"May 2016",
"None",
"April 2016",
]);
checkDatasets(assert, graph, "data", { data: [56, 26, 4, 105, 48] });
});
QUnit.test("fill_temporal is true by default", async function (assert) {
assert.expect(1);
await makeView({
serverData,
type: "graph",
resModel: "foo",
mockRPC: function (route, args) {
if (args.method === "web_read_group") {
assert.strictEqual(
args.kwargs.context.fill_temporal,
true,
"The observal state of fill_temporal should be true"
);
}
},
});
});
QUnit.test("fill_temporal can be changed throught the context", async function (assert) {
assert.expect(1);
await makeView({
serverData,
type: "graph",
resModel: "foo",
context: { fill_temporal: false },
mockRPC: function (route, args) {
if (args.method === "web_read_group") {
assert.strictEqual(
args.kwargs.context.fill_temporal,
false,
"The observal state of fill_temporal should be false"
);
}
},
});
});
QUnit.test("fake data in line chart", async function (assert) {
assert.expect(1);
patchDate(2020, 4, 19, 1, 0, 0);
serverData.models.foo.records = [];
const graph = await makeView({
type: "graph",
resModel: "foo",
serverData,
context: { search_default_date_filter: 1 },
arch: `
<graph type="line">
<field name="date"/>
</graph>
`,
searchViewArch: `
<search>
<filter name="date_filter" domain="[]" date="date" default_period="third_quarter"/>
</search>
`,
});
await toggleSearchBarMenu(target);
await toggleMenuItem(target, "Date: Previous period");
checkLabels(assert, graph, ["", ""]);
});
QUnit.test("no filling color for period of comparison", async function (assert) {
assert.expect(1);
patchDate(2020, 4, 19, 1, 0, 0);
serverData.models.foo.records.forEach((r) => {
if (r.date) {
r.date = r.date.replace(/\d\d\d\d/, "2019");
}
});
const graph = await makeView({
type: "graph",
resModel: "foo",
serverData,
context: { search_default_date_filter: 1 },
arch: `
<graph type="line" stacked="0">
<field name="product_id"/>
</graph>
`,
searchViewArch: `
<search>
<filter name="date_filter" domain="[]" date="date" default_period="this_year"/>
</search>
`,
});
await toggleSearchBarMenu(target);
await toggleMenuItem(target, "Date: Previous period");
checkDatasets(assert, graph, "backgroundColor", {
backgroundColor: undefined,
});
});
QUnit.test("group by a non stored, sortable field", async function (assert) {
assert.expect(1);
// When a field is non-stored but sortable it's inherited
// from a stored field, so it can be sortable
serverData.models.foo.fields.date.store = false;
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
groupBy: ["date:month"],
arch: `<graph type="line"/>`,
config: {
views: [[false, "search"]],
},
});
checkLabels(assert, graph, ["January 2016", "March 2016", "May 2016", "April 2016"]);
});
QUnit.test("graph_groupbys should be also used after first load", async function (assert) {
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
groupBy: ["date:quarter"],
arch: `<graph/>`,
irFilters: [
{
user_id: [2, "Mitchell Admin"],
name: "Favorite",
id: 1,
context: `{
"group_by": [],
"graph_measure": "revenue",
"graph_mode": "bar",
"graph_groupbys": ["color_id"],
}`,
sort: "[]",
domain: "",
is_default: false,
model_id: "foo",
action_id: false,
},
],
});
checkModeIs(assert, graph, "bar");
checkLabels(assert, graph, ["Q1 2016", "Q2 2016", "None"]);
checkLegend(assert, graph, "Count");
await toggleSearchBarMenu(target);
await toggleMenuItem(target, "Favorite");
checkModeIs(assert, graph, "bar");
checkLabels(assert, graph, ["None", "red"]);
checkLegend(assert, graph, "Revenue");
});
QUnit.test("order='desc' on arch", async function (assert) {
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph order="desc">
<field name="date"/>
</graph>
`,
});
checkDatasets(assert, graph, ["data", "label"], {
data: [2, 2, 2, 1, 1],
label: "Count",
});
});
QUnit.test("order='asc' on arch", async function (assert) {
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph order="asc">
<field name="date"/>
</graph>
`,
});
checkDatasets(assert, graph, ["data", "label"], {
data: [1, 1, 2, 2, 2],
label: "Count",
});
});
QUnit.test("renders banner_route", async (assert) => {
await makeView({
type: "graph",
resModel: "foo",
serverData,
arch: `
<graph banner_route="/mybody/isacage">
<field name="foo"/>
</graph>`,
async mockRPC(route) {
if (route === "/mybody/isacage") {
assert.step(route);
return { html: `<div class="setmybodyfree">myBanner</div>` };
}
},
});
assert.verifySteps(["/mybody/isacage"]);
assert.containsOnce(target, ".setmybodyfree");
});
QUnit.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 function (assert) {
patchDate(2023, 5, 15, 8, 0, 0);
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph sample="1">
<field name="date" interval="year"/>
</graph>
`,
domain: Domain.FALSE.toList(),
});
checkDatasets(assert, graph, ["data"], { data: [SampleServer.MAIN_RECORDSET_SIZE] });
}
);
QUnit.test(
"no class 'o_view_sample_data' when real data are presented",
async function (assert) {
serverData.models.foo.records = [];
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph sample="1">
<field name="date"/>
</graph>
`,
});
assert.containsOnce(target, ".o_graph_view .o_view_sample_data");
assert.ok(getChart(graph).data.datasets.length);
await selectMode(target, "line");
assert.containsOnce(target, ".o_graph_view .o_view_sample_data");
assert.ok(getChart(graph).data.datasets.length);
await toggleMenu(target, "Measures");
await toggleMenuItem(target, "Revenue");
assert.containsNone(target, ".o_graph_view .o_view_sample_data");
assert.notOk(getChart(graph).data.datasets.length);
}
);
QUnit.test("single chart rendering on search", async function (assert) {
patchWithCleanup(GraphRenderer.prototype, {
setup() {
super.setup(...arguments);
onRendered(() => {
assert.step("rendering");
});
},
});
await makeView({
serverData,
type: "graph",
resModel: "foo",
});
assert.verifySteps(["rendering"]);
await validateSearch(target);
assert.verifySteps(["rendering"]);
});
QUnit.test("apply default filter label", async function (assert) {
const graphView = registry.category("views").get("graph");
class CustomGraphModel extends graphView.Model {
_getDefaultFilterLabel(fields) {
return "None";
}
}
registry.category("views").add("custom_graph", {
...graphView,
Model: CustomGraphModel,
});
const graph = await makeView({
serverData,
type: "graph",
resModel: "foo",
arch: `
<graph js_class="custom_graph">
<field name="product_id"/>
<field name="color_id"/>
</graph>
`,
});
checkLabels(assert, graph, ["xphone", "xpad"]);
checkLegend(assert, graph, ["None", "red", "Sum"]);
await selectMode(target, "line");
checkLabels(assert, graph, ["xphone", "xpad"]);
checkLegend(assert, graph, ["None", "red"]);
await selectMode(target, "pie");
checkLabels(assert, graph, ["xphone / None", "xphone / red", "xpad / None"]);
checkLegend(assert, graph, ["xphone / None", "xphone / red", "xpad / None"]);
});
QUnit.test("missing property field definition is fetched", async function (assert) {
Object.assign(serverData.models.foo.fields, {
properties: {
string: "Properties",
type: "properties",
definition_record: "parent_id",
definition_record_field: "properties_definition",
name: "properties",
},
parent_id: {
string: "Parent",
type: "many2one",
relation: "foo",
name: "parent_id",
},
properties_definition: {
string: "Properties",
type: "properties_definition",
},
});
const graph = await makeView({
type: "graph",
resModel: "foo",
serverData,
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,
},
],
mockRPC(_, { method, kwargs }) {
if (method === "web_read_group" && kwargs.groupby?.includes("properties.my_char")) {
assert.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",
};
}
},
});
assert.verifySteps([`["properties.my_char"]`]);
checkLabels(assert, graph, ["None", "aaa"]);
checkDatasets(
assert,
graph,
["data", "label"],
[
{
data: [2, 1],
label: "Count",
},
]
);
});
QUnit.test("missing deleted property field definition is created", async function (assert) {
Object.assign(serverData.models.foo.fields, {
properties: {
string: "Properties",
type: "properties",
definition_record: "parent_id",
definition_record_field: "properties_definition",
name: "properties",
},
parent_id: {
string: "Parent",
type: "many2one",
relation: "foo",
name: "parent_id",
},
properties_definition: {
string: "Properties",
type: "properties_definition",
},
});
const graph = await makeView({
type: "graph",
resModel: "foo",
serverData,
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,
},
],
mockRPC(_, { method, kwargs }) {
if (method === "web_read_group" && kwargs.groupby?.includes("properties.my_char")) {
assert.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 {};
}
},
});
assert.verifySteps([`["properties.my_char"]`]);
checkLabels(assert, graph, ["None", "aaa"]);
checkDatasets(
assert,
graph,
["data", "label"],
[
{
data: [2, 1],
label: "Count",
},
]
);
});
QUnit.test("limit dataset amount", async function (assert) {
serverData.models.project = {
fields: {
id: { type: "integer" },
name: { type: "char" },
},
records: [],
};
serverData.models.stage = {
fields: {
id: { type: "integer" },
name: { type: "char" },
},
records: [],
};
serverData.models.task = {
fields: {
id: { type: "integer" },
name: { type: "char" },
project_id: {
type: "many2one",
relation: "project",
sortable: true,
string: "Project",
},
stage_id: { type: "many2one", relation: "stage", sortable: true, string: "Stage" },
},
records: [],
};
for (let i = 1; i <= 600; i++) {
serverData.models.project.records.push({
id: i,
name: `Project ${i}`,
});
serverData.models.stage.records.push({
id: i,
name: `Stage ${i}`,
});
serverData.models.task.records.push({
id: i,
project_id: i,
stage_id: i,
name: `Task ${i}`,
});
}
const graph = await makeView({
serverData,
type: "graph",
resModel: "task",
arch: `
<graph>
<field name="project_id"/>
<field name="stage_id"/>
</graph>
`,
});
assert.strictEqual(graph.model.data.exceeds, true);
assert.strictEqual(graph.model.data.datasets.length, 80);
assert.strictEqual(graph.model.data.labels.length, 80);
assert.containsN(target, `.o_graph_alert`, 1);
patchWithCleanup(GraphModel.prototype, {
notify() {
assert.step("rerender");
},
});
await click(target, `.o_graph_load_all_btn`);
assert.verifySteps(["rerender"]);
assert.strictEqual(graph.model.data.exceeds, false);
assert.strictEqual(graph.model.data.datasets.length, 600);
assert.strictEqual(graph.model.data.labels.length, 600);
});
});