In this commit, we extend the search view to allow the selection of some intervals for date or datetime fields in groupby menu. The valid intervals are day, week, month, quarter, and year. It is also possible to add a button 'Group By' in the control panel owned by a graph view in case it is declared 'embedded' (e.g. in a dashboard view). Note that have three new generic reusable widgets have been introduced in this commit: DropdownMenu, GroupByMenu, and FiltersMenu.
628 lines
23 KiB
JavaScript
628 lines
23 KiB
JavaScript
odoo.define('web.graph_tests', function (require) {
|
|
"use strict";
|
|
|
|
var concurrency = require('web.concurrency');
|
|
var GraphView = require('web.GraphView');
|
|
var testUtils = require('web.test_utils');
|
|
|
|
var createView = testUtils.createView;
|
|
|
|
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'},
|
|
},
|
|
records: [
|
|
{id: 1, foo: 3, bar: true, product_id: 37},
|
|
{id: 2, foo: 53, bar: true, product_id: 37, color_id: 7},
|
|
{id: 3, foo: 2, bar: true, product_id: 37},
|
|
{id: 4, foo: 24, bar: false, product_id: 37},
|
|
{id: 5, foo: 4, bar: false, product_id: 41},
|
|
{id: 6, foo: 63, bar: false, product_id: 41},
|
|
{id: 7, foo: 42, bar: false, product_id: 41},
|
|
]
|
|
},
|
|
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', function (assert) {
|
|
assert.expect(4);
|
|
|
|
var graph = createView({
|
|
View: GraphView,
|
|
model: "foo",
|
|
data: this.data,
|
|
arch: '<graph string="Partners">' +
|
|
'<field name="bar"/>' +
|
|
'</graph>',
|
|
});
|
|
|
|
var done = assert.async();
|
|
return concurrency.delay(0).then(function () {
|
|
assert.strictEqual(graph.$('div.o_graph_svg_container svg.nvd3-svg').length, 1,
|
|
"should contain a div with a svg element");
|
|
|
|
assert.strictEqual(graph.renderer.state.mode, "bar",
|
|
"should be in bar chart mode by default");
|
|
|
|
// here, we would like to test the svg in the dom. However, it is
|
|
// not so easy, because there is an animation which means that we
|
|
// don't really have a nice way to find the proper rect elements.
|
|
// So, instead we will do some white box testing.
|
|
assert.strictEqual(graph.model.chart.data[0].value, 3,
|
|
"should have first datapoint with value 3");
|
|
assert.strictEqual(graph.model.chart.data[1].value, 4,
|
|
"should have first datapoint with value 4");
|
|
graph.destroy();
|
|
done();
|
|
});
|
|
});
|
|
|
|
QUnit.test('default type attribute', function (assert) {
|
|
assert.expect(1);
|
|
|
|
var graph = 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('switching mode', function (assert) {
|
|
assert.expect(6);
|
|
|
|
var graph = 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.notOk(graph.$buttons.find('button[data-mode="bar"]').hasClass('active'),
|
|
'bar type button should not be active');
|
|
assert.ok(graph.$buttons.find('button[data-mode="line"]').hasClass('active'),
|
|
'line type button should be active');
|
|
graph.$buttons.find('button[data-mode="bar"]').click();
|
|
assert.strictEqual(graph.renderer.state.mode, "bar", "should be in bar chart mode by default");
|
|
assert.notOk(graph.$buttons.find('button[data-mode="line"]').hasClass('active'),
|
|
'line type button should not be active');
|
|
assert.ok(graph.$buttons.find('button[data-mode="bar"]').hasClass('active'),
|
|
'bar type button should be active');
|
|
graph.destroy();
|
|
});
|
|
|
|
QUnit.test('displaying line chart with only 1 data point', function (assert) {
|
|
assert.expect(2);
|
|
|
|
// this test makes sure the line chart does not crash when only one data
|
|
// point is displayed. This was the case since a line cannot be drawn with
|
|
// only one point of reference.
|
|
var done = assert.async();
|
|
this.data.foo.records = this.data.foo.records.slice(0,1);
|
|
var graph = createView({
|
|
View: GraphView,
|
|
model: "foo",
|
|
data: this.data,
|
|
arch: '<graph string="Partners" type="line">' +
|
|
'<field name="bar"/>' +
|
|
'</graph>',
|
|
});
|
|
return concurrency.delay(0).then(function () {
|
|
assert.ok(!graph.$('svg').length, "should not have a svg");
|
|
assert.ok(graph.$('.o_view_nocontent').length, "should have an error message");
|
|
graph.destroy();
|
|
done();
|
|
});
|
|
});
|
|
|
|
QUnit.test('switching measures', function (assert) {
|
|
var done = assert.async();
|
|
assert.expect(4);
|
|
|
|
var rpcCount = 0;
|
|
|
|
var graph = 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);
|
|
},
|
|
});
|
|
return concurrency.delay(0).then(function () {
|
|
assert.ok(graph.$('text.nv-legend-text:contains(Count)').length,
|
|
"should have used the correct measure");
|
|
assert.ok(graph.$buttons.find('li[data-field="foo"]').length,
|
|
"should have foo in the list of measures");
|
|
graph.$buttons.find('li[data-field="foo"] a').click();
|
|
|
|
return concurrency.delay(0);
|
|
}).then(function () {
|
|
assert.ok(graph.$('text.nv-legend-text:contains(Foo)').length,
|
|
"should now use the Foo measure");
|
|
assert.strictEqual(rpcCount, 2, "should have done 2 rpcs (2 readgroups)");
|
|
graph.destroy();
|
|
done();
|
|
});
|
|
});
|
|
|
|
QUnit.test('no content helper', function (assert) {
|
|
var done = assert.async();
|
|
assert.expect(4);
|
|
|
|
var graph = createView({
|
|
View: GraphView,
|
|
model: "foo",
|
|
data: this.data,
|
|
arch: '<graph string="Gloups">' +
|
|
'<field name="product_id"/>' +
|
|
'</graph>',
|
|
});
|
|
return concurrency.delay(0).then(function () {
|
|
assert.ok(graph.$('div.o_graph_svg_container svg.nvd3-svg').length,
|
|
"should contain a div with a svg element");
|
|
assert.notOk(graph.$('div.o_view_nocontent').length,
|
|
"should not display the no content helper");
|
|
graph.update({domain: [['product_id', '=', 4]]});
|
|
|
|
assert.notOk(graph.$('div.o_graph_svg_container svg.nvd3-svg').length,
|
|
"should not contain a div with a svg element");
|
|
assert.ok(graph.$('div.o_view_nocontent').length,
|
|
"should display the no content helper");
|
|
graph.destroy();
|
|
done();
|
|
});
|
|
});
|
|
|
|
QUnit.test('can reload with other group by', function (assert) {
|
|
var done = assert.async();
|
|
assert.expect(4);
|
|
|
|
var graph = createView({
|
|
View: GraphView,
|
|
model: "foo",
|
|
data: this.data,
|
|
arch: '<graph string="Gloups">' +
|
|
'<field name="product_id"/>' +
|
|
'</graph>',
|
|
});
|
|
return concurrency.delay(0).then(function () {
|
|
assert.ok(graph.$('text:contains(xphone)').length,
|
|
"should contain a text element with product in legend");
|
|
assert.notOk(graph.$('text:contains(red)').length,
|
|
"should not contain a text element with color in legend");
|
|
|
|
graph.update({groupBy: ['color_id']});
|
|
|
|
return concurrency.delay(0);
|
|
}).then(function () {
|
|
assert.notOk(graph.$('text:contains(xphone)').length,
|
|
"should not contain a text element with product in legend");
|
|
assert.ok(graph.$('text:contains(red)').length,
|
|
"should contain a text element with color in legend");
|
|
graph.destroy();
|
|
done();
|
|
});
|
|
});
|
|
|
|
QUnit.test('getContext correctly returns mode, measure, groupbys and interval mapping', function (assert) {
|
|
var done = assert.async();
|
|
assert.expect(4);
|
|
|
|
var graph = createView({
|
|
View: GraphView,
|
|
model: "foo",
|
|
data: this.data,
|
|
arch: '<graph string="Gloups">' +
|
|
'<field name="product_id"/>' +
|
|
'</graph>',
|
|
});
|
|
return concurrency.delay(0).then(function () {
|
|
assert.deepEqual(graph.getContext(), {
|
|
graph_mode: 'bar',
|
|
graph_measure: '__count__',
|
|
graph_groupbys: ['product_id'],
|
|
graph_intervalMapping: {},
|
|
}, "context should be correct");
|
|
|
|
graph.$buttons.find('li[data-field="foo"] a').click(); // change measure
|
|
|
|
return concurrency.delay(0);
|
|
}).then(function () {
|
|
assert.deepEqual(graph.getContext(), {
|
|
graph_mode: 'bar',
|
|
graph_measure: 'foo',
|
|
graph_groupbys: ['product_id'],
|
|
graph_intervalMapping: {},
|
|
}, "context should be correct");
|
|
|
|
graph.$buttons.find('button[data-mode="line"]').click(); // change mode
|
|
|
|
return concurrency.delay(0);
|
|
}).then(function () {
|
|
assert.deepEqual(graph.getContext(), {
|
|
graph_mode: 'line',
|
|
graph_measure: 'foo',
|
|
graph_groupbys: ['product_id'],
|
|
graph_intervalMapping: {},
|
|
}, "context should be correct");
|
|
|
|
graph.update({groupBy: ['product_id', 'color_id']}); // change groupbys
|
|
|
|
return concurrency.delay(0);
|
|
}).then(function () {
|
|
assert.deepEqual(graph.getContext(), {
|
|
graph_mode: 'line',
|
|
graph_measure: 'foo',
|
|
graph_groupbys: ['product_id', 'color_id'],
|
|
graph_intervalMapping: {},
|
|
}, "context should be correct");
|
|
|
|
graph.destroy();
|
|
done();
|
|
});
|
|
});
|
|
|
|
QUnit.test('correctly uses graph_ keys from the context', function (assert) {
|
|
var done = assert.async();
|
|
assert.expect(6);
|
|
|
|
var graph = 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'],
|
|
},
|
|
},
|
|
});
|
|
return concurrency.delay(0).then(function () {
|
|
// check measure
|
|
assert.strictEqual(graph.$('text.nv-legend-text:contains(Foo)').length, 1,
|
|
"should now use the 'foo' measure");
|
|
|
|
// check mode
|
|
assert.strictEqual(graph.renderer.state.mode, "line", "should be in line chart mode");
|
|
assert.notOk(graph.$buttons.find('button[data-mode="bar"]').hasClass('active'),
|
|
'bar chart button should not be active');
|
|
assert.ok(graph.$buttons.find('button[data-mode="line"]').hasClass('active'),
|
|
'line chart button should be active');
|
|
|
|
// check groupbys
|
|
assert.strictEqual(graph.$('text:contains(xphone)').length, 0,
|
|
"should not contain a text element with product in legend");
|
|
assert.strictEqual(graph.$('text:contains(red)').length, 1,
|
|
"should contain a text element with color in legend");
|
|
|
|
graph.destroy();
|
|
done();
|
|
});
|
|
});
|
|
|
|
QUnit.test('correctly use group_by key from the context', function (assert) {
|
|
var done = assert.async();
|
|
assert.expect(2);
|
|
|
|
var graph = 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',
|
|
},
|
|
},
|
|
});
|
|
return concurrency.delay(0).then(function () {
|
|
assert.strictEqual(graph.$('text:contains(xphone)').length, 0,
|
|
'should not contain a text element with product in legend');
|
|
assert.strictEqual(graph.$('text:contains(red)').length, 1,
|
|
'should contain a text element with color in legend');
|
|
graph.destroy();
|
|
done();
|
|
});
|
|
});
|
|
|
|
QUnit.test('correctly uses graph_ keys from the context (at reload)', function (assert) {
|
|
var done = assert.async();
|
|
assert.expect(8);
|
|
|
|
var graph = 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.ok(graph.$buttons.find('button[data-mode="bar"]').hasClass('active'),
|
|
'bar chart button should be active');
|
|
|
|
var reloadParams = {
|
|
context: {
|
|
graph_measure: 'foo',
|
|
graph_mode: 'line',
|
|
graph_groupbys: ['color_id'],
|
|
},
|
|
};
|
|
graph.reload(reloadParams);
|
|
return concurrency.delay(0).then(function () {
|
|
// check measure
|
|
assert.strictEqual(graph.$('text.nv-legend-text:contains(Foo)').length, 1,
|
|
"should now use the 'foo' measure");
|
|
|
|
// check mode
|
|
assert.strictEqual(graph.renderer.state.mode, "line", "should be in line chart mode");
|
|
assert.notOk(graph.$buttons.find('button[data-mode="bar"]').hasClass('active'),
|
|
'bar chart button should not be active');
|
|
assert.ok(graph.$buttons.find('button[data-mode="line"]').hasClass('active'),
|
|
'line chart button should be active');
|
|
|
|
// check groupbys
|
|
assert.strictEqual(graph.$('text:contains(xphone)').length, 0,
|
|
"should not contain a text element with product in legend");
|
|
assert.strictEqual(graph.$('text:contains(red)').length, 1,
|
|
"should contain a text element with color in legend");
|
|
|
|
graph.destroy();
|
|
done();
|
|
});
|
|
});
|
|
|
|
QUnit.test('reload graph with correct fields', function (assert) {
|
|
assert.expect(2);
|
|
|
|
var graph = 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);
|
|
},
|
|
});
|
|
|
|
graph.reload({groupBy: []});
|
|
|
|
graph.destroy();
|
|
});
|
|
|
|
QUnit.test('initial groupby is kept when reloading', function (assert) {
|
|
var done = assert.async();
|
|
assert.expect(4);
|
|
|
|
var graph = 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);
|
|
},
|
|
});
|
|
|
|
|
|
return concurrency.delay(0).then(function () {
|
|
assert.strictEqual(graph.$('.nv-groups rect').length, 2,
|
|
"should display two groups");
|
|
|
|
graph.reload({groupBy: []});
|
|
return concurrency.delay(0).then(function () {
|
|
assert.strictEqual(graph.$('.nv-groups rect').length, 2,
|
|
"should still display two groups");
|
|
|
|
graph.destroy();
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
QUnit.test('use a many2one as a measure should work (without groupBy)', function (assert) {
|
|
assert.expect(3);
|
|
|
|
var graph = createView({
|
|
View: GraphView,
|
|
model: "foo",
|
|
data: this.data,
|
|
arch: '<graph string="Partners">' +
|
|
'<field name="product_id" type="measure"/>' +
|
|
'</graph>',
|
|
});
|
|
var done = assert.async();
|
|
return concurrency.delay(0).then(function () {
|
|
assert.strictEqual(graph.$('div.o_graph_svg_container svg.nvd3-svg').length, 1,
|
|
"should contain a div with a svg element");
|
|
|
|
assert.strictEqual(graph.renderer.state.mode, "bar",
|
|
"should be in bar chart mode by default");
|
|
assert.strictEqual(graph.model.chart.data[0].value, 2,
|
|
"should have a datapoint with value 2");
|
|
graph.destroy();
|
|
done();
|
|
});
|
|
});
|
|
|
|
QUnit.test('use a many2one as a measure should work (with groupBy)', function (assert) {
|
|
assert.expect(4);
|
|
|
|
var graph = createView({
|
|
View: GraphView,
|
|
model: "foo",
|
|
data: this.data,
|
|
arch: '<graph string="Partners">' +
|
|
'<field name="bar" type="row"/>' +
|
|
'<field name="product_id" type="measure"/>' +
|
|
'</graph>',
|
|
});
|
|
var done = assert.async();
|
|
return concurrency.delay(0).then(function () {
|
|
assert.strictEqual(graph.$('div.o_graph_svg_container svg.nvd3-svg').length, 1,
|
|
"should contain a div with a svg element");
|
|
|
|
assert.strictEqual(graph.renderer.state.mode, "bar",
|
|
"should be in bar chart mode by default");
|
|
assert.strictEqual(graph.model.chart.data[0].value, 1,
|
|
"should have first datapoint with value 2");
|
|
assert.strictEqual(graph.model.chart.data[1].value, 2,
|
|
"should have second datapoint with value 2");
|
|
graph.destroy();
|
|
done();
|
|
});
|
|
});
|
|
|
|
QUnit.test('use a many2one as a measure and as a groupby should work', function (assert) {
|
|
assert.expect(2);
|
|
|
|
var graph = createView({
|
|
View: GraphView,
|
|
model: "foo",
|
|
data: this.data,
|
|
arch: '<graph string="Partners">' +
|
|
'<field name="product_id" type="row"/>' +
|
|
'</graph>',
|
|
viewOptions: {
|
|
additionalMeasures: ['product_id'],
|
|
},
|
|
});
|
|
var done = assert.async();
|
|
return concurrency.delay(0).then(function () {
|
|
// need to set the measure this way because it cannot be set in the
|
|
// arch.
|
|
graph.$buttons.find('li[data-field="product_id"] a').click();
|
|
|
|
assert.strictEqual(graph.model.chart.data[0].value, 1,
|
|
"should have first datapoint with value 1");
|
|
assert.strictEqual(graph.model.chart.data[1].value, 1,
|
|
"should have second datapoint with value 1");
|
|
|
|
graph.destroy();
|
|
done();
|
|
});
|
|
});
|
|
|
|
QUnit.test('not use a many2one as a measure by default', function (assert) {
|
|
assert.expect(1);
|
|
|
|
var graph = createView({
|
|
View: GraphView,
|
|
model: "foo",
|
|
data: this.data,
|
|
arch: '<graph string="Partners">' +
|
|
'<field name="product_id"/>' +
|
|
'</graph>',
|
|
});
|
|
var done = assert.async();
|
|
return concurrency.delay(0).then(function () {
|
|
assert.notOk(graph.measures.product_id,
|
|
"should not have product_id as measure");
|
|
graph.destroy();
|
|
done();
|
|
});
|
|
});
|
|
|
|
QUnit.test('use a many2one as a measure if set as additional fields', function (assert) {
|
|
assert.expect(1);
|
|
|
|
var graph = createView({
|
|
View: GraphView,
|
|
model: "foo",
|
|
data: this.data,
|
|
arch: '<graph string="Partners">' +
|
|
'<field name="product_id"/>' +
|
|
'</graph>',
|
|
viewOptions: {
|
|
additionalMeasures: ['product_id'],
|
|
},
|
|
});
|
|
var done = assert.async();
|
|
return concurrency.delay(0).then(function () {
|
|
assert.ok(graph.measures.product_id,
|
|
"should have product_id as measure");
|
|
graph.destroy();
|
|
done();
|
|
});
|
|
});
|
|
|
|
QUnit.test('graph view crash when moving from search view using Down key', function (assert) {
|
|
assert.expect(1);
|
|
|
|
var graph = 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();
|
|
});
|
|
});
|
|
|
|
});
|