1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/timesheet_grid/static/tests/timesheet_timer_grid_tests.js
2024-12-10 09:04:09 +07:00

1712 lines
72 KiB
JavaScript

/** @odoo-module */
import { Domain } from "@web/core/domain";
import { registry } from "@web/core/registry";
import { ormService } from "@web/core/orm_service";
import { contains } from "@web/../tests/utils";
import { serializeDateTime, serializeDate, deserializeDate } from "@web/core/l10n/dates";
import { makeFakeUserService } from "@web/../tests/helpers/mock_services";
import {
click,
editInput,
getFixture,
getNodesTextContent,
nextTick,
triggerEvent,
clickOpenM2ODropdown,
selectDropdownItem,
} from "@web/../tests/helpers/utils";
import { removeFacet, toggleSearchBarMenu } from "@web/../tests/search/helpers";
import { getPyEnv } from "@bus/../tests/helpers/mock_python_environment";
import { start } from "@mail/../tests/helpers/test_utils";
import { setupTestEnv } from "@hr_timesheet/../tests/hr_timesheet_common_tests";
import { timerService } from "@timer/services/timer_service";
import { timesheetGridUOMService } from "@timesheet_grid/services/timesheet_grid_uom_service";
import { TimesheetGridSetupHelper } from "./helpers";
const { DateTime } = luxon;
const serviceRegistry = registry.category("services");
let serverData, target, timesheetGridSetup;
QUnit.module("Views", (hooks) => {
hooks.beforeEach(async () => {
timesheetGridSetup = new TimesheetGridSetupHelper(true);
const result = await timesheetGridSetup.setupTimesheetGrid();
serverData = result.serverData;
let grid = serverData.views["analytic.line,false,grid"].replace(
'js_class="timesheet_grid"',
'js_class="timer_timesheet_grid"'
);
grid = grid.replace('widget="float_time"', 'widget="timesheet_uom"');
serverData.views["analytic.line,false,grid"] = grid;
grid = serverData.views["analytic.line,1,grid"].replace(
'js_class="timesheet_grid"',
'js_class="timer_timesheet_grid"'
);
grid = grid.replace('widget="float_time"', 'widget="timesheet_uom"');
serverData.views["analytic.line,1,grid"] = grid;
target = getFixture();
setupTestEnv();
const serviceRegistry = registry.category("services");
serviceRegistry.add("orm", ormService, { force: true });
serviceRegistry.add("timer", timerService, { force: true });
const gridComponentsRegistry = registry.category("grid_components");
if (gridComponentsRegistry.contains("timesheet_uom")) {
gridComponentsRegistry.remove("timesheet_uom"); // the component will be added by timesheet_grid_uom_service
}
serviceRegistry.add("timesheet_grid_uom", timesheetGridUOMService, { force: true });
});
QUnit.module("TimerTimesheetGridView");
QUnit.test("sample data in timesheet timer grid view", async function (assert) {
serverData.views["analytic.line,false,grid"] = serverData.views[
"analytic.line,false,grid"
].replace('js_class="timer_timesheet_grid"', 'js_class="timer_timesheet_grid" sample="1"');
const { openView } = await start({
serverData,
async mockRPC(route, args) {
if (args.method === "get_running_timer") {
return {
step_timer: 30,
};
} else if (args.method === "action_start_new_timesheet_timer") {
return false;
} else if (args.method === "get_daily_working_hours") {
assert.strictEqual(args.model, "hr.employee");
return {};
} else if (args.method === "get_server_time") {
assert.strictEqual(args.model, "timer.timer");
return serializeDateTime(DateTime.now());
} else if (args.method === "get_last_validated_timesheet_date") {
return false;
} else if (route === '/web/dataset/call_kw/project.project/get_create_edit_project_ids') {
return [];
}
return timesheetGridSetup.mockTimesheetGridRPC(route, args);
},
});
await openView({
res_model: "analytic.line",
views: [[false, "grid"]],
domain: Domain.FALSE.toString(),
context: { group_by: ["project_id", "task_id"] },
});
await click(target, ".o_grid_navigation_buttons button[data-hotkey=t]");
assert.containsOnce(
target,
".o_grid_view",
"The view should be correctly rendered with the sample data enabled when no data is found"
);
});
QUnit.test("basic timesheet timer grid view", async function (assert) {
const { openView } = await start({
serverData,
async mockRPC(route, args) {
if (args.method === "get_running_timer") {
return {
step_timer: 30,
};
} else if (args.method === "action_start_new_timesheet_timer") {
return false;
} else if (args.method === "get_daily_working_hours") {
assert.strictEqual(args.model, "hr.employee");
return {};
} else if (args.method === "get_server_time") {
assert.strictEqual(args.model, "timer.timer");
return serializeDateTime(DateTime.now());
} else if (args.method === "action_timer_unlink") {
return null;
} else if (args.method === "get_last_validated_timesheet_date") {
return "2017-01-25";
} else if (route === '/web/dataset/call_kw/project.project/get_create_edit_project_ids') {
return [];
}
return timesheetGridSetup.mockTimesheetGridRPC(route, args);
},
});
await openView({
res_model: "analytic.line",
views: [[false, "grid"]],
context: { group_by: ["project_id", "task_id"] },
});
assert.containsOnce(target, ".timesheet-timer", "should have rendered the timer header");
assert.containsOnce(
target,
".btn_start_timer",
"should have rendered the start timer button"
);
assert.containsN(target, ".o_grid_row_title", 5, "should have 5 rows rendered");
assert.containsN(
target,
"button.btn_timer_line",
5,
"should have rendered a start button before each line"
);
const timerButtonsTextList = getNodesTextContent(
target.querySelectorAll("button.btn_timer_line")
);
assert.deepEqual(
timerButtonsTextList,
timerButtonsTextList.map((text) => text.toUpperCase()),
"The character displayed in the button should be in uppercase"
);
assert.containsNone(
target,
"button.btn_timer_line.btn-danger",
"No row with a running timer"
);
await click(target, ".btn_start_timer");
assert.containsNone(
target,
".btn_start_timer",
"The start button should no longer rendered since a timer will be running"
);
assert.containsOnce(
target,
".btn_stop_timer",
"A stop button should be rendered instead of the start one"
);
assert.strictEqual(
document.activeElement,
target.querySelector(".btn_stop_timer"),
"The stop button should be focused"
);
assert.containsOnce(
target,
".o_timer_discard button.stop-timer",
"Cancel button should be rendered to be able to unlink the timer"
);
assert.containsOnce(
target,
".timesheet-timer .o_field_widget[name=project_id]",
"The project_id field should be rendered inside the timer header"
);
assert.containsOnce(
target,
".timesheet-timer .o_field_widget[name=task_id]",
"The task_id field should be rendered inside the timer header"
);
assert.containsOnce(
target,
".timesheet-timer .o_field_widget[name=name]",
"The name field should be rendered inside the timer header"
);
await click(target, ".btn_stop_timer");
assert.containsOnce(
target,
".btn_stop_timer",
"A stop button should be still there since the project_id is invalid because it is required and empty"
);
assert.containsOnce(
target,
".timesheet-timer .o_field_widget.o_field_invalid[name=project_id]",
"The project_id field should be invalid since it is required and empty"
);
await click(target, ".o_timer_discard button");
assert.containsOnce(
target,
".btn_start_timer",
"The start button should be rendered since a timer has been dropped"
);
assert.containsOnce(
target,
".o_grid_add_line .btn-link",
"should not have Add a line button"
);
});
QUnit.test("basic timesheet timer grid view without Add a line button", async function (assert) {
const { openView } = await start({
serverData,
async mockRPC(route, args) {
if (args.method === "get_running_timer") {
return {
step_timer: 30,
};
} else if (args.method === "action_start_new_timesheet_timer") {
return false;
} else if (args.method === "get_daily_working_hours") {
assert.strictEqual(args.model, "hr.employee");
return {};
} else if (args.method === "get_server_time") {
assert.strictEqual(args.model, "timer.timer");
return serializeDateTime(DateTime.now());
} else if (args.method === "action_timer_unlink") {
return null;
} else if (args.method === "get_last_validated_timesheet_date") {
return "2017-01-30";
} else if (route === '/web/dataset/call_kw/project.project/get_create_edit_project_ids') {
return [];
}
return timesheetGridSetup.mockTimesheetGridRPC(route, args);
},
});
await openView({
res_model: "analytic.line",
views: [[false, "grid"]],
context: { group_by: ["project_id", "task_id"] },
});
assert.containsN(
target,
".o_grid_add_line .btn-link",
0,
"should not have Add a line button"
);
});
QUnit.test("Timer already running", async function (assert) {
const pyEnv = getPyEnv();
pyEnv.mockServer.models["analytic.line"].records.push({
id: 10,
unit_amount: 5740 / 3600, // 01:35:40
project_id: 1,
task_id: 1,
name: "Description",
});
const { openView } = await start({
serverData,
async mockRPC(route, args) {
if (args.method === "get_running_timer") {
return {
id: 10,
start: 5740, // 01:35:40
project_id: 1,
task_id: 1,
description: "Description",
step_timer: 30,
};
} else if (args.method === "action_start_new_timesheet_timer") {
return false;
} else if (args.method === "get_daily_working_hours") {
assert.strictEqual(args.model, "hr.employee");
return {};
} else if (args.method === "get_server_time") {
assert.strictEqual(args.model, "timer.timer");
return serializeDateTime(DateTime.now());
} else if (args.method === "get_last_validated_timesheet_date") {
return false;
} else if (route === '/web/dataset/call_kw/project.project/get_create_edit_project_ids') {
return [];
}
return timesheetGridSetup.mockTimesheetGridRPC(route, args);
},
});
await openView({
res_model: "analytic.line",
views: [[false, "grid"]],
context: { group_by: ["project_id", "task_id"] },
});
await nextTick();
assert.containsOnce(
target,
".btn_stop_timer",
"should have rendered the stop timer button"
);
assert.containsOnce(
target,
".o_grid_row_timer .fa-stop",
"a row should have the timer running"
);
assert.strictEqual(
target.querySelector(".timesheet-timer .o_field_widget[name=project_id] input").value,
"P1"
);
assert.strictEqual(
target.querySelector(".timesheet-timer .o_field_widget[name=task_id] input").value,
"BS task"
);
assert.strictEqual(
target.querySelector(".timesheet-timer .o_field_widget[name=name] input").value,
"Description"
);
await nextTick();
assert.ok(
target
.querySelector(".timesheet-timer div[name=display_timer] span")
.textContent.includes("01:35:4"),
"timer is set"
);
});
QUnit.test("stop running timer then restart new one", async function (assert) {
const pyEnv = getPyEnv();
pyEnv.mockServer.models["analytic.line"].records.push({
id: 10,
unit_amount: 5740 / 3600, // 01:35:40
project_id: 1,
task_id: 1,
name: "Description",
});
let timerRunning = true;
const { openView } = await start({
serverData,
async mockRPC(route, args) {
if (args.method === "get_running_timer") {
const runningTimer = {
step_timer: 30,
};
if (timerRunning) {
return {
...runningTimer,
id: 10,
start: 5740, // 01:35:40
project_id: 1,
task_id: 1,
description: "Description",
};
}
return runningTimer;
} else if (args.method === "action_start_new_timesheet_timer") {
return false;
} else if (args.method === "get_daily_working_hours") {
assert.strictEqual(args.model, "hr.employee");
return {};
} else if (args.method === "get_server_time") {
assert.strictEqual(args.model, "timer.timer", "get_server_time");
return serializeDateTime(DateTime.now());
} else if (args.method === "action_add_time_to_timer") {
return null;
} else if (args.method === "action_timer_stop") {
timerRunning = false;
return 0.15;
} else if (args.method === "get_last_validated_timesheet_date") {
return false;
} else if (args.model === "analytic.line" && args.method === "web_read_group" && !timerRunning) {
assert.step("view reloaded");
} else if (route === '/web/dataset/call_kw/project.project/get_create_edit_project_ids') {
return [];
}
return timesheetGridSetup.mockTimesheetGridRPC(route, args);
},
});
await openView({
res_model: "analytic.line",
views: [[false, "grid"]],
context: { group_by: ["project_id", "task_id"] },
});
assert.containsOnce(
target,
".timesheet-timer .btn_stop_timer",
"The stop button should be rendered since a timer is running"
);
await click(target, ".timesheet-timer .btn_stop_timer");
assert.containsNone(
target,
".o_grid_row_timer .fa-stop",
"No row should have a timer running"
);
assert.verifySteps(
[],
"when stopping the timer, a reload of the view should not be triggered since there was an active row for the current timesheet",
);
await click(target, ".btn_start_timer");
assert.containsOnce(target, ".btn_stop_timer");
assert.containsNone(
target,
".o_grid_row_timer .fa-stop",
"No row should have a timer running"
);
assert.strictEqual(
target.querySelector(".timesheet-timer .o_field_widget[name=project_id] input").value,
"",
"project_id in the timer header should be reset"
);
assert.strictEqual(
target.querySelector(".timesheet-timer .o_field_widget[name=task_id] input").value,
"",
"task_id in the timer header should be reset"
);
assert.strictEqual(
target.querySelector(".timesheet-timer .o_field_widget[name=name] input").value,
"",
"name field in the timer header should be reset"
);
assert.ok(
target
.querySelector(".timesheet-timer div[name=display_timer] span")
.textContent.includes("00:00:0"),
"timer is reset"
);
});
QUnit.test("drop running timer then restart new one", async function (assert) {
const pyEnv = getPyEnv();
pyEnv.mockServer.models["analytic.line"].records.push({
id: 10,
unit_amount: 5740 / 3600, // 01:35:40
project_id: 1,
task_id: 1,
name: "Description",
});
let timerRunning = true;
const { openView } = await start({
serverData,
async mockRPC(route, args) {
if (args.method === "get_running_timer") {
const runningTimer = {
step_timer: 30,
};
if (timerRunning) {
return {
...runningTimer,
id: 10,
start: 5740, // 01:35:40
project_id: 1,
task_id: 1,
description: "Description",
};
}
return runningTimer;
} else if (args.method === "action_start_new_timesheet_timer") {
return false;
} else if (args.method === "get_daily_working_hours") {
assert.strictEqual(args.model, "hr.employee");
return {};
} else if (args.method === "get_server_time") {
assert.strictEqual(
args.model,
"timer.timer",
"get_server_time should be called"
);
return serializeDateTime(DateTime.now());
} else if (args.method === "action_add_time_to_timer") {
return null;
} else if (args.method === "action_timer_unlink") {
timerRunning = false;
return null;
} else if (args.method === "get_last_validated_timesheet_date") {
return false;
} else if (route === '/web/dataset/call_kw/project.project/get_create_edit_project_ids') {
return [];
}
return timesheetGridSetup.mockTimesheetGridRPC(route, args);
},
});
await openView({
res_model: "analytic.line",
views: [[false, "grid"]],
context: { group_by: ["project_id", "task_id"] },
});
await triggerEvent(document.activeElement, "", "keydown", { key: "Escape" });
await nextTick();
assert.containsNone(
target,
".o_grid_row_timer .fa-stop",
"No row should have a timer running"
);
assert.strictEqual(
document.activeElement,
target.querySelector(".btn_start_timer"),
"start button should be focused"
);
await click(document.activeElement);
assert.containsOnce(target, ".btn_stop_timer");
assert.containsNone(
target,
".o_grid_row_timer .fa-stop",
"No row should have a timer running"
);
assert.strictEqual(
target.querySelector(".timesheet-timer .o_field_widget[name=project_id] input").value,
"",
"project_id in the timer header should be reset"
);
assert.strictEqual(
target.querySelector(".timesheet-timer .o_field_widget[name=task_id] input").value,
"",
"task_id in the timer header should be reset"
);
assert.strictEqual(
target.querySelector(".timesheet-timer .o_field_widget[name=name] input").value,
"",
"name field in the timer header should be reset"
);
assert.ok(
target
.querySelector(".timesheet-timer div[name=display_timer] span")
.textContent.includes("00:00:0"),
"timer is reset"
);
});
QUnit.test("Start buttons with groupBy", async function (assert) {
const { openView } = await start({
serverData,
async mockRPC(route, args) {
if (args.method === "get_running_timer") {
return {
step_timer: 30,
};
} else if (args.method === "get_daily_working_hours") {
assert.strictEqual(args.model, "hr.employee");
return {};
} else if (args.method === "get_last_validated_timesheet_date") {
return false;
} else if (route === '/web/dataset/call_kw/project.project/get_create_edit_project_ids') {
return [];
}
return timesheetGridSetup.mockTimesheetGridRPC(route, args);
},
});
await openView({
res_model: "analytic.line",
views: [[false, "grid"]],
});
await toggleSearchBarMenu(target);
let groupBys = target.querySelectorAll("span.o_menu_item");
let groupByProject, groupByTask;
for (const gb of groupBys) {
if (gb.textContent === "Task") {
groupByTask = gb;
} else if (gb.textContent === "Project") {
groupByProject = gb;
}
}
await click(groupByTask, "");
await click(groupByProject, "");
assert.containsN(
target,
"button.btn_timer_line",
5,
"The timer button should be rendered for each row displayed in the grid since the project_id is in the rowFields"
);
groupBys = target.querySelectorAll("span.o_menu_item");
for (const gb of groupBys) {
if (gb.textContent === "Project") {
groupByProject = gb;
break;
}
}
await click(groupByProject, ""); // remove the project_id in the groupby
assert.containsNone(
target,
"button.btn_timer_line",
"The timer button should not be rendered in any row in the grid view since the project_id field is no longer in the rowFields"
);
groupBys = target.querySelectorAll("span.o_menu_item");
groupByProject = groupByTask = null;
for (const gb of groupBys) {
if (gb.textContent === "Task") {
groupByTask = gb;
} else if (gb.textContent === "Project") {
groupByProject = gb;
}
if (groupByProject && groupByTask) {
break;
}
}
await click(groupByTask, ""); // remove task_id in the groupby
await click(groupByProject, ""); // add the project_id in the groupby
assert.containsN(
target,
"button.btn_timer_line",
2,
"The timer button should be rendered for each row displayed in the grid since the project_id is in the rowFields"
);
});
QUnit.test("Start button with shift", async function (assert) {
const pyEnv = getPyEnv();
let timesheetId = 6;
const { openView } = await start({
serverData,
async mockRPC(route, args) {
if (args.method === "get_running_timer") {
return {
step_timer: 30,
};
} else if (args.method === "get_daily_working_hours") {
assert.strictEqual(args.model, "hr.employee");
return {};
} else if (args.method === "action_start_new_timesheet_timer") {
return false;
} else if (args.method === "action_add_time_to_timesheet") {
const { project_id, task_id } = args.args[1];
pyEnv.mockServer.models["analytic.line"].records.push({
id: timesheetId,
project_id,
task_id,
date: "2017-01-25",
unit_amount: 0.5,
});
return timesheetId++;
} else if (args.method === "get_server_time") {
assert.strictEqual(args.model, "timer.timer");
return serializeDateTime(DateTime.now());
} else if (args.method === "get_last_validated_timesheet_date") {
return false;
} else if (route === '/web/dataset/call_kw/project.project/get_create_edit_project_ids') {
return [];
}
return timesheetGridSetup.mockTimesheetGridRPC(route, args);
},
});
await openView({
res_model: "analytic.line",
views: [[false, "grid"]],
context: { group_by: ["task_id", "project_id"] },
});
assert.strictEqual(
// FIXME: can we assume the test are not translated?
target.querySelector(".timesheet-timer > div > div").textContent,
"Press Enter or [a] to launch the timerPress Shift + [A] to add 30 min",
"The text displayed next to Start button should be the default one"
);
// activeElement should be the start button
triggerEvent(document.activeElement, "", "keydown", { key: "Shift" });
await nextTick();
assert.strictEqual(
target.querySelector(".timesheet-timer > div > div").textContent,
"Press Enter or [a] to launch the timerPress Shift + [A] to add 30 min",
"The text displayed next to Start button should be the default one"
);
const timerButtonsTextList = getNodesTextContent(
target.querySelectorAll("button.btn_timer_line")
);
assert.deepEqual(
timerButtonsTextList,
timerButtonsTextList.map((text) => text.toUpperCase()),
"The character displayed in the button should be in uppercase"
);
assert.containsNone(
target,
"button.btn_timer_line .text-lowercase",
"The letter displayed in each button should be in uppercase"
);
await triggerEvent(target, ".btn_start_timer", "keydown", {
key: "A",
which: "65",
shiftKey: true,
});
await triggerEvent(target, ".btn_start_timer", "keydown", {
key: "A",
which: "65",
shiftKey: true,
});
await triggerEvent(target, ".btn_start_timer", "keydown", {
key: "A",
which: "65",
shiftKey: true,
});
const cellEls = target.querySelectorAll(
".o_grid_row.o_grid_highlightable:not(.o_grid_row_title,.o_grid_row_total,.o_grid_column_total)"
);
let firstTodayCellEl;
for (const node of cellEls) {
if (node.querySelector(".bg-info")) {
firstTodayCellEl = node;
break;
}
}
assert.strictEqual(firstTodayCellEl.textContent, "1:30");
await triggerEvent(window, "", "keyup", { key: "Shift" });
assert.strictEqual(
target.querySelector(".timesheet-timer > div > div").textContent,
"Press Enter or [a] to launch the timerPress Shift + [A] to add 30 min"
);
assert.containsN(
target,
"button.btn_timer_line .text-lowercase",
5,
"The character on the button displayed in each row should be in lowercase"
);
await click(target, ".timesheet-timer .btn_start_timer");
await triggerEvent(target, ".btn_stop_timer", "keydown", { key: "Shift" });
assert.containsN(
target,
"button.btn_timer_line .text-lowercase",
5,
"The character on the button displayed in each row should still be in lowercase"
);
});
QUnit.test("Start timer from button line", async function (assert) {
const pyEnv = getPyEnv();
let timesheetId = 6;
const { openView } = await start({
serverData,
async mockRPC(route, args) {
if (args.method === "get_running_timer") {
return {
step_timer: 30,
};
} else if (args.method === "get_daily_working_hours") {
assert.strictEqual(args.model, "hr.employee");
return {};
} else if (args.method === "action_start_new_timesheet_timer") {
const { project_id, task_id } = args.args[0];
if (!project_id) {
return false;
}
const newTimesheet = {
id: timesheetId++,
project_id,
task_id,
date: serializeDateTime(DateTime.now()),
unit_amount: 0.0,
};
pyEnv.mockServer.models["analytic.line"].records.push(newTimesheet);
return newTimesheet;
} else if (args.method === "action_add_time_to_timesheet") {
const { project_id, task_id } = args.args[1];
pyEnv.mockServer.models["analytic.line"].records.push({
id: timesheetId,
project_id,
task_id,
date: "2017-01-25",
unit_amount: 0.5,
});
return timesheetId++;
} else if (args.method === "get_server_time") {
assert.strictEqual(args.model, "timer.timer");
return serializeDateTime(DateTime.now());
} else if (args.method === "action_timer_stop") {
return null;
} else if (args.method === "get_last_validated_timesheet_date") {
return false;
} else if (route === '/web/dataset/call_kw/project.project/get_create_edit_project_ids') {
return [];
}
return timesheetGridSetup.mockTimesheetGridRPC(route, args);
},
});
await openView({
res_model: "analytic.line",
views: [[false, "grid"]],
context: { group_by: ["task_id", "project_id"] },
});
function getRowWithTimerRunningOrNot() {
const timerButtons = target.querySelectorAll("button.btn_timer_line");
const timerButtonsHasDangerClass = [];
for (const button of timerButtons) {
timerButtonsHasDangerClass.push(button.classList.contains("btn-danger"));
}
return timerButtonsHasDangerClass;
}
assert.containsOnce(
target,
".btn_start_timer",
"No timer running so start button should be displayed"
);
assert.containsNone(
target,
".btn_stop_timer",
"No timer running so stop button should not be displayed"
);
assert.containsN(
target,
".o_grid_row_title",
5,
"5 rows should be displayed in the grid view"
);
assert.containsN(
target,
".btn_timer_line",
5,
"A timer button should be displayed in each row"
);
await click(target.querySelector("button.btn_timer_line"), "");
assert.containsNone(
target,
".btn_start_timer",
"A timer should be running and so the start button should not be displayed"
);
assert.containsOnce(
target,
".btn_stop_timer",
"A timer should be running and so the stop button should be displayed instead of start one"
);
assert.containsOnce(
target,
"button.btn_timer_line.btn-danger .fa-stop",
"A row should have the timer button red with stop icon to notify the timer is running in that row"
);
assert.containsN(
target,
"button.btn_timer_line:not(.btn-danger)",
4,
"4 rows should not have a timer running"
);
assert.deepEqual(
getRowWithTimerRunningOrNot(),
[true, false, false, false, false],
"Only first row should have the timer runnning"
);
assert.strictEqual(
target.querySelector(".timesheet-timer .o_field_widget[name=project_id] input").value,
"P1",
"project_id in the timer header should be the one in the first row"
);
assert.strictEqual(
target.querySelector(".timesheet-timer .o_field_widget[name=task_id] input").value,
"",
"task_id in the timer header should be the one in the first row (no task)"
);
assert.strictEqual(
target.querySelector(".timesheet-timer .o_field_widget[name=name] input").value,
"",
"name field in the timer header should be empty (default value)"
);
assert.ok(
target
.querySelector(".timesheet-timer div[name=display_timer] span")
.textContent.includes("00:00:0"),
"timer should start to 0"
);
await click(target.querySelector("button.btn_timer_line:not(.btn-danger)"), "");
assert.containsNone(
target,
".btn_start_timer",
"A timer should be running and so the start button should not be displayed"
);
assert.containsOnce(
target,
".btn_stop_timer",
"A timer should be running and so the stop button should be displayed instead of start one"
);
assert.containsOnce(
target,
"button.btn_timer_line.btn-danger .fa-stop",
"A row should have the timer button red with stop icon to notify the timer is running in that row"
);
assert.containsN(
target,
"button.btn_timer_line:not(.btn-danger)",
4,
"4 rows should not have a timer running"
);
assert.deepEqual(getRowWithTimerRunningOrNot(), [false, true, false, false, false]);
assert.strictEqual(
target.querySelector(".timesheet-timer .o_field_widget[name=project_id] input").value,
"Webocalypse Now",
"project_id in the timer header should be the one in the first row"
);
assert.strictEqual(
target.querySelector(".timesheet-timer .o_field_widget[name=task_id] input").value,
"Another BS task",
"task_id in the timer header should be the one in the first row (Another BS task is expected)"
);
assert.strictEqual(
target.querySelector(".timesheet-timer .o_field_widget[name=name] input").value,
"",
"name field in the timer header should be empty (default value)"
);
assert.ok(
target
.querySelector(".timesheet-timer div[name=display_timer] span")
.textContent.includes("00:00:0"),
"timer should start to 0"
);
});
QUnit.test("Change description running timer", async function (assert) {
const pyEnv = getPyEnv();
pyEnv.mockServer.models["analytic.line"].records.push({
id: 10,
unit_amount: 5740 / 3600, // 01:35:40
project_id: 1,
task_id: 1,
name: "Description",
});
let timerRunning = true;
const { openView } = await start({
serverData,
async mockRPC(route, args) {
if (args.method === "get_running_timer") {
if (timerRunning) {
return {
id: 10,
start: 5740, // 01:35:40
project_id: 1,
task_id: 1,
description: "/",
step_timer: 30,
};
}
return {
step_timer: 30,
};
} else if (args.method === "action_start_new_timesheet_timer") {
return false;
} else if (args.method === "get_daily_working_hours") {
assert.strictEqual(args.model, "hr.employee");
return {};
} else if (args.method === "get_server_time") {
assert.strictEqual(args.model, "timer.timer");
return serializeDateTime(DateTime.now());
} else if (args.method === "action_timer_stop") {
timerRunning = false;
return null;
} else if (args.method === "get_last_validated_timesheet_date") {
return false;
} else if (route === '/web/dataset/call_kw/project.project/get_create_edit_project_ids') {
return [];
}
return timesheetGridSetup.mockTimesheetGridRPC(route, args);
},
});
await openView({
res_model: "analytic.line",
views: [[false, "grid"]],
context: { group_by: ["project_id", "task_id"] },
});
await editInput(target, ".timesheet-timer div[name=name] input", "Description");
assert.strictEqual(
target.querySelector(".timesheet-timer div[name=name] input").value,
"Description",
"The `Description` should correctly be written in the name description in the timer header"
);
});
QUnit.test(
"Check that individual and total overtime is properly displayed",
async function (assert) {
const { openView } = await start({
serverData,
async mockRPC(route, args) {
if (args.method === "get_running_timer") {
return {
step_timer: 30,
};
} else if (args.method === "action_start_new_timesheet_timer") {
return false;
} else if (args.method === "get_daily_working_hours") {
assert.strictEqual(args.model, "hr.employee");
const [serializedDateStart, serializedDateEnd] = args.args;
const dailyWorkingHours = {
[serializedDateStart]: 0,
[serializedDateEnd]: 0,
};
const generateNext = (dateStart) => dateStart.plus({ days: 1 });
const dateStart = deserializeDate(serializedDateStart);
const dateEnd = deserializeDate(serializedDateEnd);
for (
let currentDate = generateNext(dateStart);
currentDate < dateEnd;
currentDate = generateNext(currentDate)
) {
dailyWorkingHours[serializeDate(currentDate)] = 7;
}
return dailyWorkingHours;
} else if (args.method === "get_last_validated_timesheet_date") {
return false;
} else if (route === '/web/dataset/call_kw/project.project/get_create_edit_project_ids') {
return [];
}
return timesheetGridSetup.mockTimesheetGridRPC(route, args);
},
});
await openView({
res_model: "analytic.line",
views: [[false, "grid"]],
context: { group_by: ["project_id", "task_id"] },
});
const columnTotalEls = target.querySelectorAll(".o_grid_column_total");
const columnTotalWithBarchartTotalTitle = {
danger: [],
warning: [],
get total() {
return this.danger.length + this.warning.length;
},
};
const emptyColumnTotalCells = [];
let columnTotalEl;
for (const node of columnTotalEls) {
if (!columnTotalEl && node.querySelector(".o_grid_bar_chart_total_title")) {
columnTotalEl = node;
}
if (!node.classList.contains("o_grid_bar_chart_container")) {
const columnTotalTitleEl = node.querySelector(".o_grid_bar_chart_total_title");
if (!columnTotalTitleEl) {
emptyColumnTotalCells.push(node);
} else {
if (columnTotalTitleEl.querySelector("span.text-danger")) {
columnTotalWithBarchartTotalTitle.danger.push(node);
}
else if (columnTotalTitleEl.querySelector("span.text-warning")) {
columnTotalWithBarchartTotalTitle.warning.push(node);
}
}
}
}
assert.strictEqual(
emptyColumnTotalCells.length,
4,
"4 column totals should not have any number since the employee has recorded nothing"
);
assert.strictEqual(
columnTotalWithBarchartTotalTitle.total,
4,
"4 column totals should have a total displayed"
);
assert.strictEqual(
columnTotalWithBarchartTotalTitle.danger.length,
3,
"3 column totals should have a total displayed in red since the employee has not done all his working hours"
);
assert.strictEqual(
columnTotalWithBarchartTotalTitle.warning.length,
1,
"1 column totals should have a total displayed in orange since the employee has done extra working hours"
);
assert.containsN(
target,
".o_grid_bar_chart_container .o_grid_bar_chart_overtime",
4,
"4 overtimes indication should be displayed in 4 cells displaying barchart total"
);
assert.containsN(
target,
".o_grid_bar_chart_container:not(.o_grid_highlighted) .o_grid_bar_chart_overtime",
4,
"4 overtimes indication should be displayed in 4 cells displaying barchart total should not be visible"
);
await nextTick();
await triggerEvent(columnTotalEl, "", "mouseover");
// The overtime of the total column hovered should be visible
await contains(
".o_grid_bar_chart_container.o_grid_highlighted .o_grid_bar_chart_overtime"
);
const overTimeColor = ["text-danger","text-warning","text-danger","text-danger"];
const columnTotalOverTimeEls = target.querySelectorAll(".o_grid_bar_chart_container .o_grid_bar_chart_overtime");
columnTotalOverTimeEls.forEach((element, i) => {
assert.hasClass(element, overTimeColor[i], "Daily overtime should have been displayed in different color.");
});
assert.containsOnce(
target,
".o_grid_highlightable.position-md-sticky.end-0.d-flex.align-items-center.justify-content-center.fw-bold.text-bg-danger",
"Total overtime should be displayed in red because employees have not done all work of the week"
);
}
);
QUnit.test("Start timer and cancel it", async (assert) => {
const { openView } = await start({
serverData,
async mockRPC(route, args) {
if (args.method === "get_running_timer") {
return {
step_timer: 30,
};
} else if (args.method === "action_start_new_timesheet_timer") {
return {
start: 0,
project_id: false,
task_id: false,
description: "",
};
} else if (args.method === "get_daily_working_hours") {
assert.strictEqual(args.model, "hr.employee");
return {};
} else if (args.method === "get_server_time") {
assert.strictEqual(args.model, "timer.timer");
return serializeDateTime(DateTime.now());
} else if (args.method === "action_timer_unlink") {
return null;
} else if (
args.method === "name_search" &&
["project.project", "project.task"].includes(args.model)
) {
args.kwargs.args =
args.model === "project.project" ? [["allow_timesheets", "=", true]] : [];
} else if (route === '/web/dataset/call_kw/project.project/get_create_edit_project_ids') {
return [];
}
return timesheetGridSetup.mockTimesheetGridRPC(route, args);
},
});
await openView({
res_model: "analytic.line",
views: [[false, "grid"]],
context: { group_by: ["project_id", "task_id"] },
});
assert.containsOnce(target, ".btn_start_timer", "The timer should be running.");
await click(target, ".btn_start_timer");
assert.containsNone(target, ".btn_start_timer", "The timer should be running.");
assert.containsOnce(target, ".btn_stop_timer", "The stop button should be displayed.");
assert.containsOnce(
target,
".o_timer_discard button",
"The cancel button should be displayed"
);
await clickOpenM2ODropdown(target, "project_id");
await click(target.querySelector("div[name='project_id'] li > a"), "");
assert.containsOnce(
target,
".btn_timer_line.btn-danger",
"The timer is running on the row with the project selected"
);
let rowTimerButton = target.querySelector(".btn_timer_line.btn-danger");
let containerRowTimerButton = rowTimerButton.closest(".o_grid_highlightable");
let rowTitle = target.querySelector(
`.o_grid_row_title[data-grid-row='${containerRowTimerButton.dataset.gridRow}']`
);
assert.ok(
rowTitle.textContent.indexOf("P1") !== -1,
"The row title with the timer running should contain the project selected in the timer header."
);
assert.ok(
rowTitle.textContent.indexOf("BS task") === -1,
"The row title with the timer running should not contain a task name."
);
await click(target, ".o_field_task_with_hours[name=task_id] input");
await click(target.querySelector("div[name='task_id'] li > a"), "");
assert.containsOnce(
target,
".btn_timer_line.btn-danger",
"The timer is running on the row with the project and task selected in the timer header."
);
rowTimerButton = target.querySelector(".btn_timer_line.btn-danger");
containerRowTimerButton = rowTimerButton.closest(".o_grid_highlightable");
rowTitle = target.querySelector(
`.o_grid_row_title[data-grid-row='${containerRowTimerButton.dataset.gridRow}']`
);
assert.ok(
rowTitle.textContent.indexOf("P1") !== -1,
"The row title with the timer running should contain the project selected in the timer header."
);
assert.ok(
rowTitle.textContent.indexOf("BS task") !== -1,
"The row title with the timer running should not contain a task name."
);
await click(target, ".o_timer_discard button");
assert.containsNone(target, ".btn_timer_line.btn-danger", "The timer should be cancelled");
assert.containsNone(
target,
".btn_stop_timer",
"The stop button should no longer be displayed."
);
assert.containsNone(
target,
".o_timer_discard button",
"The cancel button should no longer be displayed."
);
assert.containsOnce(target, ".btn_start_timer", "The timer should no longer be running.");
});
QUnit.test(
"Start and stop timer with GridTimerButton (keyboard shortcut)",
async function (assert) {
const pyEnv = getPyEnv();
let timesheetId = 6;
const { openView } = await start({
serverData,
async mockRPC(route, args) {
if (args.method === "get_running_timer") {
return {
step_timer: 30,
};
} else if (args.method === "get_daily_working_hours") {
assert.strictEqual(args.model, "hr.employee");
return {};
} else if (args.method === "action_start_new_timesheet_timer") {
const { project_id, task_id } = args.args[0];
if (!project_id) {
return false;
}
const newTimesheet = {
id: timesheetId++,
project_id,
task_id,
date: serializeDateTime(DateTime.now()),
unit_amount: 0.0,
};
pyEnv.mockServer.models["analytic.line"].records.push(newTimesheet);
return newTimesheet;
} else if (args.method === "get_server_time") {
assert.strictEqual(args.model, "timer.timer");
return serializeDateTime(DateTime.now());
} else if (args.method === "action_timer_stop") {
return 0.25;
} else if (route === '/web/dataset/call_kw/project.project/get_create_edit_project_ids') {
return [];
}
return timesheetGridSetup.mockTimesheetGridRPC(route, args);
},
});
await openView({
res_model: "analytic.line",
views: [[false, "grid"]],
});
await nextTick();
assert.containsOnce(target, ".btn_start_timer", "No timer should be running.");
const gridTimerButton = target.querySelector('button.btn_timer_line');
const gridTimerButtonContainer = gridTimerButton.closest(".o_grid_highlightable");
assert.strictEqual(
target.querySelector(
`.o_grid_row.o_grid_highlightable.o_grid_cell_today[data-grid-row="${gridTimerButtonContainer.dataset.gridRow}"]`
).textContent,
"0:00"
);
await triggerEvent(document.activeElement, "", "keydown", { key: "a" });
assert.containsNone(target, ".btn_start_timer", "A timer should be running");
assert.containsOnce(
target,
".btn_timer_line.btn-danger",
"The row with the GridTimerButton with 'a' letter in the grid view should have the timer running"
);
assert.containsOnce(
target,
"button.btn_timer_line.btn-danger",
"The row with the running timer should be the one with the 'a' letter in the GridTimerButton"
);
await triggerEvent(document.activeElement, "", "keydown", { key: "a" });
assert.containsOnce(target, ".btn_start_timer", "The timer should be stopped");
assert.containsNone(
target,
".btn_timer_line.fa-stop-danger",
"The running timer in the row with the GridTimerButton with 'a' letter in the grid should be stopped"
);
assert.strictEqual(
target.querySelector(
`.o_grid_cell_today[data-grid-row="${gridTimerButtonContainer.dataset.gridRow}"]`
).textContent,
"0:15",
"The today cell in the row with `A` button should be increased by 15 minutes."
);
}
);
QUnit.test(
"Start timer and start another with GridTimerButton (keyboard shortcut)",
async function (assert) {
const pyEnv = getPyEnv();
let timesheetId = 6;
const { openView } = await start({
serverData,
async mockRPC(route, args) {
if (args.method === "get_running_timer") {
return {
step_timer: 30,
};
} else if (args.method === "get_daily_working_hours") {
assert.strictEqual(args.model, "hr.employee");
return {};
} else if (args.method === "action_start_new_timesheet_timer") {
const { project_id, task_id } = args.args[0];
if (!project_id) {
return false;
}
const newTimesheet = {
id: timesheetId++,
project_id,
task_id,
date: serializeDateTime(DateTime.now()),
unit_amount: 0.0,
};
pyEnv.mockServer.models["analytic.line"].records.push(newTimesheet);
return newTimesheet;
} else if (args.method === "get_server_time") {
assert.strictEqual(args.model, "timer.timer");
return serializeDateTime(DateTime.now());
} else if (args.method === "action_timer_stop") {
return 0.25;
} else if (route === '/web/dataset/call_kw/project.project/get_create_edit_project_ids') {
return [];
}
return timesheetGridSetup.mockTimesheetGridRPC(route, args);
},
});
await openView({
res_model: "analytic.line",
views: [[false, "grid"]],
});
assert.containsOnce(target, ".btn_start_timer", "No timer should be running.");
const gridTimerButton = target.querySelector('button.btn_timer_line');
const gridTimerButtonContainer = gridTimerButton.closest(".o_grid_highlightable");
assert.strictEqual(
target.querySelector(
`.o_grid_cell_today[data-grid-row="${gridTimerButtonContainer.dataset.gridRow}"]`
).textContent,
"0:00"
);
await triggerEvent(document.activeElement, "", "keydown", { key: "a" });
assert.containsNone(target, ".btn_start_timer", "A timer should be running");
assert.containsOnce(
target,
".btn_timer_line.btn-danger",
"The row with the GridTimerButton with 'a' letter in the grid view should have the timer running"
);
assert.containsOnce(
target,
".o_grid_row_timer.o_grid_highlightable[data-row='1']",
"The row with the running timer should be the one with the 'a' letter in the GridTimerButton"
);
await triggerEvent(document.activeElement, "", "keydown", { key: "b" });
assert.containsNone(target, ".btn_start_timer", "A timer should be running");
assert.containsOnce(
target,
".btn_timer_line.btn-danger",
"The row with the GridTimerButton with 'b' letter in the grid view should have the timer running"
);
assert.containsOnce(
target,
".o_grid_row_timer.o_grid_highlightable[data-row='2']",
"The row with the running timer should be the one with the 'b' letter in the GridTimerButton"
);
assert.strictEqual(
target.querySelector(
`.o_grid_cell_today[data-grid-row="${gridTimerButtonContainer.dataset.gridRow}"]`
).textContent,
"0:15",
"The today cell in the row with `A` button should be increased by 15 minutes."
);
}
);
QUnit.test(
"Check correct display of GridTimerButton (keyboard shortcut) when there are more than 26 rows in the grid view",
async function (assert) {
const pyEnv = getPyEnv();
let timesheetId = 6;
const project_vals = [];
for (let i = 0; i <= 26; i++) {
project_vals.push( { display_name: `Proj${i}`, allow_timesheets: true } );
}
let proj_ids = pyEnv["project.project"].create(project_vals);
const timesheet_vals = [];
proj_ids.forEach((id) => {
const newTimesheet = {
id: timesheetId++,
project_id: id,
date: serializeDateTime(DateTime.now()),
unit_amount: 1.0,
description: "",
};
timesheet_vals.push(newTimesheet);
});
pyEnv.mockServer.models["analytic.line"].records.push(...timesheet_vals);
const { openView } = await start({
serverData,
async mockRPC(route, args) {
if (args.method === "get_running_timer") {
return {
step_timer: 30,
};
} else if (args.method === "action_start_new_timesheet_timer") {
return false;
} else if (args.method === "get_daily_working_hours") {
assert.strictEqual(args.model, "hr.employee");
return {};
} else if (args.method === "get_server_time") {
assert.strictEqual(args.model, "timer.timer");
return serializeDateTime(DateTime.now());
} else if (args.method === "action_timer_unlink") {
return null;
} else if (route === '/web/dataset/call_kw/project.project/get_create_edit_project_ids') {
return [];
}
return timesheetGridSetup.mockTimesheetGridRPC(route, args);
},
});
await openView({
res_model: "analytic.line",
views: [[false, "grid"]],
context: { group_by: ["project_id", "task_id"] },
});
assert.containsN(target, ".o_grid_row_title", 32, "The view should have 32 total rows rendered.");
assert.containsOnce(target, ".btn_start_timer", "No timer should be running.");
assert.containsN(target, ".btn_timer_line.btn-outline-secondary:has(.fa-play)", 6, "Having 32 total records and only 26 letters for the shortcuts, the last 6 of the GridTimerButton should have fa-play icon instead of a letter");
assert.strictEqual(
$(".o_grid_row:has(.btn_timer_line.btn-outline-secondary:has(.fa-play)):first").prevAll(".o_grid_row:has(.btn_timer_line)").length,
26,
"The first fa-play GridTimerButton should be after all the letter shortcut GridTimerButton"
);
assert.strictEqual(
$(".o_grid_row:has(.btn_timer_line.btn-outline-secondary:has(.fa-play)):last").nextAll(".o_grid_row:has(.btn_timer_line)").length,
0,
"There shouldn't be any GridTimerButton after the last fa-play GridTimerButton"
);
}
);
QUnit.test(
"Start timer and create a new project and a new task",
async function (assert) {
serviceRegistry.add(
"user",
makeFakeUserService((group) => group === "project.group_project_manager"),
{ force: true }
);
let reload = false;
const pyEnv = getPyEnv();
const { openView } = await start({
serverData,
async mockRPC(route, args) {
if (args.method === "get_running_timer") {
return {
step_timer: 30,
};
} else if (args.method === "action_start_new_timesheet_timer") {
return {
rate: 0,
project_id: false,
task_id: false,
description: "",
};
} else if (args.method === "get_daily_working_hours") {
assert.strictEqual(args.model, "hr.employee");
return {};
} else if (args.method === "get_server_time") {
assert.strictEqual(args.model, "timer.timer");
return serializeDateTime(DateTime.now());
} else if (args.method === "action_timer_stop") {
// The newly created timesheet need to be pushed on the mockserver model otherwise it will not be fetched
// on the reload of the view. The new line will then not be created either, failing the assert on row count.
let timesheet_id = args.args[0];
let project_id = 3;
let task_id = false;
if (timesheet_id == 8){
project_id = 1;
task_id = 4;
}
const newTimesheet = {
rate: 0,
id: timesheet_id,
project_id: project_id,
task_id: task_id,
date: serializeDateTime(DateTime.now()),
description: "",
};
pyEnv.mockServer.models["analytic.line"].records.push(newTimesheet);
reload = true;
return 0.25;
} else if (args.model === "analytic.line" && args.method === "web_read_group" && reload) {
assert.step("view reloaded");
reload = false;
} else if (args.method === "name_search" && ["project.project", "project.task"].includes(args.model)) {
args.kwargs.args = args.model === "project.project" ? [["allow_timesheets", "=", true]] : [];
} else if (route === '/web/dataset/call_kw/project.project/get_create_edit_project_ids') {
return [];
}
return timesheetGridSetup.mockTimesheetGridRPC(route, args);
},
});
await openView({
res_model: "analytic.line",
views: [[false, "grid"]],
context: { group_by: ["project_id", "task_id"] },
});
// Create a new timesheet with a new project.
assert.containsN(target, ".o_grid_row_title", 5, "The view should have 5 rows rendered.");
await click(target, ".btn_start_timer");
await clickOpenM2ODropdown(target, "project_id");
target.querySelector(".o_field_widget[name=project_id] input").focus();
await editInput(target, ".o_field_widget[name=project_id] input", "a new project");
await click(target, ".o_field_widget[name=project_id] input");
assert.strictEqual(
target.querySelector(".timesheet-timer .o_field_widget[name=project_id] input").value,
"a new project",
'The project_id in the timer header should be set to "a new project".'
);
await selectDropdownItem(target, "project_id", `Create "a new project"`);
await click(target, ".btn_stop_timer");
assert.containsN(target, ".o_grid_row_title", 6, "The view should have 6 rows rendered.");
assert.verifySteps(
["view reloaded"],
"When stopping the timer, a reload of the view should be triggered since there were no active row for the current project."
);
// Create a new timesheet with a new task in an existing project.
await click(target, ".btn_start_timer");
await clickOpenM2ODropdown(target, "project_id");
await click(target.querySelector("div[name='project_id'] li > a"), "");
await click(target, ".o_field_task_with_hours[name=task_id] input");
target.querySelector(".o_field_widget[name=task_id] input").focus();
await editInput(target, ".o_field_widget[name=task_id] input", "a new task");
await click(target, ".o_field_widget[name=task_id] input");
assert.strictEqual(
target.querySelector(".timesheet-timer .o_field_widget[name=task_id] input").value,
"a new task",
'The task_id in the timer header should be set to "New task".'
);
await selectDropdownItem(target, "task_id", `Create "a new task"`);
await click(target, ".btn_stop_timer");
assert.containsN(target, ".o_grid_row_title", 7, "The view should have 7 rows rendered.");
assert.verifySteps(
["view reloaded"],
"When stopping the timer, a reload of the view should be triggered since there were no active row for the tuple project > task."
);
}
);
QUnit.test("Switch view with GroupBy and start the timer", async function (assert) {
serverData.views["analytic.line,1,kanban"] =
`<kanban js_class="timesheet_timer_kanban">
<templates>
<field name="name"/>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<field name="employee_id"/>
<field name="project_id"/>
<field name="task_id"/>
<field name="date"/>
<field name="display_timer"/>
</div>
</t>
</templates>
</kanban>`;
const { openView } = await start({
serverData,
async mockRPC(route, { method }) {
switch (method) {
case "get_running_timer":
return { step_timer: 30 };
case "action_start_new_timesheet_timer":
return false;
case "get_daily_working_hours":
return {};
case "get_server_time":
return serializeDateTime(DateTime.now());
case "get_create_edit_project_ids":
return [];
default:
return timesheetGridSetup.mockTimesheetGridRPC(...arguments);
}
}
});
await openView({
res_model: "analytic.line",
views: [[false, "grid"], [false, "kanban"]],
context: { group_by: ["project_id", "task_id"], my_timesheet_display_timer: 1 },
});
await nextTick();
await click(target, ".o_switch_view.o_kanban");
await nextTick();
await click(target, ".btn_start_timer");
assert.containsNone(
target,
"button.btn_start_timer",
"Timer should be running"
);
});
QUnit.test("Total cell bg color", async function (assert) {
const { openView } = await start({
serverData,
async mockRPC(route, args) {
if (args.method === "get_running_timer") {
return {
step_timer: 30,
};
} else if (args.method === "get_daily_working_hours") {
assert.strictEqual(args.model, "hr.employee");
return {
"2017-01-24": 4,
"2017-01-25": 4,
};
} else if (route === '/web/dataset/call_kw/project.project/get_create_edit_project_ids') {
return [];
}
return timesheetGridSetup.mockTimesheetGridRPC(route, args);
},
});
await openView({
res_model: "analytic.line",
views: [[false, "grid"]],
context: { group_by: ["project_id", "task_id"] },
});
assert.containsOnce(target, ".o_grid_highlightable.text-bg-warning", "total should be an overtime (10 > 8)");
});
QUnit.test("display sample data and then data + fetch last validate timesheet date", async (assert) => {
serverData.views["analytic.line,false,grid"] = serverData.views["analytic.line,false,grid"].replace("<grid", "<grid sample='1'");
const { openView } = await start({
serverData,
async mockRPC(route, args) {
if (args.method === "get_running_timer") {
return {
step_timer: 30,
};
} else if (args.method === "get_daily_working_hours") {
assert.strictEqual(args.model, "hr.employee");
return {
"2017-01-24": 4,
"2017-01-25": 4,
};
} else if (args.method === "get_last_validated_timesheet_date") {
assert.step("get_last_validated_timesheet_date");
} else if (route === '/web/dataset/call_kw/project.project/get_create_edit_project_ids') {
return [];
}
return await timesheetGridSetup.mockTimesheetGridRPC(route, args);
},
});
await openView({
res_model: "analytic.line",
views: [[false, "grid"]],
context: { search_default_nothing: 1 },
});
assert.containsOnce(target, ".o_view_sample_data");
await removeFacet(target);
assert.containsNone(target, ".o_grid_sample_data");
assert.containsN(target, ".o_grid_row_title", 6);
assert.verifySteps(["get_last_validated_timesheet_date"]); // the rpc should be called only once
});
});