import { expect, test } from "@odoo/hoot"; import { queryAllTexts } from "@odoo/hoot-dom"; import { Deferred, animationFrame, mockDate } from "@odoo/hoot-mock"; import { onRendered } from "@odoo/owl"; import { contains, defineModels, editFavoriteName, fields, getService, makeMockServer, mockService, models, mountView, mountWithCleanup, onRpc, patchWithCleanup, saveFavorite, switchView, toggleMenu, toggleMenuItem, toggleMenuItemOption, toggleSaveFavorite, toggleSearchBarMenu, validateSearch, } from "@web/../tests/web_test_helpers"; import { checkDatasets, checkLabels, checkLegend, checkModeIs, checkTooltip, checkYTicks, clickOnDataset, clickOnLegend, clickSort, getChart, getGraphModel, getGraphModelMetaData, getGraphRenderer, getModeButton, getScaleY, getYAxisLabel, selectMode, setupChartJsForTests, } from "./graph_test_helpers"; import { DEFAULT_BG, getBorderWhite, getColors, lightenColor } from "@web/core/colors/colors"; import { Domain } from "@web/core/domain"; import { registry } from "@web/core/registry"; import { SampleServer } from "@web/model/sample_server"; import { GraphArchParser } from "@web/views/graph/graph_arch_parser"; import { GraphRenderer } from "@web/views/graph/graph_renderer"; import { graphView } from "@web/views/graph/graph_view"; import { WebClient } from "@web/webclient/webclient"; class Color extends models.Model { name = fields.Char(); _records = [ { id: 1, name: "black", }, { id: 2, name: "red", }, ]; } class Product extends models.Model { name = fields.Char(); _records = [ { id: 100, name: "xphone", }, { id: 200, name: "xpad", }, ]; } class Foo extends models.Model { bar = fields.Boolean({ default: false }); color_id = fields.Many2one({ relation: "color" }); color_ids = fields.Many2many({ relation: "color" }); date = fields.Date(); foo = fields.Integer(); product_id = fields.Many2one({ relation: "product" }); revenue = fields.Float(); _records = [ { id: 1, foo: 3, bar: true, product_id: 100, date: "2016-01-01", revenue: 1, color_ids: [2], }, { id: 2, foo: 53, bar: true, product_id: 100, color_id: 2, date: "2016-01-03", revenue: 2, color_ids: [1], }, { id: 3, foo: 2, bar: true, product_id: 100, date: "2016-03-04", revenue: 3, color_ids: [1, 2], }, { id: 4, foo: 24, product_id: 100, date: "2016-03-07", revenue: 4, color_ids: [2], }, { id: 5, foo: 4, product_id: 200, date: "2016-05-01", revenue: 5, color_ids: [1, 2], }, { id: 6, foo: 63, product_id: 200, }, { id: 7, foo: 42, product_id: 200, }, { id: 8, foo: 48, product_id: 200, date: "2016-04-01", revenue: 8, }, ]; _views = { graph: /* xml */ ``, search: /* xml */ ` `, }; } defineModels([Foo, Color, Product]); setupChartJsForTests(); test('graph view with "class" attribute', async () => { await mountView({ type: "graph", resModel: "foo", arch: ``, }); expect(".o_graph_view").toHaveClass("foobar-class"); }); test("simple bar chart rendering", async () => { const view = await mountView({ type: "graph", resModel: "foo" }); const { measure, mode, order, stacked } = getGraphModelMetaData(view); expect(".o_graph_view").toHaveClass("o_view_controller"); expect(".o_graph_canvas_container canvas").toHaveCount(1); expect(measure).toBe("__count", { message: `the active measure should be "__count" by default`, }); expect(mode).toBe("bar", { message: "should be in bar chart mode by default" }); expect(order).toBe(null, { message: "should not be ordered by default" }); expect(stacked).toBe(true, { message: "bar charts should be stacked by default" }); checkLabels(view, ["Total"]); checkDatasets(view, ["backgroundColor", "borderColor", "data", "label", "stack"], { backgroundColor: "#4EA7F2", borderColor: undefined, data: [8], label: "Count", stack: "", }); checkLegend(view, "Count"); checkTooltip(view, { lines: [{ label: "Total", value: "8" }] }, 0); }); test("simple bar chart rendering with no data", async () => { Foo._records = []; const view = await mountView({ type: "graph", resModel: "foo" }); expect(".o_graph_canvas_container canvas").toHaveCount(1); expect(".o_nocontent_help").toHaveCount(0); checkLabels(view, []); checkDatasets(view, [], []); }); test("simple bar chart rendering (one groupBy)", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); expect(".o_graph_canvas_container canvas").toHaveCount(1); checkLabels(view, ["false", "true"]); checkDatasets(view, ["backgroundColor", "borderColor", "data", "label"], { backgroundColor: "#4EA7F2", borderColor: undefined, data: [5, 3], label: "Count", }); checkLegend(view, "Count"); checkTooltip(view, { lines: [{ label: "false", value: "5" }] }, 0); checkTooltip(view, { lines: [{ label: "true", value: "3" }] }, 1); }); test("simple bar chart rendering (two groupBy)", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); expect(".o_graph_canvas_container canvas").toHaveCount(1); checkLabels(view, ["false", "true"]); checkDatasets( view, ["backgroundColor", "borderColor", "data", "label"], [ { backgroundColor: "#4EA7F2", borderColor: undefined, data: [1, 3], label: "xphone", }, { backgroundColor: "#EA6175", borderColor: undefined, data: [4, 0], label: "xpad", }, { backgroundColor: "#343a40", borderColor: "rgba(0,0,0,.3)", data: [5, 3], label: "Sum", }, ] ); checkLegend(view, ["xphone", "xpad", "Sum"]); checkTooltip(view, { lines: [{ label: "false / xphone", value: "1" }] }, 0, 0); checkTooltip(view, { lines: [{ label: "true / xphone", value: "3" }] }, 1, 0); checkTooltip(view, { lines: [{ label: "false / xpad", value: "4" }] }, 0, 1); checkTooltip(view, { lines: [{ label: "true / xpad", value: "0" }] }, 1, 1); }); test("bar chart rendering (no groupBy, several domains)", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, groupBy: [], comparison: { domains: [ { arrayRepr: [["bar", "=", true]], description: "True group" }, { arrayRepr: [["bar", "=", false]], description: "False group" }, ], }, }); checkLabels(view, ["Total"]); checkDatasets( view, ["backgroundColor", "borderColor", "data", "label"], [ { backgroundColor: "#4EA7F2", borderColor: undefined, data: [6], label: "True group", }, { backgroundColor: "#EA6175", borderColor: undefined, data: [17], label: "False group", }, ] ); checkLegend(view, ["True group", "False group"]); checkTooltip( view, { title: "Revenue", lines: [{ label: "Total / True group", value: "6" }], }, 0, 0 ); checkTooltip( view, { title: "Revenue", lines: [{ label: "Total / False group", value: "17" }], }, 0, 1 ); }); test("bar chart rendering (one groupBy, several domains)", async () => { Foo._records = [ { bar: true, foo: 1, revenue: 14 }, { bar: true, foo: 2, revenue: 0 }, { bar: false, foo: 1, revenue: 12 }, { bar: false, foo: 2, revenue: -4 }, { bar: false, foo: 3, revenue: 2 }, { bar: false, foo: 4, revenue: 0 }, ]; const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, comparison: { domains: [ { arrayRepr: [["bar", "=", true]], description: "True group" }, { arrayRepr: [["bar", "=", false]], description: "False group" }, ], }, }); checkLabels(view, ["1", "2", "3", "4"]); checkDatasets( view, ["backgroundColor", "borderColor", "data", "label"], [ { backgroundColor: "#4EA7F2", borderColor: undefined, data: [14, 0, 0, 0], label: "True group", }, { backgroundColor: "#EA6175", borderColor: undefined, data: [12, -4, 2, 0], label: "False group", }, ] ); checkLegend(view, ["True group", "False group"]); checkTooltip( view, { title: "Revenue", lines: [{ label: "1 / True group", value: "14" }], }, 0, 0 ); checkTooltip( view, { title: "Revenue", lines: [{ label: "1 / False group", value: "12" }], }, 0, 1 ); checkTooltip( view, { title: "Revenue", lines: [{ label: "2 / False group", value: "-4" }], }, 1, 1 ); checkTooltip( view, { title: "Revenue", lines: [{ label: "3 / False group", value: "2" }], }, 2, 1 ); }); test("bar chart many2many groupBy", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); expect(".o_graph_canvas_container canvas").toHaveCount(1); checkLabels(view, ["black", "red", "None"]); checkDatasets(view, ["backgroundColor", "borderColor", "data", "label"], { backgroundColor: "#4EA7F2", borderColor: undefined, data: [10, 13, 8], label: "Revenue", }); checkLegend(view, "Revenue"); checkTooltip(view, { lines: [{ label: "black", value: "10" }], title: "Revenue" }, 0); checkTooltip(view, { lines: [{ label: "red", value: "13" }], title: "Revenue" }, 1); checkTooltip(view, { lines: [{ label: "None", value: "8" }], title: "Revenue" }, 2); }); test("differentiate many2many values with same label", async () => { Color._records.push({ id: 3, name: "red" }); Foo._records.push({ color_ids: [3], revenue: 14 }); const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); expect(".o_graph_canvas_container canvas").toHaveCount(1); checkLabels(view, ["black", "red", "red (2)", "None"]); checkDatasets(view, ["backgroundColor", "borderColor", "data", "label"], { backgroundColor: "#4EA7F2", borderColor: undefined, data: [10, 13, 14, 8], label: "Revenue", }); checkTooltip(view, { lines: [{ label: "black", value: "10" }], title: "Revenue" }, 0); checkTooltip(view, { lines: [{ label: "red", value: "13" }], title: "Revenue" }, 1); checkTooltip(view, { lines: [{ label: "red (2)", value: "14" }], title: "Revenue" }, 2); checkTooltip(view, { lines: [{ label: "None", value: "8" }], title: "Revenue" }, 3); }); test("bar chart rendering (one groupBy, several domains with date identification)", async () => { Foo._records = [ { date: "2021-01-04", revenue: 12 }, { date: "2021-01-12", revenue: 5 }, { date: "2021-01-19", revenue: 15 }, { date: "2021-01-26", revenue: 2 }, { date: "2021-02-04", revenue: 14 }, { date: "2021-02-17", revenue: 0 }, { date: false, revenue: 0 }, ]; const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, comparison: { domains: [ { arrayRepr: [ ["date", ">=", "2021-02-01"], ["date", "<=", "2021-02-28"], ], description: "February 2021", }, { arrayRepr: [ ["date", ">=", "2021-01-01"], ["date", "<=", "2021-01-31"], ], description: "January 2021", }, ], fieldName: "date", }, }); checkLabels(view, ["W05 2021", "W07 2021", "", ""]); checkDatasets( view, ["backgroundColor", "borderColor", "data", "label"], [ { backgroundColor: "#4EA7F2", borderColor: undefined, data: [14, 0], label: "February 2021", }, { backgroundColor: "#EA6175", borderColor: undefined, data: [12, 5, 15, 2], label: "January 2021", }, ] ); checkLegend(view, ["February 2021", "January 2021"]); checkTooltip( view, { title: "Revenue", lines: [{ label: "W05 2021 / February 2021", value: "14" }], }, 0, 0 ); checkTooltip( view, { title: "Revenue", lines: [{ label: "W01 2021 / January 2021", value: "12" }], }, 0, 1 ); checkTooltip( view, { title: "Revenue", lines: [{ label: "W02 2021 / January 2021", value: "5" }], }, 1, 1 ); checkTooltip( view, { title: "Revenue", lines: [{ label: "W03 2021 / January 2021", value: "15" }], }, 2, 1 ); checkTooltip( view, { title: "Revenue", lines: [{ label: "W04 2021 / January 2021", value: "2" }], }, 3, 1 ); }); test("bar chart rendering (two groupBy, several domains with no date identification)", async () => { Foo._records = [ { date: "2021-01-04", bar: false, revenue: 12 }, { date: "2021-01-12", bar: true, revenue: 5 }, { date: "2021-02-04", bar: false, revenue: 14 }, { date: "2021-02-17", bar: true, revenue: 0 }, { date: false, bar: false, revenue: 0 }, ]; const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, comparison: { domains: [ { arrayRepr: [ ["date", ">=", "2021-02-01"], ["date", "<=", "2021-02-28"], ], description: "February 2021", }, { arrayRepr: [ ["date", ">=", "2021-01-01"], ["date", "<=", "2021-01-31"], ], description: "January 2021", }, ], fieldName: "date", }, }); checkLabels(view, ["false", "true"]); checkDatasets( view, ["backgroundColor", "borderColor", "data", "label"], [ { backgroundColor: "#4EA7F2", borderColor: undefined, data: [14, 0], label: "February 2021 / W05 2021", }, { backgroundColor: "#EA6175", borderColor: undefined, data: [0, 0], label: "February 2021 / W07 2021", }, { backgroundColor: "#43C5B1", borderColor: undefined, data: [12, 0], label: "January 2021 / W01 2021", }, { backgroundColor: "#F4A261", borderColor: undefined, data: [0, 5], label: "January 2021 / W02 2021", }, ] ); checkLegend(view, [ "February 2021 / W05 2021", "February 2021 / W07 2021", "January 2021 / W01 2021", "January 2021 / W02 2021", ]); checkTooltip( view, { title: "Revenue", lines: [{ label: "false / February 2021 / W05 2021", value: "14" }], }, 0, 0 ); checkTooltip( view, { title: "Revenue", lines: [{ label: "false / January 2021 / W01 2021", value: "12" }], }, 0, 2 ); checkTooltip( view, { title: "Revenue", lines: [{ label: "true / January 2021 / W02 2021", value: "5" }], }, 1, 3 ); }); test("line chart rendering (no groupBy)", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ``, }); expect(".o_graph_canvas_container canvas").toHaveCount(1); expect(getGraphModelMetaData(view).mode).toBe("line"); checkLabels(view, ["", "Total", ""]); checkDatasets(view, ["backgroundColor", "borderColor", "data", "label", "stack"], { backgroundColor: "#a7d3f9", borderColor: "#4EA7F2", data: [undefined, 8], label: "Count", stack: undefined, }); checkLegend(view, "Count"); checkTooltip(view, { lines: [{ label: "Total", value: "8" }] }, 1); }); test("line chart rendering (one groupBy)", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); expect(".o_graph_canvas_container canvas").toHaveCount(1); checkLabels(view, ["false", "true"]); checkDatasets(view, ["backgroundColor", "borderColor", "data", "label"], { backgroundColor: "#a7d3f9", borderColor: "#4EA7F2", data: [5, 3], label: "Count", }); checkLegend(view, "Count"); checkTooltip(view, { lines: [{ label: "false", value: "5" }] }, 0); checkTooltip(view, { lines: [{ label: "true", value: "3" }] }, 1); }); test("line chart rendering (two groupBy)", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); expect(".o_graph_canvas_container canvas").toHaveCount(1); checkLabels(view, ["false", "true"]); checkDatasets( view, ["backgroundColor", "borderColor", "data", "label"], [ { backgroundColor: "#a7d3f9", borderColor: "#4EA7F2", data: [1, 3], label: "xphone", }, { backgroundColor: "#f5b0ba", borderColor: "#EA6175", data: [4, 0], label: "xpad", }, ] ); checkLegend(view, ["xphone", "xpad"]); checkTooltip( view, { lines: [ { label: "false / xpad", value: "4" }, { label: "false / xphone", value: "1" }, ], }, 0 ); checkTooltip( view, { lines: [ { label: "true / xphone", value: "3" }, { label: "true / xpad", value: "0" }, ], }, 1 ); }); test("line chart many2many groupBy", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); expect(".o_graph_canvas_container canvas").toHaveCount(1); checkLabels(view, ["black", "red"]); checkDatasets(view, ["backgroundColor", "borderColor", "data", "label"], { backgroundColor: "#a7d3f9", borderColor: "#4EA7F2", data: [10, 13], label: "Revenue", }); checkLegend(view, "Revenue"); checkTooltip(view, { lines: [{ label: "black", value: "10" }], title: "Revenue" }, 0); checkTooltip(view, { lines: [{ label: "red", value: "13" }], title: "Revenue" }, 1); }); test("Check if values in tooltip are correctly sorted when groupBy filter are applied", async () => { Foo._records = [ { product_id: 100, foo: 1, revenue: 12 }, { product_id: 100, foo: 2, revenue: 5 }, { product_id: 100, foo: 3, revenue: 1.45e2 }, { product_id: 100, foo: 4, revenue: -9 }, { product_id: 200, foo: 5, revenue: 0 }, { product_id: 200, foo: 6, revenue: -1 }, { product_id: 200, foo: 7, revenue: Math.PI }, { product_id: 200, foo: 8, revenue: 80.67 }, ]; const view = await mountView({ type: "graph", resModel: "foo", arch: ` `, }); checkTooltip( view, { lines: [ { label: "xphone / 3", value: "145.00" }, { label: "xphone / 1", value: "12.00" }, { label: "xphone / 2", value: "5.00" }, { label: "xphone / 5", value: "0.00" }, { label: "xphone / 6", value: "0.00" }, { label: "xphone / 7", value: "0.00" }, { label: "xphone / 8", value: "0.00" }, { label: "xphone / 4", value: "-9.00" }, ], title: "Revenue", }, 0 ); checkTooltip( view, { lines: [ { label: "xpad / 8", value: "80.67" }, { label: "xpad / 7", value: "3.14" }, { label: "xpad / 1", value: "0.00" }, { label: "xpad / 2", value: "0.00" }, { label: "xpad / 3", value: "0.00" }, { label: "xpad / 4", value: "0.00" }, { label: "xpad / 5", value: "0.00" }, { label: "xpad / 6", value: "-1.00" }, ], title: "Revenue", }, 1 ); }); test("format total in hh:mm when measure is unit_amount", async () => { Foo._fields.unit_amount = fields.Float({ string: "Unit Amount" }); Foo._records = [{ id: 1, unit_amount: 8 }]; const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); const { measure, fieldAttrs } = getGraphModelMetaData(view); expect(".o_graph_view").toHaveClass("o_view_controller"); expect("div.o_graph_canvas_container canvas").toHaveCount(1); expect(measure).toBe("unit_amount", { message: `the measure should be "unit_amount"` }); checkLegend(view, "Unit Amount"); checkLabels(view, ["Total"]); expect(fieldAttrs[measure].widget).toBe("float_time", { message: "should be a float_time widget", }); checkYTicks(view, [ "00:00", "01:00", "02:00", "03:00", "04:00", "05:00", "06:00", "07:00", "08:00", ]); checkTooltip(view, { title: "Unit Amount", lines: [{ label: "Total", value: "08:00" }] }, 0); }); test("Stacked button visible in the line chart", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); const model = getGraphModel(view); await selectMode("line"); checkModeIs(view, "line"); expect(model.metaData.stacked).toBe(true, { message: "graph should be stacked." }); expect(getScaleY(view).stacked).toBe(true, { message: "The y axes should have stacked property set to true", }); expect(`button.o_graph_button[data-tooltip="Stacked"]`).toHaveCount(1); await contains(`button.o_graph_button[data-tooltip="Stacked"]`).click(); expect(model.metaData.stacked).toBe(false, { message: "graph should be a classic line chart.", }); expect(getScaleY(view).stacked).toBe(undefined, { message: "The y axes should have stacked property set to undefined", }); }); test("Stacked line prop click false", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); await contains(`button.o_graph_button[data-tooltip="Stacked"]`).click(); expect(getGraphModel(view).metaData.stacked).toBe(false, { message: "graph should be a classic line chart.", }); expect(!!getScaleY(view).stacked).toBe(false, { message: "the y axes should have a stacked property set to false since the stacked property in line chart is false.", }); expect(getGraphRenderer(view).getElementOptions().line.fill).toBe(false, { message: "The fill property should be false since the stacked property is false.", }); const expectedDatasets = [ { backgroundColor: "#a7d3f9", borderColor: "#4EA7F2", originIndex: 0, pointBackgroundColor: "#4EA7F2", }, { backgroundColor: "#f5b0ba", borderColor: "#EA6175", originIndex: 0, pointBackgroundColor: "#EA6175", }, ]; const keysToEvaluate = [ "backgroundColor", "borderColor", "originIndex", "pointBackgroundColor", ]; checkDatasets(view, keysToEvaluate, expectedDatasets); }); test("Stacked prop and default line chart", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); expect(getGraphModel(view).metaData.mode).toBe("line", { message: "should be in line chart mode.", }); expect(getGraphModel(view).metaData.stacked).toBe(true, { message: "should be stacked by default.", }); expect(getScaleY(view).stacked).toBe(true, { message: "the stacked property in y axes should be true when the stacked is enabled in line chart", }); expect(getGraphRenderer(view).getElementOptions().line.fill).toBe(true, { message: "The fill property should be true to add backgroundColor in line chart.", }); const expectedDatasets = []; const keysToEvaluate = [ "backgroundColor", "borderColor", "originIndex", "pointBackgroundColor", ]; const datasets = getChart(view).data.datasets; const colors = getColors(undefined, "sm"); for (let i = 0; i < datasets.length; i++) { const expectedColor = colors[i]; expectedDatasets.push({ backgroundColor: lightenColor(expectedColor, 0.5), borderColor: expectedColor, originIndex: 0, pointBackgroundColor: expectedColor, }); } checkDatasets(view, keysToEvaluate, expectedDatasets); }); test("Cumulative prop and default line chart", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); expect(getGraphModel(view).metaData.mode).toBe("line", { message: "should be in line chart mode.", }); expect(getGraphModel(view).metaData.cumulated).toBe(false, { message: "should not be cumulative by default.", }); await contains('[data-tooltip="Cumulative"]').click(); expect(getGraphModel(view).metaData.cumulated).toBe(true, { message: "should be in cumulative", }); const expectedDatasets = [ { data: [1, 4], }, { data: [4, 4], }, ]; checkDatasets(view, ["data"], expectedDatasets); }); test("Default cumulative prop", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); expect(getGraphModel(view).metaData.mode).toBe("line", { message: "should be in line chart mode.", }); expect(getGraphModel(view).metaData.cumulated).toBe(true, { message: "should be in cumulative", }); expect(getGraphModel(view).metaData.cumulatedStart).toBe(false, { message: "should have cumulated start opted-out", }); }); test("Cumulative prop and cumulated start", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, searchViewArch: /* xml */ ` `, context: { search_default_filter_after_march: 1, }, }); expect(getGraphModel(view).metaData.mode).toBe("line", { message: "should be in line chart mode", }); expect(getGraphModel(view).metaData.cumulated).toBe(true, { message: "should be in cumulative", }); expect(getGraphModel(view).metaData.cumulatedStart).toBe(true, { message: "should have cumulated start opted-in", }); const expectedDatasets = [ { data: [4, 4, 4], }, { data: [0, 1, 2], }, ]; checkDatasets(view, ["data"], expectedDatasets); }); test("line chart rendering (no groupBy, several domains)", async () => { const view = await mountView({ resModel: "foo", type: "graph", arch: /* xml */ ` `, comparison: { domains: [ { arrayRepr: [["bar", "=", true]], description: "True group" }, { arrayRepr: [["bar", "=", false]], description: "False group" }, ], }, }); checkLabels(view, ["", "Total", ""]); checkDatasets( view, ["backgroundColor", "borderColor", "data", "label"], [ { backgroundColor: "#a7d3f9", borderColor: "#4EA7F2", data: [undefined, 6], label: "True group", }, { backgroundColor: "#f5b0ba", borderColor: "#EA6175", data: [undefined, 17], label: "False group", }, ] ); checkLegend(view, ["True group", "False group"]); checkTooltip( view, { title: "Revenue", lines: [ { label: "Total / False group", value: "17" }, { label: "Total / True group", value: "6" }, ], }, 1 ); }); test("line chart rendering (one groupBy, several domains)", async () => { Foo._records = [ { bar: true, foo: 1, revenue: 14 }, { bar: true, foo: 2, revenue: 0 }, { bar: false, foo: 1, revenue: 12 }, { bar: false, foo: 2, revenue: -4 }, { bar: false, foo: 3, revenue: 2 }, { bar: false, foo: 4, revenue: 0 }, ]; const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, comparison: { domains: [ { arrayRepr: [["bar", "=", true]], description: "True group" }, { arrayRepr: [["bar", "=", false]], description: "False group" }, ], }, }); checkLabels(view, ["1", "2", "3", "4"]); checkDatasets( view, ["backgroundColor", "borderColor", "data", "label"], [ { backgroundColor: "#a7d3f9", borderColor: "#4EA7F2", data: [14, 0, 0, 0], label: "True group", }, { backgroundColor: "#f5b0ba", borderColor: "#EA6175", data: [12, -4, 2, 0], label: "False group", }, ] ); checkLegend(view, ["True group", "False group"]); checkTooltip( view, { title: "Revenue", lines: [ { label: "1 / True group", value: "14" }, { label: "1 / False group", value: "12" }, ], }, 0 ); checkTooltip( view, { title: "Revenue", lines: [ { label: "2 / True group", value: "0" }, { label: "2 / False group", value: "-4" }, ], }, 1 ); checkTooltip( view, { title: "Revenue", lines: [ { label: "3 / False group", value: "2" }, { label: "3 / True group", value: "0" }, ], }, 2 ); checkTooltip( view, { title: "Revenue", lines: [ { label: "4 / True group", value: "0" }, { label: "4 / False group", value: "0" }, ], }, 3 ); }); test("line chart rendering (one groupBy, several domains with date identification)", async () => { Foo._records = [ { date: "2021-01-04", revenue: 12 }, { date: "2021-01-12", revenue: 5 }, { date: "2021-01-19", revenue: 15 }, { date: "2021-01-26", revenue: 2 }, { date: "2021-02-04", revenue: 14 }, { date: "2021-02-17", revenue: 0 }, { date: false, revenue: 0 }, ]; const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, comparison: { domains: [ { arrayRepr: [ ["date", ">=", "2021-02-01"], ["date", "<=", "2021-02-28"], ], description: "February 2021", }, { arrayRepr: [ ["date", ">=", "2021-01-01"], ["date", "<=", "2021-01-31"], ], description: "January 2021", }, ], fieldName: "date", }, }); checkLabels(view, ["W05 2021", "W07 2021", "", ""]); checkDatasets( view, ["backgroundColor", "borderColor", "data", "label"], [ { backgroundColor: "#a7d3f9", borderColor: "#4EA7F2", data: [14, 0], label: "February 2021", }, { backgroundColor: "#f5b0ba", borderColor: "#EA6175", data: [12, 5, 15, 2], label: "January 2021", }, ] ); checkLegend(view, ["February 2021", "January 2021"]); checkTooltip( view, { title: "Revenue", lines: [ { label: "W05 2021 / February 2021", value: "14" }, { label: "W01 2021 / January 2021", value: "12" }, ], }, 0 ); checkTooltip( view, { title: "Revenue", lines: [ { label: "W02 2021 / January 2021", value: "5" }, { label: "W07 2021 / February 2021", value: "0" }, ], }, 1 ); checkTooltip( view, { title: "Revenue", lines: [{ label: "W03 2021 / January 2021", value: "15" }], }, 2 ); checkTooltip( view, { title: "Revenue", lines: [{ label: "W04 2021 / January 2021", value: "2" }], }, 3 ); }); test("line chart rendering (one groupBy, several domains with date identification) without stacked attribute", async () => { Foo._records = [ { date: "2021-01-04", revenue: 12 }, { date: "2021-01-12", revenue: 5 }, { date: "2021-01-19", revenue: 15 }, { date: "2021-01-26", revenue: 2 }, { date: "2021-02-04", revenue: 14 }, { date: "2021-02-17", revenue: 0 }, { date: false, revenue: 0 }, ]; await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, comparison: { domains: [ { arrayRepr: [ ["date", ">=", "2021-02-01"], ["date", "<=", "2021-02-28"], ], description: "February 2021", }, { arrayRepr: [ ["date", ">=", "2021-01-01"], ["date", "<=", "2021-01-31"], ], description: "January 2021", }, ], fieldName: "date", }, }); expect(".o_graph_button[data-tooltip=Stacked]").not.toHaveClass("active", { message: "The stacked mode should be disabled", }); }); test("line chart rendering (two groupBy, several domains with no date identification)", async () => { Foo._records = [ { date: "2021-01-04", bar: false, revenue: 12 }, { date: "2021-01-12", bar: true, revenue: 5 }, { date: "2021-02-04", bar: false, revenue: 14 }, { date: "2021-02-17", bar: true, revenue: 0 }, { date: false, bar: false, revenue: 0 }, ]; const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, comparison: { domains: [ { arrayRepr: [ ["date", ">=", "2021-02-01"], ["date", "<=", "2021-02-28"], ], description: "February 2021", }, { arrayRepr: [ ["date", ">=", "2021-01-01"], ["date", "<=", "2021-01-31"], ], description: "January 2021", }, ], fieldName: "date", }, }); checkLabels(view, ["false", "true"]); checkDatasets( view, ["backgroundColor", "borderColor", "data", "label"], [ { backgroundColor: "#a7d3f9", borderColor: "#4EA7F2", data: [14, 0], label: "February 2021 / W05 2021", }, { backgroundColor: "#f5b0ba", borderColor: "#EA6175", data: [0, 0], label: "February 2021 / W07 2021", }, { backgroundColor: "#a1e2d8", borderColor: "#43C5B1", data: [12, 0], label: "January 2021 / W01 2021", }, { backgroundColor: "#fad1b0", borderColor: "#F4A261", data: [0, 5], label: "January 2021 / W02 2021", }, ] ); checkLegend(view, [ "February 2021 / W05 2021", "February 2021 / W07 2021", "January 2021 / W01 2021", "January 2021 / W02 2021", ]); checkTooltip( view, { title: "Revenue", lines: [ { label: "false / February 2021 / W05 2021", value: "14" }, { label: "false / January 2021 / W01 2021", value: "12" }, { label: "false / February 2021 / W07 2021", value: "0" }, { label: "false / January 2021 / W02 2021", value: "0" }, ], }, 0 ); checkTooltip( view, { title: "Revenue", lines: [ { label: "true / January 2021 / W02 2021", value: "5" }, { label: "true / February 2021 / W05 2021", value: "0" }, { label: "true / February 2021 / W07 2021", value: "0" }, { label: "true / January 2021 / W01 2021", value: "0" }, ], }, 1 ); }); test("displaying line chart with only 1 data point", async () => { // this test makes sure the line chart does not crash when only one data // point is displayed. Foo._records = Foo._records.filter((id) => id === 1); await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ``, }); expect("canvas").toHaveCount(1); }); test("pie chart rendering (no groupBy)", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ``, }); expect(".o_graph_canvas_container canvas").toHaveCount(1); expect(getGraphModelMetaData(view).mode).toBe("pie"); checkLabels(view, ["Total"]); checkDatasets(view, ["backgroundColor", "borderColor", "data", "label", "stack"], { backgroundColor: ["#4EA7F2"], borderColor: getBorderWhite(), data: [8], label: "", stack: undefined, }); checkLegend(view, "Total"); checkTooltip(view, { lines: [{ label: "Total", value: "8 (100.00%)" }] }, 0); }); test("pie chart rendering (one groupBy)", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); expect(".o_graph_canvas_container canvas").toHaveCount(1); checkLabels(view, ["false", "true"]); checkDatasets(view, ["backgroundColor", "borderColor", "data"], { backgroundColor: ["#4EA7F2", "#EA6175"], borderColor: getBorderWhite(), data: [5, 3], }); checkLegend(view, ["false", "true"]); checkTooltip(view, { lines: [{ label: "false", value: "5 (62.50%)" }] }, 0); checkTooltip(view, { lines: [{ label: "true", value: "3 (37.50%)" }] }, 1); }); test("pie chart many2many groupby", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); expect(".o_graph_canvas_container canvas").toHaveCount(1); checkLabels(view, ["black", "red", "None"]); checkDatasets(view, ["backgroundColor", "borderColor", "data"], { backgroundColor: ["#4EA7F2", "#EA6175", "#43C5B1"], borderColor: getBorderWhite(), data: [10, 13, 8], }); checkLegend(view, ["black", "red", "None"]); checkTooltip(view, { lines: [{ label: "black", value: "10 (32.26%)" }], title: "Revenue" }, 0); checkTooltip(view, { lines: [{ label: "red", value: "13 (41.94%)" }], title: "Revenue" }, 1); checkTooltip(view, { lines: [{ label: "None", value: "8 (25.81%)" }], title: "Revenue" }, 2); }); test("pie chart rendering (two groupBy)", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); expect(".o_graph_canvas_container canvas").toHaveCount(1); checkLabels(view, ["false / xphone", "false / xpad", "true / xphone"]); checkDatasets(view, ["backgroundColor", "borderColor", "data", "label"], { backgroundColor: ["#4EA7F2", "#EA6175", "#43C5B1"], borderColor: getBorderWhite(), data: [1, 4, 3], label: "", }); checkLegend(view, ["false / xphone", "false / xpad", "true / xphone"]); checkTooltip(view, { lines: [{ label: "false / xphone", value: "1 (12.50%)" }] }, 0); checkTooltip(view, { lines: [{ label: "false / xpad", value: "4 (50.00%)" }] }, 1); checkTooltip(view, { lines: [{ label: "true / xphone", value: "3 (37.50%)" }] }, 2); }); test("pie chart rendering (no groupBy, several domains)", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, comparison: { domains: [ { arrayRepr: [["bar", "=", true]], description: "True group" }, { arrayRepr: [["bar", "=", false]], description: "False group" }, ], }, }); checkLabels(view, ["Total"]); checkDatasets( view, ["backgroundColor", "borderColor", "data", "label"], [ { backgroundColor: ["#4EA7F2"], borderColor: getBorderWhite(), data: [6], label: "True group", }, { backgroundColor: ["#4EA7F2"], borderColor: getBorderWhite(), data: [17], label: "False group", }, ] ); checkLegend(view, ["Total"]); checkTooltip( view, { title: "Revenue", lines: [{ label: "True group / Total", value: "6 (100.00%)" }], }, 0, 0 ); checkTooltip( view, { title: "Revenue", lines: [{ label: "False group / Total", value: "17 (100.00%)" }], }, 0, 1 ); }); test("pie chart rendering (one groupBy, several domains)", async () => { Foo._records = [ { bar: true, foo: 1, revenue: 14 }, { bar: true, foo: 2, revenue: 0 }, { bar: false, foo: 1, revenue: 12 }, { bar: false, foo: 2, revenue: 5 }, { bar: false, foo: 3, revenue: 0 }, { bar: false, foo: 4, revenue: 2 }, ]; const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, comparison: { domains: [ { arrayRepr: [["bar", "=", true]], description: "True group" }, { arrayRepr: [["bar", "=", false]], description: "False group" }, ], }, }); checkLabels(view, ["1", "2", "4"]); checkDatasets( view, ["backgroundColor", "borderColor", "data", "label"], [ { backgroundColor: ["#4EA7F2", "#EA6175", "#43C5B1"], borderColor: getBorderWhite(), data: [14, 0, 0], label: "True group", }, { backgroundColor: ["#4EA7F2", "#EA6175", "#43C5B1"], borderColor: getBorderWhite(), data: [12, 5, 2], label: "False group", }, ] ); checkLegend(view, ["1", "2", "4"]); checkTooltip( view, { title: "Revenue", lines: [{ label: "True group / 1", value: "14 (100.00%)" }], }, 0, 0 ); checkTooltip( view, { title: "Revenue", lines: [{ label: "False group / 1", value: "12 (63.16%)" }], }, 0, 1 ); checkTooltip( view, { title: "Revenue", lines: [{ label: "False group / 2", value: "5 (26.32%)" }], }, 1, 1 ); checkTooltip( view, { title: "Revenue", lines: [{ label: "False group / 4", value: "2 (10.53%)" }], }, 2, 1 ); }); test("pie chart rendering (one groupBy, several domains with date identification)", async () => { Foo._records = [ { date: "2021-01-04" }, { date: "2021-01-12" }, { date: "2021-01-19" }, { date: "2021-01-26" }, { date: "2021-02-04" }, { date: "2021-02-17" }, { date: false }, ]; const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, comparison: { domains: [ { arrayRepr: [ ["date", ">=", "2021-02-01"], ["date", "<=", "2021-02-28"], ], description: "February 2021", }, { arrayRepr: [ ["date", ">=", "2021-01-01"], ["date", "<=", "2021-01-31"], ], description: "January 2021", }, ], fieldName: "date", }, }); checkLabels(view, ["W05 2021, W01 2021", "W07 2021, W02 2021", "W03 2021", "W04 2021"]); checkDatasets( view, ["backgroundColor", "borderColor", "data", "label"], [ { backgroundColor: ["#4EA7F2", "#EA6175", "#43C5B1", "#F4A261"], borderColor: getBorderWhite(), data: [1, 1, 0, 0], label: "February 2021", }, { backgroundColor: ["#4EA7F2", "#EA6175", "#43C5B1", "#F4A261"], borderColor: getBorderWhite(), data: [1, 1, 1, 1], label: "January 2021", }, ] ); checkLegend(view, ["W05 2021, W01 2021", "W07 2021, W02 2021", "W03 2021", "W04 2021"]); checkTooltip( view, { lines: [{ label: "February 2021 / W05 2021", value: "1 (50.00%)" }], }, 0, 0 ); checkTooltip( view, { lines: [{ label: "January 2021 / W01 2021", value: "1 (25.00%)" }], }, 0, 1 ); checkTooltip( view, { lines: [{ label: "February 2021 / W07 2021", value: "1 (50.00%)" }], }, 1, 0 ); checkTooltip( view, { lines: [{ label: "January 2021 / W02 2021", value: "1 (25.00%)" }], }, 1, 1 ); checkTooltip( view, { lines: [{ label: "January 2021 / W03 2021", value: "1 (25.00%)" }], }, 2, 1 ); checkTooltip( view, { lines: [{ label: "January 2021 / W04 2021", value: "1 (25.00%)" }], }, 3, 1 ); }); test("pie chart rendering (two groupBy, several domains with no date identification)", async () => { Foo._records = [ { date: "2021-01-04", bar: false, revenue: 12 }, { date: "2021-01-12", bar: true, revenue: 5 }, { date: "2021-02-04", bar: false, revenue: 14 }, { date: "2021-02-17", bar: true, revenue: 0 }, { date: false, bar: false, revenue: 0 }, ]; const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, comparison: { domains: [ { arrayRepr: [ ["date", ">=", "2021-02-01"], ["date", "<=", "2021-02-28"], ], description: "February 2021", }, { arrayRepr: [ ["date", ">=", "2021-01-01"], ["date", "<=", "2021-01-31"], ], description: "January 2021", }, ], fieldName: "date", }, }); checkLabels(view, ["false / W05 2021", "false / W01 2021", "true / W02 2021"]); checkDatasets( view, ["backgroundColor", "borderColor", "data", "label"], [ { backgroundColor: ["#4EA7F2", "#EA6175", "#43C5B1"], borderColor: getBorderWhite(), data: [14, 0, 0], label: "February 2021", }, { backgroundColor: ["#4EA7F2", "#EA6175", "#43C5B1"], borderColor: getBorderWhite(), data: [0, 12, 5], label: "January 2021", }, ] ); checkLegend(view, ["false / W05 2021", "false / W01 2021", "true / W02 2021"]); checkTooltip( view, { title: "Revenue", lines: [{ label: "February 2021 / false / W05 2021", value: "14 (100.00%)" }], }, 0, 0 ); checkTooltip( view, { title: "Revenue", lines: [{ label: "January 2021 / false / W01 2021", value: "12 (70.59%)" }], }, 1, 1 ); checkTooltip( view, { title: "Revenue", lines: [{ label: "January 2021 / true / W02 2021", value: "5 (29.41%)" }], }, 2, 1 ); }); test("pie chart rendering (no data)", async () => { Foo._records = []; const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ``, }); checkLabels(view, ["No data"]); checkDatasets( view, ["backgroundColor", "borderColor", "data", "label"], [ { backgroundColor: [DEFAULT_BG], borderColor: getBorderWhite(), data: [1], label: null, }, ] ); checkLegend(view, ["No data"]); checkTooltip(view, { lines: [{ label: "No data", value: "0 (100.00%)" }] }, 0); }); test("pie chart rendering (no data, several domains)", async () => { Foo._records = [{ product_id: 100, bar: true }]; const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, comparison: { domains: [ { arrayRepr: [["bar", "=", true]], description: "True group" }, { arrayRepr: [["bar", "=", false]], description: "False group" }, ], }, }); checkLabels(view, ["xphone", "No data"]); checkDatasets( view, ["backgroundColor", "borderColor", "data", "label"], [ { backgroundColor: ["#4EA7F2"], borderColor: getBorderWhite(), data: [1], label: "True group", }, { backgroundColor: ["#4EA7F2", DEFAULT_BG], borderColor: getBorderWhite(), data: [undefined, 1], label: "False group", }, ] ); checkLegend(view, ["xphone", "No data"]); checkTooltip(view, { lines: [{ label: "True group / xphone", value: "1 (100.00%)" }] }, 0, 0); checkTooltip(view, { lines: [{ label: "False group / No data", value: "0 (100.00%)" }] }, 1, 1); }); test("pie chart rendering (mix of positive and negative values)", async () => { Foo._records = [ { bar: true, revenue: 2 }, { bar: false, revenue: -3 }, ]; const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); expect(".o_view_nocontent").toHaveCount(0); expect(".o_graph_canvas_container").toHaveCount(1); checkDatasets(view, ["backgroundColor", "borderColor", "data", "label", "stack"], { backgroundColor: ["#4EA7F2"], borderColor: getBorderWhite(), data: [2], label: "", stack: undefined, }); }); test("pie chart toggling dataset hides label", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: ``, }); checkLabels(view, ["Total"]); await clickOnLegend(view, "Total"); expect(getChart(view).legend.legendItems[0].hidden).toBe(true); }); test("mode props", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ``, }); expect(getGraphModelMetaData(view).mode).toBe("pie", { message: "should be in pie chart mode", }); expect(getChart(view).config.type).toBe("pie"); }); test("field id not in groupBy", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); checkLabels(view, ["Total"]); checkDatasets(view, ["backgroundColor", "data", "label", "originIndex", "stack"], { backgroundColor: "#4EA7F2", data: [8], label: "Count", originIndex: 0, stack: "", }); checkLegend(view, "Count"); }); test("props modifications", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, searchViewArch: /* xml */ ` `, }); checkModeIs(view, "bar"); expect(getYAxisLabel(view)).toBe("Count"); await selectMode("line"); checkModeIs(view, "line"); await toggleMenu("Measures"); await toggleMenuItem("Revenue"); expect(getYAxisLabel(view)).toBe("Revenue"); await toggleSearchBarMenu(); await toggleMenuItem("Color"); checkModeIs(view, "line"); expect(getYAxisLabel(view)).toBe("Revenue"); }); test("switching mode", async () => { const view = await mountView({ type: "graph", resModel: "foo" }); checkModeIs(view, "bar"); await selectMode("bar"); // click on the active mode does not change anything checkModeIs(view, "bar"); await selectMode("line"); checkModeIs(view, "line"); await selectMode("pie"); checkModeIs(view, "pie"); }); test("switching measure", async () => { const checkMeasure = (measure) => { const yAxe = getChart(view).config.options.scales.y; expect(yAxe.title.text).toBe(measure); expect(`.o_menu_item:contains(${measure})`).toHaveClass("selected"); }; const view = await mountView({ type: "graph", resModel: "foo" }); await toggleMenu("Measures"); checkMeasure("Count"); checkLegend(view, "Count"); await toggleMenuItem("Foo"); checkMeasure("Foo"); checkLegend(view, "Foo"); }); test("process default view description", async () => { expect(new GraphArchParser().parse()).toEqual({ fields: {}, fieldAttrs: {}, groupBy: [], measures: [], }); }); test("process simple arch (no field tag)", async () => { const { env } = await makeMockServer(); const fooFields = env["foo"]._fields; const arch1 = /* xml */ ` `; expect(new GraphArchParser().parse(arch1, fooFields)).toEqual({ disableLinking: true, fields: fooFields, fieldAttrs: {}, groupBy: [], measures: [], mode: "line", order: "ASC", }); const arch2 = /* xml */ ` `; expect(new GraphArchParser().parse(arch2, fooFields)).toEqual({ disableLinking: false, fields: fooFields, fieldAttrs: {}, groupBy: [], measures: [], stacked: false, title: "Title", }); }); test("process arch with field tags", async () => { Foo._fields.fighters = fields.Text(); const { env } = await makeMockServer(); const fooFields = env["foo"]._fields; const arch = /* xml */ ` `; expect(new GraphArchParser().parse(arch, fooFields)).toEqual({ fields: fooFields, fieldAttrs: { bar: { isInvisible: true, string: "My invisible field" }, fighters: { string: "FooFighters" }, }, measure: "revenue", measures: ["revenue"], groupBy: ["date:day", "foo"], mode: "pie", }); }); test("process arch with non stored field tags of type measure", async () => { Foo._fields.revenue.store = false; const { env } = await makeMockServer(); const fooFields = env["foo"]._fields; const arch = ` `; expect(new GraphArchParser().parse(arch, fooFields)).toEqual({ fields: fooFields, fieldAttrs: {}, measure: "foo", measures: ["revenue", "foo"], groupBy: ["product_id"], }); }); test("displaying chart data with three groupbys", async () => { // this test makes sure the line chart shows all data labels (X axis) when // it is grouped by several fields const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); checkLabels(view, ["xphone", "xpad"]); checkLegend(view, ["false / None", "true / red", "true / None", "Sum"]); await selectMode("line"); checkLabels(view, ["xphone", "xpad"]); checkLegend(view, ["false / None", "true / red", "true / None"]); await selectMode("pie"); checkLabels(view, [ "xphone / false / None", "xphone / true / red", "xphone / true / None", "xpad / false / None", ]); checkLegend(view, [ "xphone / false / None", "xphone / true / red", "xphone / true / None", "xpad / false / None", ]); }); test("no content helper", async () => { Foo._records = []; await mountView({ type: "graph", resModel: "foo", noContentHelp: /* xml */ `

This helper should not be displayed in graph views

`, }); expect(".o_graph_canvas_container canvas").toHaveCount(1); expect(".o_view_nocontent").toHaveCount(0); expect(".abc").toHaveCount(0); }); test("no content helper after update", async () => { await mountView({ type: "graph", resModel: "foo", noContentHelp: /* xml */ `

This helper should not be displayed in graph views

`, config: { views: [[false, "search"]], }, }); expect(".o_graph_canvas_container canvas").toHaveCount(1); expect(".o_view_nocontent").toHaveCount(0); expect(".abc").toHaveCount(0); await toggleSearchBarMenu(); await toggleMenuItem("False Domain"); expect(".o_graph_canvas_container canvas").toHaveCount(1); expect(".o_view_nocontent").toHaveCount(0); expect(".abc").toHaveCount(0); }); test("can reload with other group by", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, searchViewArch: /* xml */ ` `, }); checkLabels(view, ["xphone", "xpad"]); await toggleSearchBarMenu(); await toggleMenuItem("Color"); checkLabels(view, ["red", "None"]); }); test("save params succeeds", async () => { expect.assertions(4); const expectedContexts = [ { graph_mode: "bar", graph_measure: "__count", graph_groupbys: ["product_id"], graph_order: null, graph_stacked: true, group_by: [], }, { graph_mode: "bar", graph_measure: "foo", graph_groupbys: ["product_id"], graph_order: null, graph_stacked: true, group_by: [], }, { graph_mode: "line", graph_measure: "foo", graph_cumulated: false, graph_groupbys: ["product_id"], graph_order: null, graph_stacked: true, group_by: [], }, { graph_mode: "line", graph_measure: "foo", graph_cumulated: false, graph_groupbys: ["product_id", "color_id"], graph_order: null, graph_stacked: true, group_by: ["product_id", "color_id"], }, ]; let serverId = 1; onRpc("create_or_replace", ({ args }) => { expect(args[0].context).toEqual(expectedContexts.shift()); return serverId++; }); await mountView({ resModel: "foo", type: "graph", arch: /* xml */ ` `, searchViewId: false, searchViewArch: /* xml */ ` `, }); await toggleSaveFavorite(); await editFavoriteName("First Favorite"); await saveFavorite(); await toggleMenu("Measures"); await toggleMenuItem("Foo"); await toggleSaveFavorite(); await editFavoriteName("Second Favorite"); await saveFavorite(); await selectMode("line"); await toggleSaveFavorite(); await editFavoriteName("Third Favorite"); await saveFavorite(); await toggleMenuItem("Product"); await toggleMenuItem("Color"); await editFavoriteName("Fourth Favorite"); await saveFavorite(); }); test("correctly uses graph_ keys from the context", async () => { Foo._records.at(-1).color_id = 1; const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, context: { graph_measure: "foo", graph_mode: "line", graph_groupbys: ["color_id"], }, }); checkLabels(view, ["black", "red"]); checkLegend(view, "Foo"); checkModeIs(view, "line"); expect(getYAxisLabel(view)).toBe("Foo"); expect(getGraphModelMetaData(view).mode).toBe("line"); }); test("correctly uses graph_ keys from the context (at reload)", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, searchViewArch: /* xml */ ` `, }); checkLegend(view, "Count"); expect(getYAxisLabel(view)).toBe("Count"); checkModeIs(view, "bar"); await toggleSearchBarMenu(); await toggleMenuItem("Context"); checkLegend(view, "Foo"); expect(getYAxisLabel(view)).toBe("Foo"); checkModeIs(view, "line"); }); test("correctly use group_by key from the context", async () => { Foo._records.at(-1).color_id = 1; const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, searchViewArch: /* xml */ ` `, context: { search_default_filter_with_context: 1, }, }); checkLabels(view, ["black", "red"]); checkLegend(view, "Foo"); checkModeIs(view, "line"); expect(getYAxisLabel(view)).toBe("Foo"); expect(getGraphModelMetaData(view).mode).toBe("line"); }); test("an invisible field should not be used as groupBy", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); checkLabels(view, ["Total"]); }); test("format values as float in case at least one value is not an number", async () => { Foo._records = [ { bar: false, revenue: 1.5 }, { bar: true, revenue: 2 }, ]; const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); checkDatasets(view, "data", { data: [1.5, 2] }); checkLabels(view, ["false", "true"]); checkTooltip(view, { title: "Revenue", lines: [{ label: "false", value: "1.50" }] }, 0); checkTooltip(view, { title: "Revenue", lines: [{ label: "true", value: "2.00" }] }, 1); }); test("the active measure description is the arch string attribute in priority", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); checkTooltip(view, { title: "FooFighters", lines: [{ label: "Total", value: "239" }] }, 0); await toggleMenu("Measures"); await toggleMenuItem("Nirvana"); checkTooltip(view, { title: "Nirvana", lines: [{ label: "Total", value: "23" }] }, 0); }); test("reload graph with correct fields", async () => { expect.assertions(2); onRpc("web_read_group", ({ kwargs }) => { expect(kwargs.fields).toEqual(["__count", "foo:sum"]); }); await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, searchViewArch: /* xml */ ` `, }); await toggleSearchBarMenu(); await toggleMenuItem("False Domain"); }); test("initial groupby is kept when reloading", async () => { expect.assertions(7); onRpc("web_read_group", ({ kwargs }) => { expect(kwargs.groupby).toEqual(["product_id"]); }); const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, searchViewArch: /* xml */ ` `, }); checkLabels(view, ["xphone", "xpad"]); checkLegend(view, "Foo"); checkDatasets(view, "data", { data: [82, 157] }); expect(getYAxisLabel(view)).toBe("Foo"); await toggleSearchBarMenu(); await toggleMenuItem("False Domain"); expect(".o_graph_canvas_container").toHaveCount(0); }); test("use a many2one as a measure should work (without groupBy)", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); checkLabels(view, ["Total"]); checkLegend(view, "Product"); checkDatasets(view, "data", { data: [2] }); expect(getYAxisLabel(view)).toBe("Product"); }); test("use a many2one as a measure should work (with groupBy)", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); checkLabels(view, ["false", "true"]); checkLegend(view, "Product"); checkDatasets(view, "data", { data: [2, 1] }); }); test("use a many2one as a measure and as a groupby should work", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); checkLabels(view, ["xphone", "xpad"]); checkLegend(view, "Product"); checkDatasets(view, "data", { data: [1, 1] }); expect(getYAxisLabel(view)).toBe("Product"); }); test("differentiate many2one values with same label", async () => { Product._records.push({ id: 300, name: "xphone" }); Foo._records.push({ product_id: 300 }); const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); checkLabels(view, ["xphone", "xpad", "xphone (2)"]); }); test("not use a many2one as a measure by default", async () => { await mountView({ type: "graph", resModel: "foo", viewId: false, }); await toggleMenu("Measures"); expect(queryAllTexts(".o-dropdown--menu .o_menu_item")).toEqual(["Foo", "Revenue", "Count"]); }); test.tags("desktop"); test("graph view crash when moving from search view using Down key", async () => { await mountView({ type: "graph", resModel: "foo" }); await contains(".o_searchview input").press("ArrowDown"); expect(".o_graph_view").toHaveCount(1); }); test("graph measures should be alphabetically sorted (exception: 'Count' is last)", async () => { Foo._fields.bouh = fields.Integer(); await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); await toggleMenu("Measures"); expect(queryAllTexts(".o-dropdown--menu .o_menu_item")).toEqual([ "Bouh", "Foo", "Revenue", "Count", ]); }); test("a many2one field can be added as measure in arch", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); checkLegend(view, "Product"); expect(getYAxisLabel(view)).toBe("Product"); }); test("non store fields defined on the arch are present in the measures", async () => { Foo._fields.revenue.store = false; await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); await toggleMenu("Measures"); expect(queryAllTexts(`.o_menu_item`)).toEqual(["Foo", "Revenue", "Count"]); }); test("graph view `graph_measure` field in context", async () => { const view = await mountView({ type: "graph", resModel: "foo", viewId: false, context: { graph_measure: "product_id", }, }); expect(getYAxisLabel(view)).toBe("Product"); checkLegend(view, "Product"); checkTooltip(view, { title: "Product", lines: [{ label: "Total", value: "2" }] }, 0); }); test("`graph_measure` in context is prefered to measure in arch", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, context: { graph_measure: "product_id", }, }); expect(getYAxisLabel(view)).toBe("Product"); checkLegend(view, "Product"); checkTooltip(view, { title: "Product", lines: [{ label: "Total", value: "2" }] }, 0); }); test("None should appear in bar, pie graph but not in line graph with multiple groupbys", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); const someNone = () => getChart(view).data.labels.some((l) => /none/i.test(l)); expect(someNone()).toBe(false); await selectMode("bar"); expect(someNone()).toBe(true); await selectMode("pie"); expect(someNone()).toBe(true); // None should not appear after switching back to line chart await selectMode("line"); expect(someNone()).toBe(false); }); test("an invisible field can not be found in the 'Measures' menu", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); checkTooltip(view, { lines: [{ label: "Total", value: "8" }] }, 0); await toggleMenu("Measures"); expect(".o_menu_item:contains(Revenue)").toHaveCount(0, { message: `"Revenue" can not be found in the "Measures" menu`, }); }); test("graph view only keeps finer groupby filter option for a given groupby", async () => { const view = await mountView({ type: "graph", resModel: "foo", groupBy: ["date:year", "product_id", "date", "date:quarter"], arch: /* xml */ ``, }); checkLabels(view, ["January 2016", "March 2016", "April 2016", "May 2016"]); checkLegend(view, ["xphone", "xpad"]); checkDatasets( view, ["label", "data"], [ { label: "xphone", data: [2, 2, 0, 0], }, { label: "xpad", data: [0, 0, 1, 1], }, ] ); }); test("action name is displayed in breadcrumbs", async () => { await mountWithCleanup(WebClient); await getService("action").doAction({ name: "Glou glou", res_model: "foo", type: "ir.actions.act_window", views: [[false, "graph"]], }); expect(".o_breadcrumb .active:first").toHaveText("Glou glou"); }); test("clicking on bar charts triggers a do_action", async () => { expect.assertions(6); mockService("action", { doAction(actionRequest, options) { expect(actionRequest).toEqual({ context: { allowed_company_ids: [1], lang: "en", tz: "taht", uid: 7 }, domain: [["bar", "=", false]], name: "Foo Analysis", res_model: "foo", target: "current", type: "ir.actions.act_window", views: [ [false, "list"], [false, "form"], ], }); expect(options).toEqual({ viewType: "list" }); }, }); const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); checkModeIs(view, "bar"); checkDatasets(view, ["domains"], { domains: [[["bar", "=", false]], [["bar", "=", true]]], }); await clickOnDataset(view); }); test("Clicking on bar charts removes group_by and search_default_* context keys", async () => { expect.assertions(2); mockService("action", { doAction(actionRequest, options) { expect(actionRequest).toEqual({ context: { allowed_company_ids: [1], lang: "en", tz: "taht", uid: 7 }, domain: [["bar", "=", false]], name: "Foo Analysis", res_model: "foo", target: "current", type: "ir.actions.act_window", views: [ [false, "list"], [false, "form"], ], }); expect(options).toEqual({ viewType: "list" }); }, }); const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, context: { search_default_user: 1, group_by: "bar", }, }); await clickOnDataset(view); }); test("clicking on a pie chart trigger a do_action with correct views", async () => { expect.assertions(6); Foo._views[["list", 364]] = /* xml */ ``; Foo._views[["form", 29]] = /* xml */ `
`; mockService("action", { doAction(actionRequest, options) { expect(actionRequest).toEqual({ context: { allowed_company_ids: [1], lang: "en", tz: "taht", uid: 7 }, domain: [["bar", "=", false]], name: "Foo Analysis", res_model: "foo", target: "current", type: "ir.actions.act_window", views: [ [364, "list"], [29, "form"], ], }); expect(options).toEqual({ viewType: "list" }); }, }); const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, config: { views: [ [364, "list"], [29, "form"], ], }, }); checkModeIs(view, "pie"); checkDatasets(view, ["domains"], { domains: [[["bar", "=", false]], [["bar", "=", true]]], }); await clickOnDataset(view); }); test('graph view with attribute disable_linking="1"', async () => { mockService("action", { doAction() { throw new Error("should not perform a `doAction`"); }, }); const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); checkModeIs(view, "bar"); checkDatasets(view, ["domains"], { domains: [[["bar", "=", false]], [["bar", "=", true]]], }); await clickOnDataset(view); }); test("graph view without invisible attribute on field", async () => { await mountView({ type: "graph", resModel: "foo", }); await toggleMenu("Measures"); expect(".o_menu_item").toHaveCount(3, { message: "there should be three menu items in the measures dropdown (count, revenue and foo)", }); expect(".o_menu_item:contains(Revenue)").toHaveCount(1); expect(".o_menu_item:contains(Foo)").toHaveCount(1); expect(".o_menu_item:contains(Count)").toHaveCount(1); }); test("graph view with invisible attribute on field", async () => { await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); await toggleMenu("Measures"); expect(".o_menu_item").toHaveCount(2, { message: "there should be only two menu items in the measures dropdown (count and foo)", }); expect(".o_menu_item:contains(Revenue)").toHaveCount(0); }); test("graph view sort by measure", async () => { // change last record from foo as there are 4 records count for each product Product._records.push({ id: 150, name: "zphone" }); Foo._records.at(-1).product_id = 150; const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); expect(".fa-sort-amount-asc").toHaveCount(1); expect(".fa-sort-amount-desc").toHaveCount(1); checkLegend(view, "Count", "measure should be by count"); expect(".fa-sort-amount-desc").toHaveClass("active"); checkDatasets(view, "data", { data: [4, 3, 1] }); await clickSort("asc"); expect(".fa-sort-amount-asc").toHaveClass("active"); checkDatasets(view, "data", { data: [1, 3, 4] }); await clickSort("desc"); expect(".fa-sort-amount-desc").toHaveClass("active"); checkDatasets(view, "data", { data: [4, 3, 1] }); // again click on descending button to deactivate order await clickSort("desc"); expect(".fa-sort-amount-desc").not.toHaveClass("active"); checkDatasets(view, "data", { data: [4, 1, 3] }); // set line mode await selectMode("line"); expect(".fa-sort-amount-asc").toHaveCount(1); expect(".fa-sort-amount-desc").toHaveCount(1); checkLegend(view, "Count", "measure should be by count"); expect(".fa-sort-amount-desc").not.toHaveClass("active"); checkDatasets(view, "data", { data: [4, 1, 3] }); await clickSort("asc"); expect(".fa-sort-amount-asc").toHaveClass("active"); checkDatasets(view, "data", { data: [1, 3, 4] }); await clickSort("desc"); expect(".fa-sort-amount-desc").toHaveClass("active"); checkDatasets(view, "data", { data: [4, 3, 1] }); }); test("graph view sort by measure for grouped data", async () => { // change last record from foo as there are 4 records count for each product Product._records.push({ id: 150, name: "zphone" }); Foo._records.at(-1).product_id = 150; const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); checkLegend(view, ["false", "true", "Sum"], "measure should be by count"); checkDatasets(view, "data", [{ data: [1, 1, 3] }, { data: [3, 0, 0] }, { data: [4, 1, 3] }]); await clickSort("asc"); expect(".fa-sort-amount-asc").toHaveClass("active"); checkDatasets(view, "data", [{ data: [1, 3, 1] }, { data: [0, 0, 3] }, { data: [1, 3, 4] }]); await clickSort("desc"); expect(".fa-sort-amount-desc").toHaveClass("active"); checkDatasets(view, "data", [{ data: [1, 3, 1] }, { data: [3, 0, 0] }, { data: [4, 3, 1] }]); // again click on descending button to deactivate order await clickSort("desc"); expect(".fa-sort-amount-desc").not.toHaveClass("active"); checkDatasets(view, "data", [{ data: [1, 1, 3] }, { data: [3, 0, 0] }, { data: [4, 1, 3] }]); }); test("graph view sort by measure for multiple grouped data", async () => { // change last record from foo as there are 4 records count for each product Product._records.push({ id: 150, name: "zphone" }); Foo._records.at(-1).product_id = 150; Foo._records.splice( 0, 4, { id: 1, foo: 48, bar: false, product_id: 200, date: "2016-04-01" }, { id: 2, foo: 49, bar: false, product_id: 200, date: "2016-04-01" }, { id: 3, foo: 50, bar: true, product_id: 100, date: "2016-01-03" }, { id: 4, foo: 50, bar: true, product_id: 200, date: "2016-01-03" } ); const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); checkLegend(view, ["xphone", "xpad", "zphone", "Sum"], "measure should be by count"); checkDatasets(view, "data", [ { data: [1, 0, 0, 0] }, { data: [1, 2, 1, 2] }, { data: [0, 1, 0, 0] }, { data: [2, 3, 1, 2] }, ]); await clickSort("asc"); expect(".fa-sort-amount-asc").toHaveClass("active"); checkDatasets(view, "data", [ { data: [1, 1, 2, 2] }, { data: [0, 1, 0, 0] }, { data: [0, 0, 0, 1] }, { data: [1, 2, 2, 3] }, ]); await clickSort("desc"); expect(".fa-sort-amount-desc").toHaveClass("active"); checkDatasets(view, "data", [ { data: [1, 0, 0, 0] }, { data: [2, 1, 2, 1] }, { data: [0, 1, 0, 0] }, { data: [3, 2, 2, 1] }, ]); // again click on descending button to deactivate order await clickSort("desc"); expect(".fa-sort-amount-desc").not.toHaveClass("active"); checkDatasets(view, "data", [ { data: [1, 0, 0, 0] }, { data: [1, 2, 1, 2] }, { data: [0, 1, 0, 0] }, { data: [2, 3, 1, 2] }, ]); }); test("empty graph view with sample data", async () => { await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, context: { search_default_false_domain: 1, }, searchViewArch: /* xml */ ` `, noContentHelp: /* xml */ `

click to add a foo

`, }); expect(".o_graph_view .o_content").toHaveClass("o_view_sample_data"); expect(".o_view_nocontent").toHaveCount(1); expect(".o_graph_canvas_container canvas").toHaveCount(1); await toggleSearchBarMenu(); await toggleMenuItem("False Domain"); expect(".o_graph_view .o_content").not.toHaveClass("o_view_sample_data"); expect(".o_view_nocontent").toHaveCount(0); expect(".o_graph_canvas_container canvas").toHaveCount(1); }); test("non empty graph view with sample data", async () => { await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, searchViewArch: /* xml */ ` `, noContentHelp: /* xml */ `

click to add a foo

`, }); expect(".o_content").not.toHaveClass("o_view_sample_data"); expect(".o_view_nocontent").toHaveCount(0); expect(".o_graph_canvas_container canvas").toHaveCount(1); await toggleSearchBarMenu(); await toggleMenuItem("False Domain"); expect(".o_content").not.toHaveClass("o_view_sample_data"); expect(".o_graph_canvas_container canvas").toHaveCount(0); expect(".o_view_nocontent").toHaveCount(1); }); test("empty graph view without sample data after filter", async () => { await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, domain: Domain.FALSE.toList(), noContentHelp: /* xml */ `

click to add a foo

`, }); expect(".o_graph_canvas_container canvas").toHaveCount(0); expect(".o_view_nocontent").toHaveCount(1); }); test.tags("desktop"); test("reload chart with switchView button keep internal state", async () => { Foo._views.list = /* xml */ ``; await mountWithCleanup(WebClient); await getService("action").doAction({ name: "Foo Action 1", res_model: "foo", type: "ir.actions.act_window", views: [ [false, "graph"], [false, "list"], ], }); expect(getModeButton("bar")).toHaveClass("active"); await selectMode("line"); expect(getModeButton("line")).toHaveClass("active"); await switchView("graph"); expect(getModeButton("line")).toHaveClass("active"); }); test("fallback on initial groupby when the groupby from control panel has 0 length", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, searchViewArch: /* xml */ ` `, context: { search_default_group_by_foo: 1, }, }); checkLabels(view, ["2", "3", "4", "24", "42", "48", "53", "63"]); await toggleSearchBarMenu(); await toggleMenuItem("Foo"); checkLabels(view, ["xphone", "xpad"]); }); test("change mode, stacked, or order via the graph buttons does not reload datapoints, change measure does", async () => { onRpc("web_read_group", ({ kwargs }) => { expect.step(kwargs.fields); }); const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); checkModeIs(view, "line"); await selectMode("bar"); checkModeIs(view, "bar"); expect(`[data-tooltip="Stacked"]`).toHaveClass("active"); await contains(`[data-tooltip="Stacked"]`).click(); expect(`[data-tooltip="Stacked"]`).not.toHaveClass("active"); expect(`[data-tooltip="Ascending"]`).not.toHaveClass("active"); await contains(`[data-tooltip="Ascending"]`).click(); expect(`[data-tooltip="Ascending"]`).toHaveClass("active"); await toggleMenu("Measures"); await toggleMenuItem("Foo"); expect.verifySteps([ ["__count"], // first load ["__count", "foo:sum"], // reload due to change in measure ]); }); test("concurrent reloads: add a filter, and directly toggle a measure", async () => { let def; onRpc("web_read_group", () => def); const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, searchViewArch: /* xml */ ` `, }); checkDatasets(view, ["data", "label"], { data: [4, 4], label: "Count", }); // Set a domain (this reload is delayed) def = new Deferred(); await toggleSearchBarMenu(); await toggleMenuItem("My Filter"); checkDatasets(view, ["data", "label"], { data: [4, 4], label: "Count", }); // Toggle a measure await toggleMenu("Measures"); await toggleMenuItem("Foo"); checkDatasets(view, ["data", "label"], { data: [4, 4], label: "Count", }); def.resolve(); await animationFrame(); checkDatasets(view, ["data", "label"], { data: [82, 4], label: "Foo", }); }); test("change graph mode while loading a filter", async () => { let def; onRpc("web_read_group", () => def); const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, searchViewArch: /* xml */ ` `, }); checkDatasets(view, ["data", "label"], { data: [4, 4], label: "Count", }); checkModeIs(view, "line"); // Set a domain (this reload is delayed) def = new Deferred(); await toggleSearchBarMenu(); await toggleMenuItem("My Filter"); checkDatasets(view, ["data", "label"], { data: [4, 4], label: "Count", }); checkModeIs(view, "line"); // Change graph mode await selectMode("bar"); checkDatasets(view, ["data", "label"], { data: [4, 4], label: "Count", }); checkModeIs(view, "line"); def.resolve(); await animationFrame(); checkDatasets(view, ["data", "label"], { data: [1], label: "Count", }); checkModeIs(view, "bar"); }); test("only process most recent data for concurrent groupby", async () => { let def; onRpc(() => def); const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, searchViewArch: /* xml */ ` `, }); checkLabels(view, ["xphone", "xpad"]); checkDatasets(view, "data", { data: [82, 157] }); def = new Deferred(); await toggleSearchBarMenu(); await toggleMenuItem("Color"); await toggleMenuItem("Color"); await toggleMenuItem("Date"); await toggleMenuItemOption("Date", "Month"); checkLabels(view, ["xphone", "xpad"]); checkDatasets(view, "data", { data: [82, 157] }); def.resolve(); await animationFrame(); checkLabels(view, ["January 2016", "March 2016", "April 2016", "May 2016", "None"]); checkDatasets(view, "data", { data: [56, 26, 48, 4, 105] }); }); test("fill_temporal is true by default", async () => { expect.assertions(1); onRpc("web_read_group", ({ kwargs }) => { expect(kwargs.context.fill_temporal).toBe(true, { message: "The observable state of fill_temporal should be true", }); }); await mountView({ type: "graph", resModel: "foo" }); }); test("fill_temporal can be changed throught the context", async () => { expect.assertions(1); onRpc("web_read_group", ({ kwargs }) => { expect(kwargs.context.fill_temporal).toBe(false, { message: "The observable state of fill_temporal should be false", }); }); await mountView({ type: "graph", resModel: "foo", context: { fill_temporal: false, }, }); }); test("fake data in line chart", async () => { mockDate("2020-05-19 01:00:00"); Foo._records = []; await mountView({ type: "graph", resModel: "foo", context: { search_default_date_filter: 1, }, arch: /* xml */ ` `, searchViewArch: /* xml */ ` `, }); await toggleSearchBarMenu(); await toggleMenuItem("Date: Previous period"); expect(".o_graph_canvas_container").toHaveCount(0); }); test("no filling color for period of comparison", async () => { mockDate("2020-05-19 01:00:00"); for (const record of Foo._records) { record.date = record.date?.replace(/^\d{4}/, "2019"); } const view = await mountView({ type: "graph", resModel: "foo", context: { search_default_date_filter: 1, }, arch: /* xml */ ` `, searchViewArch: /* xml */ ` `, }); await toggleSearchBarMenu(); await toggleMenuItem("Date: Previous period"); checkDatasets(view, "backgroundColor", { backgroundColor: "#a7d3f9" }); }); test("group by a non stored, sortable field", async () => { // When a field is non-stored but sortable it's inherited // from a stored field, so it can be sortable Foo._fields.date.store = false; const view = await mountView({ type: "graph", resModel: "foo", groupBy: ["date:month"], arch: /* xml */ ``, }); checkLabels(view, ["January 2016", "March 2016", "April 2016", "May 2016"]); }); test("graph_groupbys should be also used after first load", async () => { const view = await mountView({ type: "graph", resModel: "foo", viewId: false, groupBy: ["date:quarter"], irFilters: [ { user_id: [2, "Mitchell Admin"], name: "Favorite", id: 1, context: JSON.stringify({ group_by: [], graph_measure: "revenue", graph_mode: "bar", graph_groupbys: ["color_id"], }), sort: "[]", domain: "", is_default: false, model_id: "foo", action_id: false, }, ], }); checkModeIs(view, "bar"); checkLabels(view, ["Q1 2016", "Q2 2016", "None"]); checkLegend(view, "Count"); await toggleSearchBarMenu(); await toggleMenuItem("Favorite"); checkModeIs(view, "bar"); checkLabels(view, ["red", "None"]); checkLegend(view, "Revenue"); }); test("order='desc' on arch", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); checkDatasets(view, ["data", "label"], { data: [2, 2, 2, 1, 1], label: "Count", }); }); test("order='asc' on arch", async () => { const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); checkDatasets(view, ["data", "label"], { data: [1, 1, 2, 2, 2], label: "Count", }); }); test("In the middle of a year, a graph view grouped by a date field with granularity 'year' should have a single group of SampleServer.MAIN_RECORDSET_SIZE records", async () => { mockDate("2023-06-15 08:00:00"); const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, domain: Domain.FALSE.toList(), }); checkDatasets(view, ["data"], { data: [SampleServer.MAIN_RECORDSET_SIZE] }); }); test("no class 'o_view_sample_data' when real data are presented", async () => { Foo._records = []; const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); expect(".o_graph_view .o_view_sample_data").toHaveCount(1); expect(getChart(view).data.datasets.length).toBeGreaterThan(0); await selectMode("line"); expect(".o_graph_view .o_view_sample_data").toHaveCount(1); expect(getChart(view).data.datasets.length).toBeGreaterThan(0); await toggleMenu("Measures"); await toggleMenuItem("Revenue"); expect(".o_graph_view .o_view_sample_data").toHaveCount(0); expect(".o_graph_canvas_container").toHaveCount(0); }); test("single chart rendering on search", async () => { patchWithCleanup(GraphRenderer.prototype, { setup() { super.setup(); onRendered(() => expect.step("rendering")); }, }); await mountView({ type: "graph", resModel: "foo", }); expect.verifySteps(["rendering"]); await validateSearch(); expect.verifySteps(["rendering"]); }); test("apply default filter label", async () => { class CustomGraphModel extends graphView.Model { _getDefaultFilterLabel(fields) { return "None"; } } registry.category("views").add("custom_graph", { ...graphView, Model: CustomGraphModel, }); const view = await mountView({ type: "graph", resModel: "foo", arch: /* xml */ ` `, }); checkLabels(view, ["xphone", "xpad"]); checkLegend(view, ["red", "None", "Sum"]); await selectMode("line"); checkLabels(view, ["xphone", "xpad"]); checkLegend(view, ["red", "None"]); await selectMode("pie"); checkLabels(view, ["xphone / red", "xphone / None", "xpad / None"]); checkLegend(view, ["xphone / red", "xphone / None", "xpad / None"]); }); test("missing property field definition is fetched", async function () { Foo._fields.properties_definition = fields.PropertiesDefinition(); Foo._fields.parent_id = fields.Many2one({ relation: "foo" }); Foo._fields.properties = fields.Properties({ definition_record: "parent_id", definition_record_field: "properties_definition", }); onRpc(({ method, kwargs }) => { if (method === "web_read_group" && kwargs.groupby?.includes("properties.my_char")) { expect.step(JSON.stringify(kwargs.groupby)); return { groups: [ { "properties.my_char": false, __domain: [["properties.my_char", "=", false]], __count: 2, }, { "properties.my_char": "aaa", __domain: [["properties.my_char", "=", "aaa"]], __count: 1, }, ], length: 2, }; } else if (method === "get_property_definition") { return { name: "my_char", type: "char", }; } }); const view = await mountView({ type: "graph", resModel: "foo", arch: ``, irFilters: [ { user_id: [2, "Mitchell Admin"], name: "My Filter", id: 5, context: `{"group_by": ['properties.my_char']}`, sort: "[]", domain: "[]", is_default: true, model_id: "foo", action_id: false, }, ], }); expect.verifySteps([`["properties.my_char"]`]); checkLabels(view, ["None", "aaa"]); checkDatasets( view, ["data", "label"], [ { data: [2, 1], label: "Count", }, ] ); }); test("missing deleted property field definition is created", async function () { Foo._fields.properties_definition = fields.PropertiesDefinition(); Foo._fields.parent_id = fields.Many2one({ relation: "foo" }); Foo._fields.properties = fields.Properties({ definition_record: "parent_id", definition_record_field: "properties_definition", }); onRpc(({ method, kwargs }) => { if (method === "web_read_group" && kwargs.groupby?.includes("properties.my_char")) { expect.step(JSON.stringify(kwargs.groupby)); return { groups: [ { "properties.my_char": false, __domain: [["properties.my_char", "=", false]], __count: 2, }, { "properties.my_char": "aaa", __domain: [["properties.my_char", "=", "aaa"]], __count: 1, }, ], length: 2, }; } else if (method === "get_property_definition") { return {}; } }); const view = await mountView({ type: "graph", resModel: "foo", arch: ``, irFilters: [ { user_id: [2, "Mitchell Admin"], name: "My Filter", id: 5, context: `{"group_by": ['properties.my_char']}`, sort: "[]", domain: "[]", is_default: true, model_id: "foo", action_id: false, }, ], }); expect.verifySteps([`["properties.my_char"]`]); checkLabels(view, ["None", "aaa"]); checkDatasets( view, ["data", "label"], [ { data: [2, 1], label: "Count", }, ] ); });