19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:56 +01:00
parent a2f74aefd8
commit 4a4d12c333
844 changed files with 212348 additions and 270090 deletions

View file

@ -1,232 +0,0 @@
/** @odoo-module */
import { browser } from "@web/core/browser/browser";
import { click, getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
import { setupControlPanelServiceRegistry, toggleGroupByMenu, toggleMenuItem, toggleMenuItemOption } from "@web/../tests/search/helpers";
import { makeView } from "@web/../tests/views/helpers";
import { registry } from "@web/core/registry";
import { makeFakeNotificationService, fakeCookieService } from "@web/../tests/helpers/mock_services";
import { getFirstElementForXpath } from './project_test_utils';
const serviceRegistry = registry.category("services");
QUnit.module("Project", {}, () => {
QUnit.module("Views", (hooks) => {
let makeViewParams;
let target;
hooks.beforeEach(async (assert) => {
target = getFixture();
const serverData = {
models: {
burndown_chart: {
fields: {
date: { string: "Date", type: "date", store: true, sortable: true },
project_id: { string: "Project", type: "many2one", relation: "project", store: true, sortable: true },
stage_id: { string: "Stage", type: "many2one", relation: "stage", store: true, sortable: true },
nb_tasks: { string: "Number of Tasks", type: "integer", store: true, sortable: true, group_operator: "sum" }
},
records: [
{ id: 1, project_id: 1, stage_id: 1, date: "2020-01-01", nb_tasks: 10 },
{ id: 2, project_id: 1, stage_id: 2, date: "2020-02-01", nb_tasks: 5 },
{ id: 3, project_id: 1, stage_id: 3, date: "2020-03-01", nb_tasks: 2 },
],
},
project: {
fields: {
name: { string: "Project Name", type: "char" },
},
records: [{ id: 1, name: "Project A" }]
},
stage: {
fields: {
name: { string: "Stage Name", type: "char" },
},
records: [
{ id: 1, name: "Todo" },
{ id: 2, name: "In Progress" },
{ id: 3, name: "Done" },
],
}
},
views: {
"burndown_chart,false,graph": `
<graph type="line">
<field name="date" string="Date" interval="month"/>
<field name="stage_id"/>
<field name="nb_tasks" type="measure"/>
</graph>
`,
"burndown_chart,false,search": `
<search/>
`,
},
};
makeViewParams = {
serverData,
resModel: "burndown_chart",
type: "burndown_chart",
};
setupControlPanelServiceRegistry();
const notificationMock = () => {
assert.step("notification_triggered");
return () => {};
};
registry.category("services").add("notification", makeFakeNotificationService(notificationMock), {
force: true,
});
serviceRegistry.add("cookie", fakeCookieService);
});
QUnit.module("BurndownChart");
QUnit.test("check that the sort buttons are invisible", async function (assert) {
await makeView(makeViewParams);
assert.containsNone(target, '.o_cp_bottom_left:has(.btn-group[role=toolbar][aria-label="Sort graph"])', "The sort buttons are not rendered.");
});
async function makeBurnDownChartWithSearchView(makeViewOverwriteParams = { }) {
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
clearTimeout: () => {},
});
await makeView({
...makeViewParams,
searchViewId: false,
searchViewArch: `
<search string="Burndown Chart">
<filter string="Date" name="date" context="{'group_by': 'date'}" />
<filter string="Stage" name="stage" context="{'group_by': 'stage_id'}" />
</search>
`,
searchViewFields: {
date: {
name: "date",
string: "Date",
type: "date",
store: true,
sortable: true,
searchable: true,
},
stage_id: {
name: "stage_id",
string: "Stage",
type: "many2one",
store: true,
sortable: true,
searchable: true,
},
},
context: { ...makeViewParams.context, 'search_default_date': 1, 'search_default_stage': 1 },
...makeViewOverwriteParams,
});
}
async function testBurnDownChartWithSearchView(stepsTriggeringNotification, assert) {
await makeBurnDownChartWithSearchView();
await stepsTriggeringNotification();
assert.verifySteps(['notification_triggered']);
}
async function openGroupByMainMenu(target) {
await toggleGroupByMenu(target);
}
async function openGroupByDateMenu(target) {
await openGroupByMainMenu(target);
await toggleMenuItem(target, 'Date');
}
async function toggleGroupByStageMenu(target) {
await openGroupByMainMenu(target);
await toggleMenuItem(target, 'Stage');
}
async function toggleSelectedGroupByDateItem(target) {
await openGroupByDateMenu(target);
const selectedGroupByDateItemXpath = `//div
[contains(@class, 'o_group_by_menu')]
//button
[contains(@class, 'o_menu_item')]
[contains(., 'Date')]
/following-sibling::div
/span
[contains(@class, 'o_item_option')]
[contains(@class, 'selected')]`;
const selectedGroupByDateItemElement = getFirstElementForXpath(target, selectedGroupByDateItemXpath);
await toggleMenuItemOption(target, 'Date', selectedGroupByDateItemElement.innerText);
}
QUnit.test("check that removing the group by 'Date: Month > Stage' in the search bar triggers a notification", async function (assert) {
const stepsTriggeringNotification = async () => {
const removeFilterXpath = `//div[contains(@class, 'o_searchview_facet')]
[.//span[@class='o_facet_value']
[contains(., 'Date: Month')]]
/i[contains(@class, 'o_facet_remove')]`;
const removeFilterElement = getFirstElementForXpath(target, removeFilterXpath);
await click(removeFilterElement);
};
await testBurnDownChartWithSearchView(stepsTriggeringNotification, assert);
});
QUnit.test("check that removing the group by 'Date' triggers a notification", async function (assert) {
const stepsTriggeringNotification = async () => {
await toggleSelectedGroupByDateItem(target);
};
await testBurnDownChartWithSearchView(stepsTriggeringNotification, assert);
});
QUnit.test("check that removing the group by 'Stage' triggers a notification", async function (assert) {
const stepsTriggeringNotification = async () => {
await toggleGroupByStageMenu(target);
};
await testBurnDownChartWithSearchView(stepsTriggeringNotification, assert);
});
QUnit.test("check that adding a group by 'Date' actually toggle it", async function (assert) {
await makeBurnDownChartWithSearchView();
await openGroupByDateMenu(target);
const firstNotSelectedGroupByDateItemXpath = `//div
[contains(@class, 'o_group_by_menu')]
//button
[contains(@class, 'o_menu_item')]
[contains(., 'Date')]
/following-sibling::div
/span
[contains(@class, 'o_item_option')]
[not(contains(@class, 'selected'))]`;
const firstNotSelectedGroupByDateItemElement = getFirstElementForXpath(target, firstNotSelectedGroupByDateItemXpath);
await toggleMenuItemOption(target, 'Date', firstNotSelectedGroupByDateItemElement.innerText);
const groupByDateSubMenuXpath = `//div
[contains(@class, 'o_group_by_menu')]
//button
[contains(@class, 'o_menu_item')]
[contains(., 'Date')]
/following-sibling::div`;
const groupByDateSubMenuElement = getFirstElementForXpath(target, groupByDateSubMenuXpath);
const selectedGroupByDateItemElements = groupByDateSubMenuElement.querySelectorAll('span.o_item_option.selected');
assert.equal(selectedGroupByDateItemElements.length, 1, 'There is only one selected item.');
assert.equal(firstNotSelectedGroupByDateItemElement.innerText, selectedGroupByDateItemElements[0].innerText, 'The selected item is the one we clicked on.');
});
function checkGroupByOrder(assert) {
const dateSearchFacetXpath = `//div[contains(@class, 'o_searchview_facet')]
[.//span[@class='o_facet_value']
[contains(., 'Date: Month')]]`;
const dateSearchFacetElement = getFirstElementForXpath(target, dateSearchFacetXpath);
const dateSearchFacetParts = dateSearchFacetElement.querySelectorAll('.o_facet_value');
assert.equal(dateSearchFacetParts.length, 2);
assert.equal(dateSearchFacetParts[0].innerText, 'Date: Month');
assert.equal(dateSearchFacetParts[1].innerText, 'Stage');
}
QUnit.test("check that the group by is always sorted 'Date' first, 'Stage' second", async function (assert) {
await makeBurnDownChartWithSearchView({context: {...makeViewParams.context, 'search_default_date': 1, 'search_default_stage': 1}});
checkGroupByOrder(assert);
});
QUnit.test("check that the group by is always sorted 'Date' first, 'Stage' second", async function (assert) {
await makeBurnDownChartWithSearchView({context: {...makeViewParams.context, 'search_default_stage': 1, 'search_default_date': 1}});
checkGroupByOrder(assert);
});
});
});

View file

@ -0,0 +1,9 @@
import { models } from "@web/../tests/web_test_helpers";
export class ProjectTask extends models.ServerModel {
_name = "project.task";
plan_task_in_calendar(idOrIds, values) {
return this.write(idOrIds, values);
}
}

View file

@ -1,76 +0,0 @@
/** @odoo-module **/
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import {
editInput,
getFixture,
mount,
patchWithCleanup,
} from "@web/../tests/helpers/utils";
import { PortalFileInput } from "@project/project_sharing/components/portal_file_input/portal_file_input";
import { registry } from "@web/core/registry";
import { session } from "@web/session";
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
const serviceRegistry = registry.category("services");
let target;
// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------
async function createFileInput({ mockPost, mockAdd, props }) {
serviceRegistry.add("notification", {
start: () => ({
add: mockAdd || (() => {}),
}),
});
serviceRegistry.add("http", {
start: () => ({
post: mockPost || (() => {}),
}),
});
const env = await makeTestEnv();
await mount(PortalFileInput, target, { env, props });
}
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
QUnit.module("Project", ({ beforeEach }) => {
beforeEach(() => {
patchWithCleanup(odoo, { csrf_token: "dummy" });
target = getFixture();
});
QUnit.module("PortalComponents");
QUnit.test("uploading a file that is too heavy in portal will send a notification", async (assert) => {
serviceRegistry.add("localization", makeFakeLocalizationService());
patchWithCleanup(session, { max_file_upload_size: 2 });
await createFileInput({
props: {
onUpload(files) {
assert.deepEqual(files, [null]);
},
},
mockAdd: (message) => {
assert.step("notification");
assert.strictEqual(
message,
"The selected file (4B) is over the maximum allowed file size (2B)."
);
},
});
const file = new File(["test"], "fake_file.txt", { type: "text/plain" });
await editInput(target, ".o_file_input input", file);
assert.verifySteps(
["notification"],
"Only the notification will be triggered and the file won't be uploaded."
);
});
});

View file

@ -1,33 +0,0 @@
/** @odoo-module */
import { getFixture } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let makeViewParams, target;
QUnit.module("Project", (hooks) => {
hooks.beforeEach(() => {
makeViewParams = {
type: "form",
resModel: "project.project",
serverData: {
models: {
"project.project": {
fields: {
id: { string: "Id", type: "integer" },
},
records: [{ id: 1, display_name: "First record" }],
},
},
},
arch: `<form js_class="project_form"><field name="display_name"/></form>`,
};
target = getFixture();
setupViewRegistries();
});
QUnit.module("Form");
QUnit.test("project form view", async function (assert) {
await makeView(makeViewParams);
assert.containsOnce(target, ".o_form_view");
});
});

View file

@ -0,0 +1,71 @@
import { beforeEach, expect, test } from "@odoo/hoot";
import { click } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { mountView, onRpc } from "@web/../tests/web_test_helpers";
import { defineProjectModels, ProjectProject } from "./project_models";
defineProjectModels();
beforeEach(() => {
ProjectProject._records = [
{
id: 1,
name: "Project A",
},
];
ProjectProject._views = {
kanban: `
<kanban class="o_kanban_test" edit="0">
<template>
<t t-name="card">
<field name="is_favorite" widget="project_is_favorite" nolabel="1"/>
<field name="name"/>
</t>
</template>
</kanban>
`,
};
});
test("Check is_favorite field is still editable even if the record/view is in readonly.", async () => {
onRpc("project.project", "web_save", ({ args }) => {
const [ids, vals] = args;
expect(ids).toEqual([1]);
expect(vals).toEqual({ is_favorite: true });
expect.step("web_save");
});
await mountView({
resModel: "project.project",
type: "kanban",
});
expect("div[name=is_favorite] .o_favorite").toHaveCount(1);
expect.verifySteps([]);
await click("div[name=is_favorite] .o_favorite");
await animationFrame();
expect.verifySteps(["web_save"]);
});
test("Check is_favorite field is readonly if the field is readonly", async () => {
onRpc("project.project", "web_save", () => {
expect.step("web_save");
});
ProjectProject._views["kanban"] = ProjectProject._views["kanban"].replace(
'widget="project_is_favorite"',
'widget="project_is_favorite" readonly="1"'
);
await mountView({
resModel: "project.project",
type: "kanban",
});
expect("div[name=is_favorite] .o_favorite").toHaveCount(1);
expect.verifySteps([]);
await click("div[name=is_favorite] .o_favorite");
await animationFrame();
expect.verifySteps([]);
});

View file

@ -0,0 +1,192 @@
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { defineModels, fields, models } from "@web/../tests/web_test_helpers";
export class ProjectProject extends models.Model {
_name = "project.project";
name = fields.Char();
is_favorite = fields.Boolean();
is_template = fields.Boolean();
active = fields.Boolean({ default: true });
stage_id = fields.Many2one({ relation: "project.project.stage" });
date = fields.Date({ string: "Expiration Date" });
date_start = fields.Date();
user_id = fields.Many2one({ relation: "res.users", falsy_value_label: "👤 Unassigned" });
allow_task_dependencies = fields.Boolean({ string: "Task Dependencies", default: false });
allow_milestones = fields.Boolean({ string: "Milestones", default: false });
allow_recurring_tasks = fields.Boolean({ string: "Recurring Tasks", default: false });
_records = [
{
id: 1,
name: "Project 1",
stage_id: 1,
date: "2024-01-09 07:00:00",
date_start: "2024-01-03 12:00:00",
},
{ id: 2, name: "Project 2", stage_id: 2 },
];
_views = {
list: '<list><field name="name"/></list>',
form: '<form><field name="name"/></form>',
};
check_access_rights() {
return Promise.resolve(true);
}
get_template_tasks(projectId) {
return this.env["project.task"].search_read(
[
["project_id", "=", projectId],
["is_template", "=", true],
],
["id", "name"]
);
}
check_features_enabled() {
let allow_task_dependencies = false;
let allow_milestones = false;
let allow_recurring_tasks = false;
for (const record of this) {
if (record.allow_task_dependencies) {
allow_task_dependencies = true;
}
if (record.allow_milestones) {
allow_milestones = true;
}
if (record.allow_recurring_tasks) {
allow_recurring_tasks = true;
}
}
return { allow_task_dependencies, allow_milestones, allow_recurring_tasks };
}
}
export class ProjectProjectStage extends models.Model {
_name = "project.project.stage";
name = fields.Char();
_records = [
{ id: 1, name: "Stage 1" },
{ id: 2, name: "Stage 2" },
];
_views = {
list: '<list><field name="name"/></list>',
form: '<form><field name="name"/></form>',
};
}
export class ProjectTask extends models.Model {
_name = "project.task";
name = fields.Char();
parent_id = fields.Many2one({ relation: "project.task" });
child_ids = fields.One2many({
relation: "project.task",
relation_field: "parent_id",
});
subtask_count = fields.Integer();
sequence = fields.Integer({ string: "Sequence", default: 10 });
closed_subtask_count = fields.Integer();
project_id = fields.Many2one({ relation: "project.project", falsy_value_label: "🔒 Private" });
display_in_project = fields.Boolean({ default: true });
stage_id = fields.Many2one({ relation: "project.task.type" });
milestone_id = fields.Many2one({ relation: "project.milestone" });
state = fields.Selection({
selection: [
["01_in_progress", "In Progress"],
["02_changes_requested", "Changes Requested"],
["03_approved", "Approved"],
["1_canceled", "Cancelled"],
["1_done", "Done"],
["04_waiting_normal", "Waiting Normal"],
],
});
user_ids = fields.Many2many({
string: "Assignees",
relation: "res.users",
falsy_value_label: "👤 Unassigned",
});
priority = fields.Selection({
selection: [
["0", "Low"],
["1", "High"],
],
});
partner_id = fields.Many2one({ string: "Partner", relation: "res.partner" });
planned_date_begin = fields.Datetime({ string: "Start Date" });
date_deadline = fields.Datetime({ string: "Stop Date" });
depend_on_ids = fields.Many2many({ relation: "project.task" });
closed_depend_on_count = fields.Integer();
is_closed = fields.Boolean();
is_template = fields.Boolean({ string: "Is Template", default: false });
plan_task_in_calendar(idOrIds, values) {
return this.write(idOrIds, values);
}
_records = [
{
id: 1,
name: "Regular task 1",
project_id: 1,
stage_id: 1,
milestone_id: 1,
state: "01_in_progress",
user_ids: [7],
},
{
id: 2,
name: "Regular task 2",
project_id: 1,
stage_id: 1,
state: "03_approved",
},
{
id: 3,
name: "Private task 1",
project_id: false,
stage_id: 1,
state: "04_waiting_normal",
},
];
}
export class ProjectTaskType extends models.Model {
_name = "project.task.type";
name = fields.Char();
sequence = fields.Integer();
_records = [
{ id: 1, name: "Todo" },
{ id: 2, name: "In Progress" },
{ id: 3, name: "Done" },
];
}
export class ProjectMilestone extends models.Model {
_name = "project.milestone";
name = fields.Char();
_records = [{ id: 1, name: "Milestone 1" }];
}
export function defineProjectModels() {
defineMailModels();
defineModels(projectModels);
}
export const projectModels = {
ProjectProject,
ProjectProjectStage,
ProjectTask,
ProjectTaskType,
ProjectMilestone,
};

View file

@ -0,0 +1,154 @@
import { beforeEach, expect, describe, test } from "@odoo/hoot";
import { click } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { mountView } from "@web/../tests/web_test_helpers";
import { defineProjectModels, ProjectProject, ProjectTask } from "./project_models";
defineProjectModels();
describe.current.tags("desktop");
beforeEach(() => {
ProjectProject._records = [
{
id:5,
name: "Project One"
},
];
ProjectTask._records = [
{
id: 1,
name: 'task one',
project_id: 5,
closed_subtask_count: 1,
closed_depend_on_count: 1,
subtask_count: 4,
child_ids: [2, 3, 4, 7],
depend_on_ids: [5,6],
state: '04_waiting_normal',
},
{
name: 'task two',
parent_id: 1,
closed_subtask_count: 0,
subtask_count: 0,
child_ids: [],
depend_on_ids: [],
state: '03_approved'
},
{
name: 'task three',
parent_id: 1,
closed_subtask_count: 0,
subtask_count: 0,
child_ids: [],
depend_on_ids: [],
state: '02_changes_requested'
},
{
name: 'task four',
parent_id: 1,
closed_subtask_count: 0,
subtask_count: 0,
child_ids: [],
depend_on_ids: [],
state: '1_done'
},
{
name: 'task five',
closed_subtask_count: 0,
subtask_count: 1,
child_ids: [6],
depend_on_ids: [],
state: '03_approved'
},
{
name: 'task six',
parent_id: 5,
closed_subtask_count: 0,
subtask_count: 0,
child_ids: [],
depend_on_ids: [],
state: '1_canceled'
},
{
name: 'task seven',
parent_id: 1,
closed_subtask_count: 0,
subtask_count: 0,
child_ids: [],
depend_on_ids: [],
state: '01_in_progress',
},
];
ProjectTask._views = {
form: `
<form>
<field name="closed_depend_on_count" invisible="1"/>
<field name="child_ids" widget="subtasks_one2many">
<list editable="bottom">
<field name="display_in_project" force_save="1"/>
<field name="project_id" widget="project"/>
<field name="name"/>
<field name="state"/>
</list>
</field>
<field name="depend_on_ids" widget="notebook_task_one2many" context="{ 'closed_X2M_count': closed_depend_on_count }">
<list editable="bottom">
<field name="display_in_project" force_save="1"/>
<field name="project_id" widget="project"/>
<field name="name"/>
<field name="state"/>
</list>
</field>
</form>
`,
};
});
test("test Project Task Calendar Popover with task_stage_with_state_selection widget", async () => {
await mountView({
resModel: "project.task",
type: "form",
resId: 1,
});
expect('div[name="child_ids"] .o_data_row').toHaveCount(4, {
message: "The subtasks list should display all subtasks by default, thus we are looking for 4 in total"
});
expect('div[name="depend_on_ids"] .o_data_row').toHaveCount(2, {
message: "The depend on tasks list should display all blocking tasks by default, thus we are looking for 2 in total"
});
expect("div[name='child_ids'] .o_field_x2many_list_row_add a.o_toggle_closed_task_button").toHaveText("Hide closed tasks");
expect("div[name='depend_on_ids'] .o_field_x2many_list_row_add a.o_toggle_closed_task_button").toHaveText("Hide closed tasks");
await click("div[name='child_ids'] .o_field_x2many_list_row_add a.o_toggle_closed_task_button");
await animationFrame();
expect("div[name='child_ids'] .o_field_x2many_list_row_add a.o_toggle_closed_task_button").toHaveText("Show closed tasks");
expect("div[name='depend_on_ids'] .o_field_x2many_list_row_add a.o_toggle_closed_task_button").toHaveText("Hide closed tasks");
expect('div[name="child_ids"] .o_data_row').toHaveCount(3, {
message: "The subtasks list should only display the open subtasks of the task, in this case 3"
});
expect('div[name="depend_on_ids"] .o_data_row').toHaveCount(2, {
message: "The depend on tasks list should still display all blocking tasks, in this case 2"
});
await click("div[name='depend_on_ids'] .o_field_x2many_list_row_add a.o_toggle_closed_task_button");
await animationFrame();
expect("div[name='child_ids'] .o_field_x2many_list_row_add a.o_toggle_closed_task_button").toHaveText("Show closed tasks");
expect("div[name='depend_on_ids'] .o_field_x2many_list_row_add a.o_toggle_closed_task_button").toHaveText("1 closed tasks");
expect('div[name="child_ids"] .o_data_row').toHaveCount(3, {
message: "The subtasks list should only display the open subtasks of the task, in this case 3"
});
expect('div[name="depend_on_ids"] .o_data_row').toHaveCount(1, {
message: "The depend on tasks list should only display open tasks, in this case 1"
});
});

View file

@ -0,0 +1,178 @@
import { beforeEach, expect, describe, test } from "@odoo/hoot";
import { animationFrame, click } from "@odoo/hoot-dom";
import { getService, mountWithCleanup } from "@web/../tests/web_test_helpers";
import { WebClient } from "@web/webclient/webclient";
import { defineProjectModels, ProjectProject, ProjectTask } from "./project_models";
defineProjectModels();
describe.current.tags("mobile");
beforeEach(() => {
ProjectProject._records = [
{
id: 5,
name: "Project One",
},
];
ProjectTask._records = [
{
id: 1,
name: "task one",
project_id: 5,
closed_subtask_count: 1,
closed_depend_on_count: 1,
subtask_count: 4,
child_ids: [2, 3, 4, 7],
depend_on_ids: [5, 6],
state: "04_waiting_normal",
},
{
id: 2,
name: "task two",
project_id: 5,
parent_id: 1,
closed_subtask_count: 0,
subtask_count: 0,
child_ids: [],
depend_on_ids: [],
state: "03_approved",
},
{
id: 3,
name: "task three",
project_id: 5,
parent_id: 1,
closed_subtask_count: 0,
subtask_count: 0,
child_ids: [],
depend_on_ids: [],
state: "02_changes_requested",
},
{
id: 4,
name: "task four",
project_id: 5,
parent_id: 1,
closed_subtask_count: 0,
subtask_count: 0,
child_ids: [],
depend_on_ids: [],
state: "1_done",
},
{
id: 5,
name: "task five",
project_id: 5,
closed_subtask_count: 0,
subtask_count: 1,
child_ids: [6],
depend_on_ids: [],
state: "03_approved",
},
{
id: 6,
name: "task six",
project_id: 5,
parent_id: 5,
closed_subtask_count: 0,
subtask_count: 0,
child_ids: [],
depend_on_ids: [],
state: "1_canceled",
},
{
id: 7,
name: "task seven",
project_id: 5,
parent_id: 1,
closed_subtask_count: 0,
subtask_count: 0,
child_ids: [],
depend_on_ids: [],
state: "01_in_progress",
},
];
ProjectTask._views = {
form: `
<form>
<field name="name"/>
<field name="child_ids" widget="subtasks_one2many">
<kanban>
<templates>
<t t-name="card">
<main>
<field name="name" class="fw-bold fs-5"/>
<field name="project_id" widget="project"/>
<field name="state"/>
</main>
</t>
</templates>
</kanban>
</field>
<field name="depend_on_ids" widget="notebook_task_one2many">
<kanban>
<templates>
<t t-name="card">
<main>
<field name="name" class="fw-bold fs-5"/>
<field name="project_id" widget="project"/>
<field name="state"/>
</main>
</t>
</templates>
</kanban>
</field>
</form>
`,
search: `<search/>`,
};
});
test("test open subtask in form view instead of form view dialog", async () => {
await mountWithCleanup(WebClient);
await getService("action").doAction({
name: "Tasks",
res_model: "project.task",
type: "ir.actions.act_window",
res_id: 1,
views: [[false, "form"]],
});
expect("div[name='name'] input").toHaveValue("task one");
expect("div[name='child_ids'] .o_kanban_record:not(.o_kanban_ghost,.o-kanban-button-new)").toHaveCount(4, {
message:
"The subtasks list should display all subtasks by default, thus we are looking for 4 in total",
});
await click("div[name='child_ids'] .o_kanban_record:first-child");
await animationFrame();
expect(document.body).not.toHaveClass("modal-open");
expect("div[name='name'] input").toHaveValue("task two");
});
test("test open task dependencies in form view instead of form view dialog", async () => {
await mountWithCleanup(WebClient);
await getService("action").doAction({
name: "Tasks",
res_model: "project.task",
type: "ir.actions.act_window",
res_id: 1,
views: [[false, "form"]],
});
expect("div[name='name'] input").toHaveValue("task one");
expect("div[name='depend_on_ids'] .o_kanban_record:not(.o_kanban_ghost,.o-kanban-button-new)").toHaveCount(2, {
message:
"The depend on tasks list should display all blocking tasks by default, thus we are looking for 2 in total",
});
await click("div[name='depend_on_ids'] .o_kanban_record:first-child");
await animationFrame();
expect(document.body).not.toHaveClass("modal-open");
expect("div[name='name'] input").toHaveValue("task five");
});

View file

@ -0,0 +1,68 @@
import { expect, test, describe } from "@odoo/hoot";
import { animationFrame } from "@odoo/hoot-mock";
import {
mountView,
onRpc,
contains,
toggleKanbanColumnActions
} from "@web/../tests/web_test_helpers";
import { defineProjectModels } from "./project_models";
defineProjectModels();
describe.current.tags("desktop");
const listViewParams = {
resModel: "project.project",
type: "list",
actionMenus: {},
arch: `
<list multi_edit="1" js_class="project_project_list">
<field name="name"/>
</list>
`,
}
test("project.project (list) show archive/unarchive action for project manager", async () => {
onRpc("has_group", ({ args }) => args[1] === "project.group_project_manager");
await mountView(listViewParams);
await contains("input.form-check-input").click();
await contains(`.o_cp_action_menus .dropdown-toggle`).click();
expect(`.oi-archive`).toHaveCount(1, { message: "Archive action should be visible" });
expect(`.oi-unarchive`).toHaveCount(1, { message: "Unarchive action should be visible" });
});
test("project.project (list) hide archive/unarchive action for project user", async () => {
onRpc("has_group", ({ args }) => args[1] === "project.group_project_user");
await mountView(listViewParams);
await contains("input.form-check-input").click();
await contains(`.o_cp_action_menus .dropdown-toggle`).click();
expect(`.o-dropdown--menu span:contains(Archive)`).toHaveCount(0, { message: "Archive action should not be visible" });
expect(`.o-dropdown--menu span:contains(Unarchive)`).toHaveCount(0, { message: "Unarchive action should not be visible" });
});
test("project.project (kanban) hide archive/unarchive action for project user", async () => {
onRpc("has_group", ({ args }) => args[1] === "project.group_project_user");
await mountView({
resModel: "project.project",
type: "kanban",
actionMenus: {},
arch: `
<kanban js_class="project_project_kanban">
<field name="stage_id"/>
<templates>
<t t-name="card">
<div>
<field name="name"/>
</div>
</t>
</templates>
</kanban>
`,
groupBy: ['stage_id']
});
toggleKanbanColumnActions();
await animationFrame();
await expect('.o_column_archive_records').toHaveCount(0, { message: "Archive action should not be visible" });
await expect('.o_column_unarchive_records').toHaveCount(0, { message: "Unarchive action should not be visible" });
});

View file

@ -0,0 +1,44 @@
import { expect, test, describe } from "@odoo/hoot";
import { mockDate, runAllTimers } from "@odoo/hoot-mock";
import { click, queryAllTexts } from "@odoo/hoot-dom";
import { mountView, onRpc } from "@web/../tests/web_test_helpers";
import { defineProjectModels } from "./project_models";
describe.current.tags("desktop");
defineProjectModels();
test("check 'Edit' and 'View Tasks' buttons are in Project Calendar Popover", async () => {
mockDate("2024-01-03 12:00:00", 0);
onRpc(({ method, model, args }) => {
if (model === "project.project" && method === "action_view_tasks") {
expect.step("view tasks");
return false;
} else if (method === "has_access") {
return true;
}
});
await mountView({
resModel: "project.project",
type: "calendar",
arch: `
<calendar date_start="date_start" mode="week" js_class="project_project_calendar">
<field name="name"/>
</calendar>
`,
});
expect(".fc-event-main").toHaveCount(1);
await click(".fc-event-main");
await runAllTimers();
expect(".o_popover").toHaveCount(1);
expect(".o_popover .card-footer .btn").toHaveCount(3);
expect(queryAllTexts(".o_popover .card-footer .btn")).toEqual(["Edit", "View Tasks", ""]);
expect(".o_popover .card-footer .btn i.fa-trash").toHaveCount(1);
await click(".o_popover .card-footer a:contains(View Tasks)");
await click(".o_popover .card-footer a:contains(Edit)");
expect.verifySteps(["view tasks"]);
});

View file

@ -0,0 +1,228 @@
import { expect, test, beforeEach } from "@odoo/hoot";
import { click } from "@odoo/hoot-dom";
import {
mountView,
contains,
onRpc,
toggleMenuItem,
toggleActionMenu,
clickSave,
mockService,
} from "@web/../tests/web_test_helpers";
import { defineProjectModels, ProjectProject } from "./project_models";
defineProjectModels();
beforeEach(() => {
ProjectProject._records = [
{
id: 1,
name: "Project 1",
allow_milestones: false,
allow_task_dependencies: false,
allow_recurring_tasks: false,
},
{
id: 2,
name: "Project 2",
allow_milestones: false,
allow_task_dependencies: false,
allow_recurring_tasks: false,
},
];
mockService("action", {
doAction(actionRequest) {
if (actionRequest === "reload_context") {
expect.step("reload_context");
} else {
return super.doAction(...arguments);
}
},
});
});
test("project.project (form)", async () => {
await mountView({
resModel: "project.project",
resId: 1,
type: "form",
arch: `
<form js_class="form_description_expander">
<field name="name"/>
</form>
`,
});
expect(".o_form_view").toHaveCount(1);
});
const formViewParams = {
resModel: "project.project",
type: "form",
actionMenus: {},
resId: 1,
arch: `
<form js_class="project_project_form">
<field name="active"/>
<field name="name"/>
<field name="allow_task_dependencies"/>
<field name="allow_milestones"/>
<field name="allow_recurring_tasks"/>
</form>
`,
};
onRpc("project.project", "check_features_enabled", ({ method }) => expect.step(method));
onRpc("web_save", ({ method }) => expect.step(method));
test("project.project (form) hide archive action for project user", async () => {
onRpc("has_group", ({ args }) => args[1] === "project.group_project_user");
await mountView(formViewParams);
await toggleActionMenu();
expect(`.o-dropdown--menu span:contains(Archive)`).toHaveCount(0, { message: "Archive action should not be visible" });
expect.verifySteps(["check_features_enabled"]);
});
test("project.project (form) show archive action for project manager", async () => {
onRpc("has_group", () => true);
await mountView(formViewParams);
await toggleActionMenu();
expect(`.o-dropdown--menu span:contains(Archive)`).toHaveCount(1, { message: "Arhive action should be visible" });
await toggleMenuItem("Archive");
await contains(`.modal-footer .btn-primary`).click();
await toggleActionMenu();
expect(`.o-dropdown--menu span:contains(Unarchive)`).toHaveCount(1, { message: "Unarchive action should be visible" });
await toggleMenuItem("UnArchive");
expect.verifySteps(["check_features_enabled"]);
});
test("reload the page when allow_milestones is enabled on at least one project", async () => {
// No project has allow_milestones enabled
await mountView(formViewParams);
await click("div[name='allow_milestones'] input");
await clickSave();
expect.verifySteps([
"check_features_enabled",
"web_save",
"check_features_enabled",
"reload_context",
]);
});
test("do not reload the page when allow_milestones is enabled and there already exists one project with the feature enabled", async () => {
// Set a project with allow_milestones enabled
ProjectProject._records[1].allow_milestones = true;
await mountView(formViewParams);
await click("div[name='allow_milestones'] input");
await clickSave();
// No reload should be triggered
expect.verifySteps(["check_features_enabled", "web_save", "check_features_enabled"]);
});
test("reload the page when allow_milestones is disabled on all projects", async () => {
// Set a project with allow_milestones enabled
ProjectProject._records[0].allow_milestones = true;
await mountView(formViewParams);
await click("div[name='allow_milestones'] input");
await clickSave();
expect.verifySteps([
"check_features_enabled",
"web_save",
"check_features_enabled",
"reload_context",
]);
});
test("reload the page when allow_task_dependencies is enabled on at least one project", async () => {
// No project has allow_task_dependencies enabled
await mountView(formViewParams);
await click("div[name='allow_task_dependencies'] input");
await clickSave();
expect.verifySteps([
"check_features_enabled",
"web_save",
"check_features_enabled",
"reload_context",
]);
});
test("do not reload the page when allow_task_dependencies is enabled and there already exists one project with the feature enabled", async () => {
// Set a project with allow_task_dependencies enabled
ProjectProject._records[1].allow_task_dependencies = true;
await mountView(formViewParams);
await click("div[name='allow_task_dependencies'] input");
await clickSave();
// No reload should be triggered
expect.verifySteps(["check_features_enabled", "web_save", "check_features_enabled"]);
});
test("reload the page when allow_task_dependencies is disabled on all projects", async () => {
// Set a project with allow_task_dependencies enabled
ProjectProject._records[0].allow_task_dependencies = true;
await mountView(formViewParams);
await click("div[name='allow_task_dependencies'] input");
await clickSave();
expect.verifySteps([
"check_features_enabled",
"web_save",
"check_features_enabled",
"reload_context",
]);
});
test("reload the page when allow_recurring_tasks is enabled on at least one project", async () => {
// No project has allow_recurring_tasks enabled
await mountView(formViewParams);
await click("div[name='allow_recurring_tasks'] input");
await clickSave();
expect.verifySteps([
"check_features_enabled",
"web_save",
"check_features_enabled",
"reload_context",
]);
});
test("do not reload the page when allow_recurring_tasks is enabled and there already exists one project with the feature enabled", async () => {
// Set a project with allow_recurring_tasks enabled
ProjectProject._records[1].allow_recurring_tasks = true;
await mountView(formViewParams);
await click("div[name='allow_recurring_tasks'] input");
await clickSave();
// No reload should be triggered
expect.verifySteps(["check_features_enabled", "web_save", "check_features_enabled"]);
});
test("reload the page when allow_recurring_tasks is disabled on all projects", async () => {
// Set a project with allow_recurring_tasks enabled
ProjectProject._records[0].allow_recurring_tasks = true;
await mountView(formViewParams);
await click("div[name='allow_recurring_tasks'] input");
await clickSave();
expect.verifySteps([
"check_features_enabled",
"web_save",
"check_features_enabled",
"reload_context",
]);
});

View file

@ -0,0 +1,45 @@
import { expect, test } from "@odoo/hoot";
import { click } from "@odoo/hoot-dom";
import { fields, mountView } from "@web/../tests/web_test_helpers";
import { defineProjectModels, ProjectProject } from "./project_models";
defineProjectModels();
test("project.project (kanban): check that ProjectStateSelectionField does not propose `Set Status`", async () => {
Object.assign(ProjectProject._fields, {
last_update_status: fields.Selection({
string: "Status",
selection: [
["on_track", "On Track"],
["at_risk", "At Risk"],
["off_track", "Off Track"],
["on_hold", "On Hold"],
],
}),
last_update_color: fields.Integer({ string: "Update State Color" }),
});
ProjectProject._records = [
{
id: 1,
last_update_status: "on_track",
last_update_color: 20,
},
];
await mountView({
resModel: "project.project",
type: "kanban",
arch: `
<kanban class="o_kanban_test">
<template>
<t t-name="card">
<field name="last_update_status" widget="project_state_selection"/>
</t>
</template>
</kanban>
`,
});
await click("div[name='last_update_status'] button.dropdown-toggle");
expect(".dropdown-menu .dropdown-item:contains('Set Status')").toHaveCount(0);
});

View file

@ -0,0 +1,124 @@
import { expect, test, describe } from "@odoo/hoot";
import { queryAll } from "@odoo/hoot-dom";
import { mountWithCleanup, onRpc } from "@web/../tests/web_test_helpers";
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { ProjectRightSidePanel } from "@project/components/project_right_side_panel/project_right_side_panel";
defineMailModels();
describe.current.tags("desktop");
const FAKE_DATA = {
user: {
is_project_user: true,
},
buttons: [
{
icon: "check",
text: "Tasks",
number: "0 / 0",
action_type: "object",
action: "action_view_tasks",
show: true,
sequence: 1,
},
],
show_project_profitability_helper: false,
show_milestones: true,
milestones: {
data: [
{
id: 1,
name: "Milestone Zero",
},
],
},
profitability_items: {
costs: {
data: [],
},
revenues: {
data: [],
},
},
};
test("Right side panel will not be rendered without data and settings set false", async () => {
onRpc(() => {
const deepCopy = JSON.parse(JSON.stringify(FAKE_DATA));
deepCopy.buttons.pop();
deepCopy.milestones.data.pop();
deepCopy.show_milestones = false;
return { ...deepCopy };
});
await mountWithCleanup(ProjectRightSidePanel, {
props: {
context: { active_id: 1 },
domain: new Array(),
},
});
expect(queryAll("div.o_rightpanel").length).toBe(0, {
message: "Right panel should not be rendered",
});
});
test("Right side panel will be rendered if settings are turned on but doesnt have any data", async () => {
onRpc(() => {
const deepCopy = JSON.parse(JSON.stringify(FAKE_DATA));
deepCopy.buttons.pop();
deepCopy.milestones.data.pop();
deepCopy.show_milestones = true;
return { ...deepCopy };
});
await mountWithCleanup(ProjectRightSidePanel, {
props: {
context: { active_id: 1 },
domain: new Array(),
},
});
expect(queryAll("div.o_rightpanel").length).toBe(1, {
message: "Right panel should be rendered",
});
});
test("Right side panel will be not rendered if settings are turned off but does have data", async () => {
onRpc(() => {
const deepCopy = JSON.parse(JSON.stringify(FAKE_DATA));
deepCopy.show_milestones = false;
return { ...deepCopy };
});
await mountWithCleanup(ProjectRightSidePanel, {
props: {
context: { active_id: 1 },
domain: new Array(),
},
});
expect(queryAll("div.o_rightpanel").length).toBe(0, {
message: "Right panel should not be rendered",
});
});
test("Right side panel will be rendered if both setting is turned on and does have data", async () => {
onRpc(() => {
return { ...FAKE_DATA };
});
await mountWithCleanup(ProjectRightSidePanel, {
props: {
context: { active_id: 1 },
domain: new Array(),
},
});
expect(queryAll("div.o_rightpanel").length).toBe(1, {
message: "Right panel should be rendered",
});
});

View file

@ -1,64 +0,0 @@
/** @odoo-module */
import { getFixture, click } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let makeViewParams, target;
QUnit.module("Project", (hooks) => {
hooks.beforeEach(() => {
makeViewParams = {
type: "kanban",
resModel: "project.project",
serverData: {
models: {
"project.project": {
fields: {
id: {string: "Id", type: "integer"},
last_update_status: {
string: "Status",
type: "selection",
selection: [
["on_track", "On Track"],
["at_risk", "At Risk"],
["off_track", "Off Track"],
["on_hold", "On Hold"],
["to_define", "Set Status"],
],
},
last_update_color: {
string: "Update State Color",
type: "integer",
},
},
records: [
{id: 1, last_update_status: "on_track", last_update_color: 20},
],
},
},
},
arch: `
<kanban class="o_kanban_test">
<field name="last_update_status"/>
<field name="last_update_color"/>
<template>
<t t-name="kanban-box">
<div>
<field name="last_update_status" widget="project_state_selection" options="{'color_field': 'last_update_color'}"/>
</div>
</t>
</template>
</kanban>`,
};
target = getFixture();
setupViewRegistries();
});
QUnit.module("Components", (hooks) => {
QUnit.module("ProjectStateSelectionField");
QUnit.test("Check that ProjectStateSelectionField does not propose `Set Status`", async function (assert) {
await makeView(makeViewParams);
await click(target, 'div[name="last_update_status"] button.dropdown-toggle');
assert.containsNone(target, 'div[name="last_update_status"] .dropdown-menu .dropdown-item:contains("Set Status")');
});
});
});

View file

@ -0,0 +1,134 @@
import { describe, expect, test } from "@odoo/hoot";
import { animationFrame } from "@odoo/hoot-mock";
import { WebClient } from "@web/webclient/webclient";
import { clickOnDataset, setupChartJsForTests } from "@web/../tests/views/graph/graph_test_helpers";
import {
contains,
fields,
getService,
mockService,
models,
mountWithCleanup,
} from "@web/../tests/web_test_helpers";
import { defineProjectModels, projectModels } from "./project_models";
describe.current.tags("desktop");
class ReportProjectTaskUser extends models.Model {
_name = "report.project.task.user";
project_id = fields.Many2one({ relation: "project.project" });
display_in_project = fields.Boolean();
task_id = fields.Many2one({ relation: "project.task" });
nbr = fields.Integer({ string: "# of Tasks" });
_records = [
{ id: 4, project_id: 1, display_in_project: true },
{ id: 6, project_id: 1, display_in_project: true },
{ id: 9, project_id: 2, display_in_project: true },
];
_views = {
graph: /* xml */ `
<graph string="Tasks Analysis" sample="1" js_class="project_task_analysis_graph">
<field name="project_id"/>
</graph>
`,
pivot: /* xml */ `
<pivot string="Tasks Analysis" display_quantity="1" sample="1" js_class="project_task_analysis_pivot">
<field name="project_id"/>
</pivot>
`,
};
}
projectModels.ReportProjectTaskUser = ReportProjectTaskUser;
projectModels.ProjectTask._views = {
form: /* xml */ `<form><field name="name"/></form>`,
list: /* xml */ `<list><field name="name"/></list>`,
search: /* xml */ `<search><field name="name"/></search>`,
};
defineProjectModels();
setupChartJsForTests();
async function mountView(viewName, ctx = {}) {
const view = await mountWithCleanup(WebClient);
await getService("action").doAction({
id: 1,
name: "tasks analysis",
res_model: "report.project.task.user",
type: "ir.actions.act_window",
views: [[false, viewName]],
context: ctx,
});
return view;
}
test("report.project.task.user (graph): clicking on a bar leads to project.task list", async () => {
mockService("action", {
doAction({ res_model }) {
expect.step(res_model);
return super.doAction(...arguments);
},
});
const view = await mountView("graph");
await animationFrame();
await clickOnDataset(view);
await animationFrame();
expect(".o_list_renderer").toBeDisplayed({
message: "Clicking on a bar should open a list view",
});
// The model of the list view that is opened consequently should be "project.task"
expect.verifySteps(["report.project.task.user", "project.task"]);
});
test("report.project.task.user (pivot): clicking on a cell leads to project.task list", async () => {
mockService("action", {
doAction({ res_model }) {
expect.step(res_model);
return super.doAction(...arguments);
},
});
await mountView("pivot");
await animationFrame();
await contains(".o_pivot_cell_value").click();
await animationFrame();
expect(".o_list_renderer").toBeDisplayed({
message: "Clicking on a cell should open a list view",
});
// The model of the list view that is opened consequently should be "project.task"
expect.verifySteps(["report.project.task.user", "project.task"]);
});
test("report.project.task.user: fix the domain, in case field is not present in main model", async () => {
mockService("action", {
doAction({ domain, res_model }) {
if (res_model === "project.task") {
expect(domain).toEqual(["&", ["display_in_project", "=", true], "&", [1, "=", 1], ["id", "=", 1]]);
}
return super.doAction(...arguments);
},
});
ReportProjectTaskUser._records = [
{ id: 1, nbr: 1, task_id: 1, display_in_project: true },
{ id: 2, nbr: 1, task_id: 2, display_in_project: true },
];
ReportProjectTaskUser._views = {
graph: /* xml */ `
<graph string="Tasks Analysis" sample="1" js_class="project_task_analysis_graph">
<field name="task_id"/>
<field name="nbr"/>
</graph>
`
};
const view = await mountView("graph", { group_by: ["task_id", "nbr"] });
await animationFrame();
await clickOnDataset(view);
await animationFrame();
expect(`.o_list_renderer .o_data_row`).toHaveCount(1);
});

View file

@ -0,0 +1,182 @@
import { describe, expect, test } from "@odoo/hoot";
import { click, queryAll } from "@odoo/hoot-dom";
import {
defineModels,
fields,
mockService,
models,
mountView,
toggleMenuItem,
toggleMenuItemOption,
toggleSearchBarMenu,
} from "@web/../tests/web_test_helpers";
import { defineProjectModels } from "./project_models";
class ProjectTaskBurndownChartReport extends models.Model {
_name = "project.task.burndown.chart.report";
date = fields.Date();
project_id = fields.Many2one({ relation: "project.project" });
stage_id = fields.Many2one({ relation: "project.task.type" });
is_closed = fields.Selection({
string: "Burnup chart",
selection: [
["closed", "Closed tasks"],
["open", "Open tasks"],
],
});
nb_tasks = fields.Integer({
string: "Number of Tasks",
type: "integer",
aggregator: "sum",
});
_records = [
{
id: 1,
project_id: 1,
stage_id: 1,
is_closed: "open",
date: "2020-01-01",
nb_tasks: 10,
},
{
id: 2,
project_id: 1,
stage_id: 2,
is_closed: "open",
date: "2020-02-01",
nb_tasks: 5,
},
{
id: 3,
project_id: 1,
stage_id: 3,
is_closed: "closed",
date: "2020-03-01",
nb_tasks: 2,
},
];
}
defineProjectModels();
defineModels([ProjectTaskBurndownChartReport]);
describe.current.tags("desktop");
mockService("notification", () => ({
add() {
expect.step("notification");
},
}));
const mountViewParams = {
resModel: "project.task.burndown.chart.report",
type: "graph",
arch: `
<graph type="line" js_class="burndown_chart">
<field name="date" string="Date" interval="month"/>
<field name="stage_id"/>
<field name="is_closed"/>
<field name="nb_tasks" type="measure"/>
</graph>
`,
};
async function mountViewWithSearch(mountViewContext = null) {
await mountView({
...mountViewParams,
searchViewId: false,
searchViewArch: `
<search string="Burndown Chart">
<filter name="date" context="{'group_by': 'date'}"/>
<filter name="stage" context="{'group_by': 'stage_id'}"/>
<filter name="is_closed" context="{'group_by': 'is_closed'}"/>
</search>
`,
context: mountViewContext || {
search_default_date: 1,
search_default_stage: 1,
},
});
}
async function toggleGroupBy(fieldLabel) {
await toggleSearchBarMenu();
await toggleMenuItem(fieldLabel);
}
function checkGroupByOrder() {
const searchFacets = queryAll(".o_facet_value");
expect(searchFacets).toHaveCount(2);
const [dateSearchFacet, stageSearchFacet] = searchFacets;
expect(dateSearchFacet).toHaveText("Date: Month");
expect(stageSearchFacet).toHaveText("Stage");
}
test("burndown.chart: check that the sort buttons are invisible", async () => {
await mountView(mountViewParams);
expect(".o_cp_bottom_left:has(.btn-group[role=toolbar][aria-label='Sort graph'])").toHaveCount(
0,
{
message: "The sort buttons shouldn't be rendered",
}
);
});
test("burndown.chart: check that removing the group by 'Date: Month > Stage' in the search bar triggers a notification", async () => {
await mountViewWithSearch();
await click(".o_facet_remove");
// Only the notification will be triggered and the file won't be uploaded.
expect.verifySteps(["notification"]);
});
test("burndown.chart: check that removing the group by 'Date' triggers a notification", async () => {
await mountViewWithSearch();
await toggleGroupBy("Date");
await toggleMenuItemOption("Date", "Month");
// Only the notification will be triggered and the file won't be uploaded.
expect.verifySteps(["notification"]);
});
test("burndown.chart: check that adding a group by 'Date' actually toggles it", async () => {
await mountViewWithSearch();
await toggleGroupBy("Date");
await toggleMenuItemOption("Date", "Year");
expect(".o_accordion_values .selected").toHaveCount(1, {
message: "There should be only one selected item",
});
expect(".o_accordion_values .selected").toHaveText("Year", {
message: "The selected item should be the one we clicked on",
});
});
test("burndown.chart: check that groupby 'Date > Stage' results in 'Date > Stage'", async () => {
await mountViewWithSearch({
search_default_date: 1,
search_default_stage: 1,
});
checkGroupByOrder();
});
test("burndown.chart: check that groupby 'Stage > Date' results in 'Date > Stage'", async () => {
await mountViewWithSearch({
search_default_stage: 1,
search_default_date: 1,
});
checkGroupByOrder();
});
test("burndown.chart: check the toggle between 'Stage' and 'Burnup chart'", async () => {
await mountViewWithSearch();
await toggleGroupBy("Stage");
const searchFacets = queryAll(".o_facet_value");
expect(searchFacets).toHaveCount(2);
const [dateSearchFacet, stageSearchFacet] = searchFacets;
expect(dateSearchFacet).toHaveText("Date: Month");
expect(stageSearchFacet).toHaveText("Burnup chart");
await toggleMenuItem("Burnup chart");
checkGroupByOrder();
});

View file

@ -0,0 +1,313 @@
import { expect, test, beforeEach, describe } from "@odoo/hoot";
import { mockDate, animationFrame, runAllTimers } from "@odoo/hoot-mock";
import { click, queryAllTexts, queryFirst, queryOne, waitFor } from "@odoo/hoot-dom";
import { contains, mountView, onRpc } from "@web/../tests/web_test_helpers";
import { defineProjectModels, ProjectTask } from "./project_models";
import { serializeDateTime } from "@web/core/l10n/dates";
describe.current.tags("desktop");
defineProjectModels();
beforeEach(() => {
mockDate("2024-01-03 12:00:00", +0);
ProjectTask._views["form"] = `
<form>
<field name="id"/>
<field name="name"/>
<field name="date_deadline"/>
<field name="planned_date_begin"/>
</form>
`;
ProjectTask._records = [
{
id: 1,
name: "Task-1",
date_deadline: "2024-01-09 07:00:00",
create_date: "2024-01-03 12:00:00",
project_id: 1,
stage_id: 1,
state: "01_in_progress",
user_ids: [],
display_name: "Task-1",
},
{
id: 10,
name: "Task-10",
project_id: 1,
stage_id: 1,
state: "01_in_progress",
user_ids: [],
display_name: "Task-10",
},
{
id: 11,
name: "Task-11",
project_id: 1,
stage_id: 1,
state: "1_done",
user_ids: [],
display_name: "Task-11",
is_closed: true,
},
];
onRpc("has_access", () => true);
});
const calendarMountParams = {
resModel: "project.task",
type: "calendar",
arch: `
<calendar date_start="date_deadline" mode="month"
js_class="project_task_calendar">
<field name="project_id" widget="project" invisible="context.get('default_project_id', False)"/>
<field name="stage_id" invisible="not project_id or not stage_id" widget="task_stage_with_state_selection"/>
</calendar>
`,
};
test("test Project Task Calendar Popover with task_stage_with_state_selection widget", async () => {
await mountView(calendarMountParams);
await click("a.fc-daygrid-event");
// Skipping setTimeout while clicking event in calendar for calendar popover to appear.
// There is a timeout set in the useCalendarPopover.
await runAllTimers();
expect(queryOne(".o_field_task_stage_with_state_selection > div").childElementCount).toBe(2);
});
test("test task_stage_with_state_selection widget with non-editable state", async () => {
await mountView({
...calendarMountParams,
arch: `
<calendar date_start="date_deadline" mode="month"
js_class="project_task_calendar">
<field name="project_id" widget="project" invisible="context.get('default_project_id', False)"/>
<field name="stage_id" invisible="not project_id or not stage_id" widget="task_stage_with_state_selection" options="{'state_readonly': True}"/>
</calendar>
`,
});
await click("a.fc-daygrid-event");
// Skipping setTimeout while clicking event in calendar for calendar popover to appear.
// There is a timeout set in the useCalendarPopover.
await runAllTimers();
await click("button[title='In Progress']");
expect(".project_task_state_selection_menu").toHaveCount(0);
});
test("test task_stage_with_state_selection widget with editable state", async () => {
await mountView({
...calendarMountParams,
arch: `
<calendar date_start="date_deadline" mode="month"
js_class="project_task_calendar">
<field name="project_id" widget="project" invisible="context.get('default_project_id', False)"/>
<field name="stage_id" invisible="not project_id or not stage_id" widget="task_stage_with_state_selection" options="{'state_readonly': False}"/>
</calendar>
`,
});
await click("a.fc-daygrid-event");
// Skipping setTimeout while clicking event in calendar for calendar popover to appear.
// There is a timeout set in the useCalendarPopover.
await runAllTimers();
await click(".o-dropdown div[title='In Progress']");
await animationFrame();
expect(".project_task_state_selection_menu").toHaveCount(1);
await click(".o_status_green"); // Checking if click on the state in selection menu works(changes in record)
await animationFrame();
expect(".o-dropdown .o_status").toHaveStyle({ color: "rgb(0, 136, 24)" });
});
test("Display closed tasks as past event", async () => {
ProjectTask._records.push({
id: 2,
name: "Task-2",
date_deadline: "2024-01-09 07:00:00",
create_date: "2024-01-03 12:00:00",
project_id: 1,
stage_id: 1,
state: "1_done",
user_ids: [],
display_name: "Task-2",
});
ProjectTask._records.push({
id: 3,
name: "Task-3",
date_deadline: "2024-01-09 07:00:00",
create_date: "2024-01-03 12:00:00",
project_id: 1,
stage_id: 1,
state: "1_canceled",
user_ids: [],
display_name: "Task-3",
});
ProjectTask._records.push({
id: 4,
name: "Task-4",
date_deadline: "2024-01-09 07:00:00",
create_date: "2024-01-03 12:00:00",
project_id: 1,
stage_id: 1,
state: "1_canceled",
user_ids: [],
display_name: "Task-4",
is_closed: true,
});
await mountView(calendarMountParams);
expect(".o_event").toHaveCount(4);
expect(".o_event.o_past_event").toHaveCount(3);
});
test("tasks to schedule should not be visible in the sidebar if no default project set in the context", async () => {
onRpc("project.task", "search_read", ({ method }) => {
expect.step(method);
});
onRpc("project.task", "web_search_read", () => {
expect.step("fetch tasks to schedule");
});
await mountView(calendarMountParams);
expect(".o_calendar_view").toHaveCount(1);
expect(".o_task_to_plan_draggable").toHaveCount(0);
expect.verifySteps(["search_read"]);
});
test("tasks to plan should be visible in the sidebar when `default_project_id` is set in the context", async () => {
onRpc("project.task", "search_read", ({ method }) => {
expect.step(method);
});
onRpc("project.task", "web_search_read", () => {
expect.step("fetch tasks to schedule");
});
await mountView({
...calendarMountParams,
context: { default_project_id: 1 },
});
expect(".o_calendar_view").toHaveCount(1);
expect(".o_task_to_plan_draggable").toHaveCount(2);
expect(queryAllTexts(".o_task_to_plan_draggable")).toEqual(['Task-10', 'Task-11']);
expect(".o_calendar_view .o_calendar_sidebar h5").toHaveText("Drag Tasks to Schedule");
expect.verifySteps(["search_read", "fetch tasks to schedule"]);
});
test("search domain should be taken into account in Tasks to Schedule", async () => {
onRpc("project.task", "search_read", ({ method }) => {
expect.step(method);
});
onRpc("project.task", "web_search_read", ({ method }) => {
expect.step("fetch tasks to schedule");
});
await mountView({
...calendarMountParams,
context: { default_project_id: 1 },
domain: [['is_closed', '=', false]],
});
expect(".o_calendar_view").toHaveCount(1);
expect(".o_task_to_plan_draggable").toHaveCount(1);
expect(".o_task_to_plan_draggable").toHaveText('Task-10');
expect(".o_calendar_view .o_calendar_sidebar h5").toHaveText("Drag Tasks to Schedule");
expect.verifySteps(["search_read", "fetch tasks to schedule"]);
});
test("planned dates used in search domain should not be taken into account in Tasks to Schedule", async () => {
onRpc("project.task", "search_read", ({ method }) => {
expect.step(method);
});
onRpc("project.task", "web_search_read", ({ method }) => {
expect.step("fetch tasks to schedule");
});
await mountView({
...calendarMountParams,
context: { default_project_id: 1 },
domain: [['is_closed', '=', false], ['date_deadline', '!=', false], ['planned_date_begin', '!=', false]],
});
expect(".o_calendar_view").toHaveCount(1);
expect(".o_task_to_plan_draggable").toHaveCount(1);
expect(".o_task_to_plan_draggable").toHaveText('Task-10');
expect(".o_calendar_view .o_calendar_sidebar h5").toHaveText("Drag Tasks to Schedule");
expect.verifySteps(["search_read", "fetch tasks to schedule"]);
});
test("test drag and drop a task to schedule in calendar view in month scale", async () => {
let expectedDate = null;
onRpc("project.task", "search_read", ({ method }) => {
expect.step(method);
});
onRpc("project.task", "web_search_read", ({ method }) => {
expect.step("fetch tasks to schedule");
});
onRpc("project.task", "plan_task_in_calendar", ({ args }) => {
const [taskIds, vals] = args;
expect(taskIds).toEqual([10]);
const expectedDateDeadline = serializeDateTime(expectedDate.set({ hours: 19 }));
expect(vals).toEqual({
date_deadline: expectedDateDeadline,
});
expect.step("plan task");
});
await mountView({
...calendarMountParams,
context: { default_project_id: 1 },
});
expect(".o_task_to_plan_draggable").toHaveCount(2);
const { drop, moveTo } = await contains(".o_task_to_plan_draggable:first").drag();
const dateCell = queryFirst(".fc-day.fc-day-today.fc-daygrid-day");
expectedDate = luxon.DateTime.fromISO(dateCell.dataset.date);
await moveTo(dateCell);
expect(dateCell).toHaveClass("o-highlight");
await drop();
expect.verifySteps(["search_read", "fetch tasks to schedule", "plan task", "search_read"]);
expect(".o_task_to_plan_draggable").toHaveCount(1);
expect(".o_task_to_plan_draggable").toHaveText("Task-11");
});
test("project.task (calendar): toggle sub-tasks", async () => {
ProjectTask._records = [
{
id: 1,
project_id: 1,
name: "Task 1",
stage_id: 1,
display_in_project: true,
date_deadline: "2024-01-09 07:00:00",
create_date: "2024-01-03 12:00:00",
},
{
id: 2,
project_id: 1,
name: "Task 2",
stage_id: 1,
display_in_project: false,
date_deadline: "2024-01-09 07:00:00",
create_date: "2024-01-03 12:00:00",
}
];
await mountView(calendarMountParams);
expect(".o_event").toHaveCount(1);
expect(".o_control_panel_navigation button i.fa-sliders").toHaveCount(1);
await click(".o_control_panel_navigation button i.fa-sliders");
await waitFor("span.o-dropdown-item");
expect("span.o-dropdown-item").toHaveText("Show Sub-Tasks");
await click("span.o-dropdown-item");
await animationFrame();
expect(".o_event").toHaveCount(2);
});

View file

@ -0,0 +1,59 @@
import { beforeEach, expect, test } from "@odoo/hoot";
import { mountView, quickCreateKanbanColumn } from "@web/../tests/web_test_helpers";
import { defineProjectModels, ProjectTask } from "./project_models";
defineProjectModels();
const kanbanViewParams = {
resModel: "project.task",
type: "kanban",
arch: `<kanban default_group_by="stage_id" js_class="project_task_kanban">
<templates>
<t t-name="card"/>
</templates>
</kanban>`,
};
beforeEach(() => {
ProjectTask._records = [
{
id: 1,
name: "My task",
project_id: false,
user_ids: [],
date_deadline: false,
},
];
});
test("project.task (kanban): Can create stage if we are in tasks of specific project", async () => {
await mountView({
...kanbanViewParams,
context: {
default_project_id: 1,
},
});
expect(".o_column_quick_create").toHaveCount(1, {
message: "should have a quick create column",
});
expect(".o_column_quick_create.o_quick_create_folded").toHaveCount(1, {
message: "Add column button should be visible",
});
await quickCreateKanbanColumn();
expect(".o_column_quick_create input").toHaveCount(1, {
message: "the input should be visible",
});
});
test("project.task (kanban): Cannot create stage if we are not in tasks of specific project", async () => {
await mountView({
...kanbanViewParams,
});
expect(".o_column_quick_create").toHaveCount(0, {
message: "quick create column should not be visible",
});
expect(".o_column_quick_create.o_quick_create_folded").toHaveCount(0, {
message: "Add column button should not be visible",
});
});

View file

@ -0,0 +1,93 @@
import { describe, expect, test } from "@odoo/hoot";
import { animationFrame, click, waitFor } from "@odoo/hoot-dom";
import { mountView, onRpc } from "@web/../tests/web_test_helpers";
import { defineProjectModels, ProjectTask } from "./project_models";
defineProjectModels();
describe.current.tags("desktop");
const viewParams = {
resModel: "project.task",
type: "kanban",
arch: `
<kanban default_group_by="stage_id" js_class="project_task_kanban">
<templates>
<t t-name="card">
<field name="name"/>
</t>
</templates>
</kanban>`,
context: {
active_model: "project.project",
default_project_id: 1,
},
};
test("stages nocontent helper should be displayed in the project Kanban", async () => {
ProjectTask._records = [];
await mountView({
resModel: "project.task",
type: "kanban",
arch: `
<kanban default_group_by="stage_id" js_class="project_task_kanban">
<templates>
<t t-name="card">
<field name="name"/>
</t>
</templates>
</kanban>
`,
context: {
active_model: "project.task.type.delete.wizard",
default_project_id: 1,
},
});
expect(".o_kanban_header").toHaveCount(1);
expect(".o_kanban_stages_nocontent").toHaveCount(1);
});
test("quick create button is visible when the user has access rights.", async () => {
onRpc("has_group", () => true);
await mountView(viewParams);
await animationFrame();
expect(".o_column_quick_create").toHaveCount(1);
});
test("quick create button is not visible when the user not have access rights", async () => {
onRpc("has_group", () => false);
await mountView(viewParams);
await animationFrame();
expect(".o_column_quick_create").toHaveCount(0);
});
test("project.task (kanban): toggle sub-tasks", async () => {
ProjectTask._records = [
{
id: 1,
project_id: 1,
name: "Task 1",
stage_id: 1,
display_in_project: true,
},
{
id: 2,
project_id: 1,
name: "Task 2",
stage_id: 1,
display_in_project: false,
}
];
await mountView(viewParams);
expect(".o_kanban_record").toHaveCount(1);
expect(".o_control_panel_navigation button i.fa-sliders").toHaveCount(1);
await click(".o_control_panel_navigation button i.fa-sliders");
await waitFor("span.o-dropdown-item");
expect("span.o-dropdown-item").toHaveText("Show Sub-Tasks");
await click("span.o-dropdown-item");
await animationFrame();
expect(".o_kanban_record").toHaveCount(2);
});

View file

@ -0,0 +1,83 @@
import { describe, expect, test } from "@odoo/hoot";
import { check, click, queryAll, queryOne, waitFor } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { mountView } from "@web/../tests/web_test_helpers";
import { defineProjectModels, ProjectTask } from "./project_models";
defineProjectModels();
describe.current.tags("desktop");
test("project.task (list): cannot edit stage_id with different projects", async () => {
ProjectTask._records = [
{
id: 1,
project_id: 1,
stage_id: 1,
},
{
id: 2,
project_id: 2,
stage_id: 1,
},
];
await mountView({
resModel: "project.task",
type: "list",
arch: `
<list multi_edit="1" js_class="project_task_list">
<field name="project_id"/>
<field name="stage_id"/>
</list>
`,
});
const [firstRow, secondRow] = queryAll(".o_data_row");
await check(".o_list_record_selector input", { root: firstRow });
await animationFrame();
expect(queryAll("[name=stage_id]")).not.toHaveClass("o_readonly_modifier");
await check(".o_list_record_selector input", { root: secondRow });
await animationFrame();
expect(queryOne("[name=stage_id]", { root: firstRow })).toHaveClass("o_readonly_modifier");
expect(queryOne("[name=stage_id]", { root: secondRow })).toHaveClass("o_readonly_modifier");
});
test("project.task (list): toggle sub-tasks", async () => {
ProjectTask._records = [
{
id: 1,
project_id: 1,
name: "Task 1",
stage_id: 1,
display_in_project: true,
},
{
id: 2,
project_id: 1,
name: "Task 2",
stage_id: 1,
display_in_project: false,
}
];
await mountView({
resModel: "project.task",
type: "list",
arch: `
<list multi_edit="1" js_class="project_task_list">
<field name="project_id"/>
<field name="stage_id"/>
</list>
`,
});
expect(".o_data_row").toHaveCount(1);
expect(".o_control_panel_navigation button i.fa-sliders").toHaveCount(1);
await click(".o_control_panel_navigation button i.fa-sliders");
await waitFor("span.o-dropdown-item");
expect("span.o-dropdown-item").toHaveText("Show Sub-Tasks");
await click("span.o-dropdown-item");
await animationFrame();
expect(".o_data_row").toHaveCount(2);
});

View file

@ -0,0 +1,32 @@
import { expect, test } from "@odoo/hoot";
import { press } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { mountView } from "@web/../tests/web_test_helpers";
import { ProjectTask, defineProjectModels } from "./project_models";
defineProjectModels();
test("project.task (form): check ProjectTaskPrioritySwitch", async () => {
ProjectTask._records = [{ id: 1, priority: "0" }];
await mountView({
resModel: "project.task",
type: "form",
arch: `
<form class="o_kanban_test">
<field name="priority" widget="priority_switch"/>
</form>
`,
});
expect("div[name='priority'] .fa-star-o").toHaveCount(1, {
message: "The low priority should display the fa-star-o (empty) icon",
});
await press("alt+r");
await animationFrame();
expect("div[name='priority'] .fa-star").toHaveCount(1, {
message:
"After using the alt+r hotkey the priority should be set to high and the widget should display the fa-star (filled) icon",
});
});

View file

@ -0,0 +1,40 @@
import { expect, test } from "@odoo/hoot";
import { mountView } from "@web/../tests/web_test_helpers";
import { defineProjectModels } from "./project_models";
defineProjectModels();
test("ProjectMany2one: project.task form view with private task", async () => {
await mountView({
resModel: "project.task",
resId: 3,
type: "form",
arch: `
<form>
<field name="name"/>
<field name="project_id" widget="project"/>
</form>
`,
});
expect("div[name='project_id'] .o_many2one").toHaveClass("o_many2one private_placeholder w-100");
expect("div[name='project_id'] .o_many2one input").toHaveAttribute("placeholder", "Private");
});
test("ProjectMany2one: project.task list view", async () => {
await mountView({
resModel: "project.task",
type: "list",
arch: `
<list>
<field name="name"/>
<field name="project_id" widget="project"/>
</list>
`,
});
expect("div[name='project_id']").toHaveCount(3);
expect("div[name='project_id'] .o_many2one").toHaveCount(2);
expect("div[name='project_id'] span.text-danger.fst-italic.text-muted").toHaveCount(1);
expect("div[name='project_id'] span.text-danger.fst-italic.text-muted").toHaveText("🔒 Private");
});

View file

@ -0,0 +1,85 @@
import { expect, test, describe } from "@odoo/hoot";
import { click, queryAllTexts } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { mountView } from "@web/../tests/web_test_helpers";
import { defineProjectModels, ProjectTask } from "./project_models";
describe.current.tags("desktop");
defineProjectModels();
test("project.task (kanban): check task state widget", async () => {
await mountView({
resModel: "project.task",
type: "kanban",
arch: `
<kanban js_class="project_task_kanban">
<templates>
<t t-name="card">
<field name="state" widget="project_task_state_selection" class="project_task_state_test"/>
</t>
</templates>
</kanban>
`,
});
expect(".o-dropdown--menu").toHaveCount(0, {
message: "If the state button has not been pressed yet, no dropdown should be displayed",
});
await click("div[name='state']:first-child button.dropdown-toggle");
await animationFrame();
expect(".o-dropdown--menu").toHaveCount(1, {
message: "Once the button has been pressed the dropdown should appear",
});
await click(".o-dropdown--menu span.text-danger");
await animationFrame();
expect("div[name='state']:first-child button.dropdown-toggle i.fa-times-circle").toBeVisible({
message:
"If the canceled state as been selected, the fa-times-circle icon should be displayed",
});
await click("div[name='state'] i.fa-hourglass-o");
await animationFrame();
expect(".o-dropdown--menu").toHaveCount(0, {
message: "When trying to click on the waiting icon, no dropdown menu should display",
});
});
test("project.task (form): check task state widget", async () => {
ProjectTask._views = {
form: `<form js_class="project_task_form">
<field name="project_id"/>
<field name="name"/>
<field name="state" widget="project_task_state_selection" nolabel="1"/>
</form>`,
};
await mountView({
resModel: "project.task",
resId: 1,
type: "form",
});
await click("button.o_state_button");
await animationFrame();
expect(queryAllTexts(".state_selection_field_menu > .dropdown-item")).toEqual([
"In Progress",
"Changes Requested",
"Approved",
"Cancelled",
"Done",
]);
await click("button.o_state_button");
await mountView({
resModel: "project.task",
resId: 3,
type: "form",
});
await click("button.o_state_button:contains('Waiting')");
await animationFrame();
expect(queryAllTexts(".state_selection_field_menu > .dropdown-item")).toEqual([
"Cancelled",
"Done",
]);
});

View file

@ -0,0 +1,345 @@
import { beforeEach, describe, destroy, expect, test } from "@odoo/hoot";
import { animationFrame } from "@odoo/hoot-mock";
import { click, edit, queryOne } from "@odoo/hoot-dom";
import { Command, mountView, MockServer, mockService, onRpc } from "@web/../tests/web_test_helpers";
import { defineProjectModels, ProjectTask } from "./project_models";
defineProjectModels();
describe.current.tags("desktop");
beforeEach(() => {
ProjectTask._records = [
{
id: 1,
name: "Task 1 (Project 1)",
project_id: 1,
child_ids: [2, 3, 4, 7],
closed_subtask_count: 1,
subtask_count: 4,
user_ids: [7],
state: "01_in_progress",
},
{
id: 2,
name: "Task 2 (Project 1)",
project_id: 1,
parent_id: 1,
child_ids: [],
closed_subtask_count: 0,
subtask_count: 0,
state: "03_approved",
},
{
id: 3,
name: "Task 3 (Project 1)",
project_id: 1,
parent_id: 1,
closed_subtask_count: 0,
subtask_count: 0,
child_ids: [],
state: "02_changes_requested",
},
{
id: 4,
name: "Task 4 (Project 1)",
project_id: 1,
parent_id: 1,
closed_subtask_count: 0,
subtask_count: 0,
child_ids: [],
state: "1_done",
},
{
id: 5,
name: "Task 5 (Private)",
closed_subtask_count: 0,
subtask_count: 1,
child_ids: [6],
state: "03_approved",
},
{
id: 6,
name: "Task 6 (Private)",
parent_id: 5,
closed_subtask_count: 0,
subtask_count: 0,
child_ids: [],
state: "1_canceled",
},
{
id: 7,
name: "Task 7 (Project 1)",
project_id: 1,
parent_id: 1,
closed_subtask_count: 0,
subtask_count: 0,
child_ids: [],
state: "01_in_progress",
user_ids: [7],
},
{
id: 8,
name: "Task 1 (Project 2)",
project_id: 2,
child_ids: [],
},
];
ProjectTask._views = {
kanban: `
<kanban js_class="project_task_kanban">
<field name="subtask_count"/>
<field name="project_id"/>
<field name="closed_subtask_count"/>
<field name="child_ids"/>
<field name="sequence"/>
<field name="user_ids"/>
<field name="state"/>
<templates>
<t t-name="card">
<div>
<field name="display_name" widget="name_with_subtask_count"/>
<t t-if="record.project_id.raw_value and record.subtask_count.raw_value">
<widget name="subtask_counter"/>
</t>
<widget name="subtask_kanban_list"/>
</div>
</t>
</templates>
</kanban>
`,
form: `
<form>
<field name="parent_id"/>
<field name="child_ids" context="{'default_parent_id': id}" widget="subtasks_one2many">
<list editable="bottom" open_form_view="True">
<field name="project_id" widget="project"/>
<field name="name"/>
</list>
</field>
</form>
`,
};
});
test("project.task (kanban): check subtask list", async () => {
await mountView({
resModel: "project.task",
type: "kanban",
});
expect(".o_field_name_with_subtask_count:contains('(1/4 sub-tasks)')").toHaveCount(1, {
message:
"Task title should also display the number of (closed) sub-tasks linked to the task",
});
expect(".subtask_list_button").toHaveCount(1, {
message:
"Only kanban boxes of parent tasks having open subtasks should have the drawdown button, in this case this is 1",
});
expect(".subtask_list").toHaveCount(0, {
message: "If the drawdown button is not clicked, the subtasks list should be hidden",
});
await click(".subtask_list_button");
await animationFrame();
expect(".subtask_list").toHaveCount(1, {
message:
"Clicking on the button should make the subtask list render, in this case we are expectig 1 list",
});
expect(".subtask_list_row").toHaveCount(3, {
message: "The list rendered should show the open subtasks of the task, in this case 3",
});
expect(".subtask_state_widget_col").toHaveCount(3, {
message:
"Each of the list's rows should have 1 state widget, thus we are looking for 3 in total",
});
expect(".subtask_user_widget_col").toHaveCount(3, {
message:
"Each of the list's rows should have 1 user widgets, thus we are looking for 3 in total",
});
expect(".subtask_name_col").toHaveCount(3, {
message:
"Each of the list's rows should display the subtask's name, thus we are looking for 3 in total",
});
await click(".subtask_list_button");
await animationFrame();
expect(".subtask_list").toHaveCount(0, {
message:
"If the drawdown button is clicked again, the subtasks list should be hidden again",
});
});
test("project.task (kanban): check closed subtask count update", async () => {
let checkSteps = false;
onRpc(({ method, model }) => {
if (checkSteps) {
expect.step(`${model}/${method}`);
}
});
await mountView({
resModel: "project.task",
type: "kanban",
});
checkSteps = true;
expect(queryOne(".subtask_list_button").parentNode).toHaveText("1/4");
await click(".subtask_list_button");
await animationFrame();
const inProgressStatesSelector = `
.subtask_list
.o_field_widget.o_field_project_task_state_selection.subtask_state_widget_col
.o_status:not(.o_status_green,.o_status_bubble)
`;
expect(inProgressStatesSelector).toHaveCount(1, {
message: "The state of the subtask should be in progress",
});
await click(inProgressStatesSelector);
await animationFrame();
await click(".project_task_state_selection_menu .fa-check-circle");
await animationFrame();
expect(inProgressStatesSelector).toHaveCount(0, {
message: "The state of the subtask should no longer be in progress",
});
expect.verifySteps([
"project.task/web_read",
"project.task/onchange",
"project.task/web_save",
]);
});
test("project.task (kanban): check subtask creation", async () => {
let checkSteps = false;
onRpc(({ args, method, model }) => {
if (checkSteps) {
expect.step(`${model}/${method}`);
}
if (model === "project.task" && method === "create") {
const [{ display_name, parent_id, sequence }] = args[0];
expect(display_name).toBe("New Subtask");
expect(parent_id).toBe(1);
expect(sequence).toBe(11, { message: "Sequence should be 11" });
const newSubtaskId = MockServer.env["project.task"].create({
name: display_name,
parent_id,
state: "01_in_progress",
sequence: sequence,
});
MockServer.env["project.task"].write(parent_id, {
child_ids: [Command.link(newSubtaskId)],
});
return [newSubtaskId];
}
});
await mountView({
resModel: "project.task",
type: "kanban",
});
checkSteps = true;
expect(queryOne(".subtask_list_button").parentNode).toHaveText("1/4");
await click(".subtask_list_button");
await animationFrame();
await click(".subtask_create");
await animationFrame();
await click(".subtask_create_input input");
await edit("New Subtask", { confirm: "enter" });
await animationFrame();
expect(".subtask_list_row").toHaveCount(4, {
message:
"The subtasks list should now display the subtask created on the card, thus we are looking for 4 in total",
});
expect.verifySteps([
"project.task/web_read",
"project.task/create",
"project.task/web_read",
]);
});
test("project.task (form): check that the subtask of another project can be added", async () => {
await mountView({
resModel: "project.task",
resId: 7,
type: "form",
});
await click(".o_field_x2many_list_row_add a");
await animationFrame();
await click(".o_field_project input");
await animationFrame();
await click(".o_field_project li");
await animationFrame();
await click(".o_field_project input");
await edit("aaa");
await click(".o_form_button_save");
await animationFrame();
expect(".o_field_project").toHaveText("Project 1");
});
test("project.task (form): check focus on new subtask's name", async () => {
await mountView({
resModel: "project.task",
type: "form",
});
await click(".o_field_x2many_list_row_add a");
await animationFrame();
expect(".o_field_char input").toBeFocused({
message: "Upon clicking on 'Add a line', the new subtask's name should be focused.",
});
});
test("project.task (kanban): check subtask creation when input is empty", async () => {
await mountView({
resModel: "project.task",
type: "kanban",
});
await click(".subtask_list_button");
await animationFrame();
await click(".subtask_create");
await animationFrame();
await click(".subtask_create_input input");
await edit("");
await click(".subtask_create_input button");
await animationFrame();
expect(".subtask_create_input input").toHaveClass("o_field_invalid", {
message: "input field should be displayed as invalid",
});
expect(".o_notification_content").toHaveInnerHTML("Invalid Display Name", {
message: "The content of the notification should contain 'Display Name'.",
});
expect(".o_notification_bar").toHaveClass("bg-danger", {
message: "The notification bar should have type 'danger'.",
});
});
test("project.task: Parent id is set when creating new task from subtask form's 'View' button", async () => {
mockService("action", {
doAction(params) {
return mountView({
resModel: params.res_model,
resId: params.res_id,
type: "form",
context: params.context,
});
},
});
const taskFormView = await mountView({
resModel: "project.task",
resId: 1,
type: "form",
});
await click("tbody .o_data_row:nth-child(1) .o_list_record_open_form_view button.btn-link");
// Destroying this view for sanicity of display
destroy(taskFormView);
await animationFrame();
await click(".o_form_view .o_form_button_create");
await animationFrame();
expect("div[name='parent_id'] input").toHaveValue(
MockServer.current._models[ProjectTask._name].find((rec) => rec.id === 1).name
);
});

View file

@ -0,0 +1,148 @@
import { beforeEach, expect, test } from "@odoo/hoot";
import { animationFrame, hover } from "@odoo/hoot-dom";
import { contains, mockService, mountView, onRpc } from "@web/../tests/web_test_helpers";
import { defineProjectModels, ProjectTask } from "./project_models";
defineProjectModels();
function addTemplateTasks() {
ProjectTask._records.push(
{
id: 4,
name: "Template Task 1",
project_id: 1,
stage_id: 1,
state: "01_in_progress",
is_template: true,
},
{
id: 5,
name: "Template Task 2",
project_id: 1,
stage_id: 1,
state: "01_in_progress",
is_template: true,
}
);
}
beforeEach(() => {
ProjectTask._views = {
form: `
<form js_class="project_task_form">
<field name="name"/>
</form>
`,
kanban: `
<kanban js_class="project_task_kanban">
<templates>
<t t-name="card">
<field name="name"/>
</t>
</templates>
</kanban>
`,
list: `
<list js_class="project_task_list">
<field name="name"/>
</list>
`,
};
});
for (const [viewType, newButtonClass] of [
["form", ".o_form_button_create"],
["kanban", ".o-kanban-button-new"],
["list", ".o_list_button_add"],
]) {
test(`template dropdown in ${viewType} view of a project with no template`, async () => {
await mountView({
resModel: "project.task",
resId: 1,
type: viewType,
context: {
default_project_id: 1,
},
});
expect(newButtonClass).toHaveCount(1, {
message: "The “New” button should be displayed",
});
expect(newButtonClass).not.toHaveClass("dropdown-toggle", {
message: "The “New” button should not be a dropdown since there is no template",
});
// Test that we can create a new record without errors
await contains(`${newButtonClass}`).click();
});
test(`template dropdown in ${viewType} view of a project with one template with showing Edit and Delete actions`, async () => {
addTemplateTasks();
onRpc(({ method }) => {
if (method === "unlink") {
expect.step(method);
}
});
mockService("action", {
doAction(action) {
if (action.res_id === 4 && action.res_model === "project.task") {
expect.step("task template opened");
}
},
});
await mountView({
resModel: "project.task",
resId: 1,
type: viewType,
context: {
default_project_id: 1,
},
});
expect(newButtonClass).toHaveCount(1, {
message: "The “New” button should be displayed",
});
expect(newButtonClass).toHaveClass("dropdown-toggle", {
message: "The “New” button should be a dropdown since there is a template",
});
await contains(newButtonClass).click();
expect("button.dropdown-item:contains('New Task')").toHaveCount(1, {
message: "The “New Task” button should be in the dropdown",
});
expect("button.dropdown-item:contains('Template Task 1')").toHaveCount(1, {
message: "There should be a button named after the task template",
});
await hover("button.dropdown-item:contains('Template Task 1')");
await animationFrame();
await contains(".o_template_icon_group:first > i.fa-trash").click();
expect(".modal-body").toHaveCount(1, {
message: "A confirmation modal should appear when deleting a template",
});
await contains(".modal-footer .btn-primary").click();
expect.verifySteps(["unlink"]);
await animationFrame();
await contains(".o_template_icon_group:first > i.fa-pencil").click();
expect.verifySteps(["task template opened"]);
});
}
test("template dropdown should not appear when not in the context of a specific project", async () => {
addTemplateTasks();
await mountView({
resModel: "project.task",
type: "kanban",
});
expect(".o-kanban-button-new").not.toHaveClass("dropdown-toggle", {
message:
"The “New” button should not be a dropdown since there is no project in the context",
});
});

View file

@ -1,6 +0,0 @@
/** @odoo-module */
export function getFirstElementForXpath(target, xpath) {
const xPathResult = document.evaluate(xpath, target, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
return xPathResult.singleNodeValue;
}

View file

@ -0,0 +1,48 @@
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { expect, test } from "@odoo/hoot";
import { defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
class ProjectUpdate extends models.Model {
_name = "project.update";
status = fields.Selection({
selection: [
["on_track", "On Track"],
["at_risk", "At Risk"],
["off_track", "Off Track"],
["on_hold", "On Hold"],
["done", "Done"],
],
});
_records = [{ id: 1, status: "on_track" }];
}
defineMailModels();
defineModels([ProjectUpdate]);
test("project.update (kanban): check that ProjectStatusWithColorSelectionField is displaying the correct informations", async () => {
await mountView({
resModel: "project.update",
type: "kanban",
arch: `
<kanban class="o_kanban_test">
<template>
<t t-name="card">
<field name="status" widget="status_with_color" readonly="1" status_label="test status label"/>
</t>
</template>
</kanban>
`,
});
expect("div[name='status'] .o_color_bubble_20").toHaveCount(1, {
message: "In readonly a status bubble should be displayed",
});
expect("div[name='status'] .o_stat_text:contains('test status label')").toHaveCount(1, {
message: "If the status_label prop has been set, its value should be displayed as well",
});
expect("div[name='status'] .o_stat_value:contains('On Track')").toHaveCount(1, {
message: "The value of the selection should be displayed",
});
});

View file

@ -1,63 +1,78 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
import tour from 'web_tour.tour';
tour.register('personal_stage_tour', {
test: true,
url: '/web',
},
[tour.stepUtils.showAppsMenuItem(), {
registry.category("web_tour.tours").add('personal_stage_tour', {
url: '/odoo',
steps: () => [stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="project.menu_main_pm"]',
run: "click",
}, {
content: "Open Pig Project",
trigger: '.o_kanban_record:contains("Pig")',
run: "click",
}, {
// Default is grouped by stage, user should not be able to create/edit a column
content: "Check that there is no create column",
trigger: "body:not(.o_column_quick_create)",
run: function () {},
}, {
content: "Check that there is no create column",
trigger: "body:not(.o_column_edit)",
run: function () {},
trigger: "body:not(.o_group_edit)",
}, {
content: "Check that there is no create column",
trigger: "body:not(.o_column_delete)",
run: function () {},
trigger: "body:not(.o_group_delete)",
}, {
content: "Go to tasks",
trigger: 'button[data-menu-xmlid="project.menu_project_management"]',
run: "click",
},{
content: "Go to my tasks", // My tasks is grouped by personal stage by default
trigger: 'a[data-menu-xmlid="project.menu_project_management"]',
trigger: 'a[data-menu-xmlid="project.menu_project_management_my_tasks"]',
run: "click",
}, {
content: "Check that we can create a new stage",
trigger: '.o_column_quick_create .o_quick_create_folded'
trigger: '.o_column_quick_create.o_quick_create_folded div',
run: "click",
}, {
content: "Create a new personal stage",
trigger: 'input.form-control.o_input',
run: 'text Never',
trigger: 'input.form-control',
run: "edit Never",
}, {
content: "Confirm create",
trigger: '.o_kanban_add',
run: "click",
}, {
content: "Check that column exists",
trigger: '.o_kanban_header:contains("Never")',
run: function () {},
}, {
content: 'Open column edit dropdown',
trigger: '.o_kanban_header:eq(0)',
run: function () {
document.querySelector('.o_kanban_config.dropdown .dropdown-toggle').dispatchEvent(new Event('click'));
},
content: "Check that column exists && Open column edit dropdown",
trigger: ".o_kanban_header:contains(Never)",
run: "hover && click .o_kanban_header:contains(Never) .dropdown-toggle",
}, {
content: "Try editing inbox",
trigger: ".dropdown-item.o_column_edit",
trigger: ".dropdown-item.o_group_edit",
run: "click",
}, {
content: "Change title",
trigger: 'div.o_field_char[name="name"] input',
run: 'text (Todo)',
run: "edit ((Todo))",
}, {
content: "Save changes",
trigger: '.btn-primary:contains("Save")',
run: "click",
}, {
content: "Check that column was updated",
trigger: '.o_kanban_header:contains("Todo")',
}]);
run: "click",
}, {
content: "Create a personal task from the quick create form",
trigger: '.o-kanban-button-new',
run: "click",
}, {
content: "Create a new personal task",
trigger: 'input.o_input:not(.o_searchview_input)',
run: "edit New Test Task",
}, {
content: "Confirm create",
trigger: '.o_kanban_add',
run: "click",
}, {
content: "Check that task exists",
trigger: '.o_kanban_record:contains("New Test Task")',
}]});

View file

@ -1,79 +1,85 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
import tour from 'web_tour.tour';
tour.register('burndown_chart_tour', {
test: true,
url: '/web',
},
[tour.stepUtils.showAppsMenuItem(), {
registry.category("web_tour.tours").add('burndown_chart_tour', {
url: '/odoo',
steps: () => [stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="project.menu_main_pm"]',
run: "click",
}, {
content: 'Open "Burndown Chart Test" project menu',
trigger: '.o_kanban_record:contains("Burndown Chart Test") .o_kanban_manage_toggle_button',
trigger: ".o_kanban_record:contains(Burndown Chart Test)",
run: `hover && click .o_kanban_record:contains(Burndown Chart Test) .o_dropdown_kanban .dropdown-toggle`,
}, {
content: `Open "Burndown Chart Test" project's "Burndown Chart" view`,
trigger: '.o_kanban_record:contains("Burndown Chart Test") .o_kanban_manage_reporting div[role="menuitem"] a:contains("Burndown Chart")',
}, {
trigger: '.o_kanban_manage_reporting div[role="menuitem"] a:contains("Burndown Chart")',
run: "click",
},
{
trigger: ".o_graph_renderer",
},
{
content: 'The sort buttons are not rendered',
trigger: '.o_cp_bottom_left:not(:has(.btn-group[role=toolbar][aria-label="Sort graph"]))',
extra_trigger: '.o_graph_renderer',
trigger: '.o_graph_renderer:not(:has(.btn-group[role=toolbar][aria-label="Sort graph"]))',
run: "click",
}, {
content: 'Remove the project search "Burndown Chart Test"',
trigger: '.o_searchview_facet:contains("Burndown Chart Test") .o_facet_remove',
trigger: ".o_searchview_facet:contains(Burndown Chart Test)",
run: "hover && click .o_facet_remove",
}, {
content: 'Search Burndown Chart',
trigger: 'input.o_searchview_input',
run: `text Burndown`,
run: `edit Burndown`,
}, {
content: 'Validate search',
trigger: '.o_searchview_autocomplete .o_menu_item:contains("Project")',
trigger: '.o_searchview_autocomplete .o-dropdown-item:contains("Project")',
run: "click",
}, {
content: 'Remove the group by "Date: Month > Stage"',
trigger: '.o_searchview_facet:contains("Date: Month") .o_facet_remove',
trigger: '.o_searchview_facet:contains("Stage") .o_facet_remove',
run: "click",
}, {
content: 'A "The Burndown Chart must be grouped by Date and Stage" notification is shown when trying to remove the group by "Date: Month > Stage"',
trigger: '.o_notification_manager .o_notification:contains("The Burndown Chart must be grouped by Date and Stage") button.o_notification_close',
trigger: '.o_notification_manager .o_notification:contains("The report should be grouped either by ") button.o_notification_close',
run: "click",
}, {
content: 'Open the group by menu',
trigger: '.o_group_by_menu button',
content: 'Open the search panel menu',
trigger: '.o_control_panel .o_searchview_dropdown_toggler',
run: "click",
}, {
content: 'The Stage group menu item is invisible',
trigger: '.o_group_by_menu:not(:has(.o_menu_item:contains("Stage")))',
content: 'The Stage group menu item is visible',
trigger: '.o_group_by_menu .o_menu_item:contains("Stage")',
run: "click",
}, {
content: 'Open the Date group by sub menu',
trigger: '.o_group_by_menu button.o_menu_item:contains("Date")',
run: function () {
this.$anchor[0].dispatchEvent(new Event('mouseenter'));
},
run: "click",
}, {
content: 'Click on the selected Date sub menu',
trigger: '.o_group_by_menu button.o_menu_item:contains("Date") + * .dropdown-item.selected',
run: function () {
this.$anchor[0].dispatchEvent(new Event('click'));
},
run: "click",
}, {
content: 'A "The Burndown Chart must be grouped by Date" notification is shown when trying to remove the group by "Date: Month > Stage"',
trigger: '.o_notification_manager .o_notification:contains("The Burndown Chart must be grouped by Date") button.o_notification_close',
run: "click",
}, {
content: 'Open the filter menu',
trigger: '.o_filter_menu button',
content: 'Open the search panel menu',
trigger: '.o_control_panel .o_searchview_dropdown_toggler',
run: "click",
}, {
content: 'Open the Date filter sub menu',
trigger: '.o_filter_menu button.o_menu_item:contains("Date")',
run: function () {
this.$anchor[0].dispatchEvent(new Event('mouseenter'));
},
run: "click",
}, {
content: 'Click on the first Date filter sub menu',
trigger: '.o_filter_menu .o_menu_item:contains("Date") + * .dropdown-item:first-child',
run: function () {
this.$anchor[0].dispatchEvent(new Event('click'));
},
run: "click",
}, {
content: 'Close the Date filter menu',
trigger: '.o_graph_renderer',
run: "click",
}, {
content: 'The comparison menu is not rendered',
trigger: '.o_search_options:not(:has(.o_comparison_menu))',
}]);
content: 'Open the search panel menu',
trigger: '.o_control_panel .o_searchview_dropdown_toggler',
run: "click",
}]});

View file

@ -1,115 +1,256 @@
/** @odoo-module **/
import { delay } from "@web/core/utils/concurrency";
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
import tour from 'web_tour.tour';
const projectSharingSteps = [...tour.stepUtils.goToAppSteps("project.menu_main_pm", 'Go to the Project App.'), {
trigger: '.oe_kanban_global_click :contains("Project Sharing") button.o_dropdown_kanban',
content: 'Open the project dropdown.'
const projectSharingSteps = [...stepUtils.goToAppSteps("project.menu_main_pm", 'Go to the Project App.'), {
trigger: ".o_kanban_record:contains(Project Sharing)",
content: 'Open the project dropdown.',
run: "hover && click .o_kanban_record:contains(Project Sharing) .o_dropdown_kanban .dropdown-toggle",
}, {
trigger: '.o_kanban_record:contains("Project Sharing") .dropdown-menu a:contains("Share")',
trigger: '.dropdown-menu a:contains("Share")',
content: 'Start editing the project.',
run: "click",
}, {
trigger: 'div.o_field_radio[name="access_mode"] div.o_radio_item > input[data-value="edit"]',
content: 'Select "Edit" as Access mode in the "Share Project" wizard.',
trigger: '.modal div[name="collaborator_ids"] .o_field_x2many_list_row_add > a',
content: 'Add a collaborator to the project.',
run: "click",
}, {
trigger: '.o_field_many2many_tags_email[name=partner_ids]',
trigger: '.modal div[name="collaborator_ids"] div[name="partner_id"] input',
content: 'Select the user portal as collaborator to the "Project Sharing" project.',
run: function (actions) {
actions.text('Georges', this.$anchor.find('input'));
},
run: "edit Georges",
}, {
trigger: '.ui-autocomplete a.dropdown-item:contains("Georges")',
in_modal: false,
run: "click",
}, {
trigger: 'footer > button[name="action_send_mail"]',
trigger: '.modal div[name="collaborator_ids"] div[name="access_mode"] input',
content: 'Open Access mode selection dropdown.',
run: 'click',
},{
trigger: '.o_select_menu_item:contains(Edit)',
run: 'click',
}, {
trigger: '.modal footer > button[name="action_share_record"]',
content: 'Confirm the project sharing with this portal user.',
}, {
run: "click",
},
{
trigger: "body:not(:has(.modal))",
},
{
trigger: '.o_web_client',
content: 'Go to project portal view to select the "Project Sharing" project',
run: function () {
window.location.href = window.location.origin + '/my/projects';
},
expectUnloadPage: true,
}, {
id: 'project_sharing_feature',
trigger: 'table > tbody > tr a:has(span:contains(Project Sharing))',
content: 'Select "Project Sharing" project to go to project sharing feature for this project.',
run: "click",
expectUnloadPage: true,
}, {
trigger: '.o_project_sharing',
trigger: '.o_project_sharing .o_kanban_renderer',
content: 'Wait the project sharing feature be loaded',
run: function () {},
}, {
trigger: 'button.o-kanban-button-new',
content: 'Click "Create" button',
run: 'click',
}, {
trigger: '.o_kanban_quick_create .o_field_widget[name="name"] input',
trigger: '.o_kanban_quick_create .o_field_widget[name=name] input',
content: 'Create Task',
run: 'text Test Create Task',
run: "edit Test Create Task",
}, {
content: "Check that task stages cannot be drag and dropped",
trigger: '.o_kanban_group:not(.o_group_draggable)',
}, {
trigger: '.o_kanban_quick_create .o_kanban_edit',
content: 'Go to the form view of this new task',
run: "click",
}, {
trigger: 'div[name="stage_id"] div.o_statusbar_status button[aria-checked="false"]:contains(Done)',
content: 'Change the stage of the task.',
run: "click",
}, {
trigger: '.o_portal_chatter_composer_input .o_portal_chatter_composer_body textarea',
trigger: '.o-mail-Composer-input',
content: 'Write a message in the chatter of the task',
run: 'text I create a new task for testing purpose.',
run: "edit I create a new task for testing purpose.",
}, {
trigger: '.o_portal_chatter_composer_input .o_portal_chatter_composer_body button[name="send_message"]',
trigger: '.o-mail-Composer-send:enabled',
content: 'Send the message',
run: "click",
}, {
trigger: 'ol.breadcrumb > li.o_back_button > a:contains(Project Sharing)',
content: 'Go back to the kanban view',
run: "click",
}, {
trigger: '.o_filter_menu > button',
content: 'click on filter menu in the search view',
trigger: '.o_searchview_dropdown_toggler',
content: 'open the search panel menu',
run: "click",
}, {
trigger: '.o_filter_menu > .dropdown-menu > .dropdown-item:first-child',
trigger: '.o_filter_menu .dropdown-item:first-child',
content: 'click on the first item in the filter menu',
run: "click",
}, {
trigger: '.o_group_by_menu > button',
content: 'click on group by menu in the search view',
}, {
trigger: '.o_group_by_menu > .dropdown-menu > .dropdown-item:first-child',
trigger: '.o_group_by_menu .dropdown-item:first-child',
content: 'click on the first item in the group by menu',
run: "click",
}, {
trigger: '.o_favorite_menu > button',
content: 'click on the favorite menu in the search view',
trigger: '.o_favorite_menu .o_add_favorite',
content: 'open accordion "save current search" in favorite menu',
run: "click",
}, {
trigger: '.o_favorite_menu .o_add_favorite > button',
content: 'click to "save current search" button in favorite menu',
trigger: '.o_favorite_menu .o_accordion_values .o_save_favorite',
content: 'click to "save" button in favorite menu',
run: "click",
}, {
trigger: '.o_filter_menu > button',
content: 'click on filter menu in the search view',
}, {
trigger: '.o_filter_menu > .dropdown-menu > .dropdown-item:first-child',
trigger: '.o_filter_menu .dropdown-item:first-child',
content: 'click on the first item in the filter menu',
run: "click",
}, {
trigger: '.o_group_by_menu > button',
content: 'click on group by menu in the search view',
}, {
trigger: '.o_group_by_menu > .dropdown-menu > .dropdown-item:first-child',
trigger: '.o_group_by_menu .dropdown-item:first-child',
content: 'click on the first item in the group by menu',
run: "click",
}, {
trigger: '.o_favorite_menu > button',
content: 'click on the favorite menu in the search view',
}, {
trigger: '.o_favorite_menu .o_add_favorite > button',
content: 'click to "save current search" button in favorite menu',
trigger: '.o_favorite_menu .o_accordion_values .o_save_favorite',
content: 'click to "save" button in favorite menu',
run: "click",
}, {
trigger: 'button.o_switch_view.o_list',
content: 'Go to the list view',
run: "click",
}, {
trigger: '.o_list_view',
}, {
trigger: '.o_optional_columns_dropdown_toggle',
run: "click",
}, {
trigger: '.dropdown-item:contains("Milestone")',
}, {
trigger: '.o_list_view',
content: 'Check the list view',
}];
tour.register('project_sharing_tour', {
test: true,
url: '/web',
}, projectSharingSteps);
registry.category("web_tour.tours").add('project_sharing_tour', {
url: '/odoo',
steps: () => {
return projectSharingSteps;
}
});
// The begining of the project sharing feature
const projectSharingStepIndex = projectSharingSteps.findIndex(s => s.id && s.id === 'project_sharing_feature');
tour.register('portal_project_sharing_tour', {
test: true,
url: '/my/projects',
}, projectSharingSteps.slice(projectSharingStepIndex, projectSharingSteps.length));
registry.category("web_tour.tours").add("portal_project_sharing_tour", {
url: "/my/projects",
steps: () => {
// The begining of the project sharing feature
const projectSharingStepIndex = projectSharingSteps.findIndex(s => s?.id === 'project_sharing_feature');
return projectSharingSteps.slice(projectSharingStepIndex, projectSharingSteps.length);
}
});
registry.category("web_tour.tours").add("project_sharing_with_blocked_task_tour", {
url: "/my/projects",
steps: () => [{
trigger: 'table > tbody > tr a:has(span:contains("Project Sharing"))',
content: 'Click on the portal project.',
run: "click",
expectUnloadPage: true,
}, {
trigger: 'article.o_kanban_record',
content: 'Click on the task',
run: "click",
}, {
trigger: 'a:contains("Blocked By")',
content: 'Go to the Block by task tab',
run: "click",
}, {
trigger: 'i:contains("This task is currently blocked by")',
content: 'Check that the blocked task is not visible',
},
]});
registry.category("web_tour.tours").add("portal_project_sharing_tour_with_disallowed_milestones", {
url: "/my/projects",
steps: () => [
{
id: "project_sharing_feature",
trigger: "table > tbody > tr a:has(span:contains(Project Sharing))",
content:
'Select "Project Sharing" project to go to project sharing feature for this project.',
run: "click",
expectUnloadPage: true,
},
{
trigger: ".o_project_sharing",
content: "Wait the project sharing feature be loaded",
},
{
trigger: "button.o_switch_view.o_list",
content: "Go to the list view",
run: "click",
},
{
trigger: ".o_list_view",
},
{
trigger: ".o_optional_columns_dropdown_toggle",
run: "click",
},
{
trigger: ".dropdown-item",
},
{
trigger: ".dropdown-menu",
run: function () {
const optionalFields = Array.from(
this.anchor.ownerDocument.querySelectorAll(".dropdown-item")
).map((e) => e.textContent);
if (optionalFields.includes("Milestone")) {
throw new Error(
"the Milestone field should be absent as allow_milestones is set to False"
);
}
},
},
],
});
registry.category("web_tour.tours").add("test_04_project_sharing_chatter_message_reactions", {
url: "/my/projects",
steps: () => [
{
trigger: "table > tbody > tr a:has(span:contains(Project Sharing))",
run: "click",
expectUnloadPage: true,
},
{ trigger: ".o_project_sharing" },
{ trigger: ".o_kanban_record:contains('Test Task with messages')", run: "click" },
{ trigger: ".o-mail-Message" },
{ trigger: ".o-mail-Message .o-mail-MessageReaction:contains('👀')" },
],
});
registry.category("web_tour.tours").add("portal_project_sharing_chatter_mention_users", {
url: "/my/projects",
steps: () => [
{
trigger: "table > tbody > tr a:has(span:contains(Project Sharing))",
run: "click",
expectUnloadPage: true,
},
{ trigger: ".o_project_sharing" },
{ trigger: ".o_kanban_record:contains('Test Task')", run: "click" },
{ trigger: ".o-mail-Composer-input", run: "edit @xxx" },
{
trigger: "body:not(:has(.o-mail-Composer-suggestion))",
run: async () => {
const delay_fetch = odoo.loader.modules.get(
"@mail/core/common/suggestion_hook"
).DELAY_FETCH;
await delay(delay_fetch);
},
},
{ trigger: ".o-mail-Composer-input", run: "edit @Georges" },
{ trigger: ".o-mail-Composer-suggestion:contains('Georges')" },
],
});

View file

@ -1,56 +1,65 @@
/** @odoo-module **/
import tour from 'web_tour.tour';
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
function changeFilter(filterName) {
return [
{
trigger: '.o_favorite_menu button:has(i.fa-star)',
content: 'click on the favorite menu',
trigger: ".o_control_panel_actions .o_searchview_dropdown_toggler",
content: "open searchview menu",
run: "click",
},
{
trigger: `.o_favorite_menu .dropdown-item span:contains("${filterName}")`,
run: "click",
},
{
trigger: '.o_group_by_menu button:has(i.oi-group)',
content: 'click on the groupby menu',
run: function (actions) {
this.$anchor[0].dispatchEvent(new Event('mouseenter'));
},
},
{
trigger: '.o_group_by_menu span:contains("Stage")',
content: 'click on the stage gb',
trigger: ".o_control_panel_actions .o_searchview_dropdown_toggler",
content: "close searchview menu",
run: "click",
},
];
}
tour.register('project_tags_filter_tour',
{
test: true,
url: '/web',
},
[
tour.stepUtils.showAppsMenuItem(),
{
trigger: '.o_app[data-menu-xmlid="project.menu_main_pm"]',
}, ...changeFilter("Corkscrew tail tag filter"),
{
trigger: '.o_kanban_group:has(.o_kanban_header:has(span:contains("pig"))) .o_kanban_record:has(span:contains("Pigs"))',
extra_trigger: '.o_kanban_group:has(.o_kanban_header:has(span:contains("goat"))):not(:has(.o_kanban_record))',
content: 'check that the corkscrew tail filter has taken effect',
run: () => {},
}, ...changeFilter("horned tag filter"),
{
trigger: '.o_kanban_group:has(.o_kanban_header:has(span:contains("goat"))) .o_kanban_record:has(span:contains("Goats"))',
extra_trigger: '.o_kanban_group:has(.o_kanban_header:has(span:contains("pig"))):not(:has(.o_kanban_record))',
content: 'check that the horned filter has taken effect',
run: () => {},
}, ...changeFilter("4 Legged tag filter"),
{
trigger: '.o_kanban_group:has(.o_kanban_header:has(span:contains("goat"))) .o_kanban_record:has(span:contains("Goats"))',
extra_trigger: '.o_kanban_group:has(.o_kanban_header:has(span:contains("pig"))) .o_kanban_record:has(span:contains("Pigs"))',
content: 'check that the 4 legged filter has taken effect',
run: () => {},
},
]);
registry.category("web_tour.tours").add("project_tags_filter_tour", {
url: "/odoo",
steps: () => [
stepUtils.showAppsMenuItem(),
{
trigger: '.o_app[data-menu-xmlid="project.menu_main_pm"]',
run: "click",
},
...changeFilter("Corkscrew tail tag filter"),
{
trigger:
'.o_kanban_group:has(.o_kanban_header:has(span:contains("goat"))):not(:has(.o_kanban_record))',
content: "check that the corkscrew tail filter has taken effect",
},
{
trigger:
'.o_kanban_group:has(.o_kanban_header:has(span:contains("pig"))) .o_kanban_record:has(span:contains("Pigs"))',
content: "check that the corkscrew tail filter has taken effect",
},
...changeFilter("horned tag filter"),
{
trigger:
'.o_kanban_group:has(.o_kanban_header:has(span:contains("pig"))):not(:has(.o_kanban_record))',
content: "check that the horned filter has taken effect",
},
{
trigger:
'.o_kanban_group:has(.o_kanban_header:has(span:contains("goat"))) .o_kanban_record:has(span:contains("Goats"))',
content: "check that the horned filter has taken effect",
},
...changeFilter("4 Legged tag filter"),
{
trigger:
'.o_kanban_group:has(.o_kanban_header:has(span:contains("pig"))) .o_kanban_record:has(span:contains("Pigs"))',
content: "check that the 4 legged filter has taken effect",
},
{
trigger:
'.o_kanban_group:has(.o_kanban_header:has(span:contains("goat"))) .o_kanban_record:has(span:contains("Goats"))',
content: "check that the 4 legged filter has taken effect",
},
],
});

View file

@ -0,0 +1,278 @@
/**
* Project Task history tour.
* Features tested:
* - Create / edit a task description and ensure revisions are created on write
* - Open the history dialog and check that the revisions are correctly shown
* - Select a revision and check that the content / comparison are correct
* - Click the restore button and check that the content is correctly restored
*/
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
const baseDescriptionContent = "Test project task history version";
function changeDescriptionContentAndSave(newContent) {
const newText = `${baseDescriptionContent} ${newContent}`;
return [
{
// force focus on editable so editor will create initial p (if not yet done)
trigger: "div.note-editable.odoo-editor-editable",
run: "click",
},
{
trigger: `div.note-editable[spellcheck='true'].odoo-editor-editable`,
run: `editor ${newText}`,
},
...stepUtils.saveForm(),
];
}
function insertEditorContent(newContent) {
return [
{
// force focus on editable so editor will create initial p (if not yet done)
trigger: "div.note-editable.odoo-editor-editable",
run: "click",
},
{
trigger: `div.note-editable[spellcheck='true'].odoo-editor-editable`,
run: async function () {
// Insert content as html and make the field dirty
const div = document.createElement("div");
div.appendChild(document.createTextNode(newContent));
this.anchor.removeChild(this.anchor.firstChild);
this.anchor.appendChild(div);
this.anchor.dispatchEvent(new Event("input", { bubbles: true }));
},
},
];
}
registry.category("web_tour.tours").add("project_task_history_tour", {
url: "/odoo?debug=1,tests",
steps: () => [stepUtils.showAppsMenuItem(), {
content: "Open the project app",
trigger: ".o_app[data-menu-xmlid='project.menu_main_pm']",
run: "click",
},
{
content: "Open Test History Project",
trigger: ".o_kanban_view .o_kanban_record:contains(Test History Project)",
run: "click",
},
{
content: "Open Test History Task",
trigger: ".o_kanban_view .o_kanban_record:contains(Test History Task)",
run: "click",
},
// edit the description content 3 times and save after each edit
...changeDescriptionContentAndSave("0"),
...changeDescriptionContentAndSave("1"),
...changeDescriptionContentAndSave("2"),
...changeDescriptionContentAndSave("3"),
{
content: "Go back to kanban view of tasks. this step is added because it takes some time to save the changes, so it's a sort of timeout to wait a bit for the save",
trigger: ".o_back_button a",
run: "click",
},
{
content: "Open Test History Task",
trigger: ".o_kanban_view .o_kanban_record:contains(Test History Task)",
run: "click",
},
{
content: "Open History Dialog",
trigger: ".o_form_view .o_cp_action_menus i.fa-cog",
run: "click",
},
{
trigger: ".dropdown-menu",
},
{
content: "Open History Dialog",
trigger: ".o_menu_item i.fa-history",
run: "click",
}, {
trigger: ".modal .html-history-dialog.html-history-loaded",
}, {
content: "Verify that 5 revisions are displayed (default empty description after the creation of the task + 3 edits + current version)",
trigger: ".modal .html-history-dialog .revision-list .btn",
run: function () {
const items = document.querySelectorAll(".revision-list .btn");
if (items.length !== 5) {
console.error("Expect 5 Revisions in the history dialog, got " + items.length);
}
},
}, {
content: "Verify that the active revision (revision 4) is related to the current version",
trigger: `.modal .history-container .history-content-view .history-view-inner:contains(${baseDescriptionContent} 3)`,
}, {
content: "Go to the third revision related to the second edit",
trigger: ".modal .html-history-dialog .revision-list .btn:nth-child(3)",
run: "click",
}, {
trigger: ".modal .html-history-dialog.html-history-loaded",
}, {
content: "Verify that the active revision is the one clicked in the previous step",
trigger: `.modal .history-container .history-content-view .history-view-inner:contains(${baseDescriptionContent} 1)`,
}, {
// click on the comparison tab
trigger: '.history-container .history-view-top-bar a:contains(Comparison)',
run: "click",
}, {
content: "Verify comparison text",
trigger: ".modal .history-container .history-comparison-view",
run: function () {
const comparaisonHtml = this.anchor.innerHTML;
const correctHtml = `<added>${baseDescriptionContent} 3</added><removed>${baseDescriptionContent} 1</removed>`;
if (!comparaisonHtml.includes(correctHtml)) {
console.error(`Expect comparison to be ${correctHtml}, got ${comparaisonHtml}`);
}
},
}, {
trigger: ".modal .html-history-dialog.html-history-loaded",
}, {
content: "Click on Restore History btn to get back to the selected revision in the previous step",
trigger: ".modal button.btn-primary:enabled",
run: "click",
}, {
content: "Verify the confirmation dialog is opened",
trigger: ".modal button.btn-primary:text(Restore)",
run: "click",
}, {
content: "Verify that the description contains the right text after the restore",
trigger: `div.note-editable.odoo-editor-editable`,
run: function () {
const p = this.anchor?.innerText;
const expected = `${baseDescriptionContent} 1`;
if (p !== expected) {
console.error(`Expect description to be ${expected}, got ${p}`);
}
}
}, {
content: "Go back to projects view.",
trigger: 'a[data-menu-xmlid="project.menu_projects"]',
run: "click",
}, {
trigger: ".o_kanban_view",
}, {
content: "Open Test History Project Without Tasks",
trigger: ".o_kanban_view .o_kanban_record:contains(Without tasks project)",
run: "click",
}, {
trigger: ".o_kanban_project_tasks",
}, {
content: "Switch to list view",
trigger: ".o_switch_view.o_list",
run: "click",
}, {
content: "Create a new task.",
trigger: '.o_list_button_add',
run: "click",
}, {
trigger: ".o_form_view",
}, {
trigger: 'div[name="name"] .o_input',
content: 'Set task name',
run: 'edit New task',
},
...stepUtils.saveForm(),
...changeDescriptionContentAndSave("0"),
...changeDescriptionContentAndSave("1"),
...changeDescriptionContentAndSave("2"),
...changeDescriptionContentAndSave("3"),
{
trigger: ".o_form_view",
}, {
content: "Open History Dialog",
trigger: ".o_cp_action_menus i.fa-cog",
run: "click",
}, {
trigger: ".dropdown-menu",
}, {
content: "Open History Dialog",
trigger: ".o_menu_item i.fa-history",
run: "click",
}, {
content: "Close History Dialog",
trigger: ".modal-header .btn-close",
run: "click",
}, {
content: "Go back to projects view. this step is added because Tour can't be finished with an open form view in edition mode.",
trigger: 'a[data-menu-xmlid="project.menu_projects"]',
run: "click",
}, {
content: "Verify that we are on kanban view",
trigger: 'button.o_switch_view.o_kanban.active',
}
]});
registry.category("web_tour.tours").add("project_task_last_history_steps_tour", {
url: "/odoo?debug=1,tests",
steps: () => [stepUtils.showAppsMenuItem(), {
content: "Open the project app",
trigger: ".o_app[data-menu-xmlid='project.menu_main_pm']",
run: "click",
},
{
content: "Open Test History Project",
trigger: ".o_kanban_view .o_kanban_record:contains(Test History Project)",
run: "click",
},
{
content: "Open Test History Task",
trigger: ".o_kanban_view .o_kanban_record:contains(Test History Task)",
run: "click",
},
...insertEditorContent("0"),
...stepUtils.saveForm(),
{
content: "Open History Dialog",
trigger: ".o_cp_action_menus i.fa-cog",
run: "click",
}, {
trigger: ".dropdown-menu",
}, {
content: "Open History Dialog",
trigger: ".o_menu_item i.fa-history",
run: "click",
}, {
trigger: ".modal .html-history-dialog.html-history-loaded",
}, {
content: "Verify that 2 revisions are displayed",
trigger: ".modal .html-history-dialog .revision-list .btn",
run: function () {
const items = document.querySelectorAll(".revision-list .btn");
if (items.length !== 2) {
console.error("Expect 2 Revisions in the history dialog, got " + items.length);
}
},
}, {
content: "Go to the second revision related to the initial blank document ",
trigger: ".modal .html-history-dialog .revision-list .btn:nth-child(2)",
run: "click",
}, {
trigger: ".modal .html-history-dialog.html-history-loaded",
}, {
trigger: '.modal button.btn-primary:enabled',
run: "click",
}, {
trigger: '.modal button.btn-primary:text(Restore)',
run: "click",
},
...insertEditorContent("2"),
...stepUtils.saveForm(),
...insertEditorContent("4"),
{
trigger: ".o_notebook_headers li:nth-of-type(2) a",
run: "click",
},
{
trigger: ".o_notebook_headers li:nth-of-type(1) a",
run: "click",
},
...insertEditorContent("5"),
...stepUtils.saveForm(),
],
});

View file

@ -0,0 +1,47 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
registry.category("web_tour.tours").add("project_task_templates_tour", {
url: "/odoo",
steps: () => [
stepUtils.showAppsMenuItem(),
{
trigger: '.o_app[data-menu-xmlid="project.menu_main_pm"]',
run: "click",
},
{
trigger: '.o_kanban_record span:contains("Project with Task Template")',
run: "click",
content: "Navigate to the project with a task template",
},
{
trigger: 'div.o_last_breadcrumb_item span:contains("Project with Task Template")',
content: "Wait for the kanban view to load",
},
{
trigger: ".o-kanban-button-new",
run: "click",
},
{
trigger: '.dropdown-menu button.dropdown-item:contains("Template")',
run: "click",
content: "Create a task with the template",
},
{
trigger: 'div[name="name"] .o_input',
run: "edit Task",
},
{
trigger: "button.o_form_button_save",
run: "click",
},
{
content: "Wait for save completion",
trigger: ".o_form_readonly, .o_form_saved",
},
{
trigger: 'div.note-editable.odoo-editor-editable:contains("Template description")',
content: "Check that the created task has copied the description of the template",
},
],
});

View file

@ -0,0 +1,72 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
registry.category("web_tour.tours").add("project_templates_tour", {
url: "/odoo",
steps: () => [
stepUtils.showAppsMenuItem(),
{
trigger: '.o_app[data-menu-xmlid="project.menu_main_pm"]',
run: "click",
},
{
content: "Click on New Button of Kanban view",
trigger: ".o-kanban-button-new",
run: "click",
},
{
trigger: '.dropdown-menu button.dropdown-item:contains("Project Template")',
run: "click",
content: "Create a project from the template",
},
{
trigger: '.modal div[name="name"] .o_input',
run: "edit New Project",
},
{
trigger: 'button[name="create_project_from_template"]',
run: "click",
},
{
content: "Go back to kanban view",
trigger: ".breadcrumb-item a:contains('Projects')",
run: "click",
},
{
content: "Check for created project",
trigger: ".o_kanban_record:contains('New Project')",
},
{
content: "Go to list view",
trigger: "button.o_switch_view.o_list",
run: "click",
},
{
content: "Click on New Button of List view",
trigger: ".o_list_button_add",
run: "click",
},
{
content: "Lets Create a second project from the template",
trigger: '.dropdown-menu button.dropdown-item:contains("Project Template")',
run: "click",
},
{
trigger: '.modal div[name="name"] .o_input',
run: "edit New Project 2",
},
{
trigger: 'button[name="create_project_from_template"]',
run: "click",
},
{
content: "Go back to list view",
trigger: ".breadcrumb-item a:contains('Projects')",
run: "click",
},
{
content: "Check for created project",
trigger: ".o_data_row td[name='name']:contains('New Project 2')",
},
],
});

View file

@ -0,0 +1,158 @@
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
registry.category("web_tour.tours").add('project_test_tour', {
url: '/odoo',
steps: () => [
stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="project.menu_main_pm"]',
run: "click",
},
{
trigger: '.o_project_kanban',
},
{
trigger: '.o-kanban-button-new',
run: "click",
}, {
isActive: ['.o-kanban-button-new.dropdown'], // if the project template dropdown is active
trigger: 'button.o-dropdown-item:contains("New Project")',
run: "click",
}, {
trigger: '.o_project_name input',
run: 'edit New Project',
id: 'project_creation',
}, {
trigger: '.o_open_tasks',
run: "click .modal:visible .btn.btn-primary",
}, {
trigger: ".o_kanban_project_tasks .o_column_quick_create .input-group input",
run: "edit New",
}, {
isActive: ["auto"],
trigger: ".o_kanban_project_tasks .o_column_quick_create .o_kanban_add",
run: "click",
},
{
trigger: ".o_kanban_group",
},
{
trigger: ".o_kanban_project_tasks .o_column_quick_create .input-group input",
run: "edit Done",
}, {
isActive: ["auto"],
trigger: ".o_kanban_project_tasks .o_column_quick_create .o_kanban_add",
run: "click",
},
{
trigger: ".o_kanban_group:eq(0)",
},
{
trigger: '.o-kanban-button-new',
run: "click",
},
{
trigger: ".o_kanban_project_tasks",
},
{
trigger: '.o_kanban_quick_create div.o_field_char[name=display_name] input',
run: "edit New task",
}, {
trigger: '.o_kanban_quick_create .o_kanban_add',
run: "click",
}, {
trigger: '.o_kanban_record span:contains("New task")',
run: "click",
}, {
trigger: 'a[name="sub_tasks_page"]',
content: 'Open sub-tasks notebook section',
run: 'click',
}, {
trigger: '.o_field_subtasks_one2many .o_list_renderer a[role="button"]',
content: 'Add a subtask',
run: 'click',
}, {
trigger: '.o_field_subtasks_one2many div[name="name"] input',
content: 'Set subtask name',
run: "edit new subtask",
}, {
trigger: ".o_breadcrumb .o_back_button",
content: 'Go back to kanban view',
tooltipPosition: "right",
run: "click",
}, {
trigger: ".o_kanban_record .o_widget_subtask_counter .subtask_list_button",
content: 'open sub-tasks from kanban card',
run: "click",
},
{
trigger: ".o_widget_subtask_kanban_list .subtask_list",
},
{
trigger: ".o_kanban_record .o_widget_subtask_kanban_list .subtask_create",
content: 'Create a new sub-task',
run: "click",
},
{
trigger: ".subtask_create_input",
},
{
trigger: ".o_kanban_record .o_widget_subtask_kanban_list .subtask_create_input input",
content: 'Give the sub-task a name',
run: "edit newer subtask && press Tab",
},
{
content: "wait the new record is created",
trigger: ".o_kanban_record .o_widget_subtask_kanban_list a:contains(newer subtask)",
},
{
trigger: ".o_kanban_record .o_widget_subtask_kanban_list .subtask_list_row:first-child .o_field_project_task_state_selection button",
content: 'Change the subtask state',
run: "click",
},
{
trigger: ".dropdown-menu",
},
{
trigger: ".dropdown-menu span.text-danger",
content: 'Mark the task as Canceled',
run: "click",
}, {
trigger: ".o_kanban_record .o_widget_subtask_counter .subtask_list_button:contains('1/2')",
content: 'Close the sub-tasks list',
id: "quick_create_tasks",
run: "click",
}, {
trigger: '.o_field_text[name="name"] textarea',
content: 'Set task name',
run: "edit New task",
}, {
trigger: 'div[name="user_ids"].o_field_many2many_tags_avatar input',
content: 'Assign the task to you',
run: 'click',
}, {
trigger: 'ul.ui-autocomplete a .o_avatar_many2x_autocomplete',
content: 'Assign the task to you',
run: "click",
}, {
trigger: 'a[name="sub_tasks_page"]',
content: 'Open sub-tasks notebook section',
run: 'click',
}, {
trigger: '.o_field_subtasks_one2many .o_list_renderer a[role="button"]',
content: 'Add a subtask',
run: 'click',
}, {
trigger: '.o_field_subtasks_one2many div[name="name"] input',
content: 'Set subtask name',
run: "edit new subtask",
},
{
trigger: '.o_field_many2many_tags_avatar .o_m2m_avatar',
},
{
trigger: 'button[special="save"]',
content: 'Save task',
run: "click",
},
]});

View file

@ -1,180 +1,222 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
import tour from 'web_tour.tour';
function openProjectUpdateAndReturnToTasks(view, viewClass) {
const legacyViewClass = viewClass.replace("o_", "o_legacy_");
return [{
trigger: '.o_project_updates_breadcrumb',
content: 'Open Project Update from view : ' + view,
extra_trigger: `.${viewClass}, .${legacyViewClass}`,
}, {
trigger: ".o-kanban-button-new",
content: "Create a new update from project task view : " + view,
extra_trigger: '.o_pupdate_kanban',
}, {
trigger: "button.o_form_button_cancel",
content: "Discard project update from project task view : " + view,
}, {
trigger: ".o_switch_view.o_list",
content: "Go to list of project update from view " + view,
}, {
trigger: '.o_back_button',
content: 'Go back to the task view : ' + view,
// extra_trigger: '.o_list_view, .o_legacy_list_view', // FIXME: [XBO] uncomment it when the sample data will be displayed after discarding the creation of a project update record.
},
];
}
tour.register('project_update_tour', {
test: true,
url: '/web',
},
[tour.stepUtils.showAppsMenuItem(), {
registry.category("web_tour.tours").add('project_update_tour', {
url: '/odoo',
steps: () => [stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="project.menu_main_pm"]',
}, {
run: "click",
},
{
trigger: ".o_project_kanban",
},
{
trigger: '.o-kanban-button-new',
extra_trigger: '.o_project_kanban',
width: 200,
run: "click",
}, {
isActive: ['.o-kanban-button-new.dropdown'], // if the project template dropdown is active
trigger: 'button.o-dropdown-item:contains("New Project")',
run: "click",
}, {
trigger: '.o_project_name input',
run: 'text New Project'
run: "edit New Project",
}, {
trigger: '.o_open_tasks',
run: function (actions) {
actions.auto('.modal:visible .btn.btn-primary');
},
run: "click .modal:visible .btn.btn-primary",
}, {
trigger: ".o_kanban_project_tasks .o_column_quick_create .input-group",
run: function (actions) {
actions.text("New", this.$anchor.find("input"));
},
trigger: ".o_kanban_project_tasks .o_column_quick_create .input-group input",
run: "edit New",
}, {
isActive: ["auto"],
trigger: ".o_kanban_project_tasks .o_column_quick_create .o_kanban_add",
auto: true,
}, {
trigger: ".o_kanban_project_tasks .o_column_quick_create .input-group",
extra_trigger: '.o_kanban_group',
run: function (actions) {
actions.text("Done", this.$anchor.find("input"));
},
run: "click",
},
{
trigger: ".o_kanban_group",
},
{
trigger: ".o_kanban_project_tasks .o_column_quick_create .input-group input",
run: "edit Done",
}, {
isActive: ["auto"],
trigger: ".o_kanban_project_tasks .o_column_quick_create .o_kanban_add",
auto: true,
}, {
run: "click",
},
{
trigger: ".o_kanban_group:eq(0)",
},
{
trigger: '.o-kanban-button-new',
extra_trigger: '.o_kanban_group:eq(0)'
}, {
trigger: '.o_kanban_quick_create div.o_field_char[name=name] input',
extra_trigger: '.o_kanban_project_tasks',
run: 'text New task'
}, {
run: "click",
},
{
trigger: ".o_kanban_project_tasks",
},
{
trigger: '.o_kanban_quick_create div.o_field_char[name=display_name] input',
run: "edit New task",
},
{
trigger: ".o_kanban_project_tasks",
},
{
trigger: '.o_kanban_quick_create .o_kanban_add',
extra_trigger: '.o_kanban_project_tasks'
}, {
run: "click",
},
{
trigger: ".o_kanban_group:eq(0)",
},
{
trigger: '.o-kanban-button-new',
extra_trigger: '.o_kanban_group:eq(0)'
}, {
trigger: '.o_kanban_quick_create div.o_field_char[name=name] input',
extra_trigger: '.o_kanban_project_tasks',
run: 'text Second task'
}, {
run: "click",
},
{
trigger: ".o_kanban_project_tasks",
},
{
trigger: '.o_kanban_quick_create div.o_field_char[name=display_name] input',
run: "edit Second task",
},
{
trigger: ".o_kanban_project_tasks",
},
{
trigger: '.o_kanban_quick_create .o_kanban_add',
extra_trigger: '.o_kanban_project_tasks'
run: "click",
}, {
trigger: '.o_kanban_group:nth-child(2) .o_kanban_header',
run: function () {
document.querySelector('.o_kanban_group:nth-child(2) .o_kanban_config.dropdown .dropdown-toggle').dispatchEvent(new Event('click'));
}
trigger: ".o_kanban_group:nth-child(2) .o_kanban_header",
run: "hover && click .o_kanban_group:nth-child(2) .o_kanban_header .dropdown-toggle",
}, {
trigger: ".dropdown-item.o_column_edit",
trigger: ".dropdown-item.o_group_edit",
run: "click",
}, {
trigger: ".o_field_widget[name=fold] input",
trigger: ".modal .o_field_widget[name=fold] input",
run: "click",
}, {
trigger: ".modal-footer button",
trigger: ".modal .modal-footer button",
run: "click",
},
{
trigger: "body:not(:has(.modal))",
},
{
trigger: '.o_kanban_project_tasks',
},
{
trigger: ".o_kanban_record",
run: "drag_and_drop(.o_kanban_group:eq(1))",
}, {
trigger: ".o_kanban_record .oe_kanban_content",
extra_trigger: '.o_kanban_project_tasks',
run: "drag_and_drop .o_kanban_group:eq(1) ",
trigger: ".breadcrumb-item.o_back_button",
run: "click",
}, {
trigger: ".o_project_updates_breadcrumb",
content: 'Open Updates'
trigger: ".o_kanban_record:contains('New Project')",
}, {
trigger: ".o_switch_view.o_list",
run: "click",
}, {
trigger: "tr.o_data_row td[name='name']:contains('New Project')",
run: "click",
}, {
trigger: ".nav-link:contains('Settings')",
run: "click",
}, {
trigger: "div[name='allow_milestones'] input",
run: "click",
}, {
trigger: ".o_form_button_save",
run: "click",
}, {
trigger: "button[name='action_view_tasks']",
run: "click",
}, {
trigger: ".o_control_panel_navigation button i.fa-sliders",
content: 'Open embedded actions',
run: "click",
}, {
trigger: "span.o-dropdown-item:contains('Top Menu')",
run: "click",
}, {
trigger: ".o-dropdown-item div span:contains('Dashboard')",
content: "Put Dashboard in the embedded actions",
run: "click",
}, {
trigger: ".o_embedded_actions button span:contains('Dashboard')",
content: "Open Dashboard",
run: "click",
}, {
trigger: ".o_add_milestone a",
content: "Add a first milestone"
content: "Add a first milestone",
run: "click",
}, {
trigger: ".o_list_button_add",
content: "Create new milestone",
run: "click",
}, {
trigger: "div.o_field_widget[name=name] input",
run: 'text New milestone'
run: "edit New milestone",
}, {
trigger: "div[name=deadline] .datetimepicker-input",
run: 'text 12/12/2099'
trigger: "input[data-field=deadline]",
run: "edit 12/12/2099",
}, {
trigger: ".modal-footer .o_form_button_save"
trigger: ".o_list_button_save",
run: "click",
}, {
trigger: ".o_add_milestone a",
trigger: ".o_list_button_add",
content: "Make sure the milestone is saved before continuing",
}, {
trigger: "td[data-tooltip='New milestone'] + td",
run: "click",
}, {
trigger: "input[data-field=deadline]",
run: "edit 12/12/2100 && click body"
}, {
trigger: ".o_list_button_add",
content: "Create new milestone",
run: "click",
}, {
trigger: "div.o_field_widget[name=name] input",
run: 'text Second milestone'
run: "edit Second milestone",
}, {
trigger: "div[name=deadline] .datetimepicker-input",
run: 'text 12/12/2022'
trigger: "input[data-field=deadline]",
run: "edit 12/12/2022 && click body",
}, {
trigger: ".modal-footer .o_form_button_save"
}, {
trigger: ".o_rightpanel_milestone:eq(1) .o_milestone_detail",
}, {
trigger: "div[name=deadline] .datetimepicker-input",
run: 'text 12/12/2100'
}, {
trigger: ".modal-footer .o_form_button_save"
trigger: ".breadcrumb-item.o_back_button",
run: "click",
}, {
trigger: ".o-kanban-button-new",
content: "Create a new update"
content: "Create a new update",
run: "click",
}, {
trigger: "div.o_field_widget[name=name] input",
run: 'text New update'
run: "edit New update",
}, {
trigger: ".o_form_button_save"
trigger: ".o_form_button_save",
run: "click",
}, {
trigger: ".o_field_widget[name='description'] h1:contains('Activities')",
run: function () {},
}, {
trigger: ".o_field_widget[name='description'] h3:contains('Milestones')",
run: function () {},
}, {
trigger: ".o_field_widget[name='description'] div[name='milestone'] ul li:contains('(12/12/2099 => 12/12/2100)')",
run: function () {},
}, {
trigger: ".o_field_widget[name='description'] div[name='milestone'] ul li:contains('(due 12/12/2022)')",
run: function () {},
}, {
trigger: ".o_field_widget[name='description'] div[name='milestone'] ul li:contains('(due 12/12/2100)')",
run: function () {},
}, {
trigger: '.o_back_button',
content: 'Go back to the kanban view the project',
run: "click",
}, {
trigger: '.o_switch_view.o_list',
content: 'Open List View of Project Updates',
}, {
content: 'Open List View of Dashboard',
run: "click",
},
{
trigger: '.o_list_view',
},
{
trigger: '.o_back_button',
content: 'Go back to the kanban view the project',
extra_trigger: '.o_list_view, .o_legacy_list_view',
}, {
trigger: '.o_switch_view.o_graph',
content: 'Open Graph View of Tasks',
}, ...openProjectUpdateAndReturnToTasks("Graph", "o_graph_view"), {
trigger: '.o_switch_view.o_list',
content: 'Open List View of Tasks',
extra_trigger: '.o_graph_view',
}, ...openProjectUpdateAndReturnToTasks("List", "o_list_view"), {
trigger: '.o_switch_view.o_pivot',
content: 'Open Pivot View of Tasks',
}, ...openProjectUpdateAndReturnToTasks("Pivot", "o_pivot_view"), {
trigger: '.o_switch_view.o_calendar',
content: 'Open Calendar View of Tasks',
}, ...openProjectUpdateAndReturnToTasks("Calendar", "o_calendar_view"), {
trigger: '.o_switch_view.o_activity',
content: 'Open Activity View of Tasks',
}, ...openProjectUpdateAndReturnToTasks("Activity", "o_activity_view"),
]);
run: "click",
},
]});