mirror of
https://github.com/bringout/oca-ocb-project.git
synced 2026-04-20 11:02:00 +02:00
19.0 vanilla
This commit is contained in:
parent
a2f74aefd8
commit
4a4d12c333
844 changed files with 212348 additions and 270090 deletions
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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."
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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([]);
|
||||
});
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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"
|
||||
});
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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" });
|
||||
});
|
||||
|
|
@ -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"]);
|
||||
});
|
||||
|
|
@ -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",
|
||||
]);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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",
|
||||
});
|
||||
});
|
||||
|
|
@ -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")');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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",
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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",
|
||||
});
|
||||
});
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
|
@ -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",
|
||||
]);
|
||||
});
|
||||
|
|
@ -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
|
||||
);
|
||||
});
|
||||
|
|
@ -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",
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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",
|
||||
});
|
||||
});
|
||||
|
|
@ -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")',
|
||||
}]});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}]});
|
||||
|
|
|
|||
|
|
@ -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')" },
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
],
|
||||
});
|
||||
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -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')",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -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",
|
||||
},
|
||||
]});
|
||||
|
|
@ -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",
|
||||
},
|
||||
]});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue