odoo.define('web_gantt.tests', function (require) { 'use strict'; const { browser } = require("@web/core/browser/browser"); const Domain = require('web.Domain'); var GanttView = require('web_gantt.GanttView'); var GanttRenderer = require('web_gantt.GanttRenderer'); var GanttRow = require('web_gantt.GanttRow'); const SampleServer = require('web.SampleServer'); const session = require("web.session"); var testUtils = require('web.test_utils'); const { createWebClient, doAction } = require('@web/../tests/webclient/helpers'); const { registerCleanup } = require("@web/../tests/helpers/cleanup"); const { click, editInput, getFixture, patchTimeZone, patchWithCleanup } = require('@web/../tests/helpers/utils'); const { prepareWowlFormViewDialogs } = require("@web/../tests/views/helpers"); const patchDate = testUtils.mock.patchDate; const nextTick = testUtils.nextTick; var initialDate = new Date(2018, 11, 20, 8, 0, 0); initialDate = new Date(initialDate.getTime() - initialDate.getTimezoneOffset() * 60 * 1000); const { toggleFilterMenu, toggleMenuItem } = require("@web/../tests/search/helpers"); const { markup } = owl; const FORMAT = "YYYY-MM-DD HH:mm:ss"; // This function is used to be sure that the unavailabilities will be displayed // where we want (independently of the timezone used when lauching the tests) function convertUnavailability(u) { return { start: moment(u.start).utc().format(FORMAT), stop: moment(u.stop).utc().format(FORMAT) }; } async function createView(params) { testUtils.mock.patch(SampleServer.prototype, { /** * Sometimes, it can happen that none of the generated dates is in * the interval. To fix that, we simply return the initial date. */ _getRandomDate(format) { return moment(params.viewOptions.initialDate).format(format); }, }); const view = await testUtils.createView(...arguments); const oldDestroy = view.destroy; view.destroy = function () { oldDestroy.call(this, ...arguments); testUtils.mock.unpatch(SampleServer.prototype); }; return view; } // The gantt view uses momentjs for all time computation, which bypasses // tzOffset, making it hard to test. We had two solutions to test this: // // 1. Change everywhere in gantt where we use momentjs to manual // manipulations using tzOffset so that we can manipulate it tests. // Pros: // - Consistent with other mechanisms in Odoo. // - Full coverage of the behavior since we can manipulate in tests // Cons: // - We need to change nearly everything everywhere in gantt // - The code works, it is sad to have to change it, risking to // introduce new bugs, just to be able to test this. // - Just applying the tzOffset to the date is not as easy as it // sounds. Moment is smart. It offsets the date with the offset // that you would locally have AT THAT DATE. Meaning that it // sometimes offsets of 1 hour or 2 hour in the same locale // depending as if the particular datetime we are offseting would // be in DST or not at that time. We would have to handle all // the DST conversion shenanigans manually to be correct. // // 2. Use the same computation path as the production code to compute // the expected value. // Pros: // - Very easy to implement // - Momentjs is smart, see last Con above. It does all the heavy // lifting for us and it is a well known, stable and maintained // library, so we can trust it on these matters. // Cons: // - The test relies on the behavior of Momentjs. If the library // has a bug, the gantt view will have an issue that this test // will never be able to see. // // Considering the Cons of the first option are tremendous and the one // of the second option is offest by the fact that we consider Momentjs // to be a trustworthy library, we chose option 2. It was required in // only a few test but we think it was still interesting to mention it. function getPillItemWidth($el) { return $el.attr('style').split('width: ')[1].split(';')[0]; } QUnit.module('LegacyViews', { beforeEach: function () { // Avoid animation to not have to wait until the tooltip is removed this.initialPopoverDefaultAnimation = Popover.Default.animation; Popover.Default.animation = false; this.data = { tasks: { fields: { id: {string: 'ID', type: 'integer'}, name: {string: 'Name', type: 'char'}, start: {string: 'Start Date', type: 'datetime'}, stop: {string: 'Stop Date', type: 'datetime'}, time: {string: "Time", type: "float"}, stage: {string: 'Stage', type: 'selection', selection: [['todo', 'To Do'], ['in_progress', 'In Progress'], ['done', 'Done'], ['cancel', 'Cancelled']]}, project_id: {string: 'Project', type: 'many2one', relation: 'projects'}, user_id: {string: 'Assign To', type: 'many2one', relation: 'users'}, color: {string: 'Color', type: 'integer'}, progress: {string: 'Progress', type: 'integer'}, exclude: {string: 'Excluded from Consolidation', type: 'boolean'}, stage_id: {string: "Stage", type: "many2one", relation: 'stage'} }, records: [ { id: 1, name: 'Task 1', start: '2018-11-30 18:30:00', stop: '2018-12-31 18:29:59', stage: 'todo', stage_id: 1, project_id: 1, user_id: 1, color: 0, progress: 0}, { id: 2, name: 'Task 2', start: '2018-12-17 11:30:00', stop: '2018-12-22 06:29:59', stage: 'done', stage_id: 4, project_id: 1, user_id: 2, color: 2, progress: 30}, { id: 3, name: 'Task 3', start: '2018-12-27 06:30:00', stop: '2019-01-03 06:29:59', stage: 'cancel', stage_id: 3, project_id: 1, user_id: 2, color: 10, progress: 60}, { id: 4, name: 'Task 4', start: '2018-12-19 22:30:00', stop: '2018-12-20 06:29:59', stage: 'in_progress', stage_id: 3, project_id: 1, user_id: 1, color: 1, progress: false, exclude: 0}, { id: 5, name: 'Task 5', start: '2018-11-08 01:53:10', stop: '2018-12-04 01:34:34', stage: 'done', stage_id: 2, project_id: 2, user_id: 1, color: 2, progress: 100, exclude: 1}, { id: 6, name: 'Task 6', start: '2018-11-19 23:00:00', stop: '2018-11-20 04:21:01', stage: 'in_progress', stage_id: 4, project_id: 2, user_id: 1, color: 1, progress: 0}, { id: 7, name: 'Task 7', start: '2018-12-20 10:30:12', stop: '2018-12-20 18:29:59', stage: 'cancel', stage_id: 1, project_id: 2, user_id: 2, color: 10, progress: 80}, { id: 8, name: 'Task 8', start: '2020-03-28 06:30:12', stop: '2020-03-28 18:29:59', stage: 'in_progress', stage_id: 1, project_id: 2, user_id: 2, color: 10, progress: 80}, ], }, projects: { fields: { id: {string: 'ID', type: 'integer'}, name: {string: 'Name', type: 'char'}, }, records: [ {id: 1, name: 'Project 1'}, {id: 2, name: 'Project 2'}, ], }, users: { fields: { id: {string: 'ID', type: 'integer'}, name: {string: 'Name', type: 'char'}, }, records: [ {id: 1, name: 'User 1'}, {id: 2, name: 'User 2'}, ], }, stage: { fields: { name: {string: "Name", type: "char"}, sequence: {string: "Sequence", type: "integer"} }, records: [{ id: 1, name: "in_progress", sequence: 2, }, { id: 3, name: "cancel", sequence: 4, }, { id: 2, name: "todo", sequence: 1, }, { id: 4, name: "done", sequence: 3, }] }, }; patchTimeZone(0); }, afterEach: async function() { Popover.Default.animation = this.initialPopoverDefaultAnimation; }, }, function () { QUnit.module('GanttView (legacy)'); // BASIC TESTS QUnit.test('empty ungrouped gantt rendering', async function (assert) { assert.expect(3); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, domain: [['id', '=', 0]], }); assert.containsOnce(gantt, '.o_gantt_header_container', 'should have a header'); assert.containsN(gantt, '.o_gantt_header_container .o_gantt_header_scale .o_gantt_header_cell', 31, 'should have a 31 slots for month view'); assert.containsOnce(gantt, '.o_gantt_row_container .o_gantt_row', 'should have a 1 row'); gantt.destroy(); }); QUnit.test('ungrouped gantt rendering', async function (assert) { assert.expect(20); // This is one of the few tests which have dynamic assertions, see // our justification for it in the comment at the top of this file var task2 = this.data.tasks.records[1]; var startDateUTCString = task2.start; var startDateUTC = moment.utc(startDateUTCString); var startDateLocalString = startDateUTC.local().format('DD MMM, LT'); var stopDateUTCString = task2.stop; var stopDateUTC = moment.utc(stopDateUTCString); var stopDateLocalString = stopDateUTC.local().format('DD MMM, LT'); var POPOVER_DELAY = GanttRow.prototype.POPOVER_DELAY; GanttRow.prototype.POPOVER_DELAY = 0; var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (route === '/web/dataset/search_read') { assert.strictEqual(args.model, 'tasks', "should read on the correct model"); } else if (route === '/web/dataset/call_kw/tasks/read_group') { throw Error("Should not call read_group when no groupby !"); } return this._super.apply(this, arguments); }, session: { getTZOffset: function () { return 60; }, }, }); assert.containsOnce(gantt, '.o_gantt_header_container', 'should have a header'); assert.hasClass(gantt.$buttons.find('.o_gantt_button_scale[data-value=month]'), 'active', 'month view should be activated by default'); assert.notOk(gantt.$buttons.find('.o_gantt_button_expand_rows').is(':visible'), "the expand button should be invisible (only displayed if useful)"); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), 'December 2018', 'should contain "December 2018" in header'); assert.containsN(gantt, '.o_gantt_header_container .o_gantt_header_scale .o_gantt_header_cell', 31, 'should have a 31 slots for month view'); assert.containsOnce(gantt, '.o_gantt_row_container .o_gantt_row', 'should have a 1 row'); assert.containsNone(gantt, '.o_gantt_row_container .o_gantt_row .o_gantt_row_sidebar', 'should not have a sidebar'); assert.containsN(gantt, '.o_gantt_pill_wrapper', 6, 'should have a 6 pills'); // verify that the level offset is correctly applied (add 1px gap border compensation for each level) assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_cell[data-date="2018-12-01 00:00:00"] .o_gantt_pill_wrapper:contains(Task 5)').css('margin-top'), '2px', 'task 5 should be in first level'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_cell[data-date="2018-12-17 00:00:00"] .o_gantt_pill_wrapper:contains(Task 2)').css('margin-top'), '2px', 'task 2 should be in first level'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_cell[data-date="2018-12-27 00:00:00"] .o_gantt_pill_wrapper:contains(Task 3)').css('margin-top'), '2px', 'task 3 should be in first level'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_cell[data-date="2018-12-01 00:00:00"] .o_gantt_pill_wrapper:contains(Task 1)').css('margin-top'), GanttRow.prototype.LEVEL_TOP_OFFSET + 4 + 2 +'px', 'task 1 should be in second level'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_cell[data-date="2018-12-20 00:00:00"] .o_gantt_pill_wrapper:contains(Task 4)').css('margin-top'), 2 * (GanttRow.prototype.LEVEL_TOP_OFFSET + 4) + 2 +'px', 'task 4 should be in third level'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_cell[data-date="2018-12-20 00:00:00"] .o_gantt_pill_wrapper:contains(Task 7)').css('margin-top'), 2 * (GanttRow.prototype.LEVEL_TOP_OFFSET + 4) + 2 +'px', 'task 7 should be in third level'); // test popover and local timezone assert.containsNone(gantt, 'div.popover', 'should not have a popover'); gantt.$('.o_gantt_pill:contains("Task 2")').trigger('mouseenter'); await testUtils.nextTick(); assert.containsOnce($, 'div.popover', 'should have a popover'); assert.strictEqual($('div.popover .flex-column span:nth-child(2)').text(), startDateLocalString, 'popover should display start date of task 2 in local time'); assert.strictEqual($('div.popover .flex-column span:nth-child(3)').text(), stopDateLocalString, 'popover should display start date of task 2 in local time'); gantt.destroy(); assert.containsNone(gantt, 'div.popover', 'should not have a popover anymore'); GanttRow.prototype.POPOVER_DELAY = POPOVER_DELAY; }); QUnit.test('ordered gantt view', async function (assert) { assert.expect(1); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', groupBy: ['stage_id'], viewOptions: { initialDate: initialDate, }, }) assert.strictEqual(gantt.$('.o_gantt_row_title').text().replace(/\s/g, ''), "todoin_progressdonecancel"); gantt.destroy(); }); QUnit.test('empty single-level grouped gantt rendering', async function (assert) { assert.expect(3); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['project_id'], domain: [['id', '=', 0]], }); assert.containsOnce(gantt, '.o_gantt_header_container', 'should have a header'); assert.containsN(gantt, '.o_gantt_header_container .o_gantt_header_scale .o_gantt_header_cell', 31, 'should have a 31 slots for month view'); assert.containsOnce(gantt, '.o_gantt_row_container .o_gantt_row', 'should have a 1 row'); gantt.destroy(); }); QUnit.test('single-level grouped gantt rendering', async function (assert) { assert.expect(12); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['project_id'], }); assert.containsOnce(gantt, '.o_gantt_header_container', 'should have a header'); assert.hasClass(gantt.$buttons.find('.o_gantt_button_scale[data-value=month]'), 'active', 'month view should by default activated'); assert.notOk(gantt.$buttons.find('.o_gantt_button_expand_rows').is(':visible'), "the expand button should be invisible (only displayed if useful)"); assert.strictEqual(gantt.$('.o_gantt_header_container > .o_gantt_row_sidebar').text().trim(), 'Tasks', 'should contain "Tasks" in header sidebar'); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), 'December 2018', 'should contain "December 2018" in header'); assert.containsN(gantt, '.o_gantt_header_container .o_gantt_header_scale .o_gantt_header_cell', 31, 'should have a 31 slots for month view'); assert.containsN(gantt, '.o_gantt_row_container .o_gantt_row', 2, 'should have a 2 rows'); assert.containsOnce(gantt, '.o_gantt_row_container .o_gantt_row:nth-child(2) .o_gantt_row_sidebar', 'should have a sidebar'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_row:nth-child(2) .o_gantt_row_title').text().trim(), 'Project 1', 'should contain "Project 1" in sidebar title'); assert.containsN(gantt, '.o_gantt_row_container .o_gantt_row:nth-child(2) .o_gantt_pill_wrapper', 4, 'should have a 4 pills in first row'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_row:last-child .o_gantt_row_title').text().trim(), 'Project 2', 'should contain "Project 2" in sidebar title'); assert.containsN(gantt, '.o_gantt_row_container .o_gantt_row:last-child .o_gantt_pill_wrapper', 2, 'should have a 2 pills in first row'); gantt.destroy(); }); QUnit.test('single-level grouped gantt rendering with group_expand', async function (assert) { assert.expect(12); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['project_id'], mockRPC: function (route) { if (route === '/web/dataset/call_kw/tasks/read_group') { return Promise.resolve([ { project_id: [20, "Unused Project 1"], project_id_count: 0 }, { project_id: [50, "Unused Project 2"], project_id_count: 0 }, { project_id: [2, "Project 2"], project_id_count: 2 }, { project_id: [30, "Unused Project 3"], project_id_count: 0 }, { project_id: [1, "Project 1"], project_id_count: 4 } ]); } return this._super.apply(this, arguments); }, }); assert.containsOnce(gantt, '.o_gantt_header_container', 'should have a header'); assert.hasClass(gantt.$buttons.find('.o_gantt_button_scale[data-value=month]'), 'active', 'month view should by default activated'); assert.notOk(gantt.$buttons.find('.o_gantt_button_expand_rows').is(':visible'), "the expand button should be invisible (only displayed if useful)"); assert.strictEqual(gantt.$('.o_gantt_header_container > .o_gantt_row_sidebar').text().trim(), 'Tasks', 'should contain "Tasks" in header sidebar'); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), 'December 2018', 'should contain "December 2018" in header'); assert.containsN(gantt, '.o_gantt_header_container .o_gantt_header_scale .o_gantt_header_cell', 31, 'should have a 31 slots for month view'); assert.containsN(gantt, '.o_gantt_row_container .o_gantt_row', 5, 'should have a 5 rows'); assert.containsOnce(gantt, '.o_gantt_row_container .o_gantt_row:nth-child(2) .o_gantt_row_sidebar', 'should have a sidebar'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_row:nth-child(2) .o_gantt_row_title').text().trim(), 'Unused Project 1', 'should contain "Unused Project" in sidebar title'); assert.containsN(gantt, '.o_gantt_row_container .o_gantt_row:nth-child(2) .o_gantt_pill_wrapper', 0, 'should have 0 pills in first row'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_row:last-child .o_gantt_row_title').text().trim(), 'Project 1', 'should contain "Project 1" in sidebar title'); assert.containsN(gantt, '.o_gantt_row_container .o_gantt_row:not(.o_gantt_total_row):last-child .o_gantt_pill_wrapper', 4, 'should have 4 pills in last row'); gantt.destroy(); }); QUnit.test('multi-level grouped gantt rendering', async function (assert) { assert.expect(31); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['user_id', 'project_id', 'stage'], }); assert.containsOnce(gantt, '.o_gantt_header_container', 'should have a header'); assert.hasClass(gantt.$buttons.find('.o_gantt_button_scale[data-value=month]'), 'active', 'month view should by default activated'); assert.ok(gantt.$buttons.find('.o_gantt_button_expand_rows').is(':visible'), "there should be an expand button"); assert.strictEqual(gantt.$('.o_gantt_header_container > .o_gantt_row_sidebar').text().trim(), 'Tasks', 'should contain "Tasks" in header sidebar'); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), 'December 2018', 'should contain "December 2018" in header'); assert.containsN(gantt, '.o_gantt_header_container .o_gantt_header_scale .o_gantt_header_cell', 31, 'should have a 31 slots for month view'); assert.containsN(gantt, '.o_gantt_row_container .o_gantt_row', 12, 'should have a 12 rows'); assert.containsN(gantt, '.o_gantt_row_container .o_gantt_row_group.open', 6, 'should have a 6 opened groups'); assert.containsN(gantt, '.o_gantt_row_container .o_gantt_row:not(.o_gantt_row_group)', 6, 'should have a 6 rows'); assert.containsOnce(gantt, '.o_gantt_row_container .o_gantt_row:first .o_gantt_row_sidebar', 'should have a sidebar'); // Check grouped rows assert.hasClass(gantt.$('.o_gantt_row_container .o_gantt_row:first'), 'o_gantt_row_group', '1st row should be a group'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_row:first .o_gantt_row_title').text().trim(), 'User 1', '1st row title should be "User 1"'); assert.hasClass(gantt.$('.o_gantt_row_container .o_gantt_row:nth(1)'), 'o_gantt_row_group', '2nd row should be a group'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_row:nth(1) .o_gantt_row_title').text().trim(), 'Project 1', '2nd row title should be "Project 1"'); assert.hasClass(gantt.$('.o_gantt_row_container .o_gantt_row:nth(4)'), 'o_gantt_row_group', '5th row should be a group'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_row:nth(4) .o_gantt_row_title').text().trim(), 'Project 2', '5th row title should be "Project 2"'); assert.hasClass(gantt.$('.o_gantt_row_container .o_gantt_row:nth(6)'), 'o_gantt_row_group', '7th row should be a group'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_row:nth(6) .o_gantt_row_title').text().trim(), 'User 2', '7th row title should be "User 2"'); assert.hasClass(gantt.$('.o_gantt_row_container .o_gantt_row:nth(7)'), 'o_gantt_row_group', '8th row should be a group'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_row:nth(7) .o_gantt_row_title').text().trim(), 'Project 1', '8th row title should be "Project 1"'); assert.hasClass(gantt.$('.o_gantt_row_container .o_gantt_row:nth(10)'), 'o_gantt_row_group', '11th row should be a group'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_row:nth(10) .o_gantt_row_title').text().trim(), 'Project 2', '11th row title should be "Project 2"'); // group row count and greyscale assert.strictEqual(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_consolidated_pill_title').text().replace(/\s+/g, ''), "2121", "the count should be correctly computed"); assert.strictEqual(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_pill:eq(0)').css('background-color'), "rgb(1, 126, 132)", "the 1st group pill should have the correct grey scale)"); assert.strictEqual(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_pill:eq(1)').css('background-color'), "rgb(1, 126, 132)", "the 2nd group pill should have the correct grey scale)"); assert.strictEqual(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_pill:eq(2)').css('background-color'), "rgb(1, 126, 132)", "the 3rd group pill should have the correct grey scale"); assert.strictEqual(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_pill:eq(3)').css('background-color'), "rgb(1, 126, 132)", "the 4th group pill should have the correct grey scale"); assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_pill_wrapper:eq(0)')), "calc(300% + 2px - 0px)", "the 1st group pill should have the correct width (1 to 3 dec)"); assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_pill_wrapper:eq(1)')), "calc(1600% + 15px - 0px)", "the 2nd group pill should have the correct width (4 to 19 dec)"); assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_pill_wrapper:eq(2)')), "calc(50% - 0px)", "the 3rd group pill should have the correct width (20 morning dec"); assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_pill_wrapper:eq(3)')), "calc(1150% + 10px - 0px)", "the 4th group pill should have the correct width (20 afternoon to 31 dec"); gantt.destroy(); }); QUnit.test('many2many grouped gantt rendering', async function (assert) { assert.expect(20); this.data.tasks.fields.user_ids = { string: 'Assignees', type: 'many2many', relation: 'users' }; this.data.tasks.records[0].user_ids = [1, 2]; const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: ``, viewOptions: { initialDate }, groupBy: ['user_ids'], }); assert.containsOnce(gantt, '.o_gantt_header_container', 'should have a header'); assert.hasClass(gantt.$('.o_gantt_button_scale[data-value=month]'), 'active', 'month view should by default activated'); assert.notOk(gantt.$('.o_gantt_button_expand_rows').is(':visible'), "the expand button should be invisible (only displayed if useful)"); assert.strictEqual(gantt.$('.o_gantt_header_container > .o_gantt_row_sidebar').text().trim(), 'Tasks', 'should contain "Tasks" in header sidebar'); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), 'December 2018', 'should contain "December 2018" in header'); assert.containsN(gantt, '.o_gantt_header_container .o_gantt_header_scale .o_gantt_header_cell', 31, 'should have 31 slots for month view'); assert.containsN(gantt, '.o_gantt_row_container .o_gantt_row', 3, 'should have 3 rows'); assert.containsOnce(gantt, '.o_gantt_row_container .o_gantt_row:nth-child(2) .o_gantt_row_sidebar', 'should have a sidebar'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_row:nth-child(2) .o_gantt_row_title').text().trim(), 'User 1', 'should contain "User 1" in sidebar title'); assert.containsOnce(gantt, '.o_gantt_row_container .o_gantt_row:nth-child(2) .o_gantt_pill_wrapper', 'should have a single pill in first row'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_row:nth-child(2) .o_gantt_pill_wrapper').text().trim(), 'Task 1', 'pills should have those names in first row'); assert.containsOnce(gantt, '.o_gantt_row_container .o_gantt_row:nth-child(3) .o_gantt_row_sidebar', 'should have a sidebar'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_row:nth-child(3) .o_gantt_row_title').text().trim(), 'User 2', 'should contain "User 2" in sidebar title'); assert.containsOnce(gantt, '.o_gantt_row_container .o_gantt_row:nth-child(3) .o_gantt_pill_wrapper', 'should have a single pill in second row'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_row:nth-child(3) .o_gantt_pill_wrapper').text().trim(), 'Task 1', 'pills should have those names in second row'); assert.containsOnce(gantt, '.o_gantt_row_container .o_gantt_row:last-child .o_gantt_row_sidebar', 'should have a sidebar'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_row:last-child .o_gantt_row_title').text().trim(), 'Undefined Assignees', 'should contain "Undefined Assignees" in sidebar title'); assert.containsN(gantt, '.o_gantt_row_container .o_gantt_row:last-child .o_gantt_pill_wrapper', 5, 'should have 5 pills in last row'); assert.deepEqual(gantt.$('.o_gantt_row_container .o_gantt_row:last-child .o_gantt_pill_wrapper').text().match(/(\w+\s\d)/g), [ "Task 5", "Task 2", "Task 4", "Task 7", "Task 3" ], 'pills should have those names in last row'); assert.deepEqual([...gantt.el.querySelectorAll(".o_gantt_pill")].map((el) => el.dataset.id), ["1","1","5","2","4","7","3"], "two pills should represent same record id"); gantt.destroy(); }); QUnit.test('multi-level grouped with many2many field in gantt view', async function (assert) { assert.expect(20); this.data.tasks.fields.user_ids = { string: 'Assignees', type: 'many2many', relation: 'users' }; this.data.tasks.records[0].user_ids = [1, 2]; const gantt = await createView({ View: GanttView, model: "tasks", data: this.data, arch: ``, viewOptions: { initialDate }, groupBy: ["user_ids", "project_id"], }); // Header assert.containsOnce(gantt, ".o_gantt_header_container", "should have a header"); assert.hasClass(gantt.el.querySelector(".o_gantt_button_scale[data-value=month]"), "active"); assert.containsOnce(gantt, ".btn-group:not(.d-none) > .o_gantt_button_expand_rows", "there should be an expand button"); assert.strictEqual( gantt.el.querySelector(".o_gantt_header_container > .o_gantt_row_sidebar").innerText, "Tasks", "should contain 'Tasks' in header sidebar" ); assert.strictEqual( gantt.el.querySelector(".o_gantt_header_slots > .row:first-child").innerText, "December 2018", "should contain 'December 2018' in header" ); assert.containsN(gantt, ".o_gantt_header_scale .o_gantt_header_cell", 31, "should have a 31 slots for month view"); // Body assert.containsN(gantt, ".o_gantt_row_container .o_gantt_row", 7, "should have 7 rows"); assert.containsN(gantt, ".o_gantt_row_container .o_gantt_row .o_gantt_row_sidebar", 7, "each rowshould have a sidebar"); assert.containsN(gantt, ".o_gantt_row_container .o_gantt_row_group.open", 3, "should have 3 opened groups"); assert.containsN(gantt, ".o_gantt_row_container .o_gantt_row_nogroup", 4, "should have 4 'nogroup' rows"); // Check grouped rows const rows = gantt.el.querySelectorAll(".o_gantt_row_container .o_gantt_row"); const rowGroupClasses = [...rows].map((el) => { return [...el.classList].filter((c) => c.startsWith("o_gantt_row_"))[0].substring(12); }); assert.deepEqual(rowGroupClasses, ["group", "nogroup", "group", "nogroup", "group", "nogroup", "nogroup"], "rows should be '(no)group' in this order"); const rowTitles = [...rows].map((el) => el.querySelector(".o_gantt_row_title").innerText.trim()); assert.deepEqual(rowTitles, ["User 1", "Project 1", "User 2", "Project 1", "Undefined Assignees", "Project 1", "Project 2"], "rows should have those titles"); // group row count and greyscale const groupCounts = [...rows].filter((el) => [...el.classList].includes("o_gantt_row_group")).map((el) => [...el.querySelectorAll(".o_gantt_consolidated_pill_title")].map((x) => x.innerText).join()); assert.deepEqual(groupCounts, ["1", "1", "1,1,2,1,1"], "group consolidated counts should be correctly computed"); // consolidated pills should have correct width assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_pill_wrapper:eq(0)')), "calc(3100% + 30px - 0px)"); assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(1) .o_gantt_pill_wrapper:eq(0)')), "calc(3100% + 30px - 0px)"); assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(2) .o_gantt_pill_wrapper:eq(0)')), "calc(300% + 2px - 0px)"); assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(2) .o_gantt_pill_wrapper:eq(1)')), "calc(250% + 1px - 0px)"); assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(2) .o_gantt_pill_wrapper:eq(2)')), "calc(100% - 0px)"); assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(2) .o_gantt_pill_wrapper:eq(3)')), "calc(150% - 0px)"); assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(2) .o_gantt_pill_wrapper:eq(4)')), "calc(500% + 4px - 0px)"); gantt.destroy(); }); QUnit.test('full precision gantt rendering', async function(assert) { assert.expect(1); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: new Date(2018, 10, 15, 8, 0, 0), }, groupBy: ['user_id', 'project_id'] }); assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_pill_wrapper:eq(0)')), "calc(700% + 6px - 0px)", "the group pill should have the correct width (7 days)"); gantt.destroy(); }); QUnit.test('gantt rendering, thumbnails', async function (assert) { assert.expect(2); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['user_id'], mockRPC: function (route, args) { console.log(route) if (route.endsWith('search_read')) { return Promise.resolve({ records: [ { display_name: "Task 1", id: 1, start: "2018-11-30 18:30:00", stop: "2018-12-31 18:29:59", user_id: [1, "User 2"], },{ display_name: "FALSE", id: 1, start: "2018-12-01 18:30:00", stop: "2018-12-02 18:29:59", user_id: false, } ] }) } if(route.endsWith('read_group')) { return Promise.resolve([ { user_id: [1, "User 1"], user_id_count: 3, __domain: [ ["user_id", "=", 1], ["start", "<=", "2018-12-31 23:59:59"], ["stop", ">=", "2018-12-01 00:00:00"], ] },{ user_id: false, user_id_count: 3, __domain: [ ["user_id", "=", false], ["start", "<=", "2018-12-31 23:59:59"], ["stop", ">=", "2018-12-01 00:00:00"], ] } ]) } return this._super.apply(this, arguments); } }); assert.containsN(gantt, '.o_gantt_row_thumbnail', 1, 'There should be a thumbnail per row where user_id is defined'); assert.ok(gantt.$('.o_gantt_row_thumbnail:nth(0)')[0].dataset.src.endsWith('web/image?model=users&id=1&field=image')); gantt.destroy(); }); QUnit.test('scale switching', async function (assert) { assert.expect(17); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); // default (month) assert.hasClass(gantt.$buttons.find('.o_gantt_button_scale[data-value=month]'), 'active', 'month view should be activated by default'); // switch to day view await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_scale[data-value=day]')); assert.hasClass(gantt.$buttons.find('.o_gantt_button_scale[data-value=day]'), 'active', 'day view should be activated'); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), 'Thursday, December 20, 2018', 'should contain "Thursday, December 20, 2018" in header'); assert.containsN(gantt, '.o_gantt_header_container .o_gantt_header_scale .o_gantt_header_cell', 24, 'should have a 24 slots for day view'); assert.containsN(gantt, '.o_gantt_pill_wrapper', 4, 'should have a 4 pills'); // switch to week view await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_scale[data-value=week]')); assert.hasClass(gantt.$buttons.find('.o_gantt_button_scale[data-value=week]'), 'active', 'week view should be activated'); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), '16 December 2018 - 22 December 2018', 'should contain "16 December 2018 - 22 December 2018" in header'); assert.containsN(gantt, '.o_gantt_header_container .o_gantt_header_scale .o_gantt_header_cell', 7, 'should have a 7 slots for week view'); assert.containsN(gantt, '.o_gantt_pill_wrapper', 4, 'should have a 4 pills'); // switch to month view await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_scale[data-value=month]')); assert.hasClass(gantt.$buttons.find('.o_gantt_button_scale[data-value=month]'), 'active', 'month view should be activated'); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), 'December 2018', 'should contain "December 2018" in header'); assert.containsN(gantt, '.o_gantt_header_container .o_gantt_header_scale .o_gantt_header_cell', 31, 'should have a 31 slots for month view'); assert.containsN(gantt, '.o_gantt_pill_wrapper', 6, 'should have a 6 pills'); // switch to year view await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_scale[data-value=year]')); assert.hasClass(gantt.$buttons.find('.o_gantt_button_scale[data-value=year]'), 'active', 'year view should be activated'); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), '2018', 'should contain "2018" in header'); assert.containsN(gantt, '.o_gantt_header_container .o_gantt_header_scale .o_gantt_header_cell', 12, 'should have a 12 slots for year view'); assert.containsN(gantt, '.o_gantt_pill_wrapper', 7, 'should have a 7 pills'); gantt.destroy(); }); QUnit.test('today is highlighted', async function (assert) { assert.expect(2); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', }); var dayOfMonth = moment().date(); assert.containsOnce(gantt, '.o_gantt_header_cell.o_gantt_today', "there should be an highlighted day"); assert.strictEqual(parseInt(gantt.$('.o_gantt_header_cell.o_gantt_today').text(), 10), dayOfMonth, 'the highlighted day should be today'); gantt.destroy(); }); // GANTT WITH SAMPLE="1" QUnit.test('empty grouped gantt with sample="1"', async function (assert) { assert.expect(3); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: new Date(), }, groupBy: ['project_id'], domain: Domain.FALSE_DOMAIN, }); assert.hasClass(gantt, 'o_legacy_view_sample_data'); assert.ok(gantt.$('.o_gantt_pill_wrapper').length > 0, "sample records should be displayed"); const content = gantt.$el.text(); await gantt.reload(); assert.strictEqual(gantt.$el.text(), content); gantt.destroy(); }); QUnit.test('empty gantt with sample data and default_group_by', async function (assert) { assert.expect(5); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: new Date(), }, domain: Domain.FALSE_DOMAIN, }); assert.hasClass(gantt, 'o_legacy_view_sample_data'); assert.ok(gantt.$('.o_gantt_pill_wrapper').length > 0, "sample records should be displayed"); const content = gantt.$el.text(); // trigger a reload via the search bar await testUtils.controlPanel.validateSearch(gantt); assert.hasClass(gantt, 'o_legacy_view_sample_data'); assert.ok(gantt.$('.o_gantt_pill_wrapper').length > 0, "sample records should be displayed"); assert.strictEqual(gantt.$el.text(), content); gantt.destroy(); }); QUnit.test('empty gantt with sample data and default_group_by (switch view)', async function (assert) { assert.expect(7); const views = { 'tasks,false,gantt': '', 'tasks,false,list': '', 'tasks,false,search': '', }; const serverData = {models: this.data, views}; const target = getFixture(); const webClient = await createWebClient({ serverData }); await doAction(webClient, { name: 'Gantt', res_model: 'tasks', type: 'ir.actions.act_window', views: [[false, 'gantt'], [false, 'list']], }); // the gantt view should be in sample mode assert.hasClass($(target).find('.o_view_controller'), 'o_legacy_view_sample_data'); assert.ok($(target).find('.o_gantt_pill_wrapper').length > 0, "sample records should be displayed"); const content = $(target).find('.o_view_controller').text(); // switch to list view await testUtils.dom.click($(target).find('.o_cp_bottom_right .o_switch_view.o_list')); await nextTick(); assert.containsOnce(target, '.o_list_view'); // go back to gantt view await testUtils.dom.click($(target).find('.o_cp_bottom_right .o_switch_view.o_gantt')); await nextTick(); assert.containsOnce(target, '.o_gantt_view'); // the gantt view should be still in sample mode assert.hasClass($(target).find('.o_view_controller'), 'o_legacy_view_sample_data'); assert.ok($(target).find('.o_gantt_pill_wrapper').length > 0, "sample records should be displayed"); assert.strictEqual($(target).find('.o_view_controller').text(), content); }); QUnit.test('empty gantt with sample="1"', async function (assert) { assert.expect(3); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: new Date(), }, domain: Domain.FALSE_DOMAIN, }); assert.hasClass(gantt, 'o_legacy_view_sample_data'); assert.ok(gantt.$('.o_gantt_pill_wrapper').length > 0, "sample records should be displayed"); const content = gantt.$el.text(); await gantt.reload(); assert.strictEqual(gantt.$el.text(), content); gantt.destroy(); }); QUnit.test('toggle filter on empty gantt with sample="1"', async function (assert) { assert.expect(6); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: new Date(), }, domain: Domain.FALSE_DOMAIN, }); assert.hasClass(gantt, 'o_legacy_view_sample_data'); assert.containsOnce(gantt, '.o_gantt_row_container .o_gantt_row'); assert.ok(gantt.$('.o_gantt_pill_wrapper').length > 0, "sample records should be displayed"); await gantt.reload({ domain: [['id', '<', 0]] }); assert.doesNotHaveClass(gantt, 'o_legacy_view_sample_data'); assert.containsOnce(gantt, '.o_gantt_row_container'); assert.containsNone(gantt, '.o_gantt_pill_wrapper'); gantt.destroy(); }); QUnit.test('non empty gantt with sample="1"', async function (assert) { assert.expect(6); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate }, }); assert.doesNotHaveClass(gantt, 'o_legacy_view_sample_data'); assert.containsOnce(gantt, '.o_gantt_row_container'); assert.containsN(gantt, '.o_gantt_pill_wrapper', 7); await gantt.reload({ domain: Domain.FALSE_DOMAIN }); assert.doesNotHaveClass(gantt, 'o_legacy_view_sample_data'); assert.containsNone(gantt, '.o_gantt_pill_wrapper'); assert.containsOnce(gantt, '.o_gantt_row_container'); gantt.destroy(); }); QUnit.test('non empty grouped gantt with sample="1"', async function (assert) { assert.expect(6); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate }, groupBy: ['project_id'], }); assert.doesNotHaveClass(gantt, 'o_legacy_view_sample_data'); assert.containsN(gantt, '.o_gantt_row_container .o_gantt_row', 2); assert.containsN(gantt, '.o_gantt_pill_wrapper', 7); await gantt.reload({ domain: Domain.FALSE_DOMAIN }); assert.doesNotHaveClass(gantt, 'o_legacy_view_sample_data'); assert.containsOnce(gantt, '.o_gantt_row_container'); assert.containsNone(gantt, '.o_gantt_pill_wrapper'); gantt.destroy(); }); QUnit.test('add record in empty gantt with sample="1"', async function (assert) { assert.expect(5); const today = new Date(); this.data.tasks.records = []; const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: today, }, groupBy: ['project_id'], }); const views = { 'tasks,false,form': `
`, }; await prepareWowlFormViewDialogs({ models: this.data, views }); assert.hasClass(gantt, 'o_legacy_view_sample_data'); assert.ok(gantt.$('.o_gantt_pill_wrapper').length > 0, "sample records should be displayed"); await testUtils.dom.click(gantt.$('.o_gantt_button_add')); await testUtils.modal.clickButton('Save & Close'); assert.doesNotHaveClass(gantt, 'o_legacy_view_sample_data'); assert.containsOnce(gantt, '.o_gantt_row'); assert.containsOnce(gantt, '.o_gantt_pill'); gantt.destroy(); }); QUnit.test('click add and discard in empty gantt with sample="1"', async function (assert) { assert.expect(3); const today = new Date(); this.data.tasks.records = []; const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: today, }, groupBy: ['project_id'], }); const views = { 'tasks,false,form': `
`, }; await prepareWowlFormViewDialogs({ models: this.data, views }); assert.hasClass(gantt, 'o_legacy_view_sample_data'); assert.ok(gantt.$('.o_gantt_pill_wrapper').length > 0, "sample records should be displayed"); const content = gantt.$el.text(); await testUtils.dom.click(gantt.$('.o_gantt_button_add')); await testUtils.modal.clickButton('Discard'); assert.strictEqual(gantt.$el.text(), content, "discarding should not modify the sample records previously displayed"); gantt.destroy(); }); QUnit.test('click button scale in empty gantt with sample="1"', async function (assert) { assert.expect(11); const today = new Date(); this.data.tasks.records = []; const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: today, } }); assert.hasClass(gantt, 'o_legacy_view_sample_data'); assert.ok(gantt.$('.o_gantt_pill_wrapper').length > 0, "sample records should be displayed"); const content = gantt.$el.text(); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_scale[data-value=day]')); assert.hasClass(gantt, 'o_legacy_view_sample_data'); assert.ok(gantt.$('.o_gantt_pill_wrapper').length > 0, "sample records should be displayed"); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_scale[data-value=week]')); assert.hasClass(gantt, 'o_legacy_view_sample_data'); assert.ok(gantt.$('.o_gantt_pill_wrapper').length > 0, "sample records should be displayed"); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_scale[data-value=month]')); assert.hasClass(gantt, 'o_legacy_view_sample_data'); assert.ok(gantt.$('.o_gantt_pill_wrapper').length > 0, "sample records should be displayed"); assert.strictEqual(gantt.$el.text(), content, "when we return to the default scale, the content should be the same as before"); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_scale[data-value=year]')); assert.hasClass(gantt, 'o_legacy_view_sample_data'); assert.ok(gantt.$('.o_gantt_pill_wrapper').length > 0, "sample records should be displayed"); gantt.destroy(); }); QUnit.test('click today button in empty gantt with sample="1"', async function (assert) { assert.expect(5); const today = new Date(); this.data.tasks.records = []; const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: today, } }); assert.hasClass(gantt, 'o_legacy_view_sample_data'); assert.ok(gantt.$('.o_gantt_pill_wrapper').length > 0, "sample records should be displayed"); const content = gantt.$el.text(); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_today')); assert.hasClass(gantt, 'o_legacy_view_sample_data'); assert.ok(gantt.$('.o_gantt_pill_wrapper').length > 0, "sample records should be displayed"); assert.strictEqual(gantt.$el.text(), content, "when we return to the default scale, the content should be the same as before"); gantt.destroy(); }); QUnit.test('click prev/next button in empty gantt with sample="1"', async function (assert) { assert.expect(7); const today = new Date(); this.data.tasks.records = []; const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: today, } }); assert.hasClass(gantt, 'o_legacy_view_sample_data'); assert.ok(gantt.$('.o_gantt_pill_wrapper').length > 0, "sample records should be displayed"); const content = gantt.$el.text(); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_prev')); assert.hasClass(gantt, 'o_legacy_view_sample_data'); assert.containsOnce(gantt, '.o_gantt_row_container'); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_next')); assert.hasClass(gantt, 'o_legacy_view_sample_data'); assert.ok(gantt.$('.o_gantt_pill_wrapper').length > 0, "sample records should be displayed"); assert.strictEqual(gantt.$el.text(), content); gantt.destroy(); }); QUnit.test('empty grouped gantt with sample data: keyboard navigation', async function (assert) { assert.expect(2); const gantt = await createView({ arch: '', data: this.data, domain: Domain.FALSE_DOMAIN, groupBy: ['project_id'], model: 'tasks', View: GanttView, viewOptions: { initialDate: new Date(), }, }); // Check keynav is disabled assert.hasClass( gantt.el.querySelector('.o_gantt_row:not([data-group-id=empty])'), 'o_sample_data_disabled' ); assert.containsNone(gantt.renderer, '[tabindex]:not([tabindex="-1"])'); gantt.destroy(); }); QUnit.test('no content helper when no data and sample mode', async function (assert) { assert.expect(3); const records = this.data.tasks.records; this.data.tasks.records = []; const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: new Date(2018, 10, 15, 8, 0, 0), action: { help: markup('

click to add a partner

'), } }, }); await testUtils.nextTick(); assert.containsOnce(gantt, '.o_view_nocontent', "should display the no content helper"); assert.strictEqual(gantt.$('.o_view_nocontent p.hello:contains(add a partner)').length, 1, "should have rendered no content helper from action"); this.data.tasks.records = records; await gantt.reload(); assert.containsNone(gantt, '.o_view_nocontent', "should not display the no content helper"); gantt.destroy(); }); // BEHAVIORAL TESTS QUnit.test('date navigation with timezone (1h)', async function (assert) { assert.expect(32); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (route === '/web/dataset/search_read') { assert.step(args.domain.toString()); } return this._super.apply(this, arguments); }, session: { getTZOffset: function () { return 60; }, }, }); assert.verifySteps(["start,<=,2018-12-31 22:59:59,stop,>=,2018-11-30 23:00:00"]); // month navigation await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_prev')); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), 'November 2018', 'should contain "November 2018" in header'); assert.verifySteps(["start,<=,2018-11-30 22:59:59,stop,>=,2018-10-31 23:00:00"]); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_next')); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), 'December 2018', 'should contain "December 2018" in header'); assert.verifySteps(["start,<=,2018-12-31 22:59:59,stop,>=,2018-11-30 23:00:00"]); // switch to day view and check day navigation await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_scale[data-value=day]')); assert.verifySteps(["start,<=,2018-12-20 22:59:59,stop,>=,2018-12-19 23:00:00"]); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_prev')); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), 'Wednesday, December 19, 2018', 'should contain "Wednesday, December 19, 2018" in header'); assert.verifySteps(["start,<=,2018-12-19 22:59:59,stop,>=,2018-12-18 23:00:00"]); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_next')); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), 'Thursday, December 20, 2018', 'should contain "Thursday, December 20, 2018" in header'); assert.verifySteps(["start,<=,2018-12-20 22:59:59,stop,>=,2018-12-19 23:00:00"]); // switch to week view and check week navigation await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_scale[data-value=week]')); assert.verifySteps(["start,<=,2018-12-22 22:59:59,stop,>=,2018-12-15 23:00:00"]); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_prev')); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), '09 December 2018 - 15 December 2018', 'should contain "09 December 2018 - 15 December 2018" in header'); assert.verifySteps(["start,<=,2018-12-15 22:59:59,stop,>=,2018-12-08 23:00:00"]); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_next')); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), '16 December 2018 - 22 December 2018', 'should contain "16 December 2018 - 22 December 2018" in header'); assert.verifySteps(["start,<=,2018-12-22 22:59:59,stop,>=,2018-12-15 23:00:00"]); // switch to year view and check year navigation await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_scale[data-value=year]')); assert.verifySteps(["start,<=,2018-12-31 22:59:59,stop,>=,2017-12-31 23:00:00"]); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_prev')); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), '2017', 'should contain "2017" in header'); assert.verifySteps(["start,<=,2017-12-31 22:59:59,stop,>=,2016-12-31 23:00:00"]); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_next')); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), '2018', 'should contain "2018" in header'); assert.verifySteps(["start,<=,2018-12-31 22:59:59,stop,>=,2017-12-31 23:00:00"]); gantt.destroy(); }); QUnit.test('if a on_create is specified, execute the action rather than opening a dialog. And reloads after the action', async function (assert) { assert.expect(3); var reloadCount = 0; var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (route === '/web/dataset/search_read') { reloadCount++; } return this._super.apply(this, arguments); }, }); testUtils.mock.intercept(gantt, 'do_action', function (event) { assert.strictEqual(event.data.action, 'this_is_create_action'); event.data.options.on_close(); }); assert.strictEqual(reloadCount, 1); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_add')); await testUtils.nextTick(); assert.strictEqual(reloadCount, 2); gantt.destroy(); }); QUnit.test('if a cell_create is specified to false then do not show + icon', async function (assert) { assert.expect(2); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); assert.containsOnce(gantt.$buttons, '.o_gantt_button_add', "there should be 'Add' button"); assert.containsNone(gantt, '.o_gantt_cell_add', 'should not have + icon on cell'); gantt.destroy(); }); QUnit.test('open a dialog to add a new task', async function (assert) { assert.expect(3); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); const views = { 'tasks,false,form': `
`, }; await prepareWowlFormViewDialogs({ models: this.data, views }); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_add')); // check that the dialog is opened with prefilled fields var $modal = $('.modal'); assert.strictEqual($modal.length, 1, 'There should be one modal opened'); assert.strictEqual($modal.find('.o_field_widget[name=start] .o_input').val(), '12/01/2018 00:00:00', 'the start date should be the start of the focus month'); assert.strictEqual($modal.find('.o_field_widget[name=stop] .o_input').val(), '12/31/2018 23:59:59', 'the end date should be the end of the focus month'); gantt.destroy(); }); QUnit.test('open a dialog to create/edit a task', async function (assert) { assert.expect(12); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['user_id', 'project_id', 'stage'], }); const views = { 'tasks,false,form': `
`, }; await prepareWowlFormViewDialogs({ models: this.data, views }); // open dialog to create a task await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_row_container .o_gantt_row:nth(3) .o_gantt_cell[data-date="2018-12-10 00:00:00"] .o_gantt_cell_add'), "click"); await testUtils.nextTick(); // check that the dialog is opened with prefilled fields var $modal = $('.modal'); assert.strictEqual($modal.length, 1, 'There should be one modal opened'); assert.strictEqual($modal.find('.modal-title').text(), "Create"); await testUtils.fields.editInput($modal.find('[name=name] input'), 'Task 8'); var $modalFieldStart = $modal.find('.o_field_widget[name=start]'); assert.strictEqual($modalFieldStart.find('.o_input').val(), '12/10/2018 00:00:00', 'The start field should have a value "12/10/2018 00:00:00"'); var $modalFieldStop = $modal.find('.o_field_widget[name=stop]'); assert.strictEqual($modalFieldStop.find('.o_input').val(), '12/10/2018 23:59:59', 'The stop field should have a value "12/10/2018 23:59:59"'); var $modalFieldProject = $modal.find('.o_field_widget.o_field_many2one[name=project_id]'); assert.strictEqual($modalFieldProject.find('.o_input').val(), 'Project 1', 'The project field should have a value "Project 1"'); var $modalFieldUser = $modal.find('.o_field_widget.o_field_many2one[name=user_id]'); assert.strictEqual($modalFieldUser.find('.o_input').val(), 'User 1', 'The user field should have a value "User 1"'); var $modalFieldStage = $modal.find('.o_field_widget[name=stage] select'); assert.strictEqual($modalFieldStage.val(), '"in_progress"', 'The stage field should have a value "In Progress"'); // create the task await testUtils.modal.clickButton('Save & Close'); assert.strictEqual($('.modal-lg').length, 0, 'Modal should be closed'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_row:nth(3) .o_gantt_cell[data-date="2018-12-10 00:00:00"] .o_gantt_pill').text().trim(), 'Task 8', 'Task should be created with name "Task 8"'); // open dialog to view a task await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_row_container .o_gantt_row:nth(3) .o_gantt_cell[data-date="2018-12-10 00:00:00"] .o_gantt_pill'), "click"); await testUtils.nextTick(); $modal = $('.modal-lg'); assert.strictEqual($modal.find('.modal-title').text(), "Open"); assert.strictEqual($modal.length, 1, 'There should be one modal opened'); assert.strictEqual($modal.find('[name=name] input').val(), 'Task 8', 'should open dialog for "Task 8"'); gantt.destroy(); }); QUnit.test("open a dialog to create a task when grouped by many2many field", async function (assert) { assert.expect(22); patchWithCleanup(browser, { setTimeout: (fn) => fn(), }); this.data.tasks.fields.user_ids = { string: 'Assignees', type: 'many2many', relation: 'users' }; this.data.tasks.records[0].user_ids = [1, 2]; const gantt = await createView({ View: GanttView, model: "tasks", data: this.data, arch: ``, viewOptions: { initialDate }, groupBy: ["user_ids", "project_id"], }); const views = { 'tasks,false,form': `
`, }; await prepareWowlFormViewDialogs({ models: this.data, views }); // Check grouped rows let rows = gantt.el.querySelectorAll(".o_gantt_row_container .o_gantt_row"); const rowGroupClasses = [...rows].map((el) => { return [...el.classList].filter((c) => c.startsWith("o_gantt_row_"))[0].substring(12); }); assert.deepEqual(rowGroupClasses, ["group", "nogroup", "group", "nogroup", "group", "nogroup", "nogroup"], "rows should be '(no)group' in this order"); document.createElement("a").classList.contains // Sanity check: row titles and tasks assert.deepEqual( [...rows].map((row) => row.querySelector(".o_gantt_row_title").innerText.trim()), ["User 1", "Project 1", "User 2", "Project 1", "Undefined Assignees", "Project 1", "Project 2"], "rows should have those titles"); assert.deepEqual( [...rows] .filter((row) => row.classList.contains("o_gantt_row_group")) .map((row) => row.querySelector(".o_gantt_row_title").innerText.trim()), ["User 1", "User 2", "Undefined Assignees"], "group rows should have those titles"); assert.deepEqual( [...rows] .filter((row) => row.classList.contains("o_gantt_row_nogroup")) .map((row) => row.querySelector(".o_gantt_row_title").innerText.trim()), ["Project 1", "Project 1", "Project 1", "Project 2"], "nogroup rows should have those titles"); assert.deepEqual( [...rows] .filter((row) => row.classList.contains("o_gantt_row_nogroup")) .reduce((acc, row) => { acc[row.dataset.rowId] = [...row.querySelectorAll(".o_gantt_pill_title")].map((pill) => pill.innerText); return acc; }, {}), { '[{"user_ids":[1,"User 1"]},{"project_id":[1,"Project 1"]}]': ["Task 1"], '[{"user_ids":[2,"User 2"]},{"project_id":[1,"Project 1"]}]': ["Task 1"], '[{"user_ids":false},{"project_id":[1,"Project 1"]}]': ["Task 2", "Task 4", "Task 3"], '[{"user_ids":false},{"project_id":[2,"Project 2"]}]': ["Task 5", "Task 7"], }, "nogroup rows should have those tasks"); // open dialog to create a task with two many2many values let row = gantt.el.querySelector(`[data-row-id*="User 1"][data-row-id*="Project 1"]`); await testUtils.dom.triggerMouseEvent( row.querySelector(".o_gantt_cell[data-date='2018-12-10 00:00:00'] .o_gantt_cell_add"), "click"); await testUtils.nextTick(); let $modal = $(".modal"); assert.strictEqual($modal.length, 1, "There should be one modal opened"); assert.strictEqual($modal.find(".modal-title").text(), "Create"); let $fieldDateStart = $modal.find(".o_field_widget.o_field_datetime[name=start]"); let $fieldDateStop = $modal.find(".o_field_widget.o_field_datetime[name=stop]"); let $fieldProject = $modal.find(".o_field_widget.o_field_many2one[name=project_id]"); let $fieldUser = $modal.find(".o_field_widget.o_field_many2many_tags[name=user_ids]"); assert.strictEqual($fieldDateStart.find(".o_input").val(), "12/10/2018 00:00:00", "The date start field should have a value '12/10/2018 00:00:00'"); assert.strictEqual($fieldDateStop.find(".o_input").val(), "12/10/2018 23:59:59", "The date stop field should have a value '12/10/2018 23:59:59'"); assert.strictEqual($fieldProject.find(".o_input").val(), "Project 1", "The project field should have a value 'Project 1'"); assert.containsOnce($fieldUser, ".badge", "The user field should contain a single badge"); assert.strictEqual($fieldUser.find(".badge .o_tag_badge_text").text().trim(), "User 1", "The user field should have a value 'User 1'"); await editInput(document.body, ".o_field_widget[name=user_ids] input", "User 2"); const autocompleteDropdown = document.body.querySelector(".o_dialog .o-autocomplete--dropdown-menu"); await click(autocompleteDropdown.querySelector("li a")); await testUtils.fields.editInput($modal.find("[name=name] input"), "NEWTASK 0"); await testUtils.modal.clickButton("Save & Close"); assert.strictEqual($(".modal").length, 0, "Modal should be closed"); rows = gantt.el.querySelectorAll(".o_gantt_row_container .o_gantt_row"); assert.deepEqual( [...rows] .filter((row) => row.classList.contains("o_gantt_row_nogroup")) .reduce((acc, row) => { acc[row.dataset.rowId] = [...row.querySelectorAll(".o_gantt_pill_title")].map((pill) => pill.innerText); return acc; }, {}), { '[{"user_ids":[1,"User 1"]},{"project_id":[1,"Project 1"]}]': ["Task 1", "NEWTASK 0"], '[{"user_ids":[2,"User 2"]},{"project_id":[1,"Project 1"]}]': ["Task 1", "NEWTASK 0"], '[{"user_ids":false},{"project_id":[1,"Project 1"]}]': ["Task 2", "Task 4", "Task 3"], '[{"user_ids":false},{"project_id":[2,"Project 2"]}]': ["Task 5", "Task 7"], }, "nogroup rows should have those tasks"); // open dialog to create a task with no many2many values row = gantt.el.querySelector(`[data-row-id*="Project 2"]:not([data-row-id*="User"])`); await testUtils.dom.triggerMouseEvent( row.querySelector(".o_gantt_cell[data-date='2018-12-24 00:00:00'] .o_gantt_cell_add"), "click"); await testUtils.nextTick(); $modal = $(".modal"); assert.strictEqual($modal.length, 1, "There should be one modal opened"); assert.strictEqual($modal.find(".modal-title").text(), "Create"); $fieldDateStart = $modal.find(".o_field_widget.o_field_datetime[name=start]"); $fieldDateStop = $modal.find(".o_field_widget.o_field_datetime[name=stop]"); $fieldProject = $modal.find(".o_field_widget.o_field_many2one[name=project_id]"); $fieldUser = $modal.find(".o_field_widget.o_field_many2many_tags[name=user_ids]"); assert.strictEqual($fieldDateStart.find(".o_input").val(), "12/24/2018 00:00:00", "The date start field should have a value '12/24/2018 00:00:00'"); assert.strictEqual($fieldDateStop.find(".o_input").val(), "12/24/2018 23:59:59", "The date stop field should have a value '12/24/2018 23:59:59'"); assert.strictEqual($fieldProject.find(".o_input").val(), "Project 2", "The project field should have a value 'Project 2'"); assert.containsNone($fieldUser, ".badge", "The user field should not contain badges"); await testUtils.fields.editInput($modal.find("[name=name] input"), "NEWTASK 1"); await testUtils.modal.clickButton("Save & Close"); assert.strictEqual($(".modal").length, 0, "Modal should be closed"); rows = gantt.el.querySelectorAll(".o_gantt_row_container .o_gantt_row"); assert.deepEqual( [...rows] .filter((row) => row.classList.contains("o_gantt_row_nogroup")) .reduce((acc, row) => { acc[row.dataset.rowId] = [...row.querySelectorAll(".o_gantt_pill_title")].map((pill) => pill.innerText); return acc; }, {}), { '[{"user_ids":[1,"User 1"]},{"project_id":[1,"Project 1"]}]': ["Task 1", "NEWTASK 0"], '[{"user_ids":[2,"User 2"]},{"project_id":[1,"Project 1"]}]': ["Task 1", "NEWTASK 0"], '[{"user_ids":false},{"project_id":[1,"Project 1"]}]': ["Task 2", "Task 4", "Task 3"], '[{"user_ids":false},{"project_id":[2,"Project 2"]}]': ["Task 5", "Task 7", "NEWTASK 1"], }, "nogroup rows should have those tasks"); gantt.destroy(); }); QUnit.test('open a dialog stops the resize/drag', async function (assert) { assert.expect(3); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, domain: [['id', '=', 2]], }); const views = { 'tasks,false,form': '
', }; await prepareWowlFormViewDialogs({ models: this.data, views }); // open dialog to create a task // note that these 3 events need to be triggered for jQuery draggable // to be activated await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_pill'), "mouseover"); await testUtils.nextTick(); await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_pill'), "click"); await testUtils.nextTick(); assert.containsOnce($, '.modal', 'There should be one modal opened'); // close the modal without moving the mouse by pressing ESC window.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })); await testUtils.nextTick(); assert.containsNone($, '.modal', 'There should be no modal opened'); await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_cell:first'), "mousemove"); await testUtils.nextTick(); assert.containsNone(gantt, '.o_gantt_dragging', "the pill should not be dragging"); gantt.destroy(); }); QUnit.test('open a dialog to create a task, does not have a delete button', async function(assert){ assert.expect(1); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['user_id', 'project_id', 'stage'], }); const views = { 'tasks,false,form': `
`, }; await prepareWowlFormViewDialogs({ models: this.data, views }); // open dialog to create a task await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_row_container .o_gantt_row:nth(3) .o_gantt_cell[data-date="2018-12-10 00:00:00"] .o_gantt_cell_add'), "click"); await testUtils.nextTick(); var $modal = $('.modal'); assert.containsNone($modal, '.o_btn_remove', 'There should be no delete button on create dialog'); gantt.destroy(); }); QUnit.test('open a dialog to edit a task, has a delete buttton', async function(assert){ assert.expect(1); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['user_id', 'project_id', 'stage'], }); const views = { 'tasks,false,form': `
`, }; await prepareWowlFormViewDialogs({ models: this.data, views }); // open dialog to create a task await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_row_container .o_gantt_row:nth(3) .o_gantt_cell[data-date="2018-12-10 00:00:00"] .o_gantt_cell_add'), "click"); await testUtils.nextTick(); // create the task await testUtils.modal.clickButton('Save & Close'); // open dialog to view the task await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_row_container .o_gantt_row:nth(3) .o_gantt_cell[data-date="2018-12-10 00:00:00"] .o_gantt_pill'), "click"); await testUtils.nextTick(); var $modal = $('.modal'); assert.strictEqual($modal.find('.o_form_button_remove').length, 1, 'There should be a delete button on edit dialog'); gantt.destroy(); }); QUnit.test('clicking on delete button in edit dialog triggers a confirmation dialog, clicking discard does not call unlink on the model', async function(assert){ assert.expect(4); var unlinkCallCount = 0; var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['user_id', 'project_id', 'stage'], mockRPC: function (route, args) { if (args.method === 'unlink') { unlinkCallCount++; } return this._super.apply(this, arguments); } }); const views = { 'tasks,false,form': `
`, }; await prepareWowlFormViewDialogs({ models: this.data, views }); // open dialog to create a task await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_row_container .o_gantt_row:nth(3) .o_gantt_cell[data-date="2018-12-10 00:00:00"] .o_gantt_cell_add'), "click"); await testUtils.nextTick(); // create the task await testUtils.modal.clickButton('Save & Close'); // open dialog to view the task await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_row_container .o_gantt_row:nth(3) .o_gantt_cell[data-date="2018-12-10 00:00:00"] .o_gantt_pill'), "click"); await testUtils.nextTick(); // trigger the delete button await testUtils.modal.clickButton('Remove'); await testUtils.nextTick(); var $dialog = $('.modal-dialog'); // there sould be one more dialog assert.strictEqual($dialog.length, 2, 'Should have opened a new dialog'); assert.strictEqual(unlinkCallCount, 0, 'should not call unlink on the model if dialog is cancelled'); // trigger cancel await testUtils.modal.clickButton('Cancel'); await testUtils.nextTick(); $dialog = $('.modal-dialog'); assert.strictEqual($dialog.length, 0, 'Should have closed all dialog'); assert.strictEqual(unlinkCallCount, 0, 'Unlink should not have been called'); gantt.destroy(); }); QUnit.test('clicking on delete button in edit dialog triggers a confirmation dialog, clicking ok call unlink on the model', async function(assert){ assert.expect(4); var unlinkCallCount = 0; var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['user_id', 'project_id', 'stage'], mockRPC: function (route, args) { if (args.method === 'unlink') { unlinkCallCount++; } return this._super.apply(this, arguments); } }); const views = { 'tasks,false,form': `
`, }; await prepareWowlFormViewDialogs({ models: this.data, views }); // open dialog to create a task await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_row_container .o_gantt_row:nth(3) .o_gantt_cell[data-date="2018-12-10 00:00:00"] .o_gantt_cell_add'), "click"); await testUtils.nextTick(); // create the task await testUtils.modal.clickButton('Save & Close'); // open dialog to view the task await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_row_container .o_gantt_row:nth(3) .o_gantt_cell[data-date="2018-12-10 00:00:00"] .o_gantt_pill'), "click"); await testUtils.nextTick(); // trigger the delete button await testUtils.modal.clickButton('Remove'); await testUtils.nextTick(); var $dialog = $('.modal-dialog'); // there sould be one more dialog assert.strictEqual($dialog.length, 2, 'Should have opened a new dialog'); assert.strictEqual(unlinkCallCount, 0, 'should not call unlink on the model if dialog is cancelled'); // trigger ok await testUtils.modal.clickButton('Ok'); await testUtils.nextTick(); $dialog = $('.modal-dialog'); assert.strictEqual($dialog.length, 0, 'Should have closed all dialog'); assert.strictEqual(unlinkCallCount, 1, 'Unlink should have been called'); gantt.destroy(); }); QUnit.test('create dialog with timezone', async function (assert) { assert.expect(4); patchTimeZone(60); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, session: { getTZOffset: function () { return 60; }, }, }); const mockRPC = (route, args) => { if (args.method === 'create') { assert.deepEqual(args.args, [{ name: false, project_id: false, stage: false, start: "2018-12-09 23:00:00", stop: "2018-12-10 22:59:59", user_id: false, }], "the start/stop date should take timezone into account"); } }; const views = { 'tasks,false,form': `
`, }; await prepareWowlFormViewDialogs({ models: this.data, views }, mockRPC); // open dialog to create a task await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_cell[data-date="2018-12-10 00:00:00"] .o_gantt_cell_add'), "click"); await testUtils.nextTick(); assert.strictEqual($('.modal').length, 1, 'There should be one modal opened'); assert.strictEqual($('.modal .o_field_widget[name=start] .o_input').val(), '12/10/2018 00:00:00', 'The start field should have a value "12/10/2018 00:00:00"'); assert.strictEqual($('.modal .o_field_widget[name = stop] .o_input').val(), '12/10/2018 23:59:59', 'The stop field should have a value "12/10/2018 23:59:59"'); // create the task await testUtils.modal.clickButton('Save & Close'); gantt.destroy(); }); QUnit.test('plan button is not present if edit === false and plan is not specified', async function (assert) { assert.expect(1); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', archs: { 'tasks,false,list': '', 'tasks,false,search': '', }, viewOptions: { initialDate: initialDate, }, }); const views = { 'tasks,false,list': '', 'tasks,false,search': '', }; await prepareWowlFormViewDialogs({ models: this.data, views }); assert.strictEqual(gantt.$('.o_gantt_cell_plan').length, 0); gantt.destroy(); }); QUnit.test('plan button is not present if edit === false and plan is true', async function (assert) { assert.expect(1); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); const views = { 'tasks,false,list': '', 'tasks,false,search': '', }; await prepareWowlFormViewDialogs({ models: this.data, views }); assert.strictEqual(gantt.$('.o_gantt_cell_plan').length, 0); gantt.destroy(); }); QUnit.test('plan button is not present if edit === true and plan === false', async function (assert) { assert.expect(1); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); const views = { 'tasks,false,list': '', 'tasks,false,search': '', }; await prepareWowlFormViewDialogs({ models: this.data, views }); assert.strictEqual(gantt.$('.o_gantt_cell_plan').length, 0); gantt.destroy(); }); QUnit.test('plan button is present if edit === true and plan is not set', async function (assert) { assert.expect(1); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); const views = { 'tasks,false,list': '', 'tasks,false,search': '', }; await prepareWowlFormViewDialogs({ models: this.data, views }); assert.notStrictEqual(gantt.$('.o_gantt_cell_plan').length, 0); gantt.destroy(); }); QUnit.test('plan button is present if edit === true and plan is true', async function (assert) { assert.expect(1); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); const views = { 'tasks,false,list': '', 'tasks,false,search': '', }; await prepareWowlFormViewDialogs({ models: this.data, views }); assert.notStrictEqual(gantt.$('.o_gantt_cell_plan').length, 0); gantt.destroy(); }); QUnit.test('open a dialog to plan a task', async function (assert) { assert.expect(5); this.data.tasks.records.push({ id: 41, name: 'Task 41' }); this.data.tasks.records.push({ id: 42, name: 'Task 42', stop: '2018-12-31 18:29:59' }); this.data.tasks.records.push({ id: 43, name: 'Task 43', start: '2018-11-30 18:30:00' }); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (args.method === 'write') { assert.strictEqual(args.model, 'tasks', "should write on the current model"); assert.deepEqual(args.args[0], [41, 42], "should write on the selected ids"); assert.deepEqual(args.args[1], { start: "2018-12-10 00:00:00", stop: "2018-12-10 23:59:59" }, "should write the correct values on the correct fields"); } return this._super.apply(this, arguments); }, }); const views = { 'tasks,false,list': '', 'tasks,false,search': '', }; await prepareWowlFormViewDialogs({ models: this.data, views }); // click on the plan button await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_cell[data-date="2018-12-10 00:00:00"] .o_gantt_cell_plan'), "click"); await testUtils.nextTick(); assert.strictEqual($('.modal .o_list_view').length, 1, "a list view dialog should be opened"); assert.strictEqual($('.modal .o_list_view tbody .o_data_cell').text().replace(/\s+/g, ''), "Task41Task42Task43", "the 3 records without date set should be displayed"); await testUtils.dom.click($('.modal .o_list_view tbody tr:eq(0) input')); await testUtils.dom.click($('.modal .o_list_view tbody tr:eq(1) input')); await testUtils.dom.click($('.modal .o_select_button:contains(Select)')); gantt.destroy(); }); QUnit.test('open a dialog to plan a task (with timezone)', async function (assert) { assert.expect(2); patchTimeZone(60); this.data.tasks.records.push({ id: 41, name: 'Task 41' }); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (args.method === 'write') { assert.deepEqual(args.args[0], [41], "should write on the selected id"); assert.deepEqual(args.args[1], { start: "2018-12-09 23:00:00", stop: "2018-12-10 22:59:59" }, "should write the correct start/stop taking timezone into account"); } return this._super.apply(this, arguments); }, session: { getTZOffset: function () { return 60; }, }, }); const views = { 'tasks,false,list': '', 'tasks,false,search': '', }; await prepareWowlFormViewDialogs({ models: this.data, views }); // click on the plan button await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_cell[data-date="2018-12-10 00:00:00"] .o_gantt_cell_plan'), "click"); await testUtils.nextTick(); await testUtils.dom.click($('.modal .o_list_view tbody tr:eq(0) input')); await testUtils.nextTick(); await testUtils.dom.click($('.modal .o_select_button:contains(Select)')); gantt.destroy(); }); QUnit.test('open a dialog to plan a task (multi-level)', async function (assert) { assert.expect(2); this.data.tasks.records.push({ id: 41, name: 'Task 41' }); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (args.method === 'write') { assert.deepEqual(args.args[0], [41], "should write on the selected id"); assert.deepEqual(args.args[1], { project_id: 1, stage: "todo", start: "2018-12-10 00:00:00", stop: "2018-12-10 23:59:59", user_id: 1, }, "should write on all the correct fields"); } return this._super.apply(this, arguments); }, groupBy: ['user_id', 'project_id', 'stage'], }); const views = { 'tasks,false,list': '', 'tasks,false,search': '', }; await prepareWowlFormViewDialogs({ models: this.data, views }); // click on the plan button await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_row:not(.o_gantt_row_group):first .o_gantt_cell[data-date="2018-12-10 00:00:00"] .o_gantt_cell_plan'), "click"); await testUtils.nextTick(); await testUtils.dom.click($('.modal .o_list_view tbody tr:eq(0) input')); await testUtils.nextTick(); await testUtils.dom.click($('.modal .o_select_button:contains(Select)')); gantt.destroy(); }); QUnit.test('expand/collapse rows', async function (assert) { assert.expect(8); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', groupBy: ['user_id', 'project_id', 'stage'], viewOptions: { initialDate: initialDate, }, }); assert.containsN(gantt, '.o_gantt_row_group.open', 6, "there should be 6 opened grouped (2 for the users + 2 projects by users = 6)"); assert.containsN(gantt, '.o_gantt_row_group:not(.open)', 0, "all groups should be opened"); // collapse all groups await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_collapse_rows')); assert.containsN(gantt, '.o_gantt_row_group:not(.open)', 2, "there should be 2 closed groups"); assert.containsN(gantt, '.o_gantt_row_group.open', 0, "all groups should now be closed"); // expand all groups await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_expand_rows')); assert.containsN(gantt, '.o_gantt_row_group.open', 6, "there should be 6 opened grouped"); assert.containsN(gantt, '.o_gantt_row_group:not(.open)', 0, "all groups should be opened again"); // collapse the first group await testUtils.dom.click(gantt.$('.o_gantt_row_group:first .o_gantt_row_sidebar')); assert.containsN(gantt, '.o_gantt_row_group.open', 3, "there should be three open groups"); assert.containsN(gantt, '.o_gantt_row_group:not(.open)', 1, "there should be 1 closed group"); gantt.destroy(); }); QUnit.test('collapsed rows remain collapsed at reload', async function (assert) { assert.expect(6); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', groupBy: ['user_id', 'project_id', 'stage'], viewOptions: { initialDate: initialDate, }, }); assert.containsN(gantt, '.o_gantt_row_group.open', 6, "there should be 6 opened grouped (2 for the users + 2 projects by users = 6)"); assert.containsN(gantt, '.o_gantt_row_group:not(.open)', 0, "all groups should be opened"); // collapse the first group await testUtils.dom.click(gantt.$('.o_gantt_row_group:first .o_gantt_row_sidebar')); assert.containsN(gantt, '.o_gantt_row_group.open', 3, "there should be three open groups"); assert.containsN(gantt, '.o_gantt_row_group:not(.open)', 1, "there should be 1 closed group"); // reload gantt.reload({}); assert.containsN(gantt, '.o_gantt_row_group.open', 3, "there should be three open groups"); assert.containsN(gantt, '.o_gantt_row_group:not(.open)', 1, "there should be 1 closed group"); gantt.destroy(); }); QUnit.test('resize a pill', async function (assert) { assert.expect(13); var nbWrite = 0; var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, domain: [['id', '=', 1]], mockRPC: function (route, args) { if (args.method === 'write') { assert.deepEqual(args.args[0], [1]); // initial dates -- start: '2018-11-30 18:30:00', stop: '2018-12-31 18:29:59' if (nbWrite === 0) { assert.deepEqual(args.args[1], { stop: "2018-12-30 18:29:59" }); } else { assert.deepEqual(args.args[1], { start: "2018-11-29 18:30:00" }); } nbWrite++; } return this._super.apply(this, arguments); }, }); assert.containsOnce(gantt, '.o_gantt_pill', "there should be one pill (Task 1)"); assert.containsNone(gantt, '.o_gantt_pill.ui-resizable', "the pill should not be resizable after initial rendering"); await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_pill'), 'mouseover'); assert.containsOnce(gantt, '.o_gantt_pill.ui-resizable', "the pill should be resizable after mouse enter"); assert.containsNone(gantt, '.ui-resizable-w', "there should be no left resizer for task 1 (it starts before december)"); assert.containsOnce(gantt, '.ui-resizable-e', "there should be one right resizer for task 1"); // resize to one cell smaller (-1 day) var cellWidth = gantt.$('.o_gantt_cell:first').width() + 4; await testUtils.dom.dragAndDrop( gantt.$('.ui-resizable-e'), gantt.$('.ui-resizable-e'), { position: { left: -cellWidth, top: 0 } } ); // go to previous month (november) await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_prev')); await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_pill'), 'mouseover'); assert.containsOnce(gantt, '.o_gantt_pill', "there should still be one pill (Task 1)"); assert.containsNone(gantt, '.ui-resizable-e', "there should be no right resizer for task 1 (it stops after november)"); assert.containsOnce(gantt, '.ui-resizable-w', "there should be one left resizer for task 1"); // resize to one cell smaller (-1 day) await testUtils.dom.dragAndDrop( gantt.$('.ui-resizable-w'), gantt.$('.ui-resizable-w'), { position: { left: -cellWidth, top: 0 } } ); assert.strictEqual(nbWrite, 2); gantt.destroy(); }); QUnit.test('resize pill in year mode', async function (assert) { assert.expect(2); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_pill'), 'mouseover'); assert.containsOnce(gantt, '.o_gantt_pill.ui-resizable', "in the year mode the pill should be resizable after mouse enter"); var pillWidth = gantt.$('.o_gantt_pill').width(); var cellWidth = gantt.$('.o_gantt_cell:first').width() + 4; await testUtils.dom.dragAndDrop( gantt.$('.ui-resizable-e'), gantt.$('.ui-resizable-e'), { position: { left: 2 * cellWidth, top: 0 } } ); assert.strictEqual(pillWidth , gantt.$('.o_gantt_pill').width(), "the pill should have the same width as before the resize"); gantt.destroy(); }); QUnit.test('resize a pill (2)', async function (assert) { // This test checks a tricky situation where the user resizes a pill, and // triggers the mouseup (i.e. release the mouse) over the pill. In this // case, the click should not be considered as a click on the pill to // edit it. assert.expect(6); const def = testUtils.makeTestPromise(); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', archs: { 'tasks,false,form': '
', }, viewOptions: { initialDate: initialDate, }, domain: [['id', '=', 2]], mockRPC: async function (route, args) { const result = this._super(...arguments); if (args.method === 'write') { assert.deepEqual(args.args[1], { stop: "2018-12-23 06:29:59" }); await def; } return result; }, }); assert.containsOnce(gantt, '.o_gantt_pill', "there should be one pill (Task 1)"); assert.containsNone(gantt, '.o_gantt_pill.ui-resizable', "the pill should not be resizable after initial rendering"); await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_pill'), 'mouseover'); assert.containsOnce(gantt, '.o_gantt_pill.ui-resizable', "the pill should be resizable after mouse enter"); assert.containsOnce(gantt, '.ui-resizable-e', "there should be one right resizer for task 2"); // resize to one cell larger, but do the mouseup over the pill const $resize = gantt.$('.ui-resizable-e'); const cellWidth = gantt.$('.o_gantt_cell:first').width() + 4; const options = { position: { left: 0.9 * cellWidth, // do the mouseup over the pill top: 10, }, withTrailingClick: true, mouseupTarget: gantt.$('.o_gantt_pill'), }; await testUtils.dom.dragAndDrop($resize, $resize, options); def.resolve(); assert.containsNone(document.body, '.modal', 'shoud not have opened the dialog to edit the pill'); gantt.destroy(); }); QUnit.test('create a task maintains the domain', async function (assert) { assert.expect(2); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', domain: [['user_id', '=', 2]], // I am an important line viewOptions: { initialDate: initialDate, }, }); const views = { 'tasks,false,form': '', }; await prepareWowlFormViewDialogs({ models: this.data, views }); assert.containsN(gantt, '.o_gantt_pill', 3, "the list view is filtered"); await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_cell:first .o_gantt_cell_add'), "click"); await testUtils.nextTick(); await testUtils.fields.editInput($('.modal .modal-body [name=name] input'), 'new task'); await testUtils.modal.clickButton('Save & Close'); assert.containsN(gantt, '.o_gantt_pill', 3, "the list view is still filtered after the save"); gantt.destroy(); }); QUnit.test('pill is updated after failed resized', async function (assert) { assert.expect(3); var nbRead = 0; var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, domain: [['id', '=', 7]], mockRPC: function (route, args) { if (args.method === 'write') { assert.strictEqual(true, true, "should perform a write"); return Promise.reject(); } if (route === '/web/dataset/search_read') { nbRead++; } return this._super.apply(this, arguments); }, }); var pillWidth = gantt.$('.o_gantt_pill').width(); await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_pill'), 'mouseover'); // resize to one cell larger (1 day) var cellWidth = gantt.$('.o_gantt_cell:first').width() + 4; await testUtils.dom.dragAndDrop( gantt.$('.ui-resizable-e'), gantt.$('.ui-resizable-e'), { position: { left: cellWidth, top: 0 } } ); assert.strictEqual(nbRead, 2); assert.strictEqual(pillWidth, gantt.$('.o_gantt_pill').width(), "the pill should have the same width as before the resize"); gantt.destroy(); }); QUnit.test('move a pill in the same row', async function (assert) { assert.expect(5); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, domain: [['id', '=', 7]], mockRPC: function (route, args) { if (args.method === 'write') { assert.deepEqual(args.args[0], [7], "should write on the correct record"); assert.deepEqual(args.args[1], { start: "2018-12-21 10:30:12", stop: "2018-12-21 18:29:59", }, "both start and stop date should be correctly set (+1 day)"); } return this._super.apply(this, arguments); }, }); assert.containsOnce(gantt, '.o_gantt_pill', "there should be one pill (Task 1)"); assert.doesNotHaveClass(gantt.$('.o_gantt_pill'), 'ui-draggable', "the pill should not be draggable after initial rendering"); await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_pill'), 'mouseover'); assert.hasClass(gantt.$('.o_gantt_pill'), 'ui-draggable', "the pill should be draggable after mouse enter"); // move a pill in the next cell (+1 day) var cellWidth = gantt.$('.o_gantt_header_scale .o_gantt_header_cell:first')[0].getBoundingClientRect().width + 4; await testUtils.dom.dragAndDrop( gantt.$('.o_gantt_pill'), gantt.$('.o_gantt_pill'), { position: { left: cellWidth, top: 0 } }, ); gantt.destroy(); }); QUnit.test('move a pill in the same row (with timezone)', async function (assert) { assert.expect(2); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, domain: [['id', '=', 7]], mockRPC: function (route, args) { if (args.method === 'write') { assert.deepEqual(args.args[0], [7], "should write on the correct record"); assert.deepEqual(args.args[1], { start: "2018-12-21 10:30:12", stop: "2018-12-21 18:29:59", }, "both start and stop date should be correctly set (+1 day)"); } return this._super.apply(this, arguments); }, session: { getTZOffset: function () { return 60; }, }, }); // move a pill in the next cell (+1 day) var cellWidth = gantt.$('.o_gantt_header_scale .o_gantt_header_cell:first')[0].getBoundingClientRect().width + 4; await testUtils.dom.dragAndDrop( gantt.$('.o_gantt_pill'), gantt.$('.o_gantt_pill'), { position: { left: cellWidth, top: 0 } }, ); gantt.destroy(); }); QUnit.test('move a pill in the same row (with different timezone)', async function (assert) { // find on which day daylight savings starts (or ends, it doesn't matter which) in the local timezone // we need to make sure it's not a day at the end or beginning of a month assert.expect(3); var dstDate = null; for(var d = moment('2019-01-01 12:00:00'); d.isBefore('2019-12-31'); ) { var nextDay = d.clone().add(1, 'day'); if(nextDay.month() === d.month() && nextDay.utcOffset() !== d.utcOffset()) { dstDate = d; break; } d = nextDay; } if(!dstDate) { // we can't really do the test if there is no DST :( // unfortunately, the runbot tests are executed on UTC, so we need to dummy the test in that case... // it would be ideal if we could pass a timezone to use for unit tests instead // (it's possible with the moment-timezone library, but we don't want to add an external dependency // as part of a bugfix...) dstDate = moment('2020-03-28 12:00:00'); } var initialDate = dstDate; var taskStart = dstDate.clone().hour(10).minute(30).utc(); this.data.tasks.records[7].start = taskStart.format('YYYY-MM-DD HH:mm:ss'); this.data.tasks.records[7].stop = taskStart.clone().local().hour(16).utc().format('YYYY-MM-DD HH:mm:ss'); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, domain: [['id', '=', 8]], mockRPC: function (route, args) { if (args.method === 'write') { assert.deepEqual(args.args[0], [8], "should write on the correct record"); assert.equal(moment.utc(args.args[1].start).local().hour(), 10, "start date should have the same local time as original") assert.equal(moment.utc(args.args[1].stop).local().hour(), 16, "stop date should have the same local time as original") } return this._super.apply(this, arguments); }, session: { getTZOffset: function (d) { return 60; }, }, }); // we are going to move the pill for task 8 (10/24/2020 06:30:12) by 1 cell to the right (+1 day) var cellWidth = gantt.$('.o_gantt_header_scale .o_gantt_header_cell:first')[0].getBoundingClientRect().width + 4; await testUtils.dom.dragAndDrop( gantt.$('.o_gantt_pill[data-id=8]'), gantt.$('.o_gantt_pill[data-id=8]'), { position: { left: cellWidth + 1, top: 0 } }, ); gantt.destroy(); }); QUnit.test('move a pill in another row', async function (assert) { assert.expect(4); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', groupBy: ['project_id'], viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (args.method === 'write') { assert.deepEqual(args.args[0], [7], "should write on the correct record"); assert.deepEqual(args.args[1], { project_id: 1, start: "2018-12-21 10:30:12", stop: "2018-12-21 18:29:59", }, "all modified fields should be correctly set"); } return this._super.apply(this, arguments); }, domain: [['id', 'in', [1, 7]]], }); assert.containsN(gantt, '.o_gantt_pill', 2, "there should be two pills (task 1 and task 7)"); assert.containsN(gantt, '.o_gantt_row', 2, "there should be two rows (project 1 and project 2"); // move a pill (task 7) in the other row and in the the next cell (+1 day) var cellWidth = gantt.$('.o_gantt_header_scale .o_gantt_header_cell:first')[0].getBoundingClientRect().width + 4; var cellHeight = gantt.$('.o_gantt_cell:first').height(); await testUtils.dom.dragAndDrop( gantt.$('.o_gantt_pill[data-id=7]'), gantt.$('.o_gantt_pill[data-id=7]'), { position: { left: cellWidth + 4, top: -cellHeight } }, ); gantt.destroy(); }); QUnit.test('copy a pill in another row', async function (assert) { assert.expect(4); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', groupBy: ['project_id'], viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (args.method === 'copy') { assert.deepEqual(args.args[0], 7, "should copy the correct record"); assert.deepEqual(args.args[1], { start: "2018-12-21 10:30:12", stop: "2018-12-21 18:29:59", project_id: 1 }, "should use the correct default values when copying"); } return this._super.apply(this, arguments); }, domain: [['id', 'in', [1, 7]]], }); assert.containsN(gantt, '.o_gantt_pill', 2, "there should be two pills (task 1 and task 7)"); assert.containsN(gantt, '.o_gantt_row', 2, "there should be two rows (project 1 and project 2"); // move a pill (task 7) in the other row and in the the next cell (+1 day) var cellWidth = gantt.$('.o_gantt_header_scale .o_gantt_header_cell:first')[0].getBoundingClientRect().width + 4; var cellHeight = gantt.$('.o_gantt_cell:first').height() / 2; await testUtils.dom.triggerEvent(gantt.$el, 'keydown',{ctrlKey: true}, true); await testUtils.dom.dragAndDrop( gantt.$('.o_gantt_pill[data-id=7]'), gantt.$('.o_gantt_pill[data-id=7]'), { position: { left: cellWidth, top: -cellHeight }, ctrlKey: true }, ); await testUtils.nextTick(); gantt.destroy(); }); QUnit.test('move a pill in another row in multi-level grouped', async function (assert) { assert.expect(3); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', groupBy: ['user_id', 'project_id', 'stage'], viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (args.method === 'write') { assert.deepEqual(args.args[0], [7], "should write on the correct record"); assert.deepEqual(args.args[1], { user_id: 2, }, "we should only write on user_id"); } return this._super.apply(this, arguments); }, domain: [['id', 'in', [3, 7]]], }); gantt.$('.o_gantt_pill').each(function () { testUtils.dom.triggerMouseEvent($(this), 'mouseover'); }); await testUtils.nextTick(); assert.containsN(gantt, '.o_gantt_pill.ui-draggable:not(.o_fake_draggable)', 1, "there should be only one draggable pill (Task 7)"); // move a pill (task 7) in the top-level group (User 2) var $pill = gantt.$('.o_gantt_pill.ui-draggable:not(.o_fake_draggable)'); var groupHeaderHeight = gantt.$('.o_gantt_cell:first').height(); var cellHeight = $pill.closest('.o_gantt_cell').height(); await testUtils.dom.dragAndDrop( $pill, $pill, { position: { left: 4, top: -3 * groupHeaderHeight - cellHeight } }, ); gantt.destroy(); }); QUnit.test('move a pill in another row in multi-level grouped (many2many case)', async function (assert) { assert.expect(3); this.data.tasks.fields.user_ids = { string: 'Assignees', type: 'many2many', relation: 'users' }; this.data.tasks.records[1].user_ids = [1, 2]; const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', groupBy: ['user_id', 'project_id', 'user_ids'], viewOptions: { initialDate }, mockRPC: function (route, args) { if (args.method === 'write') { assert.deepEqual(args.args[0], [2], "should write on the correct record"); assert.deepEqual(args.args[1], { "project_id": 1, "start": "2018-12-10 23:30:00", "stop": "2018-12-15 18:29:59", "user_id": 2, "user_ids": false }, "should write these changes"); } return this._super.apply(this, arguments); }, domain: [["user_id", "=", 2],["project_id", "=", 1]] }); // initialize dragging feature... gantt.$('.o_gantt_pill').each(function () { testUtils.dom.triggerMouseEvent($(this), 'mouseover'); }); await testUtils.nextTick(); // sanity check const draggable = gantt.el.querySelectorAll(".o_gantt_pill.ui-draggable:not(.o_fake_draggable)"); assert.deepEqual( [...draggable].map((el) => el.innerText), ["Task 2", "Task 2"], "there should be only 2 draggable pills (twice 'Task 2')" ); // move a pill (first task 2) in last row group (Undefined Assignees) await testUtils.dom.dragAndDrop( draggable[0], gantt.el.querySelector(".o_gantt_row_nogroup:last-child"), ); gantt.destroy(); }); QUnit.test('display closest hook when pill being dragged', async function (assert) { assert.expect(1); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, domain: [['id', 'in', [3, 7]]], }); await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_pill:first'), 'mouseover'); const cellWidth = gantt.$('.o_gantt_header_scale .o_gantt_header_cell:first')[0].getBoundingClientRect().width + 4; await testUtils.dom.dragAndDrop( gantt.$('.o_gantt_pill:first'), gantt.$('.o_gantt_pill:first'), { position: { left: cellWidth, top: 0 }, disableDrop: true }, ); assert.hasClass(gantt.$("div[data-date='2018-12-21 00:00:00']"), 'ui-drag-hover', "the hook should be displayed with dotted border"); gantt.destroy(); }); QUnit.test('closest hook should be removed on pill drop', async function (assert) { assert.expect(1); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, domain: [['id', '=', 7]], }); await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_pill'), 'mouseover'); const cellWidth = gantt.$('.o_gantt_header_scale .o_gantt_header_cell:first')[0].getBoundingClientRect().width + 4; await testUtils.dom.dragAndDrop( gantt.$('.o_gantt_pill'), gantt.$('.o_gantt_pill'), { position: { left: cellWidth, top: 0 } }, ); assert.doesNotHaveClass(gantt.$("div[data-date='2018-12-21 00:00:00']"), 'ui-drag-hover', "the hook should not be displayed with dotted border after drop"); gantt.destroy(); }); QUnit.test('grey pills should not be resizable nor draggable', async function (assert) { assert.expect(4); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['user_id', 'project_id'], domain: [['id', '=', 7]], }); gantt.$('.o_gantt_pill').each(function () { testUtils.dom.triggerMouseEvent($(this), 'mouseover'); }); await testUtils.nextTick(); assert.doesNotHaveClass(gantt.$('.o_gantt_row_group .o_gantt_pill'), 'ui-resizable', 'the group row pill should not be resizable'); assert.hasClass(gantt.$('.o_gantt_row_group .o_gantt_pill'), 'o_fake_draggable', 'the group row pill should not be draggable'); assert.hasClass(gantt.$('.o_gantt_row:not(.o_gantt_row_group) .o_gantt_pill'), 'ui-resizable', 'the pill should be resizable'); assert.hasClass(gantt.$('.o_gantt_row:not(.o_gantt_row_group) .o_gantt_pill'), 'ui-draggable', 'the pill should be draggable'); gantt.destroy(); }); QUnit.test('should not be draggable when disable_drag_drop is set', async function (assert) { assert.expect(2); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['user_id', 'project_id'], domain: [['id', '=', 7]], }); await testUtils.nextTick(); assert.doesNotHaveClass(gantt.$('.o_gantt_row_group .o_gantt_pill'), 'ui-draggable', 'the group row pill should not be draggable'); assert.doesNotHaveClass(gantt.$('.o_gantt_row:not(.o_gantt_row_group) .o_gantt_pill'), 'ui-draggable', 'the pill should not be draggable'); gantt.destroy(); }); QUnit.test('gantt_unavailability reloads when the view\'s scale changes', async function(assert){ assert.expect(11); var unavailabilityCallCount = 0; var unavailabilityScaleArg = 'none'; var reloadCount = 0; var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { var result; if (route === '/web/dataset/search_read') { reloadCount++; result = this._super.apply(this, arguments); } else if (args.method === 'gantt_unavailability') { unavailabilityCallCount++; unavailabilityScaleArg = args.args[2]; result = args.args[4]; } return Promise.resolve(result); }, }); assert.strictEqual(reloadCount, 1, 'view should have loaded') assert.strictEqual(unavailabilityCallCount, 1, 'view should have loaded unavailability'); await testUtils.dom.click(gantt.$('.o_gantt_button_scale[data-value=week]')); assert.strictEqual(reloadCount, 2, 'view should have reloaded when switching scale to week') assert.strictEqual(unavailabilityCallCount, 2, 'view should have reloaded when switching scale to week'); assert.strictEqual(unavailabilityScaleArg, 'week', 'unavailability should have been called with the week scale'); await testUtils.dom.click(gantt.$('.o_gantt_button_scale[data-value=month]')); assert.strictEqual(reloadCount, 3, 'view should have reloaded when switching scale to month') assert.strictEqual(unavailabilityCallCount, 3, 'view should have reloaded when switching scale to month'); assert.strictEqual(unavailabilityScaleArg, 'month', 'unavailability should have been called with the month scale'); await testUtils.dom.click(gantt.$('.o_gantt_button_scale[data-value=year]')); assert.strictEqual(reloadCount, 4, 'view should have reloaded when switching scale to year') assert.strictEqual(unavailabilityCallCount, 4, 'view should have reloaded when switching scale to year'); assert.strictEqual(unavailabilityScaleArg, 'year', 'unavailability should have been called with the year scale'); gantt.destroy(); }); QUnit.test('gantt_unavailability reload when period changes', async function(assert){ assert.expect(6); var unavailabilityCallCount = 0; var reloadCount = 0; var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { var result; if (route === '/web/dataset/search_read') { reloadCount++; result = this._super.apply(this, arguments); } else if (args.method === 'gantt_unavailability') { unavailabilityCallCount++; result = args.args[4]; } return Promise.resolve(result); }, }); assert.strictEqual(reloadCount, 1, 'view should have loaded') assert.strictEqual(unavailabilityCallCount, 1, 'view should have loaded unavailability'); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_next')); assert.strictEqual(reloadCount, 2, 'view should have reloaded when clicking next') assert.strictEqual(unavailabilityCallCount, 2, 'view should have reloaded unavailability when clicking next'); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_prev')); assert.strictEqual(reloadCount, 3, 'view should have reloaded when clicking prev') assert.strictEqual(unavailabilityCallCount, 3, 'view should have reloaded unavailability when clicking prev'); gantt.destroy(); }); QUnit.test('gantt_unavailability should not reload when period changes if display_unavailability is not set', async function(assert){ assert.expect(6); var unavailabilityCallCount = 0; var reloadCount = 0; var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { var result; if (route === '/web/dataset/search_read') { reloadCount++; result = this._super.apply(this, arguments); } else if (args.method === 'gantt_unavailability') { unavailabilityCallCount++; result = {}; } return Promise.resolve(result); }, }); assert.strictEqual(reloadCount, 1, 'view should have loaded') assert.strictEqual(unavailabilityCallCount, 0, 'view should not have loaded unavailability'); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_next')); assert.strictEqual(reloadCount, 2, 'view should have reloaded when clicking next') assert.strictEqual(unavailabilityCallCount, 0, 'view should not have reloaded unavailability when clicking next'); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_prev')); assert.strictEqual(reloadCount, 3, 'view should have reloaded when clicking prev') assert.strictEqual(unavailabilityCallCount, 0, 'view should not have reloaded unavailability when clicking prev'); gantt.destroy(); }); QUnit.test('cancelled drag and tooltip', async function (assert) { assert.expect(6); var POPOVER_DELAY = GanttRow.prototype.POPOVER_DELAY; GanttRow.prototype.POPOVER_DELAY = 0; this.data.tasks.records[1].start = '2018-12-16 03:00:00'; var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', archs: { 'tasks,false,form': '
', }, viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (args.method === 'write') { throw new Error('Should not do a write RPC'); } return this._super.apply(this, arguments); }, }); const views = { 'tasks,false,form': '', }; await prepareWowlFormViewDialogs({ models: this.data, views }); assert.containsN(gantt, '.o_gantt_pill', 4); const $secondPill = gantt.$('.o_gantt_pill:nth(1)'); // enable the drag feature await testUtils.dom.triggerMouseEvent($secondPill, 'mouseover'); assert.hasClass($secondPill, 'ui-draggable', "the pill should be draggable after mouse enter"); assert.containsOnce(document.body, 'div.popover'); // move the pill of a few px (not enough for it to actually move to another cell) await testUtils.dom.dragAndDrop($secondPill, $secondPill, { position: { left: 4, top: 4 }, withTrailingClick: true, }); // check popover await testUtils.dom.triggerEvents($secondPill, ['mouseover']); assert.containsOnce(document.body, 'div.popover'); // edit pill await testUtils.dom.triggerEvents($secondPill, ['click']); assert.containsOnce(document.body, '.modal .o_form_view'); gantt.destroy(); assert.containsNone(gantt, 'div.popover', 'should not have a popover anymore'); GanttRow.prototype.POPOVER_DELAY = POPOVER_DELAY; }); QUnit.test('drag&drop on other pill in grouped view', async function (assert) { assert.expect(4); var POPOVER_DELAY = GanttRow.prototype.POPOVER_DELAY; GanttRow.prototype.POPOVER_DELAY = 0; this.data.tasks.records[0].start = '2018-12-16 05:00:00'; this.data.tasks.records[0].stop = '2018-12-16 07:00:00'; this.data.tasks.records[1].stop = '2018-12-17 13:00:00'; let resolve; const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['project_id'], mockRPC: async function (route, args) { const promise = this._super.apply(this, arguments); if (args.method === 'write') { await new Promise(r => { resolve = r; }); } return promise; }, }); const views = { 'tasks,false,form': '', }; await prepareWowlFormViewDialogs({ models: this.data, views }); const $firstPill = gantt.$('.o_gantt_pill:nth(0)'); let $secondPill = gantt.$('.o_gantt_pill:nth(1)'); // enable the drag feature await testUtils.dom.triggerMouseEvent($secondPill, 'mouseover'); assert.hasClass($secondPill, 'ui-draggable', "the pill should be draggable after mouse enter"); assert.containsOnce(document.body, 'div.popover h3:contains(Task 2)', 'the pill should be display the popover'); // move the pill on hover the other pill await testUtils.dom.dragAndDrop($secondPill, $firstPill, { disableDrop: true, }); // drop the pill on the other pill, the mouse stay at the same place await testUtils.dom.triggerMouseEvent($secondPill, 'mouseup'); await testUtils.dom.triggerMouseEvent($firstPill, 'mouseover'); // wait popover is shown await testUtils.nextTick(); await testUtils.returnAfterNextAnimationFrame(); assert.containsOnce(document.body, 'div.popover h3:contains(Task 1)', 'when drag&drop the other pills should be display the popover'); // force a long transition duration (avoid intermittent error raising) $('div.popover:has(h3:contains(Task 1))').css('transition-duration', '1s'); // wait the redrawing and popover is removed resolve(); await testUtils.nextTick(); await testUtils.returnAfterNextAnimationFrame(); assert.containsNone(document.body, 'div.popover', 'should not have a popover anymore'); gantt.destroy(); GanttRow.prototype.POPOVER_DELAY = POPOVER_DELAY; }); // ATTRIBUTES TESTS QUnit.test('create attribute', async function (assert) { assert.expect(2); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); // the "Add" should not appear assert.containsNone(gantt.$buttons.find('.o_gantt_button_add'), "there should be no 'Add' button"); await testUtils.dom.click(gantt.$('.o_gantt_cell:first')); assert.strictEqual($('.modal').length, 0, "there should be no opened modal"); gantt.destroy(); }); QUnit.test('edit attribute', async function (assert) { assert.expect(4); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); const views = { 'tasks,false,form': '', }; await prepareWowlFormViewDialogs({ models: this.data, views }); assert.containsNone(gantt, '.o_gantt_pill.ui-resizable', "the pills should not be resizable"); assert.containsNone(gantt, '.o_gantt_pill.ui-draggable', "the pills should not be draggable"); await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_pill:first'), 'click'); await testUtils.nextTick(); assert.strictEqual($('.modal').length, 1, "there should be a opened modal"); assert.strictEqual($('.modal .o_form_view .o_form_readonly').length, 1, "the form view should be in readonly"); gantt.destroy(); }); QUnit.test('total_row attribute', async function (assert) { assert.expect(6); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); assert.containsOnce(gantt, '.o_gantt_row_container .o_gantt_row', 'should have 1 row'); assert.containsOnce(gantt, '.o_gantt_total_row_container .o_gantt_row_total', 'should have 1 total row'); assert.containsNone(gantt, '.o_gantt_row_container .o_gantt_row_sidebar', 'container should not have a sidebar'); assert.containsNone(gantt, '.o_gantt_total_row_container .o_gantt_row_sidebar', 'total container should not have a sidebar'); assert.containsN(gantt, '.o_gantt_row_total .o_gantt_pill ', 7, 'should have a 7 pills in the total row'); assert.strictEqual(gantt.$('.o_gantt_row_total .o_gantt_consolidated_pill_title').text().replace(/\s+/g, ''), "2123212", "the total row should be correctly computed"); gantt.destroy(); }); QUnit.test('default_scale attribute', async function (assert) { assert.expect(3); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); assert.hasClass(gantt.$buttons.find('.o_gantt_button_scale[data-value=day]'), 'active', 'day view should be activated'); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), 'Thursday, December 20, 2018', 'should contain "Thursday, December 20, 2018" in header'); assert.containsN(gantt, '.o_gantt_header_container .o_gantt_header_scale .o_gantt_header_cell', 24, 'should have a 24 slots for day view'); gantt.destroy(); }); QUnit.test('scales attribute', async function (assert) { assert.expect(3); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); assert.containsN(gantt.$buttons, '.o_gantt_button_scale', 2, 'only 2 scales should be available'); assert.strictEqual(gantt.$buttons.find('.o_gantt_button_scale').first().text().trim(), 'Month', 'Month scale should be the first option'); assert.strictEqual(gantt.$buttons.find('.o_gantt_button_scale').last().text().trim(), 'Day', 'Day scale should be the second option'); gantt.destroy(); }); QUnit.test('precision attribute', async function (assert) { assert.expect(3); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, domain: [['id', '=', 7]], mockRPC: function (route, args) { if (args.method === 'write') { assert.deepEqual(args.args[1], { stop: "2018-12-20 18:44:59" }); } return this._super.apply(this, arguments); }, }); var cellWidth = gantt.$('.o_gantt_cell:first').width() + 4; await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_pill'), 'mouseover'); // resize of a quarter await testUtils.dom.dragAndDrop( gantt.$('.ui-resizable-e'), gantt.$('.ui-resizable-e'), { disableDrop: true, position: { left: cellWidth / 4, top: 0 } } ); assert.strictEqual(gantt.$('.o_gantt_pill_resize_badge').text().trim(), "+15 minutes", "the resize should be by 15min step"); // manually trigger the drop to trigger a write var toOffset = gantt.$('.ui-resizable-e').offset(); await gantt.$('.ui-resizable-e').trigger($.Event("mouseup", { which: 1, pageX: toOffset.left + cellWidth / 4, pageY: toOffset.top })); await testUtils.nextTick(); assert.containsNone(gantt, '.o_gantt_pill_resize_badge', "the badge should disappear after drop"); gantt.destroy(); }); QUnit.test('progress attribute', async function (assert) { assert.expect(7); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['project_id'], }); assert.containsN(gantt, '.o_gantt_row_container .o_gantt_pill .o_gantt_progress', 6, 'should have 6 rows with o_gantt_progress class'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_pill:contains("Task 1") .o_gantt_progress').prop('style')['width'], '0%', 'first pill should have 0% progress'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_pill:contains("Task 2") .o_gantt_progress').prop('style')['width'], '30%', 'second pill should have 30% progress'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_pill:contains("Task 3") .o_gantt_progress').prop('style')['width'], '60%', 'third pill should have 60% progress'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_pill:contains("Task 4") .o_gantt_progress').prop('style')['width'], '0%', 'fourth pill should have 0% progress'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_pill:contains("Task 5") .o_gantt_progress').prop('style')['width'], '100%', 'fifth pill should have 100% progress'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_pill:contains("Task 7") .o_gantt_progress').prop('style')['width'], '80%', 'seventh task should have 80% progress'); gantt.destroy(); }); QUnit.test('form_view_id attribute', async function (assert) { assert.expect(1); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['project_id'], }); const views = { 'tasks,42,form': '
', }; const mockRPC = (route, args) => { if (args.method === "get_views") { assert.deepEqual(args.kwargs.views, [[42, "form"]]); } }; await prepareWowlFormViewDialogs({ models: this.data, views }, mockRPC); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_add')); await testUtils.nextTick(); gantt.destroy(); }); QUnit.test('decoration attribute', async function (assert) { assert.expect(2); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '' + '' + '', viewOptions: { initialDate: initialDate, }, }); assert.hasClass(gantt.$('.o_gantt_pill[data-id=1]'), 'decoration-info', 'should have a "decoration-info" class on task 1'); assert.doesNotHaveClass(gantt.$('.o_gantt_pill[data-id=2]'), 'decoration-info', 'should not have a "decoration-info" class on task 2'); gantt.destroy(); }); QUnit.test('decoration attribute with date', async function (assert) { assert.expect(6); const unpatchDate = patchDate(2018, 11, 19, 12, 0, 0); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '' + '', viewOptions: { initialDate: initialDate, }, }); assert.hasClass(gantt.$('.o_gantt_pill[data-id=1]'), 'decoration-danger', 'should have a "decoration-danger" class on task 1'); assert.hasClass(gantt.$('.o_gantt_pill[data-id=2]'), 'decoration-danger', 'should have a "decoration-danger" class on task 2'); assert.hasClass(gantt.$('.o_gantt_pill[data-id=5]'), 'decoration-danger', 'should have a "decoration-danger" class on task 5'); assert.doesNotHaveClass(gantt.$('.o_gantt_pill[data-id=3]'), 'decoration-danger', 'should not have a "decoration-danger" class on task 3'); assert.doesNotHaveClass(gantt.$('.o_gantt_pill[data-id=4]'), 'decoration-danger', 'should not have a "decoration-danger" class on task 4'); assert.doesNotHaveClass(gantt.$('.o_gantt_pill[data-id=7]'), 'decoration-danger', 'should not have a "decoration-danger" class on task 7'); gantt.destroy(); unpatchDate(); }); QUnit.test('consolidation feature', async function (assert) { assert.expect(25); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['user_id', 'project_id', 'stage'], }); assert.containsN(gantt, '.o_gantt_row_container .o_gantt_row', 18, 'should have a 18 rows'); assert.containsN(gantt, '.o_gantt_row_container .o_gantt_row_group.open', 12, 'should have a 12 opened groups as consolidation implies collapse_first_level'); assert.containsN(gantt, '.o_gantt_row_container .o_gantt_row:not(.o_gantt_row_group)', 6, 'should have a 6 rows'); assert.containsOnce(gantt, '.o_gantt_row_container .o_gantt_row:first .o_gantt_row_sidebar', 'should have a sidebar'); // Check grouped rows assert.hasClass(gantt.$('.o_gantt_row_container .o_gantt_row:first'), 'o_gantt_row_group', '1st row should be a group'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_row:first .o_gantt_row_title').text().trim(), 'User 1', '1st row title should be "User 1"'); assert.hasClass(gantt.$('.o_gantt_row_container .o_gantt_row:nth(9)'), 'o_gantt_row_group', '7th row should be a group'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_row:nth(9) .o_gantt_row_title').text().trim(), 'User 2', '7th row title should be "User 2"'); // Consolidation // 0 over the size of Task 5 (Task 5 is 100 but is excluded !) then 0 over the rest of Task 1, cut by Task 4 which has progress 0 assert.strictEqual(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_consolidated_pill_title ').text().replace(/\s+/g, ''), "0000", "the consolidation should be correctly computed"); assert.strictEqual(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_pill:eq(0)').css('background-color'), "rgb(40, 167, 69)", "the 1st group pill should have the correct color)"); assert.strictEqual(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_pill:eq(1)').css('background-color'), "rgb(40, 167, 69)", "the 2nd group pill should have the correct color)"); assert.strictEqual(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_pill:eq(2)').css('background-color'), "rgb(40, 167, 69)", "the 3rd group pill should have the correct color"); assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_pill_wrapper:eq(0)')), "calc(300% + 2px - 0px)", "the 1st group pill should have the correct width (1 to 3 dec)"); assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_pill_wrapper:eq(1)')), "calc(1600% + 15px - 0px)", "the 2nd group pill should have the correct width (4 to 19 dec)"); assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_pill_wrapper:eq(2)')), "calc(50% - 0px)", "the 3rd group pill should have the correct width (20 morning dec"); assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_pill_wrapper:eq(3)')), "calc(1150% + 10px - 0px)", "the 4th group pill should have the correct width (20 afternoon to 31 dec"); // 30 over Task 2 until Task 7 then 110 (Task 2 (30) + Task 7 (80)) then 30 again until end of task 2 then 60 over Task 3 assert.strictEqual(gantt.$('.o_gantt_row_group:eq(6) .o_gantt_consolidated_pill_title').text().replace(/\s+/g, ''), "301103060", "the consolidation should be correctly computed"); assert.strictEqual(gantt.$('.o_gantt_row_group:eq(6) .o_gantt_pill:eq(0)').css('background-color'), "rgb(40, 167, 69)", "the 1st group pill should have the correct color)"); assert.strictEqual(gantt.$('.o_gantt_row_group:eq(6) .o_gantt_pill:eq(1)').css('background-color'), "rgb(220, 53, 69)", "the 2nd group pill should have the correct color)"); assert.strictEqual(gantt.$('.o_gantt_row_group:eq(6) .o_gantt_pill:eq(2)').css('background-color'), "rgb(40, 167, 69)", "the 3rd group pill should have the correct color"); assert.strictEqual(gantt.$('.o_gantt_row_group:eq(6) .o_gantt_pill:eq(3)').css('background-color'), "rgb(40, 167, 69)", "the 4th group pill should have the correct color"); assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(6) .o_gantt_pill_wrapper:eq(0)')), "calc(300% + 2px - 0px)", "the 1st group pill should have the correct width (17 afternoon to 20 dec morning)"); assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(6) .o_gantt_pill_wrapper:eq(1)')), "calc(50% - 0px)", "the 2nd group pill should have the correct width (20 dec afternoon)"); assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(6) .o_gantt_pill_wrapper:eq(2)')), "calc(150% - 0px)", "the 3rd group pill should have the correct width (21 to 22 dec morning dec"); assert.strictEqual(getPillItemWidth(gantt.$('.o_gantt_row_group:eq(6) .o_gantt_pill_wrapper:eq(3)')), "calc(500% + 4px - 0px)", "the 4th group pill should have the correct width (27 afternoon to 31 dec"); gantt.destroy(); }); QUnit.test('color attribute', async function (assert) { assert.expect(2); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); assert.hasClass(gantt.$('.o_gantt_pill[data-id=1]'), 'o_gantt_color_0', 'should have a color_0 class on task 1'); assert.hasClass(gantt.$('.o_gantt_pill[data-id=2]'), 'o_gantt_color_2', 'should have a color_0 class on task 2'); gantt.destroy(); }); QUnit.test('color attribute in multi-level grouped', async function (assert) { assert.expect(2); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['user_id', 'project_id'], domain: [['id', '=', 1]], }); assert.doesNotHaveClass(gantt.$('.o_gantt_row_group .o_gantt_pill'), 'o_gantt_color_0', "the group row pill should not be colored"); assert.hasClass(gantt.$('.o_gantt_row:not(.o_gantt_row_group) .o_gantt_pill'), 'o_gantt_color_0', 'the pill should be colored'); gantt.destroy(); }); QUnit.test('color attribute on a many2one', async function (assert) { assert.expect(3); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); assert.hasClass(gantt.$('.o_gantt_pill[data-id=1]'), 'o_gantt_color_1', 'should have a color_1 class on task 1'); assert.containsN(gantt, '.o_gantt_pill.o_gantt_color_1', 4, "there should be 4 pills with color 1"); assert.containsN(gantt, '.o_gantt_pill.o_gantt_color_2', 2, "there should be 2 pills with color 2"); gantt.destroy(); }); QUnit.test('Today style with unavailabilities ("week": "day:half")', async function (assert) { assert.expect(2); const unpatchDate = patchDate(2018, 11, 19, 2, 0, 0); const unavailabilities = [{ start: '2018-12-16 10:00:00', stop: '2018-12-18 14:00:00' }].map(convertUnavailability); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: ``, viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (args.method === 'gantt_unavailability') { const rows = args.args[4]; rows.forEach(function(r) { r.unavailabilities = unavailabilities; }); return Promise.resolve(rows); } return this._super.apply(this, arguments); }, }); const cell4 = gantt.el.querySelectorAll('.o_gantt_row_container .o_gantt_cell')[3]; assert.hasClass(cell4, 'o_gantt_today'); assert.hasAttrValue(cell4, 'style', 'height: 105px;'); unpatchDate(); gantt.destroy(); }); QUnit.test('Today style of group rows', async function (assert) { assert.expect(4); const unpatchDate = patchDate(2018, 11, 19, 2, 0, 0); this.data.tasks.records = this.data.tasks.records.filter(r => r.id === 4); const unavailabilities = [{ start: '2018-12-18 09:00:00', stop: '2018-12-19 13:00:00' }].map(convertUnavailability); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: ``, viewOptions: { initialDate: initialDate, }, groupBy: ['user_id', 'project_id'], mockRPC: function (route, args) { if (args.method === 'gantt_unavailability') { const rows = args.args[4]; rows.forEach(function(r) { r.unavailabilities = unavailabilities; r.rows.forEach(sr => { sr.unavailabilities = unavailabilities; }) }); return Promise.resolve(rows); } return this._super.apply(this, arguments); }, }); let todayCells = gantt.el.querySelectorAll('div.o_gantt_today'); assert.deepEqual([...todayCells].map(c => c.getAttribute('style')), [ null, "height: 0px;", // a css rule fix a minimal height "height: 35px; background: linear-gradient(90deg, var(--Gant__DayOff-background-color) 49%, var(--Gant__DayOffToday-background-color) 50%);" ]); assert.strictEqual(window.getComputedStyle(todayCells[1]).getPropertyValue('background-color'), "rgba(0, 0, 0, 0)"); await testUtils.dom.click(todayCells[1]); todayCells = gantt.el.querySelectorAll('div.o_gantt_today'); assert.deepEqual([...todayCells].map(c => c.getAttribute('style')), [ null, "height: 0px;" ]); assert.strictEqual(window.getComputedStyle(todayCells[1]).getPropertyValue('background-color'), "rgb(251, 249, 243)"); unpatchDate(); gantt.destroy(); }); QUnit.test('style without unavailabilities', async function (assert) { assert.expect(3); const unpatchDate = patchDate(2018, 11, 5, 2, 0, 0); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (args.method === 'gantt_unavailability') { return Promise.resolve(args.args[4]); } return this._super.apply(this, arguments); }, }); const cells = gantt.el.querySelectorAll('.o_gantt_row_container .o_gantt_cell'); const cell5 = cells[4] assert.hasClass(cell5, 'o_gantt_today'); assert.hasAttrValue(cell5, 'style', 'height: 105px;'); const cell6 = cells[5] assert.hasAttrValue(cell6, 'style', 'height: 105px;'); unpatchDate(); gantt.destroy(); }); QUnit.test('Unavailabilities ("month": "day:half")', async function (assert) { assert.expect(10); const unpatchDate = patchDate(2018, 11, 5, 2, 0, 0); const unavailabilities = [{ start: '2018-12-05 09:30:00', stop: '2018-12-07 08:00:00' }, { start: '2018-12-16 09:00:00', stop: '2018-12-18 13:00:00' }].map(convertUnavailability); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (args.method === 'gantt_unavailability') { assert.strictEqual(args.model, 'tasks', "the availability should be fetched on the correct model"); assert.strictEqual(args.args[0], '2018-12-01 00:00:00', "the start_date argument should be in the server format"); assert.strictEqual(args.args[1], '2018-12-31 23:59:59', "the end_date argument should be in the server format"); const rows = args.args[4]; rows.forEach(function(r) { r.unavailabilities = unavailabilities; }); return Promise.resolve(rows); } return this._super.apply(this, arguments); }, }); const cells = gantt.el.querySelectorAll('.o_gantt_row_container .o_gantt_cell'); const cell5 = cells[4]; assert.hasClass(cell5, 'o_gantt_today'); assert.hasAttrValue(cell5, 'style', 'height: 105px; background: linear-gradient(90deg, var(--Gant__DayOffToday-background-color) 49%, var(--Gant__DayOff-background-color) 50%);'); const cell6 = cells[5]; assert.hasAttrValue(cell6, 'style', 'height: 105px; background: var(--Gant__DayOff-background-color)'); const cell7 = cells[6]; assert.hasAttrValue(cell7, 'style', 'height: 105px;'); const cell16 = cells[15]; assert.hasAttrValue(cell16,'style', 'height: 105px; background: linear-gradient(90deg, var(--Gant__Day-background-color) 49%, var(--Gant__DayOff-background-color) 50%);'); const cell17 = cells[16]; assert.hasAttrValue(cell17,'style', 'height: 105px; background: var(--Gant__DayOff-background-color)'); const cell18 = cells[17]; assert.hasAttrValue(cell18,'style', 'height: 105px; background: linear-gradient(90deg, var(--Gant__DayOff-background-color) 49%, var(--Gant__Day-background-color) 50%);'); unpatchDate(); gantt.destroy(); }); QUnit.test('Unavailabilities ("day": "hours:quarter")', async function (assert) { assert.expect(5); const unpatchDate = patchDate(2018, 11, 20, 8, 0, 0); this.data.tasks.records = []; const unavailabilities = [ { start: '2018-12-20 08:15:00', stop: '2018-12-20 08:30:00', }, { start: '2018-12-20 10:35:00', stop: '2018-12-20 12:29:00' }, { start: '2018-12-20 20:15:00', stop: '2018-12-20 20:50:00', } ].map(convertUnavailability); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: ``, viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (args.method === 'gantt_unavailability') { const rows = args.args[4]; rows.forEach(function(r) { r.unavailabilities = unavailabilities; }); return Promise.resolve(rows); } return this._super.apply(this, arguments); }, }); const cells = gantt.el.querySelectorAll('.o_gantt_row_container .o_gantt_cell'); const cell9 = cells[8]; assert.hasAttrValue(cell9, 'style', 'height: 0px; background: linear-gradient(90deg, var(--Gant__Day-background-color) 24%, var(--Gant__DayOff-background-color) 25%, var(--Gant__DayOff-background-color) 49%, var(--Gant__Day-background-color) 50%, var(--Gant__Day-background-color) 74%, var(--Gant__Day-background-color) 75%);'); const cell11 = cells[10]; assert.hasAttrValue(cell11, 'style', 'height: 0px; background: linear-gradient(90deg, var(--Gant__Day-background-color) 24%, var(--Gant__Day-background-color) 25%, var(--Gant__Day-background-color) 49%, var(--Gant__Day-background-color) 50%, var(--Gant__Day-background-color) 74%, var(--Gant__DayOff-background-color) 75%);'); const cell12 = cells[11]; assert.hasAttrValue(cell12, 'style', 'height: 0px; background: var(--Gant__DayOff-background-color)'); const cell13 = cells[12]; assert.hasAttrValue(cell13, 'style', 'height: 0px; background: linear-gradient(90deg, var(--Gant__DayOff-background-color) 24%, var(--Gant__Day-background-color) 25%, var(--Gant__Day-background-color) 49%, var(--Gant__Day-background-color) 50%, var(--Gant__Day-background-color) 74%, var(--Gant__Day-background-color) 75%);'); const cell21 = cells[20]; assert.hasAttrValue(cell21, 'style', 'height: 0px; background: linear-gradient(90deg, var(--Gant__Day-background-color) 24%, var(--Gant__DayOff-background-color) 25%, var(--Gant__DayOff-background-color) 49%, var(--Gant__DayOff-background-color) 50%, var(--Gant__DayOff-background-color) 74%, var(--Gant__Day-background-color) 75%);'); unpatchDate(); gantt.destroy(); }); QUnit.test('Unavailabilities ("month": "day:half")', async function (assert) { assert.expect(10); const unpatchDate = patchDate(2018, 11, 5, 2, 0, 0); const unavailabilities = [{ start: '2018-12-05 09:30:00', stop: '2018-12-07 08:00:00' }, { start: '2018-12-16 09:00:00', stop: '2018-12-18 13:00:00' }].map(convertUnavailability); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (args.method === 'gantt_unavailability') { assert.strictEqual(args.model, 'tasks', "the availability should be fetched on the correct model"); assert.strictEqual(args.args[0], '2018-12-01 00:00:00', "the start_date argument should be in the server format"); assert.strictEqual(args.args[1], '2018-12-31 23:59:59', "the end_date argument should be in the server format"); const rows = args.args[4]; rows.forEach(function(r) { r.unavailabilities = unavailabilities; }); return Promise.resolve(rows); } return this._super.apply(this, arguments); }, }); const cells = gantt.el.querySelectorAll('.o_gantt_row_container .o_gantt_cell'); const cell5 = cells[4]; assert.hasClass(cell5, 'o_gantt_today'); assert.hasAttrValue(cell5, 'style', 'height: 105px; background: linear-gradient(90deg, var(--Gant__DayOffToday-background-color) 49%, var(--Gant__DayOff-background-color) 50%);'); const cell6 = cells[5]; assert.hasAttrValue(cell6, 'style', 'height: 105px; background: var(--Gant__DayOff-background-color)'); const cell7 = cells[6]; assert.hasAttrValue(cell7, 'style', 'height: 105px;'); const cell16 = cells[15];; assert.hasAttrValue(cell16,'style', 'height: 105px; background: linear-gradient(90deg, var(--Gant__Day-background-color) 49%, var(--Gant__DayOff-background-color) 50%);'); const cell17 = cells[16];; assert.hasAttrValue(cell17,'style', 'height: 105px; background: var(--Gant__DayOff-background-color)'); const cell18 = cells[17];; assert.hasAttrValue(cell18,'style', 'height: 105px; background: linear-gradient(90deg, var(--Gant__DayOff-background-color) 49%, var(--Gant__Day-background-color) 50%);'); unpatchDate(); gantt.destroy(); }); QUnit.test('offset attribute', async function (assert) { assert.expect(1); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), 'Sunday, December 16, 2018', 'gantt view should be set to 4 days before initial date'); gantt.destroy(); }); QUnit.test('default_group_by attribute', async function (assert) { assert.expect(2); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); assert.containsN(gantt, '.o_gantt_row', 2, "there should be 2 rows"); assert.strictEqual(gantt.$('.o_gantt_row:last .o_gantt_row_title').text().trim(), 'User 2', 'should be grouped by user'); gantt.destroy(); }); QUnit.test('permanent_group_by attribute', async function (assert) { assert.expect(2); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); assert.containsN(gantt, '.o_gantt_row', 2, "there should be 2 rows"); assert.strictEqual(gantt.$('.o_gantt_row:last .o_gantt_row_title').text().trim(), 'User 2', 'should be grouped by user'); gantt.destroy(); }); QUnit.test('default_group_by attribute with 2 fields', async function (assert) { assert.expect(2); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate, }, }); assert.containsN(gantt, '.o_gantt_row_group', 2, 'there should be 2 rows.'); assert.containsN(gantt, '.o_gantt_row_nogroup', 4, 'there should be 4 sub rows.'); gantt.destroy(); }); QUnit.test('dynamic_range attribute', async function (assert) { assert.expect(1); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); var $headerScale = gantt.$el.find('.o_gantt_header_scale'); var $headerCells = $headerScale.find('.o_gantt_header_cell'); assert.strictEqual($headerCells[0].innerText.trim(), String(initialDate.getDate()), 'should start at the first record, not at the beginning of the month'); gantt.destroy(); }); QUnit.test('collapse_first_level attribute with single-level grouped', async function (assert) { assert.expect(13); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['project_id'], }); const views = { 'tasks,false,form': `
`, }; await prepareWowlFormViewDialogs({ models: this.data, views }); assert.containsOnce(gantt, '.o_gantt_header_container', 'should have a header'); assert.ok(gantt.$buttons.find('.o_gantt_button_expand_rows').is(':visible'), "the expand button should be visible"); assert.containsN(gantt, '.o_gantt_row_container .o_gantt_row', 4, 'should have a 4 rows'); assert.containsN(gantt, '.o_gantt_row_container .o_gantt_row.o_gantt_row_group', 2, 'should have 2 group rows'); assert.strictEqual(gantt.$('.o_gantt_row_group:eq(0) .o_gantt_row_title').text().trim(), 'Project 1', 'should contain "Project 1" in sidebar title'); assert.containsN(gantt, '.o_gantt_row:eq(1) .o_gantt_pill', 4, 'should have a 4 pills in first row'); assert.strictEqual(gantt.$('.o_gantt_row_group:eq(1) .o_gantt_row_title').text().trim(), 'Project 2', 'should contain "Project 2" in sidebar title'); assert.containsN(gantt, '.o_gantt_row:eq(3) .o_gantt_pill', 2, 'should have a 2 pills in second row'); // open dialog to create a task await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_row:nth(3) .o_gantt_cell[data-date="2018-12-10 00:00:00"] .o_gantt_cell_add'), "click"); await testUtils.nextTick(); assert.strictEqual($('.modal').length, 1, 'There should be one modal opened'); assert.strictEqual($('.modal .modal-title').text(), "Create"); assert.strictEqual($('.modal .o_field_widget[name=project_id] .o_input').val(), 'Project 2', 'project_id should be set'); assert.strictEqual($('.modal .o_field_widget[name=start] .o_input').val(), '12/10/2018 00:00:00', 'start should be set'); assert.strictEqual($('.modal .o_field_widget[name = stop] .o_input').val(), '12/10/2018 23:59:59', 'stop should be set'); gantt.destroy(); }); // CONCURRENCY TESTS QUnit.test('concurrent scale switches return in inverse order', async function (assert) { assert.expect(11); testUtils.mock.patch(GanttRenderer, { _render: function () { assert.step('render'); return this._super.apply(this, arguments); }, }); var firstReloadProm = null; var reloadProm = firstReloadProm; var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, mockRPC: function (route) { var result = this._super.apply(this, arguments); if (route === '/web/dataset/search_read') { return Promise.resolve(reloadProm).then(_.constant(result)); } return result; }, }); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), 'December 2018', "should be in 'month' scale"); assert.strictEqual(gantt.model.get().records.length, 6, "should have 6 records in the state"); // switch to 'week' scale (this rpc will be delayed) firstReloadProm = testUtils.makeTestPromise(); reloadProm = firstReloadProm; await testUtils.dom.click(gantt.$('.o_gantt_button_scale[data-value=week]')); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), 'December 2018', "should still be in 'month' scale"); assert.strictEqual(gantt.model.get().records.length, 6, "should still have 6 records in the state"); // switch to 'year' scale reloadProm = null; await testUtils.dom.click(gantt.$('.o_gantt_button_scale[data-value=year]')); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), '2018', "should be in 'year' scale"); assert.strictEqual(gantt.model.get().records.length, 7, "should have 7 records in the state"); firstReloadProm.resolve(); assert.strictEqual(gantt.$('.o_gantt_header_container > .col > .row:first-child').text().trim(), '2018', "should still be in 'year' scale"); assert.strictEqual(gantt.model.get().records.length, 7, "should still have 7 records in the state"); assert.verifySteps(['render', 'render']); // should only re-render once gantt.destroy(); testUtils.mock.unpatch(GanttRenderer); }); QUnit.test('concurrent pill resizes return in inverse order', async function (assert) { assert.expect(7); testUtils.mock.patch(GanttRenderer, { _render: function () { assert.step('render'); return this._super.apply(this, arguments); }, }); var writeProm = testUtils.makeTestPromise(); var firstWriteProm = writeProm; var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, domain: [['id', '=', 2]], mockRPC: function (route, args) { var result = this._super.apply(this, arguments); assert.step(args.method || route); if (args.method === 'write') { return Promise.resolve(writeProm).then(_.constant(result)); } return result; }, }); var cellWidth = gantt.$('.o_gantt_cell:first').width() + 4; await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_pill'), 'mouseover'); // resize to 1 cell smaller (-1 day) ; this RPC will be delayed await testUtils.dom.dragAndDrop( gantt.$('.ui-resizable-e'), gantt.$('.ui-resizable-e'), { position: { left: -cellWidth, top: 0 } } ); // resize to two cells larger (+2 days) writeProm = null; await testUtils.dom.dragAndDrop( gantt.$('.ui-resizable-e'), gantt.$('.ui-resizable-e'), { position: { left: 2 * cellWidth, top: 0 } } ); firstWriteProm.resolve(); await testUtils.nextTick(); assert.verifySteps([ '/web/dataset/search_read', 'render', 'write', 'write', '/web/dataset/search_read', // should only reload once 'render', // should only re-render once ]); gantt.destroy(); testUtils.mock.unpatch(GanttRenderer); }); QUnit.test('concurrent pill resizes and open, dialog show updated number', async function (assert) { assert.expect(1); var def = testUtils.makeTestPromise(); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, domain: [['id', '=', 2]], mockRPC: function (route, args) { var self = this; if (args.method === 'write') { var super_self = this._super return def.then(() => { return super_self.apply(self, arguments); }); } return this._super.apply(this, arguments);; }, }); const views = { 'tasks,false,form': `
`, }; await prepareWowlFormViewDialogs({ models: this.data, views }); var cellWidth = gantt.$('.o_gantt_cell:first').width() + 4; await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_pill'), 'mouseover'); await testUtils.dom.dragAndDrop( gantt.$('.ui-resizable-e'), gantt.$('.ui-resizable-e'), { position: { left: 2 * cellWidth, top: 0 } } ); await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_pill'), "click"); def.resolve(); await testUtils.nextTick(); assert.strictEqual($('.modal').find('[name=stop] input').val(), '12/24/2018 06:29:59'); gantt.destroy(); }); QUnit.test('dst spring forward', async function (assert) { assert.expect(2); // This is one of the few tests which have dynamic assertions, see // our justification for it in the comment at the top of this file. var firstStartDateUTCString = '2019-03-30 03:00:00'; var firstStartDateUTC = moment.utc(firstStartDateUTCString); var firstStartDateLocalString = firstStartDateUTC.local().format('YYYY-MM-DD hh:mm:ss'); this.data.tasks.records.push({ id: 99, name: 'DST Task 1', start: firstStartDateUTCString, stop: '2019-03-30 03:30:00', }); var secondStartDateUTCString = '2019-03-31 03:00:00'; var secondStartDateUTC = moment.utc(secondStartDateUTCString); var secondStartDateLocalString = secondStartDateUTC.local().format('YYYY-MM-DD hh:mm:ss'); this.data.tasks.records.push({ id: 100, name: 'DST Task 2', start: secondStartDateUTCString, stop: '2019-03-31 03:30:00', }); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: new Date(2019, 2, 30, 8, 0, 0), }, }); assert.containsOnce(gantt, '.o_gantt_row_container .o_gantt_cell[data-date="' + firstStartDateLocalString + '"] .o_gantt_pill_wrapper:contains(DST Task 1)', 'should be in the right cell'); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_next')); assert.containsOnce(gantt, '.o_gantt_row_container .o_gantt_cell[data-date="' + secondStartDateLocalString + '"] .o_gantt_pill_wrapper:contains(DST Task 2)', 'should be in the right cell'); gantt.destroy(); }); QUnit.test('dst fall back', async function (assert) { assert.expect(2); // This is one of the few tests which have dynamic assertions, see // our justification for it in the comment at the top of this file. var firstStartDateUTCString = '2019-10-26 03:00:00'; var firstStartDateUTC = moment.utc(firstStartDateUTCString); var firstStartDateLocalString = firstStartDateUTC.local().format('YYYY-MM-DD hh:mm:ss'); this.data.tasks.records.push({ id: 99, name: 'DST Task 1', start: firstStartDateUTCString, stop: '2019-10-26 03:30:00', }); var secondStartDateUTCString = '2019-10-27 03:00:00'; var secondStartDateUTC = moment.utc(secondStartDateUTCString); var secondStartDateLocalString = secondStartDateUTC.local().format('YYYY-MM-DD hh:mm:ss'); this.data.tasks.records.push({ id: 100, name: 'DST Task 2', start: secondStartDateUTCString, stop: '2019-10-27 03:30:00', }); var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: new Date(2019, 9, 26, 8, 0, 0), }, }); assert.containsOnce(gantt, '.o_gantt_row_container .o_gantt_cell[data-date="' + firstStartDateLocalString + '"] .o_gantt_pill_wrapper:contains(DST Task 1)', 'should be in the right cell'); await testUtils.dom.click(gantt.$buttons.find('.o_gantt_button_next')); assert.containsOnce(gantt, '.o_gantt_row_container .o_gantt_cell[data-date="' + secondStartDateLocalString + '"] .o_gantt_pill_wrapper:contains(DST Task 2)', 'should be in the right cell'); gantt.destroy(); }); // OTHER TESTS QUnit.skip('[for manual testing] scripting time of large amount of records (ungrouped)', async function (assert) { assert.expect(1); this.data.tasks.records = []; for (var i = 1; i <= 1000; i++) { this.data.tasks.records.push({ id: i, name: 'Task ' + i, start: '2018-12-01 00:00:00', stop: '2018-12-02 00:00:00', }); } createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); }); QUnit.skip('[for manual testing] scripting time of large amount of records (one level grouped)', async function (assert) { assert.expect(1); this.data.tasks.records = []; this.data.users.records = []; var i; for (i = 1; i <= 100; i++) { this.data.users.records.push({ id: i, name: i, }); } for (i = 1; i <= 10000; i++) { var day1 = (i % 30) + 1; var day2 = ((i % 30) + 2); if (day1 < 10) { day1 = '0' + day1; } if (day2 < 10) { day2 = '0' + day2; } this.data.tasks.records.push({ id: i, name: 'Task ' + i, user_id: Math.floor(Math.random() * Math.floor(100)) + 1, start: '2018-12-' + day1, stop: '2018-12-' + day2, }); } createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['user_id'], }); }); QUnit.skip('[for manual testing] scripting time of large amount of records (two level grouped)', async function (assert) { assert.expect(1); this.data.tasks.records = []; this.data.users.records = []; var stages = this.data.tasks.fields.stage.selection; var i; for (i = 1; i <= 100; i++) { this.data.users.records.push({ id: i, name: i, }); } for (i = 1; i <= 10000; i++) { this.data.tasks.records.push({ id: i, name: 'Task ' + i, stage: stages[i % 2][0], user_id: (i % 100) + 1, start: '2018-12-01 00:00:00', stop: '2018-12-02 00:00:00', }); } createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['user_id', 'stage'], }); }); QUnit.test('delete attribute on dialog', async function (assert) { assert.expect(2); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, }); const views = { 'tasks,false,form': `
`, } await prepareWowlFormViewDialogs({ models: this.data, views }); await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_row_container .o_gantt_row .o_gantt_cell[data-date="2018-12-17 00:00:00"] .o_gantt_pill'), "click"); await testUtils.nextTick(); assert.containsOnce(document.body, '.modal-dialog', 'Should have opened a new dialog'); assert.containsNone($('.modal-dialog'), '.o_form_button_remove', 'should not have the "Remove" Button form dialog'); gantt.destroy(); }); QUnit.test('move a pill in multi-level grop row after collapse and expand grouped row', async function (assert) { assert.expect(6); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', groupBy: ['project_id', 'stage'], viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (args.method === 'write') { assert.deepEqual(args.args[0], [7], "should write on the correct record"); assert.deepEqual(args.args[1], { project_id: 1, start: "2018-12-02 10:30:12", stop: "2018-12-02 18:29:59", }, "all modified fields should be correctly set"); } return this._super.apply(this, arguments); }, domain: [['id', 'in', [1, 7]]], }); assert.containsN(gantt, '.o_gantt_pill', 4, "there should be two pills (task 1 and task 7) and two pills for group header combined"); await testUtils.dom.click(gantt.$('.o_gantt_row_container > .o_gantt_row:first .o_gantt_row_title')); assert.doesNotHaveClass(gantt.$('.o_gantt_row_container > .o_gantt_row:first'), 'open', "'Project 1' group should be collapsed"); await testUtils.dom.click(gantt.$('.o_gantt_row_container > .o_gantt_row:first .o_gantt_row_title')); assert.hasClass(gantt.$('.o_gantt_row_container > .o_gantt_row:first'), 'open', "'Project 1' group should be expanded"); // move a pill (task 7) in the other row and in the the next cell (+1 day) const cellWidth = gantt.$('.o_gantt_header_scale .o_gantt_header_cell:first')[0].getBoundingClientRect().width + 4; const cellHeight = gantt.$('.o_gantt_cell:first').height(); await testUtils.dom.dragAndDrop( gantt.$('.o_gantt_pill[data-id=7]'), gantt.$('.o_gantt_pill[data-id=1]'), { position: { left: cellWidth, top: -cellHeight } }, ); assert.containsOnce(gantt, '.o_gantt_row_group', 1, "should have one group row"); gantt.destroy(); }); QUnit.test("plan dialog initial domain has the action domain as its only base", async function (assert) { assert.expect(14); const unpatchDate = patchDate(2018, 11, 20, 8, 0, 0); registerCleanup(unpatchDate); patchWithCleanup(session, { getTZOffset() { return 0; }, }); const views = { "tasks,false,gantt": ``, "tasks,false,list": ``, "tasks,false,search": ` `, }; const serverData = { models: this.data, views, }; const target = getFixture(); const webClient = await createWebClient({ serverData, mockRPC: function (route, args) { if (args.method === "web_search_read") { assert.step(args.kwargs.domain.toString()); } if (route === "/web/dataset/search_read") { assert.step(args.domain.toString()); } }, }); const ganttAction = { name: "Tasks Gantt", res_model: "tasks", type: "ir.actions.act_window", views: [[false, "gantt"]], }; // Load action without domain and open plan dialog await doAction(webClient, ganttAction); assert.verifySteps(["start,<=,2018-12-31 23:59:59,stop,>=,2018-12-01 00:00:00"]); await testUtils.dom.triggerMouseEvent($(target).find(".o_gantt_cell_plan:first"), "click"); await nextTick(); assert.verifySteps(["|,start,=,false,stop,=,false"]); // Load action WITH domain and open plan dialog await doAction(webClient, { ...ganttAction, domain: [["project_id", "=", 1]], }); assert.verifySteps(["project_id,=,1,start,<=,2018-12-31 23:59:59,stop,>=,2018-12-01 00:00:00"]); await testUtils.dom.triggerMouseEvent($(target).find(".o_gantt_cell_plan:first"), "click"); await nextTick(); assert.verifySteps(["&,project_id,=,1,|,start,=,false,stop,=,false"]); // Load action without domain, activate a filter and then open plan dialog await doAction(webClient, ganttAction); assert.verifySteps(["start,<=,2018-12-31 23:59:59,stop,>=,2018-12-01 00:00:00"]); await toggleFilterMenu(target); await nextTick(); await toggleMenuItem(target, "Project 1"); await nextTick(); assert.verifySteps(["project_id,=,1,start,<=,2018-12-31 23:59:59,stop,>=,2018-12-01 00:00:00"]); await testUtils.dom.triggerMouseEvent($(target).find(".o_gantt_cell_plan:first"), "click"); await nextTick(); assert.verifySteps(["|,start,=,false,stop,=,false"]); } ); QUnit.test('No progress bar when no option set.', async function (assert) { assert.expect(1); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: ``, viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (args.method === 'gantt_progress_bar') { // Should never assert this - will be reported in assert.expect count. assert.strictEqual(args.method, "gantt_progress_bar"); } return this._super.apply(this, arguments); }, }); const progressbar = gantt.el.querySelectorAll('.o_gantt_row_sidebar .o_gantt_progressbar'); assert.strictEqual(progressbar.length, 0, "Gantt should not include any progressbar"); gantt.destroy(); }); QUnit.test('Progress bar rpc is triggered when option set.', async function (assert) { assert.expect(8); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: ` `, viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (args.method === 'gantt_progress_bar') { assert.strictEqual(args.model, "tasks"); assert.deepEqual(args.args[0], ['user_id']); assert.deepEqual(args.args[1], {user_id: [1, 2]}); return Promise.resolve({ user_id: { 1: {value: 50, max_value: 100}, 2: {value: 25, max_value: 200}, } }); } return this._super.apply(this, arguments); }, }); const progressbar = gantt.el.querySelectorAll('.o_gantt_row_sidebar .o_gantt_progressbar'); assert.strictEqual(progressbar.length, 2, "Gantt should include two progressbars"); assert.strictEqual(progressbar[0].style.width, "50%"); assert.strictEqual(progressbar[1].style.width, "12.5%"); assert.hasClass(progressbar[0], "o_gantt_group_success", "Progress bar should have the success class"); assert.hasClass(progressbar[1], "o_gantt_group_success", "Progress bar should have the success class"); gantt.destroy(); }); QUnit.test('Progress bar warning when max_value is zero', async function (assert) { assert.expect(5); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: ` `, viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (args.method === 'gantt_progress_bar') { assert.strictEqual(args.model, "tasks"); assert.deepEqual(args.args[0], ['user_id']); assert.deepEqual(args.args[1], {user_id: [1, 2]}); return Promise.resolve({ user_id: { 1: {value: 50, max_value: 0}, } }); } return this._super.apply(this, arguments); }, }); const progressbar = gantt.el.querySelectorAll('.o_gantt_row_sidebar .o_gantt_progressbar'); assert.strictEqual(progressbar.length, 0, "Gantt should not include progressbar"); const warning = gantt.el.querySelectorAll('.o_gantt_row_sidebar .fa-exclamation-triangle'); assert.strictEqual(warning.length, 1, "Gantt should include a warning"); gantt.destroy(); }); QUnit.test('Progress bar danger when ratio > 100', async function (assert) { assert.expect(6); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: ` `, viewOptions: { initialDate: initialDate, }, mockRPC: function (route, args) { if (args.method === 'gantt_progress_bar') { assert.strictEqual(args.model, "tasks"); assert.deepEqual(args.args[0], ['user_id']); assert.deepEqual(args.args[1], {user_id: [1, 2]}); return Promise.resolve({ user_id: { 1: {value: 150, max_value: 100}, } }); } return this._super.apply(this, arguments); }, }); const progressbar = gantt.el.querySelectorAll('.o_gantt_row_sidebar .o_gantt_progressbar'); assert.strictEqual(progressbar.length, 1, "Gantt should include one progressbar"); assert.strictEqual(progressbar[0].style.width, "100%", "Progress bar should have the maximal width"); assert.hasClass(progressbar[0], "o_gantt_group_danger", "Progress bar should have the danger class"); gantt.destroy(); }); QUnit.test('Falsy search field will return an empty rows', async function (assert) { assert.expect(2); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: ` `, groupBy: ['project_id', 'user_id'], viewOptions: { initialDate: initialDate, }, domain: [["id", "=", 5]], }); const progressbar = gantt.el.querySelectorAll('.o_gantt_row_sidebar .o_gantt_progressbar'); assert.containsOnce(gantt, '.o_gantt_row_sidebar_empty', 'should have empty rows'); assert.strictEqual(progressbar.length, 0, "Gantt should not have any progressbars"); gantt.destroy(); }); QUnit.test('Search field return rows with progressbar', async function (assert) { assert.expect(6); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: ` `, groupBy: ['project_id', 'user_id'], viewOptions: { initialDate: initialDate, }, domain: [["id", "=", 2]], mockRPC: function (route, args) { if (args.method === 'gantt_progress_bar') { assert.strictEqual(args.model, "tasks"); assert.deepEqual(args.args[0], ['user_id']); assert.deepEqual(args.args[1], {user_id: [2]}); return Promise.resolve({ user_id: { 2: {value: 25, max_value: 200}, } }); } return this._super.apply(this, arguments); }, }); const progressbar = gantt.el.querySelectorAll('.o_gantt_row_sidebar .o_gantt_progressbar'); assert.containsNone(gantt, '.o_gantt_row_sidebar_empty', 'should have rows'); assert.strictEqual(gantt.$('.o_gantt_row_container .o_gantt_row:nth(1) .o_gantt_row_title').text().trim(), 'User 2', '2nd row title should be "User 2"'); assert.strictEqual(progressbar.length, 1, "Gantt should have one progressbar"); gantt.destroy(); }); QUnit.test('add record in empty gantt', async function (assert) { assert.expect(1); this.data.tasks.records = []; this.data.tasks.fields.stage_id.domain = "[('id', '!=', False)]"; var gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: '', viewOptions: { initialDate: initialDate, }, groupBy: ['project_id'], }); const views = { 'tasks,false,form': `
`, }; await prepareWowlFormViewDialogs({ models: this.data, views }); await testUtils.dom.triggerMouseEvent(gantt.$('.o_gantt_cell[data-date="2018-12-10 00:00:00"] .o_gantt_cell_add'), "click"); await testUtils.nextTick(); assert.strictEqual($('.modal').length, 1, 'There should be one modal opened'); gantt.destroy(); }); QUnit.test('Only the task name appears in the pill title when the pill_label option is not set', async function (assert) { assert.expect(1); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: ``, viewOptions: { initialDate: initialDate, }, }); const pill = gantt.el.querySelector('.o_gantt_pill_title'); assert.strictEqual(pill.textContent, 'Task 1', "The pill should not include DateTime in the title."); gantt.destroy(); }); QUnit.test('The date and task name appears in the pill title when the pill_label option is set', async function (assert) { assert.expect(2); const gantt = await createView({ View: GanttView, model: 'tasks', data: this.data, arch: ``, viewOptions: { initialDate: initialDate, }, }); const pills = gantt.el.querySelectorAll('.o_gantt_pill_title'); assert.strictEqual(pills[0].innerText, '11/30 - 12/31 - Task 1', "The task span across in week then DateTime should be displayed on the pill label."); assert.strictEqual(pills[1].innerText, 'Task 2', "The task does not span across in week scale then DateTime shouldn't be displayed on the pill label."); gantt.destroy(); }); }); });