Files
odoo_source/addons/web/static/tests/views/graph_tests.js
T
Dhruv PatelandAaron Bohy ae610d0e20 [IMP] web: graph: drill down to records in bar/pie charts
After this commit, in the graph view, when the user clicks on parts
of the bar and pie charts, it opens the list view of the concerned
records (like what was already done in the pivot view).

This is the default behavior, but it can be deactivated by setting
the attribute 'disable_linking' to true in the view.

Task 2198461

closes odoo/odoo#47796

Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
Co-authored-by: Aaron Bohy <aab@odoo.com>
2020-03-31 19:15:10 +00:00

1667 lines
62 KiB
JavaScript

odoo.define('web.graph_view_tests', function (require) {
"use strict";
var searchUtils = require('web.searchUtils');
var GraphView = require('web.GraphView');
var testUtils = require('web.test_utils');
const cpHelpers = testUtils.controlPanel;
var createActionManager = testUtils.createActionManager;
var createView = testUtils.createView;
var patchDate = testUtils.mock.patchDate;
const { INTERVAL_OPTIONS, TIME_RANGE_OPTIONS, COMPARISON_TIME_RANGE_OPTIONS } = searchUtils;
var INTERVAL_OPTION_IDS = Object.keys(INTERVAL_OPTIONS);
var TIME_RANGE_OPTION_IDS = Object.keys(TIME_RANGE_OPTIONS);
var COMPARISON_TIME_RANGE_OPTION_IDS = Object.keys(COMPARISON_TIME_RANGE_OPTIONS);
var f = (a, b) => [].concat(...a.map(d => b.map(e => [].concat(d, e))));
var cartesian = (a, b, ...c) => (b ? cartesian(f(a, b), ...c) : a);
var COMBINATIONS = cartesian(TIME_RANGE_OPTION_IDS, COMPARISON_TIME_RANGE_OPTION_IDS);
var COMBINATIONS_WITH_DATE = cartesian(TIME_RANGE_OPTION_IDS, COMPARISON_TIME_RANGE_OPTION_IDS, INTERVAL_OPTION_IDS);
QUnit.assert.checkDatasets = function (graph, keys, expectedDatasets) {
keys = keys instanceof Array ? keys : [keys];
expectedDatasets = expectedDatasets instanceof Array ?
expectedDatasets :
[expectedDatasets];
var datasets = graph.renderer.chart.data.datasets;
var actualValues = datasets.map(dataset => _.pick(dataset, keys));
this.pushResult({
result: _.isEqual(actualValues, expectedDatasets),
actual: actualValues,
expected: expectedDatasets,
});
};
QUnit.assert.checkLabels = function (graph, expectedLabels) {
var labels = graph.renderer.chart.data.labels;
this.pushResult({
result: _.isEqual(labels, expectedLabels),
actual: labels,
expected: expectedLabels,
});
};
QUnit.assert.checkLegend = function (graph, expectedLegendLabels) {
expectedLegendLabels = expectedLegendLabels instanceof Array ?
expectedLegendLabels :
[expectedLegendLabels];
var chart = graph.renderer.chart;
var actualLegendLabels = chart.config.options.legend.labels.generateLabels(chart).map(o => o.text);
this.pushResult({
result: _.isEqual(actualLegendLabels, expectedLegendLabels),
actual: actualLegendLabels,
expected: expectedLegendLabels,
});
};
QUnit.module('Views', {
beforeEach: function () {
this.data = {
foo: {
fields: {
foo: {string: "Foo", type: "integer", store: true},
bar: {string: "bar", type: "boolean"},
product_id: {string: "Product", type: "many2one", relation: 'product', store: true},
color_id: {string: "Color", type: "many2one", relation: 'color'},
date: {string: "Date", type: 'date', store: true, sortable: true},
},
records: [
{id: 1, foo: 3, bar: true, product_id: 37, date: "2016-01-01"},
{id: 2, foo: 53, bar: true, product_id: 37, color_id: 7, date: "2016-01-03"},
{id: 3, foo: 2, bar: true, product_id: 37, date: "2016-03-04"},
{id: 4, foo: 24, bar: false, product_id: 37, date: "2016-03-07"},
{id: 5, foo: 4, bar: false, product_id: 41, date: "2016-05-01"},
{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"},
]
},
product: {
fields: {
name: {string: "Product Name", type: "char"}
},
records: [{
id: 37,
display_name: "xphone",
}, {
id: 41,
display_name: "xpad",
}]
},
color: {
fields: {
name: {string: "Color", type: "char"}
},
records: [{
id: 7,
display_name: "red",
}, {
id: 14,
display_name: "black",
}]
},
};
}
}, function () {
QUnit.module('GraphView');
QUnit.test('simple graph rendering', async function (assert) {
assert.expect(5);
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Partners">' +
'<field name="bar"/>' +
'</graph>',
});
assert.containsOnce(graph, 'div.o_graph_canvas_container canvas',
"should contain a div with a canvas element");
assert.strictEqual(graph.renderer.state.mode, "bar",
"should be in bar chart mode by default");
assert.checkLabels(graph, [[true], [false]]);
assert.checkDatasets(graph,
['backgroundColor', 'data', 'label', 'originIndex', 'stack'],
{
backgroundColor: "#1f77b4",
data: [3,5],
label: "Count",
originIndex: 0,
stack: "",
}
);
assert.checkLegend(graph, 'Count');
graph.destroy();
});
QUnit.test('default type attribute', async function (assert) {
assert.expect(1);
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Partners" type="pie">' +
'<field name="bar"/>' +
'</graph>',
});
assert.strictEqual(graph.renderer.state.mode, "pie", "should be in pie chart mode by default");
graph.destroy();
});
QUnit.test('title attribute', async function (assert) {
assert.expect(1);
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph title="Partners" type="pie">' +
'<field name="bar"/>' +
'</graph>',
});
assert.strictEqual(graph.$('.o_graph_renderer label').text(), "Partners",
"should have 'Partners as title'");
graph.destroy();
});
QUnit.test('field id not in groupBy', async function (assert) {
assert.expect(1);
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Partners">' +
'<field name="id"/>' +
'</graph>',
mockRPC: function (route, args) {
if (args.method === 'read_group') {
assert.deepEqual(args.kwargs.groupby, [],
'groupby should not contain id field');
}
return this._super.apply(this, arguments);
},
});
graph.destroy();
});
QUnit.test('switching mode', async function (assert) {
assert.expect(6);
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Partners" type="line">' +
'<field name="bar"/>' +
'</graph>',
});
assert.strictEqual(graph.renderer.state.mode, "line", "should be in line chart mode by default");
assert.doesNotHaveClass(graph.$buttons.find('button[data-mode="bar"]'), 'active',
'bar type button should not be active');
assert.hasClass(graph.$buttons.find('button[data-mode="line"]'),'active',
'line type button should be active');
await testUtils.dom.click(graph.$buttons.find('button[data-mode="bar"]'));
assert.strictEqual(graph.renderer.state.mode, "bar", "should be in bar chart mode by default");
assert.doesNotHaveClass(graph.$buttons.find('button[data-mode="line"]'), 'active',
'line type button should not be active');
assert.hasClass(graph.$buttons.find('button[data-mode="bar"]'),'active',
'bar type button should be active');
graph.destroy();
});
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.
this.data.foo.records = this.data.foo.records.slice(0,1);
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Partners" type="line">' +
'<field name="bar"/>' +
'</graph>',
});
assert.containsOnce(graph, 'canvas', "should have a canvas");
graph.destroy();
});
QUnit.test('displaying chart data with multiple 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);
var graph = await createView({
View: GraphView,
model: 'foo',
data: this.data,
arch: '<graph type="bar"><field name="foo" /></graph>',
groupBy: ['product_id', 'bar', 'color_id'],
});
assert.checkLabels(graph, [['xphone'], ['xpad']]);
assert.checkLegend(graph, ['true/Undefined', 'true/red', 'false/Undefined']);
await testUtils.dom.click(graph.$buttons.find('button[data-mode="line"]'));
assert.checkLabels(graph, [['xphone'], ['xpad']]);
assert.checkLegend(graph, ['true/Undefined', 'true/red', 'false/Undefined']);
await testUtils.dom.click(graph.$buttons.find('button[data-mode="pie"]'));
assert.checkLabels(graph, [
["xphone", true, "Undefined"],
["xphone", true,"red"],
["xphone", false, "Undefined"],
["xpad", false, "Undefined"]
]);
assert.checkLegend(graph, [
'xphone/true/Undefined',
'xphone/true/red',
'xphone/false/Undefined',
'xpad/false/Undefined'
]);
graph.destroy();
});
QUnit.test('switching measures', async function (assert) {
assert.expect(2);
var rpcCount = 0;
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Gloups">' +
'<field name="product_id"/>' +
'</graph>',
mockRPC: function (route, args) {
rpcCount++;
return this._super(route, args);
},
});
await cpHelpers.toggleMenu(graph, "Measures");
await cpHelpers.toggleMenuItem(graph, "Foo");
assert.checkLegend(graph, 'Foo');
assert.strictEqual(rpcCount, 2, "should have done 2 rpcs (2 readgroups)");
graph.destroy();
});
QUnit.test('no content helper (bar chart)', async function (assert) {
assert.expect(2);
this.data.foo.records = [];
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Gloups">' +
'<field name="product_id"/>' +
'</graph>',
});
assert.containsNone(graph, 'div.o_graph_canvas_container canvas',
"should not contain a div with a canvas element");
assert.containsOnce(graph, 'div.o_view_nocontent',
"should display the no content helper");
graph.destroy();
});
QUnit.test('no content helper (pie chart)', async function (assert) {
assert.expect(2);
this.data.foo.records = [];
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph type="pie">' +
'<field name="product_id"/>' +
'</graph>',
});
assert.containsNone(graph, 'div.o_graph_canvas_container canvas',
"should not contain a div with a canvas element");
assert.containsOnce(graph, 'div.o_view_nocontent',
"should display the no content helper");
graph.destroy();
});
QUnit.test('render pie chart in comparison mode', async function (assert) {
assert.expect(2);
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
context: {
time_ranges: {
field: 'date',
range: 'this_quarter',
comparisonRange: 'previous_period',
}
},
arch: '<graph type="pie">' +
'<field name="product_id"/>' +
'</graph>',
});
assert.containsNone(graph, 'div.o_view_nocontent',
"should not display the no content helper");
assert.checkLegend(graph, 'No data');
graph.destroy();
});
QUnit.test('no content helper after update', async function (assert) {
assert.expect(4);
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Gloups">' +
'<field name="product_id"/>' +
'</graph>',
});
assert.containsOnce(graph, 'div.o_graph_canvas_container canvas',
"should contain a div with a canvas element");
assert.containsNone(graph, 'div.o_view_nocontent',
"should not display the no content helper");
await testUtils.graph.reload(graph, {domain: [['product_id', '=', 4]]});
assert.containsNone(graph, 'div.o_graph_canvas_container canvas',
"should not contain a div with a canvas element");
assert.containsOnce(graph, 'div.o_view_nocontent',
"should display the no content helper");
graph.destroy();
});
QUnit.test('can reload with other group by', async function (assert) {
assert.expect(2);
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Gloups">' +
'<field name="product_id"/>' +
'</graph>',
});
assert.checkLabels(graph, [['xphone'], ['xpad']]);
await testUtils.graph.reload(graph, {groupBy: ['color_id']});
assert.checkLabels(graph, [['Undefined'], ['red']]);
graph.destroy();
});
QUnit.test('getOwnedQueryParams correctly returns mode, measure, and groupbys', async function (assert) {
assert.expect(4);
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Gloups">' +
'<field name="product_id"/>' +
'</graph>',
});
assert.deepEqual(graph.getOwnedQueryParams(), {
context: {
graph_mode: 'bar',
graph_measure: '__count__',
graph_groupbys: ['product_id'],
}
}, "context should be correct");
await cpHelpers.toggleMenu(graph, "Measures");
await cpHelpers.toggleMenuItem(graph, "Foo");
assert.deepEqual(graph.getOwnedQueryParams(), {
context: {
graph_mode: 'bar',
graph_measure: 'foo',
graph_groupbys: ['product_id'],
},
}, "context should be correct");
await testUtils.dom.click(graph.$buttons.find('button[data-mode="line"]'));
assert.deepEqual(graph.getOwnedQueryParams(), {
context: {
graph_mode: 'line',
graph_measure: 'foo',
graph_groupbys: ['product_id'],
},
}, "context should be correct");
await testUtils.graph.reload(graph, {groupBy: ['product_id', 'color_id']}); // change groupbys
assert.deepEqual(graph.getOwnedQueryParams(), {
context: {
graph_mode: 'line',
graph_measure: 'foo',
graph_groupbys: ['product_id', 'color_id'],
},
}, "context should be correct");
graph.destroy();
});
QUnit.test('correctly uses graph_ keys from the context', async function (assert) {
assert.expect(5);
var lastOne = _.last(this.data.foo.records);
lastOne.color_id = 14;
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph><field name="product_id"/></graph>',
viewOptions: {
context: {
graph_measure: 'foo',
graph_mode: 'line',
graph_groupbys: ['color_id'],
},
},
});
// check measure name is present in legend
assert.checkLegend(graph, 'Foo');
// check mode
assert.strictEqual(graph.renderer.state.mode, "line", "should be in line chart mode");
assert.doesNotHaveClass(graph.$buttons.find('button[data-mode="bar"]'), 'active',
'bar chart button should not be active');
assert.hasClass(graph.$buttons.find('button[data-mode="line"]'),'active',
'line chart button should be active');
// check groupby values ('Undefined' is rejected in line chart) are in labels
assert.checkLabels(graph, [['red'], ['black']]);
graph.destroy();
});
QUnit.test('correctly use group_by key from the context', async function (assert) {
assert.expect(1);
var lastOne = _.last(this.data.foo.records);
lastOne.color_id = 14;
var graph = await createView({
View: GraphView,
model: 'foo',
data: this.data,
arch: '<graph><field name="product_id" /></graph>',
groupBy: ['color_id'],
viewOptions: {
context: {
graph_measure: 'foo',
graph_mode: 'line',
},
},
});
// check groupby values ('Undefined' is rejected in line chart) are in labels
assert.checkLabels(graph, [['red'], ['black']]);
graph.destroy();
});
QUnit.test('correctly uses graph_ keys from the context (at reload)', async function (assert) {
assert.expect(7);
var lastOne = _.last(this.data.foo.records);
lastOne.color_id = 14;
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph><field name="product_id"/></graph>',
});
assert.strictEqual(graph.renderer.state.mode, "bar", "should be in bar chart mode");
assert.hasClass(graph.$buttons.find('button[data-mode="bar"]'),'active',
'bar chart button should be active');
var reloadParams = {
context: {
graph_measure: 'foo',
graph_mode: 'line',
graph_groupbys: ['color_id'],
},
};
await testUtils.graph.reload(graph, reloadParams);
// check measure
assert.checkLegend(graph, 'Foo');
// check mode
assert.strictEqual(graph.renderer.state.mode, "line", "should be in line chart mode");
assert.doesNotHaveClass(graph.$buttons.find('button[data-mode="bar"]'), 'active',
'bar chart button should not be active');
assert.hasClass(graph.$buttons.find('button[data-mode="line"]'),'active',
'line chart button should be active');
// check groupby values ('Undefined' is rejected in line chart) are in labels
assert.checkLabels(graph, [['red'], ['black']]);
graph.destroy();
});
QUnit.test('reload graph with correct fields', async function (assert) {
assert.expect(2);
var graph = await createView({
View: GraphView,
model: 'foo',
data: this.data,
arch: '<graph>' +
'<field name="product_id" type="row"/>' +
'<field name="foo" type="measure"/>' +
'</graph>',
mockRPC: function (route, args) {
if (args.method === 'read_group') {
assert.deepEqual(args.kwargs.fields, ['product_id', 'foo'],
"should read the correct fields");
}
return this._super.apply(this, arguments);
},
});
await testUtils.graph.reload(graph, {groupBy: []});
graph.destroy();
});
QUnit.test('initial groupby is kept when reloading', async function (assert) {
assert.expect(8);
var graph = await createView({
View: GraphView,
model: 'foo',
data: this.data,
arch: '<graph>' +
'<field name="product_id" type="row"/>' +
'<field name="foo" type="measure"/>' +
'</graph>',
mockRPC: function (route, args) {
if (args.method === 'read_group') {
assert.deepEqual(args.kwargs.groupby, ['product_id'],
"should group by the correct field");
}
return this._super.apply(this, arguments);
},
});
assert.checkLabels(graph, [['xphone'], ['xpad']]);
assert.checkLegend(graph, 'Foo');
assert.checkDatasets(graph, 'data', {data: [82, 157]});
await testUtils.graph.reload(graph, {groupBy: []});
assert.checkLabels(graph, [['xphone'], ['xpad']]);
assert.checkLegend(graph, 'Foo');
assert.checkDatasets(graph, 'data', {data: [82, 157]});
graph.destroy();
});
QUnit.test('use a many2one as a measure should work (without groupBy)', async function (assert) {
assert.expect(4);
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Partners">' +
'<field name="product_id" type="measure"/>' +
'</graph>',
});
assert.containsOnce(graph, 'div.o_graph_canvas_container canvas',
"should contain a div with a canvas element");
assert.checkLabels(graph, [[]]);
assert.checkLegend(graph, 'Product');
assert.checkDatasets(graph, 'data', {data: [2]});
graph.destroy();
});
QUnit.test('use a many2one as a measure should work (with groupBy)', async function (assert) {
assert.expect(5);
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Partners">' +
'<field name="bar" type="row"/>' +
'<field name="product_id" type="measure"/>' +
'</graph>',
});
assert.containsOnce(graph, 'div.o_graph_canvas_container canvas',
"should contain a div with a canvas element");
assert.strictEqual(graph.renderer.state.mode, "bar",
"should be in bar chart mode by default");
assert.checkLabels(graph, [[true], [false]]);
assert.checkLegend(graph, 'Product');
assert.checkDatasets(graph, 'data', {data: [1, 2]});
graph.destroy();
});
QUnit.test('use a many2one as a measure and as a groupby should work', async function (assert) {
assert.expect(3);
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Partners">' +
'<field name="product_id" type="row"/>' +
'</graph>',
viewOptions: {
additionalMeasures: ['product_id'],
},
});
// need to set the measure this way because it cannot be set in the
// arch.
await cpHelpers.toggleMenu(graph, "Measures");
await cpHelpers.toggleMenuItem(graph, "Product");
assert.checkLabels(graph, [['xphone'], ['xpad']]);
assert.checkLegend(graph, 'Product');
assert.checkDatasets(graph, 'data', {data: [1, 1]});
graph.destroy();
});
QUnit.test('not use a many2one as a measure by default', async function (assert) {
assert.expect(1);
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Partners">' +
'<field name="product_id"/>' +
'</graph>',
});
assert.notOk(graph.measures.product_id,
"should not have product_id as measure");
graph.destroy();
});
QUnit.test('use a many2one as a measure if set as additional fields', async function (assert) {
assert.expect(1);
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Partners">' +
'<field name="product_id"/>' +
'</graph>',
viewOptions: {
additionalMeasures: ['product_id'],
},
});
assert.ok(graph.measures.find(m => m.fieldName === 'product_id'),
"should have product_id as measure");
graph.destroy();
});
QUnit.test('graph view crash when moving from search view using Down key', async function (assert) {
assert.expect(1);
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Partners" type="pie">' +
'<field name="bar"/>' +
'</graph>',
});
graph.renderer.giveFocus();
assert.ok(true,"should not generate any error");
graph.destroy();
});
QUnit.test('graph measures should be alphabetically sorted', async function (assert) {
assert.expect(2);
var data = this.data;
data.foo.fields.bouh = {string: "bouh", type: "integer"};
var graph = await createView({
View: GraphView,
model: "foo",
data: data,
arch: '<graph string="Partners">' +
'<field name="foo" type="measure"/>' +
'<field name="bouh" type="measure"/>' +
'</graph>',
});
await cpHelpers.toggleMenu(graph, "Measures");
assert.strictEqual(graph.$buttons.find('.o_graph_measures_list .dropdown-item:first').text(), 'bouh',
"Bouh should be the first measure");
assert.strictEqual(graph.$buttons.find('.o_graph_measures_list .dropdown-item:last').text(), 'Count',
"Count should be the last measure");
graph.destroy();
});
QUnit.test('Undefined should appear in bar, pie graph but not in line graph', async function (assert) {
assert.expect(3);
var graph = await createView({
View: GraphView,
model: "foo",
groupBy:['date'],
data: this.data,
arch: '<graph string="Partners" type="line">' +
'<field name="bar"/>' +
'</graph>',
});
function _indexOf (label) {
return graph.renderer._indexOf(graph.renderer.chart.data.labels, label);
}
assert.strictEqual(_indexOf(['Undefined']), -1);
await testUtils.dom.click(graph.$buttons.find('.o_graph_button[data-mode=bar]'));
assert.ok(_indexOf(['Undefined']) >= 0);
await testUtils.dom.click(graph.$buttons.find('.o_graph_button[data-mode=pie]'));
assert.ok(_indexOf(['Undefined']) >= 0);
graph.destroy();
});
QUnit.test('Undefined should appear in bar, pie graph but not in line graph with multiple groupbys', async function (assert) {
assert.expect(4);
var graph = await createView({
View: GraphView,
model: "foo",
groupBy:['date', 'color_id'],
data: this.data,
arch: '<graph string="Partners" type="line">' +
'<field name="bar"/>' +
'</graph>',
});
function _indexOf (label) {
return graph.renderer._indexOf(graph.renderer.chart.data.labels, label);
}
assert.strictEqual(_indexOf(['Undefined']), -1);
await testUtils.dom.click(graph.$buttons.find('.o_graph_button[data-mode=bar]'));
assert.ok(_indexOf(['Undefined']) >= 0);
await testUtils.dom.click(graph.$buttons.find('.o_graph_button[data-mode=pie]'));
var labels = graph.renderer.chart.data.labels;
assert.ok(labels.filter(label => /Undefined/.test(label.join(''))).length >= 1);
// Undefined should not appear after switching back to line chart
await testUtils.dom.click(graph.$buttons.find('.o_graph_button[data-mode=line]'));
assert.strictEqual(_indexOf(['Undefined']), -1);
graph.destroy();
});
QUnit.test('no comparison and no groupby', async function (assert) {
assert.expect(9);
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Partners" type="bar">' +
'<field name="foo" type="measure"/>' +
'</graph>',
});
assert.checkLabels(graph, [[]]);
assert.checkLegend(graph, 'Foo');
assert.checkDatasets(graph, 'data', {data: [239]});
await testUtils.dom.click(graph.$('.o_graph_button[data-mode=line]'));
// the labels in line chart is translated in this case to avoid to have a single
// point at the left of the screen and chart to seem empty.
assert.checkLabels(graph, [[''], [], ['']]);
assert.checkLegend(graph, 'Foo');
assert.checkDatasets(graph, 'data', {data: [undefined, 239]});
await testUtils.dom.click(graph.$('.o_graph_button[data-mode=pie]'));
assert.checkLabels(graph, [[]]);
assert.checkLegend(graph, 'Total');
assert.checkDatasets(graph, 'data', {data: [239]});
graph.destroy();
});
QUnit.test('no comparison and one groupby', async function (assert) {
assert.expect(9);
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Partners" type="bar">' +
'<field name="foo" type="measure"/>' +
'<field name="bar" type="row"/>' +
'</graph>',
});
assert.checkLabels(graph, [[true], [false]]);
assert.checkLegend(graph, 'Foo');
assert.checkDatasets(graph, 'data', {data: [58, 181]});
await testUtils.dom.click(graph.$('.o_graph_button[data-mode=line]'));
assert.checkLabels(graph, [[true], [false]]);
assert.checkLegend(graph, 'Foo');
assert.checkDatasets(graph, 'data', {data: [58, 181]});
await testUtils.dom.click(graph.$('.o_graph_button[data-mode=pie]'));
assert.checkLabels(graph, [[true], [false]]);
assert.checkLegend(graph, ['true', 'false']);
assert.checkDatasets(graph, 'data', {data: [58, 181]});
graph.destroy();
});
QUnit.test('no comparison and two groupby', async function (assert) {
assert.expect(9);
var graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Partners" type="bar">' +
'<field name="foo" type="measure"/>' +
'</graph>',
groupBy: ['product_id', 'color_id'],
});
assert.checkLabels(graph, [['xphone'], ['xpad']]);
assert.checkLegend(graph, ['Undefined', 'red']);
assert.checkDatasets(graph, ['label', 'data'], [
{
label: 'Undefined',
data: [29, 157],
},
{
label: 'red',
data: [53, 0],
}
]);
await testUtils.dom.click(graph.$('.o_graph_button[data-mode=line]'));
assert.checkLabels(graph, [['xphone'], ['xpad']]);
assert.checkLegend(graph, ['Undefined', 'red']);
assert.checkDatasets(graph, ['label', 'data'], [
{
label: 'Undefined',
data: [29, 157],
},
{
label: 'red',
data: [53, 0],
}
]);
await testUtils.dom.click(graph.$('.o_graph_button[data-mode=pie]'));
assert.checkLabels(graph, [['xphone', 'Undefined'], ['xphone', 'red'], ['xpad', 'Undefined']]);
assert.checkLegend(graph, ['xphone/Undefined', 'xphone/red', 'xpad/Undefined']);
assert.checkDatasets(graph, ['label', 'data'], {
label: '',
data: [29, 53, 157],
});
graph.destroy();
});
QUnit.test('graph view only keeps finer groupby filter option for a given groupby', async function (assert) {
assert.expect(3);
var graph = await createView({
View: GraphView,
model: "foo",
groupBy:['date:year','product_id', 'date', 'date:quarter'],
data: this.data,
arch: '<graph string="Partners" type="line">' +
'<field name="bar"/>' +
'</graph>',
});
assert.checkLabels(graph, [["January 2016"], ["March 2016"], ["May 2016"], ["April 2016"]]);
// mockReadGroup does not always sort groups -> May 2016 is before April 2016 for that reason.
assert.checkLegend(graph, ["xphone","xpad"]);
assert.checkDatasets(graph, ['label', 'data'], [
{
label: 'xphone',
data: [2, 2, 0, 0],
}, {
label: 'xpad',
data: [0, 0, 1, 1],
}
]);
graph.destroy();
});
QUnit.test('clicking on bar and pie charts triggers a do_action', async function (assert) {
assert.expect(5);
const graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Foo Analysis"><field name="bar"/></graph>',
intercepts: {
do_action: function (ev) {
assert.deepEqual(ev.data.action, {
context: {},
domain: [["bar", "=", true]],
name: "Foo Analysis",
res_model: "foo",
target: 'current',
type: 'ir.actions.act_window',
view_mode: 'list',
views: [[false, 'list'], [false, 'form']],
}, "should trigger do_action with correct action parameter");
}
},
});
await testUtils.nextTick(); // wait for the graph to be rendered
// bar mode
assert.strictEqual(graph.renderer.state.mode, "bar", "should be in bar chart mode");
assert.checkDatasets(graph, ['domain'], {
domain: [[["bar", "=", true]], [["bar", "=", false]]],
});
let myChart = graph.renderer.chart;
let meta = myChart.getDatasetMeta(0);
let rectangle = myChart.canvas.getBoundingClientRect();
let point = meta.data[0].getCenterPoint();
await testUtils.dom.triggerEvent(myChart.canvas, 'click', {
pageX: rectangle.left + point.x,
pageY: rectangle.top + point.y
});
// pie mode
await testUtils.dom.click(graph.$('.o_graph_button[data-mode=pie]'));
assert.strictEqual(graph.renderer.state.mode, "pie", "should be in pie chart mode");
myChart = graph.renderer.chart;
meta = myChart.getDatasetMeta(0);
rectangle = myChart.canvas.getBoundingClientRect();
point = meta.data[0].getCenterPoint();
await testUtils.dom.triggerEvent(myChart.canvas, 'click', {
pageX: rectangle.left + point.x,
pageY: rectangle.top + point.y
});
graph.destroy();
});
QUnit.test('clicking charts trigger a do_action with correct views', async function (assert) {
assert.expect(3);
const graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph string="Foo Analysis"><field name="bar"/></graph>',
intercepts: {
do_action: function (ev) {
assert.deepEqual(ev.data.action, {
context: {},
domain: [["bar", "=", true]],
name: "Foo Analysis",
res_model: "foo",
target: 'current',
type: 'ir.actions.act_window',
view_mode: 'list',
views: [[364, 'list'], [29, 'form']],
}, "should trigger do_action with correct action parameter");
}
},
viewOptions: {
actionViews: [{
type: 'list',
viewID: 364,
}, {
type: 'form',
viewID: 29,
}],
},
});
await testUtils.nextTick(); // wait for the graph to be rendered
assert.strictEqual(graph.renderer.state.mode, "bar", "should be in bar chart mode");
assert.checkDatasets(graph, ['domain'], {
domain: [[["bar", "=", true]], [["bar", "=", false]]],
});
let myChart = graph.renderer.chart;
let meta = myChart.getDatasetMeta(0);
let rectangle = myChart.canvas.getBoundingClientRect();
let point = meta.data[0].getCenterPoint();
await testUtils.dom.triggerEvent(myChart.canvas, 'click', {
pageX: rectangle.left + point.x,
pageY: rectangle.top + point.y
});
graph.destroy();
});
QUnit.test('graph view with attribute disable_linking="True"', async function (assert) {
assert.expect(2);
const graph = await createView({
View: GraphView,
model: "foo",
data: this.data,
arch: '<graph disable_linking="1"><field name="bar"/></graph>',
intercepts: {
do_action: function () {
throw new Error('Should not perform a do_action');
},
},
});
await testUtils.nextTick(); // wait for the graph to be rendered
assert.strictEqual(graph.renderer.state.mode, "bar", "should be in bar chart mode");
assert.checkDatasets(graph, ['domain'], {
domain: [[["bar", "=", true]], [["bar", "=", false]]],
});
let myChart = graph.renderer.chart;
let meta = myChart.getDatasetMeta(0);
let rectangle = myChart.canvas.getBoundingClientRect();
let point = meta.data[0].getCenterPoint();
await testUtils.dom.triggerEvent(myChart.canvas, 'click', {
pageX: rectangle.left + point.x,
pageY: rectangle.top + point.y
});
graph.destroy();
});
QUnit.module('GraphView: comparison mode', {
beforeEach: async function () {
this.data.foo.records[0].date = '2016-12-15';
this.data.foo.records[1].date = '2016-12-17';
this.data.foo.records[2].date = '2016-11-22';
this.data.foo.records[3].date = '2016-11-03';
this.data.foo.records[4].date = '2016-12-20';
this.data.foo.records[5].date = '2016-12-19';
this.data.foo.records[6].date = '2016-12-15';
this.data.foo.records[7].date = undefined;
this.data.foo.records.push({id: 9, foo: 48, bar: false, product_id: 41, color_id: 7, date: "2016-12-01"});
this.data.foo.records.push({id: 10, foo: 17, bar: true, product_id: 41, color_id: 7, date: "2016-12-01"});
this.data.foo.records.push({id: 11, foo: 45, bar: true, product_id: 37, color_id: 14, date: "2016-12-01"});
this.data.foo.records.push({id: 12, foo: 48, bar: false, product_id: 37, color_id: 7, date: "2016-12-10"});
this.data.foo.records.push({id: 13, foo: 48, bar: false, product_id: undefined, color_id: 14, date: "2016-11-30"});
this.data.foo.records.push({id: 14, foo: -50, bar: true, product_id: 41, color_id: 14, date: "2016-12-15"});
this.data.foo.records.push({id: 15, foo: 53, bar: false, product_id: 41, color_id: 14, date: "2016-11-01"});
this.data.foo.records.push({id: 16, foo: 53, bar: true, product_id: undefined, color_id: 7, date: "2016-09-01"});
this.data.foo.records.push({id: 17, foo: 48, bar: false, product_id: 41, color_id: undefined, date: "2016-08-01"});
this.data.foo.records.push({id: 18, foo: -156, bar: false, product_id: 37, color_id: undefined, date: "2016-07-15"});
this.data.foo.records.push({id: 19, foo: 31, bar: false, product_id: 41, color_id: 14, date: "2016-12-15"});
this.data.foo.records.push({id: 20, foo: 109, bar: true, product_id: 41, color_id: 7, date: "2015-06-01"});
this.unpatchDate = patchDate(2016, 11, 20, 1, 0, 0);
var checkOnlyToCheck = true;
var exhaustiveTest = false || checkOnlyToCheck;
var self = this;
async function* graphGenerator(combinations) {
var i = 0;
while (i < combinations.length) {
var combination = combinations[i];
if (!checkOnlyToCheck || combination.toString() in self.combinationsToCheck) {
await self.setConfig(combination);
}
if (exhaustiveTest) {
i++;
} else {
i += Math.floor(1 + Math.random() * 20);
}
yield combination;
}
}
this.combinationsToCheck = {};
this.testCombinations = async function (combinations, assert) {
for await (var combination of graphGenerator(combinations)) {
// we can check particular combinations here
if (combination.toString() in self.combinationsToCheck) {
var graph = self.actionManager.getCurrentController().widget;
if (self.combinationsToCheck[combination].errorMessage) {
assert.strictEqual(
graph.$('.o_nocontent_help p').eq(1).text().trim(),
self.combinationsToCheck[combination].errorMessage
);
} else {
assert.checkLabels(graph, self.combinationsToCheck[combination].labels);
assert.checkLegend(graph, self.combinationsToCheck[combination].legend);
assert.checkDatasets(graph, ['label', 'data'], self.combinationsToCheck[combination].datasets);
}
}
}
};
const GROUPBY_NAMES = ['Date', 'Bar', 'Product', 'Color'];
// time range menu is assumed to be closed
this.selectTimeRanges = async function (timeRangeOption, comparisonTimeRangeOption) {
comparisonTimeRangeOption = comparisonTimeRangeOption || 'previous_period';
await cpHelpers.toggleTimeRangeMenu(document);
await cpHelpers.selectRange(document, timeRangeOption);
if (!document.querySelector('div.o_time_range_section input').checked) {
await cpHelpers.toggleTimeRangeMenuBox(document);
}
await cpHelpers.selectComparisonRange(document, comparisonTimeRangeOption);
await cpHelpers.applyTimeRange(document);
await cpHelpers.toggleTimeRangeMenu(document);
};
// groupby menu is assumed to be closed
this.selectDateIntervalOption = async function (intervalOption) {
intervalOption = intervalOption || 'month';
const optionIndex = INTERVAL_OPTION_IDS.indexOf(intervalOption);
await cpHelpers.toggleGroupByMenu(document);
let wasSelected = false;
if (this.keepFirst) {
if (cpHelpers.isItemSelected(document, 2)) {
wasSelected = true;
await cpHelpers.toggleMenuItem(document, 2);
}
}
await cpHelpers.toggleMenuItem(document, 0);
if (!cpHelpers.isOptionSelected(document, 0, optionIndex)) {
await cpHelpers.toggleMenuItemOption(document, 0, optionIndex);
}
for (let i = 0; i < INTERVAL_OPTION_IDS.length; i++) {
const oId = INTERVAL_OPTION_IDS[i];
if (oId !== intervalOption && cpHelpers.isOptionSelected(document, 0, i)) {
await cpHelpers.toggleMenuItemOption(document, 0, i);
}
}
if (this.keepFirst) {
if (wasSelected && !cpHelpers.isItemSelected(document, 2)) {
await cpHelpers.toggleMenuItem(document, 2);
}
}
await cpHelpers.toggleGroupByMenu(document);
};
// groupby menu is assumed to be closed
this.selectGroupBy = async function (groupByName) {
await cpHelpers.toggleGroupByMenu(document);
const index = GROUPBY_NAMES.indexOf(groupByName);
if (!cpHelpers.isItemSelected(document, index)) {
await cpHelpers.toggleMenuItem(document, index);
}
await cpHelpers.toggleGroupByMenu(document);
};
this.setConfig = async function (combination) {
await this.selectTimeRanges(combination[0], combination[1]);
if (combination.length === 3) {
await self.selectDateIntervalOption(combination[2]);
}
};
this.setMode = async function (mode) {
await testUtils.dom.click($(`.o_control_panel .o_graph_button[data-mode="${mode}"]`));
};
// // create an action manager to test the interactions with the search view
this.actionManager = await createActionManager({
data: this.data,
archs: {
'foo,false,graph': '<graph string="Partners" type="bar">' +
'<field name="foo" type="measure"/>' +
'</graph>',
'foo,false,search': '<search>' +
'<filter name="date" string="Date" context="{\'group_by\': \'date\'}"/>' +
'<filter name="bar" string="Bar" context="{\'group_by\': \'bar\'}"/>' +
'<filter name="product_id" string="Product" context="{\'group_by\': \'product_id\'}"/>' +
'<filter name="color_id" string="Color" context="{\'group_by\': \'color_id\'}"/>' +
'</search>',
},
});
await this.actionManager.doAction({
res_model: 'foo',
type: 'ir.actions.act_window',
views: [[false, 'graph']],
flags: {
graph: {
additionalMeasures: ['product_id'],
}
},
});
},
afterEach: function () {
this.unpatchDate();
this.actionManager.destroy();
},
}, function () {
QUnit.test('comparison with one groupby equal to comparison date field', async function (assert) {
assert.expect(10);
this.combinationsToCheck = {
'last_30_days,previous_period,day': {
labels: [...Array(7).keys()].map(x => [x]),
legend: ["Last 30 Days", "Previous Period"],
datasets: [
{
data: [26, 53, 2, 63, 110, 48, 48],
label: "Last 30 Days",
},
{
data: [24, 53],
label: "Previous Period",
}
],
}
};
var combinations = COMBINATIONS_WITH_DATE;
await this.testCombinations(combinations, assert);
await this.setMode('line');
await this.testCombinations(combinations, assert);
this.combinationsToCheck['last_30_days,previous_period,day'] = {
labels: [...Array(7).keys()].map(x => [x]),
legend: [
"2016-12-15,2016-11-03",
"2016-12-17,2016-11-01",
"2016-11-22",
"2016-12-19",
"2016-12-01",
"2016-12-10",
"2016-11-30",
],
datasets: [
{
data: [26, 53, 2, 63, 110, 48, 48],
label: "Last 30 Days",
},
{
data: [24, 53, 0, 0, 0, 0, 0],
label: "Previous Period",
}
],
};
await this.setMode('pie');
await this.testCombinations(combinations, assert);
assert.ok(true, "No combination causes a crash");
});
QUnit.test('comparison with no groupby', async function (assert) {
assert.expect(10);
this.combinationsToCheck = {
'last_30_days,previous_period': {
labels: [[]],
legend: ["Last 30 Days", "Previous Period"],
datasets: [
{
data: [350],
label: "Last 30 Days",
},
{
data: [77],
label: "Previous Period",
}
],
}
};
var combinations = COMBINATIONS;
await this.testCombinations(combinations, assert);
this.combinationsToCheck['last_30_days,previous_period'] = {
labels: [[''], [], ['']],
legend: ["Last 30 Days", "Previous Period"],
datasets: [
{
data: [undefined, 350],
label: "Last 30 Days",
},
{
data: [undefined, 77],
label: "Previous Period",
}
],
};
await this.setMode('line');
await this.testCombinations(combinations, assert);
this.combinationsToCheck['last_30_days,previous_period'] = {
labels: [[]],
legend: ["Total"],
datasets: [
{
data: [350],
label: "Last 30 Days",
},
{
data: [77],
label: "Previous Period",
}
],
};
await this.setMode('pie');
await this.testCombinations(combinations, assert);
assert.ok(true, "No combination causes a crash");
});
QUnit.test('comparison with one groupby different from comparison date field', async function (assert) {
assert.expect(10);
this.combinationsToCheck = {
'last_30_days,previous_period': {
labels: [["xphone"],["xpad"],["Undefined"]],
legend: ["Last 30 Days", "Previous Period"],
datasets: [
{
data: [151, 151, 48],
label: "Last 30 Days",
},
{
data: [24, 53, 0],
label: "Previous Period",
}
],
}
};
var combinations = COMBINATIONS;
await this.selectGroupBy('Product');
await this.testCombinations(combinations, assert);
this.combinationsToCheck['last_30_days,previous_period'] = {
labels: [["xphone"],["xpad"]],
legend: ["Last 30 Days", "Previous Period"],
datasets: [
{
data: [151, 151],
label: "Last 30 Days",
},
{
data: [24, 53],
label: "Previous Period",
}
],
};
await this.setMode('line');
await this.testCombinations(combinations, assert);
this.combinationsToCheck['last_30_days,previous_period'] = {
labels: [["xphone"],["xpad"],["Undefined"]],
legend: ["xphone", "xpad", "Undefined"],
datasets: [
{
data: [151, 151, 48],
label: "Last 30 Days",
},
{
data: [24, 53, 0],
label: "Previous Period",
}
],
};
await this.setMode('pie');
await this.testCombinations(combinations, assert);
assert.ok(true, "No combination causes a crash");
});
QUnit.test('comparison with two groupby with first groupby equal to comparison date field', async function (assert) {
assert.expect(10);
this.keepFirst = true;
this.combinationsToCheck = {
'last_7_days,previous_period,day': {
labels: [...Array(3).keys()].map(x => [x]),
legend: [
"Last 7 Days/xphone",
"Last 7 Days/xpad",
"Previous Period/xphone"
],
datasets: [
{
data: [3, 53, 0],
label: "Last 7 Days/xphone",
},
{
data: [23, 0, 63],
label: "Last 7 Days/xpad",
},
{
data: [48],
label: "Previous Period/xphone",
}
],
}
};
var combinations = COMBINATIONS_WITH_DATE;
await this.selectGroupBy('Product');
await this.testCombinations(combinations, assert);
await this.setMode('line');
await this.testCombinations(combinations, assert);
this.combinationsToCheck['last_7_days,previous_period,day'] = {
labels: [[0,"xphone"], [1,"xphone"], [2, "xpad"], [0, "xpad"]],
legend: [
"2016-12-15,2016-12-10/xphone",
"2016-12-17/xphone",
"2016-12-19/xpad",
"2016-12-15,2016-12-10/xpad"
],
datasets: [
{
data: [3, 53, 63, 23],
label: "Last 7 Days",
},
{
data: [48, 0, 0, 0],
label: "Previous Period",
}
],
};
await this.setMode('pie');
await this.testCombinations(combinations, assert);
assert.ok(true, "No combination causes a crash");
this.keepFirst = false;
});
QUnit.test('comparison with two groupby with second groupby equal to comparison date field', async function (assert) {
assert.expect(8);
this.combinationsToCheck = {
'this_year,previous_period,quarter': {
labels: [["xphone"], ["xpad"],["Undefined"]],
legend: [
"This Year/Q4 2016",
"This Year/Q3 2016",
"Previous Period/Q2 2015"
],
datasets: [
{
data: [175, 208, 48],
label: "This Year/Q4 2016",
},
{
data: [-156, 48, 53],
label: "This Year/Q3 2016",
},
{
data: [0, 109, 0],
label: "Previous Period/Q2 2015",
},
]
}
};
var combinations = COMBINATIONS_WITH_DATE;
await this.selectGroupBy('Product');
await this.testCombinations(combinations, assert);
this.combinationsToCheck['this_year,previous_period,quarter'] = {
labels: [["xphone"], ["xpad"]],
legend: [
"This Year/Q4 2016",
"This Year/Q3 2016",
"Previous Period/Q2 2015"
],
datasets: [
{
data: [175, 208],
label: "This Year/Q4 2016",
},
{
data: [-156, 48],
label: "This Year/Q3 2016",
},
{
data: [0, 109],
label: "Previous Period/Q2 2015",
},
]
};
await this.setMode('line');
await this.testCombinations(combinations, assert);
this.combinationsToCheck['this_year,previous_period,quarter'] = {
errorMessage: 'Pie chart cannot mix positive and negative numbers. ' +
'Try to change your domain to only display positive results'
};
await this.setMode('pie');
await this.testCombinations(combinations, assert);
assert.ok(true, "No combination causes a crash");
});
QUnit.test('comparison with two groupby with no groupby equal to comparison date field', async function (assert) {
assert.expect(10);
this.combinationsToCheck = {
'last_month,previous_year': {
labels: [["xphone"],["Undefined"], ["xpad"]],
legend: ["Last Month/true", "Last Month/false"],
datasets: [
{
data: [2, 0, 0],
label: "Last Month/true",
},
{
data: [24, 48, 53],
label: "Last Month/false",
}
],
}
};
var combinations = COMBINATIONS;
await this.selectGroupBy('Product');
await this.selectGroupBy('Bar');
await this.testCombinations(combinations, assert);
this.combinationsToCheck['last_month,previous_year'] = {
labels: [["xphone"], ["xpad"]],
legend: ["Last Month/true", "Last Month/false"],
datasets: [
{
data: [2, 0],
label: "Last Month/true",
},
{
data: [24, 53],
label: "Last Month/false",
}
],
};
await this.setMode('line');
await this.testCombinations(combinations, assert);
this.combinationsToCheck['last_month,previous_year'] = {
labels: [
["xphone", true],
["xphone", false],
["Undefined", false],
["xpad", false],
["No data"]
],
legend: [
"xphone/true",
"xphone/false",
"Undefined/false",
"xpad/false",
"No data"
],
datasets: [
{
data: [2, 24, 48, 53],
label: "Last Month",
},
{
data: [undefined, undefined, undefined, undefined, 1],
label: "Previous Year",
}
],
};
await this.setMode('pie');
await this.testCombinations(combinations, assert);
assert.ok(true, "No combination causes a crash");
});
});
});
});