[REF] web,*: remove some legacy test helpers

Part of task~3439226

Part-of: odoo/odoo#129725
This commit is contained in:
Aaron Bohy
2023-07-26 13:22:12 +02:00
parent f478310d17
commit f40d4dc98b
17 changed files with 85 additions and 1575 deletions
@@ -7,9 +7,7 @@ import {
dragAndDrop,
getFixture,
} from '@web/../tests/helpers/utils';
import testUtils from 'web.test_utils';
import { serializeDateTime } from "@web/core/l10n/dates";
const find = testUtils.dom.find;
let target;
@@ -222,7 +220,7 @@ QUnit.module('Crm Rainbowman Triggers', {
mockRPC: getMockRpc(assert),
});
await dragAndDrop(find(target, ".o_kanban_record", "Lead 6"), target.querySelector('.o_kanban_group:nth-of-type(3)'));
await dragAndDrop($(target).find(".o_kanban_record:contains(Lead 6)")[0], target.querySelector('.o_kanban_group:nth-of-type(3)'));
assert.verifySteps(['Go, go, go! Congrats for your first deal.']);
});
@@ -234,7 +232,7 @@ QUnit.module('Crm Rainbowman Triggers', {
mockRPC: getMockRpc(assert),
});
await dragAndDrop(find(target, ".o_kanban_record", "Lead 2"), target.querySelector('.o_kanban_group:nth-of-type(3)'));
await dragAndDrop($(target).find(".o_kanban_record:contains(Lead 2)")[0], target.querySelector('.o_kanban_group:nth-of-type(3)'));
assert.verifySteps(['Boom! Team record for the past 30 days.']);
});
@@ -246,7 +244,7 @@ QUnit.module('Crm Rainbowman Triggers', {
mockRPC: getMockRpc(assert),
});
await dragAndDrop(find(target, ".o_kanban_record", "Lead 1"), target.querySelector('.o_kanban_group:nth-of-type(3)'));
await dragAndDrop($(target).find(".o_kanban_record:contains(Lead 1)")[0], target.querySelector('.o_kanban_group:nth-of-type(3)'));
assert.verifySteps(['Yeah! Deal of the last 7 days for the team.']);
});
@@ -258,7 +256,7 @@ QUnit.module('Crm Rainbowman Triggers', {
mockRPC: getMockRpc(assert),
});
await dragAndDrop(find(target, ".o_kanban_record", "Lead 8"), target.querySelector('.o_kanban_group:nth-of-type(3)'));
await dragAndDrop($(target).find(".o_kanban_record:contains(Lead 8)")[0], target.querySelector('.o_kanban_group:nth-of-type(3)'));
assert.verifySteps(['You just beat your personal record for the past 30 days.']);
});
@@ -270,7 +268,7 @@ QUnit.module('Crm Rainbowman Triggers', {
mockRPC: getMockRpc(assert),
});
await dragAndDrop(find(target, ".o_kanban_record", "Lead 10"), target.querySelector('.o_kanban_group:nth-of-type(3)'));
await dragAndDrop($(target).find(".o_kanban_record:contains(Lead 10)")[0], target.querySelector('.o_kanban_group:nth-of-type(3)'));
assert.verifySteps(['You just beat your personal record for the past 7 days.']);
});
@@ -282,7 +280,7 @@ QUnit.module('Crm Rainbowman Triggers', {
mockRPC: getMockRpc(assert),
});
await dragAndDrop(find(target, ".o_kanban_record", "Lead 8"), target.querySelector('.o_kanban_group:nth-of-type(2)'));
await dragAndDrop($(target).find(".o_kanban_record:contains(Lead 8)")[0], target.querySelector('.o_kanban_group:nth-of-type(2)'));
assert.verifySteps(["no rainbowman"]);
});
@@ -8,8 +8,6 @@ import {
getFixture,
patchDate,
} from '@web/../tests/helpers/utils';
import testUtils from 'web.test_utils';
const find = testUtils.dom.find;
const serviceRegistry = registry.category("services");
@@ -85,7 +83,7 @@ QUnit.module('Crm Forecast Model Extension', {
"2nd column March should contain 2 records");
// remove the filter
await click(find(target, '.o_searchview_facet', "Forecast"), '.o_facet_remove');
await click($(target).find(".o_searchview_facet:contains(Forecast)")[0], ".o_facet_remove");
assert.containsN(target, '.o_kanban_group', 3, "There should be 3 columns");
assert.containsN(target, '.o_kanban_group:nth-child(1) .o_kanban_record', 2,
@@ -118,7 +116,7 @@ QUnit.module('Crm Forecast Model Extension', {
"2nd column March should contain 2 records");
// remove the filter
await click(find(target, '.o_searchview_facet', "Forecast"), '.o_facet_remove');
await click($(target).find(".o_searchview_facet:contains(Forecast)")[0], ".o_facet_remove");
assert.containsN(target, '.o_kanban_group', 3, "There should be 3 columns");
assert.containsN(target, '.o_kanban_group:nth-child(1) .o_kanban_record', 2,
@@ -1,13 +1,12 @@
/** @odoo-module */
import testUtils from "web.test_utils";
import { mount } from "@web/../tests/helpers/utils";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import { getFixture, mount } from "@web/../tests/helpers/utils";
import { useService } from "@web/core/utils/hooks";
import { numberBufferService } from "@point_of_sale/app/utils/number_buffer_service";
import { registry } from "@web/core/registry";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import { Component, useState, xml } from "@odoo/owl";
QUnit.module("unit tests for NumberBuffer", {
@@ -19,7 +18,7 @@ QUnit.module("unit tests for NumberBuffer", {
QUnit.test("simple fast inputs with capture in between", async function (assert) {
assert.expect(3);
const target = testUtils.prepareTarget();
const target = getFixture();
const env = await makeTestEnv();
class Root extends Component {
@@ -3,7 +3,7 @@
import makeTestEnvironment from "web.test_env";
import Popover from "web.Popover";
import testUtils from "web.test_utils";
import { click, mount } from "@web/../tests/helpers/utils";
import { click, getFixture, mount } from "@web/../tests/helpers/utils";
import { LegacyComponent } from "@web/legacy/legacy_component";
const { useState, xml } = owl;
@@ -46,7 +46,7 @@
</Popover>
</div>`;
const target = testUtils.prepareTarget();
const target = getFixture();
const env = makeTestEnvironment();
/**
@@ -164,7 +164,7 @@
<span id="dismissPopovers">💀</span>
</div>`;
const target = testUtils.prepareTarget();
const target = getFixture();
const env = makeTestEnvironment();
const body = document.querySelector('body');
@@ -204,7 +204,7 @@
`;
Parent.components = { Popover };
const target = testUtils.prepareTarget();
const target = getFixture();
const env = makeTestEnvironment();
await mount(Parent, target, { env });
@@ -248,7 +248,7 @@
</div>
`;
const target = testUtils.prepareTarget();
const target = getFixture();
const env = makeTestEnvironment();
await mount(Parent, target, { env });
@@ -4,23 +4,14 @@
* Test Utils
*
* In this module, we define various utility functions to help simulate a mock
* environment as close as possible as a real environment. The main function is
* certainly createView, which takes a bunch of parameters and give you back an
* instance of a view, appended in the dom, ready to be tested.
* environment as close as possible as a real environment.
*/
import testUtilsCreate from "web.test_utils_create";
import testUtilsControlPanel from "web.test_utils_control_panel";
import testUtilsDom from "web.test_utils_dom";
import testUtilsFields from "web.test_utils_fields";
import testUtilsFile from "web.test_utils_file";
import testUtilsForm from "web.test_utils_form";
import testUtilsGraph from "web.test_utils_graph";
import testUtilsKanban from "web.test_utils_kanban";
import testUtilsMock from "web.test_utils_mock";
import testUtilsModal from "web.test_utils_modal";
import testUtilsPivot from "web.test_utils_pivot";
import Widget from "web.Widget";
function deprecated(fn, type) {
return function () {
@@ -31,6 +22,19 @@
};
}
/**
* Create a widget parent from given parameters.
*
* @param {Object} params This object will be given to addMockEnvironment, so
* any parameters from that method applies
* @returns {Promise<Widget>}
*/
async function createParent(params) {
const widget = new Widget();
await testUtilsMock.addMockEnvironment(widget, params);
return widget;
}
/**
* Helper function, make a promise with a public resolve function. Note that
* this is not standard and should not be used outside of tests...
@@ -94,25 +98,6 @@
return testUtilsDom.returnAfterNextAnimationFrame();
}
/**
* Awaits for an additionnal rendering frame initiated by the Owl
* compatibility layer processing.
*
* By default a simple "nextTick" will handle the rendering of any widget/
* component stuctures having at most 1 switch between the type of
* entities (Component > Widget or Widget > Component). However more time
* must be spent rendering in case we have additionnal switches. In such
* cases this function must be used (1 call for each additionnal switch)
* since it will be removed along with the compatiblity layer once the
* framework has been entirely converted, and using this helper will make
* it easier to wipe it from the code base.
*
* @returns {Promise}
*/
async function owlCompatibilityExtraNextTick() {
return testUtilsDom.returnAfterNextAnimationFrame();
}
export default {
mock: {
addMockEnvironment: testUtilsMock.addMockEnvironment,
@@ -122,106 +107,19 @@
patchDate: testUtilsMock.patchDate,
unpatch: testUtilsMock.unpatch,
getView: testUtilsMock.getView,
patchSetTimeout: testUtilsMock.patchSetTimeout,
},
controlPanel: {
// Generic interactions
toggleMenu: testUtilsControlPanel.toggleMenu,
toggleMenuItem: testUtilsControlPanel.toggleMenuItem,
toggleMenuItemOption: testUtilsControlPanel.toggleMenuItemOption,
isItemSelected: testUtilsControlPanel.isItemSelected,
isOptionSelected: testUtilsControlPanel.isOptionSelected,
getMenuItemTexts: testUtilsControlPanel.getMenuItemTexts,
// Button interactions
getButtons: testUtilsControlPanel.getButtons,
// FilterMenu interactions
toggleFilterMenu: testUtilsControlPanel.toggleFilterMenu,
toggleAddCustomFilter: testUtilsControlPanel.toggleAddCustomFilter,
applyFilter: testUtilsControlPanel.applyFilter,
addCondition: testUtilsControlPanel.addCondition,
// GroupByMenu interactions
toggleGroupByMenu: testUtilsControlPanel.toggleGroupByMenu,
toggleAddCustomGroup: testUtilsControlPanel.toggleAddCustomGroup,
selectGroup: testUtilsControlPanel.selectGroup,
applyGroup: testUtilsControlPanel.applyGroup,
// FavoriteMenu interactions
toggleFavoriteMenu: testUtilsControlPanel.toggleFavoriteMenu,
toggleSaveFavorite: testUtilsControlPanel.toggleSaveFavorite,
editFavoriteName: testUtilsControlPanel.editFavoriteName,
saveFavorite: testUtilsControlPanel.saveFavorite,
deleteFavorite: testUtilsControlPanel.deleteFavorite,
// ComparisonMenu interactions
toggleComparisonMenu: testUtilsControlPanel.toggleComparisonMenu,
// SearchBar interactions
getFacetTexts: testUtilsControlPanel.getFacetTexts,
removeFacet: testUtilsControlPanel.removeFacet,
editSearch: testUtilsControlPanel.editSearch,
validateSearch: testUtilsControlPanel.validateSearch,
// Action menus interactions
toggleActionMenu: testUtilsControlPanel.toggleActionMenu,
// Pager interactions
pagerPrevious: testUtilsControlPanel.pagerPrevious,
pagerNext: testUtilsControlPanel.pagerNext,
getPagerValue: testUtilsControlPanel.getPagerValue,
getPagerSize: testUtilsControlPanel.getPagerSize,
setPagerValue: testUtilsControlPanel.setPagerValue,
// View switcher
switchView: testUtilsControlPanel.switchView,
},
dom: {
triggerKeypressEvent: testUtilsDom.triggerKeypressEvent,
triggerMouseEvent: testUtilsDom.triggerMouseEvent,
triggerPositionalMouseEvent: testUtilsDom.triggerPositionalMouseEvent,
triggerPositionalTapEvents: testUtilsDom.triggerPositionalTapEvents,
dragAndDrop: testUtilsDom.dragAndDrop,
find: testUtilsDom.findItem,
getNode: testUtilsDom.getNode,
openDatepicker: testUtilsDom.openDatepicker,
click: testUtilsDom.click,
clickFirst: testUtilsDom.clickFirst,
clickLast: testUtilsDom.clickLast,
triggerEvents: testUtilsDom.triggerEvents,
triggerEvent: testUtilsDom.triggerEvent,
},
form: {
clickEdit: testUtilsForm.clickEdit,
clickSave: testUtilsForm.clickSave,
clickCreate: testUtilsForm.clickCreate,
clickDiscard: testUtilsForm.clickDiscard,
reload: testUtilsForm.reload,
},
graph: {
reload: testUtilsGraph.reload,
},
kanban: {
reload: testUtilsKanban.reload,
clickCreate: testUtilsKanban.clickCreate,
quickCreate: testUtilsKanban.quickCreate,
toggleGroupSettings: testUtilsKanban.toggleGroupSettings,
toggleRecordDropdown: testUtilsKanban.toggleRecordDropdown,
},
modal: {
clickButton: testUtilsModal.clickButton,
},
pivot: {
clickMeasure: testUtilsPivot.clickMeasure,
toggleMeasuresDropdown: testUtilsPivot.toggleMeasuresDropdown,
reload: testUtilsPivot.reload,
},
fields: {
many2one: {
createAndEdit: testUtilsFields.clickM2OCreateAndEdit,
clickOpenDropdown: testUtilsFields.clickOpenM2ODropdown,
clickHighlightedItem: testUtilsFields.clickM2OHighlightedItem,
clickItem: testUtilsFields.clickM2OItem,
searchAndClickItem: testUtilsFields.searchAndClickM2OItem,
},
editInput: testUtilsFields.editInput,
editSelect: testUtilsFields.editSelect,
editAndTrigger: testUtilsFields.editAndTrigger,
triggerKey: testUtilsFields.triggerKey,
triggerKeydown: testUtilsFields.triggerKeydown,
triggerKeyup: testUtilsFields.triggerKeyup,
},
file: {
createFile: testUtilsFile.createFile,
@@ -231,19 +129,11 @@
inputFiles: testUtilsFile.inputFiles,
},
createComponent: testUtilsCreate.createComponent,
createAsyncView: testUtilsCreate.createView,
createCalendarView: testUtilsCreate.createCalendarView,
createView: testUtilsCreate.createView,
createModel: testUtilsCreate.createModel,
createParent: testUtilsCreate.createParent,
createParent,
makeTestPromise: makeTestPromise,
makeTestPromiseWithAssert: makeTestPromiseWithAssert,
nextMicrotaskTick: nextMicrotaskTick,
nextTick: nextTick,
owlCompatibilityExtraNextTick,
prepareTarget: testUtilsCreate.prepareTarget,
returnAfterNextAnimationFrame: testUtilsDom.returnAfterNextAnimationFrame,
// backward-compatibility
addMockEnvironment: deprecated(testUtilsMock.addMockEnvironment, 'mock'),
@@ -253,8 +143,5 @@
openDatepicker: deprecated(testUtilsDom.openDatepicker, 'dom'),
patch: deprecated(testUtilsMock.patch, 'mock'),
patchDate: deprecated(testUtilsMock.patchDate, 'mock'),
triggerKeypressEvent: deprecated(testUtilsDom.triggerKeypressEvent, 'dom'),
triggerMouseEvent: deprecated(testUtilsDom.triggerMouseEvent, 'dom'),
triggerPositionalMouseEvent: deprecated(testUtilsDom.triggerPositionalMouseEvent, 'dom'),
unpatch: deprecated(testUtilsMock.unpatch, 'mock'),
};
@@ -1,358 +0,0 @@
/** @odoo-module alias=web.test_utils_control_panel **/
import { click, findItem, getNode, triggerEvent } from "web.test_utils_dom";
import { editInput, editSelect, editAndTrigger } from "web.test_utils_fields";
//-------------------------------------------------------------------------
// Exported functions
//-------------------------------------------------------------------------
/**
* @param {EventTarget} el
* @param {(number|string)} menuFinder
* @returns {Promise}
*/
async function toggleMenu(el, menuFinder) {
const menu = findItem(el, `.dropdown > button`, menuFinder);
await click(menu);
}
/**
* @param {EventTarget} el
* @param {(number|string)} itemFinder
* @returns {Promise}
*/
async function toggleMenuItem(el, itemFinder) {
const item = findItem(el, `.o_menu_item > a`, itemFinder);
await click(item);
}
/**
* @param {EventTarget} el
* @param {(number|string)} itemFinder
* @param {(number|string)} optionFinder
* @returns {Promise}
*/
async function toggleMenuItemOption(el, itemFinder, optionFinder) {
const item = findItem(el, `.o_menu_item > a`, itemFinder);
const option = findItem(item.parentNode, '.o_item_option > a', optionFinder);
await click(option);
}
/**
* @param {EventTarget} el
* @param {(number|string)} itemFinder
* @returns {boolean}
*/
function isItemSelected(el, itemFinder) {
const item = findItem(el, `.o_menu_item > a`, itemFinder);
return item.classList.contains('selected');
}
/**
* @param {EventTarget} el
* @param {(number|string)} itemuFinder
* @param {(number|string)} optionFinder
* @returns {boolean}
*/
function isOptionSelected(el, itemFinder, optionFinder) {
const item = findItem(el, `.o_menu_item > a`, itemFinder);
const option = findItem(item.parentNode, '.o_item_option > a', optionFinder);
return option.classList.contains('selected');
}
/**
* @param {EventTarget} el
* @returns {string[]}
*/
function getMenuItemTexts(el) {
return [...getNode(el).querySelectorAll(`.dropdown ul .o_menu_item`)].map(
e => e.innerText.trim()
);
}
/**
* @param {EventTarget} el
* @returns {HTMLButtonElement[]}
*/
function getButtons(el) {
return [...getNode(el).querySelector((`div.o_cp_bottom div.o_cp_buttons`)).children];
}
/**
* @param {EventTarget} el
* @returns {Promise}
*/
async function toggleFilterMenu(el) {
await click(getNode(el).querySelector(`.o_filter_menu button`));
}
/**
* @param {EventTarget} el
* @returns {Promise}
*/
async function toggleAddCustomFilter(el) {
await click(getNode(el).querySelector(`button.o_add_custom_filter`));
}
/**
* @param {EventTarget} el
* @returns {Promise}
*/
async function applyFilter(el) {
await click(getNode(el).querySelector(`div.o_add_filter_menu > button.o_apply_filter`));
}
/**
* @param {EventTarget} el
* @returns {Promise}
*/
async function addCondition(el) {
await click(getNode(el).querySelector(`div.o_add_filter_menu > button.o_add_condition`));
}
/**
* @param {EventTarget} el
* @returns {Promise}
*/
async function toggleGroupByMenu(el) {
await click(getNode(el).querySelector(`.o_group_by_menu button`));
}
/**
* @param {EventTarget} el
* @returns {Promise}
*/
async function toggleAddCustomGroup(el) {
await click(getNode(el).querySelector(`span.o_add_custom_group_by`));
}
/**
* @param {EventTarget} el
* @param {string} fieldName
* @returns {Promise}
*/
async function selectGroup(el, fieldName) {
await editSelect(
getNode(el).querySelector(`select.o_group_by_selector`),
fieldName
);
}
/**
* @param {EventTarget} el
* @returns {Promise}
*/
async function applyGroup(el) {
await click(getNode(el).querySelector(`div.o_add_group_by_menu > button.o_apply_group_by`));
}
/**
* @param {EventTarget} el
* @returns {Promise}
*/
async function toggleFavoriteMenu(el) {
await click(getNode(el).querySelector(`.o_favorite_menu button`));
}
/**
* @param {EventTarget} el
* @returns {Promise}
*/
async function toggleSaveFavorite(el) {
await click(getNode(el).querySelector(`.o_favorite_menu .o_add_favorite .dropdown-item`));
}
/**
* @param {EventTarget} el
* @param {string} name
* @returns {Promise}
*/
async function editFavoriteName(el, name) {
await editInput(getNode(el).querySelector(`.o_favorite_menu .o_add_favorite input[type="text"]`), name);
}
/**
* @param {EventTarget} el
* @returns {Promise}
*/
async function saveFavorite(el) {
await click(getNode(el).querySelector(`.o_favorite_menu .o_add_favorite button.o_save_favorite`));
}
/**
* @param {EventTarget} el
* @param {(string|number)} favoriteFinder
* @returns {Promise}
*/
async function deleteFavorite(el, favoriteFinder) {
const favorite = findItem(el, `.o_favorite_menu .o_menu_item`, favoriteFinder);
await click(favorite.querySelector('i.fa-trash'));
}
/**
* @param {EventTarget} el
* @returns {Promise}
*/
async function toggleComparisonMenu(el) {
await click(getNode(el).querySelector(`div.o_comparison_menu > button`));
}
/**
* @param {EventTarget} el
* @returns {Promise}
*/
function getFacetTexts(el) {
return [...getNode(el).querySelectorAll(`.o_searchview .o_searchview_facet`)].map(
facet => facet.innerText.trim()
);
}
/**
* @param {EventTarget} el
* @param {(string|number)} facetFinder
* @returns {Promise}
*/
async function removeFacet(el, facetFinder = 0) {
const facet = findItem(el, `.o_searchview .o_searchview_facet`, facetFinder);
await click(facet.querySelector('.o_facet_remove'));
}
/**
* @param {EventTarget} el
* @param {string} value
* @returns {Promise}
*/
async function editSearch(el, value) {
await editInput(getNode(el).querySelector(`.o_searchview_input`), value);
}
/**
* @param {EventTarget} el
* @returns {Promise}
*/
async function validateSearch(el) {
await triggerEvent(
getNode(el).querySelector(`.o_searchview_input`),
'keydown', { key: 'Enter' }
);
}
/**
* @param {EventTarget} el
* @param {string} [menuFinder="Action"]
* @returns {Promise}
*/
async function toggleActionMenu(el, menuFinder = "Action") {
const dropdown = findItem(el, `.o_cp_action_menus button`, menuFinder);
await click(dropdown);
}
/**
* @param {EventTarget} el
* @returns {Promise}
*/
async function pagerPrevious(el) {
await click(getNode(el).querySelector(`.o_pager button.o_pager_previous`));
}
/**
* @param {EventTarget} el
* @returns {Promise}
*/
async function pagerNext(el) {
await click(getNode(el).querySelector(`.o_pager button.o_pager_next`));
}
/**
* @param {EventTarget} el
* @returns {string}
*/
function getPagerValue(el) {
const pagerValue = getNode(el).querySelector(`.o_pager_counter .o_pager_value`);
switch (pagerValue.tagName) {
case 'INPUT':
return pagerValue.value;
case 'SPAN':
return pagerValue.innerText.trim();
}
}
/**
* @param {EventTarget} el
* @returns {string}
*/
function getPagerSize(el) {
return getNode(el).querySelector(`.o_pager_counter span.o_pager_limit`).innerText.trim();
}
/**
* @param {EventTarget} el
* @param {string} value
* @returns {Promise}
*/
async function setPagerValue(el, value) {
let pagerValue = getNode(el).querySelector(`.o_pager_counter .o_pager_value`);
if (pagerValue.tagName === 'SPAN') {
await click(pagerValue);
}
pagerValue = getNode(el).querySelector(`.o_pager_counter input.o_pager_value`);
if (!pagerValue) {
throw new Error("Pager value is being edited and cannot be changed.");
}
await editAndTrigger(pagerValue, value, ['change', 'blur']);
}
/**
* @param {EventTarget} el
* @param {string} viewType
* @returns {Promise}
*/
async function switchView(el, viewType) {
await click(getNode(el).querySelector(`button.o_switch_view.o_${viewType}`));
}
export default {
// Generic interactions
toggleMenu,
toggleMenuItem,
toggleMenuItemOption,
isItemSelected,
isOptionSelected,
getMenuItemTexts,
// Button interactions
getButtons,
// FilterMenu interactions
toggleFilterMenu,
toggleAddCustomFilter,
applyFilter,
addCondition,
// GroupByMenu interactions
toggleGroupByMenu,
toggleAddCustomGroup,
selectGroup,
applyGroup,
// FavoriteMenu interactions
toggleFavoriteMenu,
toggleSaveFavorite,
editFavoriteName,
saveFavorite,
deleteFavorite,
// ComparisonMenu interactions
toggleComparisonMenu,
// SearchBar interactions
getFacetTexts,
removeFacet,
editSearch,
validateSearch,
// Action menus interactions
toggleActionMenu,
// Pager interactions
pagerPrevious,
pagerNext,
getPagerValue,
getPagerSize,
setPagerValue,
// View switcher
switchView,
};
@@ -1,297 +0,0 @@
/** @odoo-module alias=web.test_utils_create **/
/**
* Create Test Utils
*
* This module defines various utility functions to help creating mock widgets
*
* Note that all methods defined in this module are exported in the main
* testUtils file.
*/
import concurrency from "web.concurrency";
import { useListener } from "@web/core/utils/hooks";
import dom from "web.dom";
import testUtilsMock from "web.test_utils_mock";
import Widget from "web.Widget";
import { destroy, getFixture, mount, useChild } from "@web/../tests/helpers/utils";
import { registerCleanup } from "@web/../tests/helpers/cleanup";
import { LegacyComponent } from "@web/legacy/legacy_component";
const { Component, useState, xml } = owl;
/**
* Similar as createView, but specific for calendar views. Some calendar
* tests need to trigger positional clicks on the DOM produced by fullcalendar.
* Those tests must use this helper with option positionalClicks set to true.
* This will move the rendered calendar to the body (required to do positional
* clicks), and wait for a setTimeout(0) before returning, because fullcalendar
* makes the calendar scroll to 6:00 in a setTimeout(0), which might have an
* impact according to where we want to trigger positional clicks.
*
* @param {Object} params @see createView
* @param {Object} [options]
* @param {boolean} [options.positionalClicks=false]
* @returns {Promise<CalendarController>}
*/
async function createCalendarView(params, options) {
const calendar = await createView(params);
if (!options || !options.positionalClicks) {
return calendar;
}
const viewElements = [...document.getElementById('qunit-fixture').children];
// prepend reset the scrollTop to zero so we restore it manually
let fcScroller = document.querySelector('.fc-scroller');
const scrollPosition = fcScroller.scrollTop;
viewElements.forEach(el => document.body.prepend(el));
fcScroller = document.querySelector('.fc-scroller');
fcScroller.scrollTop = scrollPosition;
const destroy = calendar.destroy;
calendar.destroy = () => {
viewElements.forEach(el => el.remove());
destroy();
};
await concurrency.delay(0);
return calendar;
}
/**
* Create a simple component environment with a basic Parent component, an
* extensible env and a mocked server. The returned value is the instance of
* the given constructor.
* @param {class} constructor Component class to instantiate
* @param {Object} [params = {}]
* @param {boolean} [params.debug]
* @param {Object} [params.env]
* @param {Object} [params.intercepts] object in which the keys represent the
* intercepted event names and the values are their callbacks.
* @param {Object} [params.props]
* @returns {Promise<Component>} instance of `constructor`
*/
async function createComponent(constructor, params = {}) {
if (!constructor) {
throw new Error(`Missing argument "constructor".`);
}
if (!(constructor.prototype instanceof Component)) {
throw new Error(`Argument "constructor" must be an Owl Component.`);
}
const cleanUp = await testUtilsMock.addMockEnvironmentOwl(Component, params);
class Parent extends LegacyComponent {
setup() {
this.Component = constructor;
this.state = useState(params.props || {});
for (const eventName in params.intercepts || {}) {
useListener(eventName, params.intercepts[eventName]);
}
useChild();
}
}
Parent.template = xml`<t t-component="Component" t-props="state"/>`;
const target = getFixture();
const env = Component.env;
const parent = await mount(Parent, target, { env });
registerCleanup(cleanUp);
registerCleanup(() => {
destroy(parent);
});
return parent.child;
}
/**
* Create a model from given parameters.
*
* @param {Object} params This object will be given to addMockEnvironment, so
* any parameters from that method applies
* @param {Class} params.Model the model class to use
* @returns {Model}
*/
async function createModel(params) {
const widget = new Widget();
const model = new params.Model(widget, params);
await testUtilsMock.addMockEnvironment(widget, params);
// override the model's 'destroy' so that it calls 'destroy' on the widget
// instead, as the widget is the parent of the model and the mockServer.
model.destroy = function () {
// remove the override to properly destroy the model when it will be
// called the second time (by its parent)
delete model.destroy;
widget.destroy();
};
return model;
}
/**
* Create a widget parent from given parameters.
*
* @param {Object} params This object will be given to addMockEnvironment, so
* any parameters from that method applies
* @returns {Promise<Widget>}
*/
async function createParent(params) {
const widget = new Widget();
await testUtilsMock.addMockEnvironment(widget, params);
return widget;
}
/**
* Create a view from various parameters. Here, a view means a javascript
* instance of an AbstractView class, such as a form view, a list view or a
* kanban view.
*
* It returns the instance of the view, properly created, with all rpcs going
* through a mock method using the data object as source, and already loaded/
* started.
*
* @param {Object} params
* @param {string} params.arch the xml (arch) of the view to be instantiated
* @param {any[]} [params.domain] the initial domain for the view
* @param {Object} [params.context] the initial context for the view
* @param {string[]} [params.groupBy] the initial groupBy for the view
* @param {Object[]} [params.favoriteFilters] the favorite filters one would like to have at initialization
* @param {integer} [params.fieldDebounce=0] the debounce value to use for the
* duration of the test.
* @param {AbstractView} params.View the class that will be instantiated
* @param {string} params.model a model name, will be given to the view
* @param {Object} params.intercepts an object with event names as key, and
* callback as value. Each key,value will be used to intercept the event.
* Note that this is particularly useful if you want to intercept events going
* up in the init process of the view, because there are no other way to do it
* after this method returns
* @param {Boolean} [params.doNotDisableAHref=false] will not preventDefault on the A elements of the view if true.
* Default is false.
* @param {Boolean} [params.touchScreen=false] will add the o_touch_device to the webclient (flag used to define a
* device with a touch screen. Default value is false
* @returns {Promise<AbstractController>} the instance of the view
*/
async function createView(params) {
const target = prepareTarget(params.debug);
const widget = new Widget();
// reproduce the DOM environment of views
const webClient = Object.assign(document.createElement('div'), {
className: params.touchScreen ? 'o_web_client o_touch_device' : 'o_web_client',
});
const actionManager = Object.assign(document.createElement('div'), {
className: 'o_action_manager',
});
const dialogContainer = Object.assign(document.createElement('div'), {
className: 'o_dialog_container',
});
target.prepend(webClient);
webClient.append(actionManager);
webClient.append(dialogContainer);
// add mock environment: mock server, session, fieldviewget, ...
const mockServer = await testUtilsMock.addMockEnvironment(widget, params);
const viewInfo = testUtilsMock.getView(mockServer, params);
params.server = mockServer;
// create the view
const View = params.View;
const modelName = params.model || 'foo';
const defaultAction = {
res_model: modelName,
context: {},
type: 'ir.actions.act_window',
};
const viewOptions = Object.assign({
action: Object.assign(defaultAction, params.action),
view: { ...viewInfo, fields: mockServer.fieldsGet(params.model) },
modelName: modelName,
ids: 'res_id' in params ? [params.res_id] : undefined,
currentId: 'res_id' in params ? params.res_id : undefined,
domain: params.domain || [],
context: params.context || {},
hasActionMenus: false,
}, params.viewOptions);
// patch the View to handle the groupBy given in params, as we can't give it
// in init (unlike the domain and context which can be set in the action)
testUtilsMock.patch(View, {
_updateMVCParams() {
this._super(...arguments);
this.loadParams.groupedBy = params.groupBy || viewOptions.groupBy || [];
testUtilsMock.unpatch(View);
},
});
if ('hasSelectors' in params) {
viewOptions.hasSelectors = params.hasSelectors;
}
let view;
if (viewInfo.type === 'controlpanel' || viewInfo.type === 'search') {
// TODO: probably needs to create an helper just for that
view = new params.View({ viewInfo, modelName });
} else {
viewOptions.controlPanelFieldsView = Object.assign(testUtilsMock.getView(mockServer, {
arch: params.archs && params.archs[params.model + ',false,search'] || '<search/>',
fields: viewInfo.fields,
model: params.model,
}), { favoriteFilters: params.favoriteFilters });
view = new params.View(viewInfo, viewOptions);
}
if (params.interceptsPropagate) {
for (const name in params.interceptsPropagate) {
testUtilsMock.intercept(widget, name, params.interceptsPropagate[name], true);
}
}
const viewController = await view.getController(widget);
// override the view's 'destroy' so that it calls 'destroy' on the widget
// instead, as the widget is the parent of the view and the mockServer.
viewController.__destroy = viewController.destroy;
viewController.destroy = function () {
// remove the override to properly destroy the viewController and its children
// when it will be called the second time (by its parent)
delete viewController.destroy;
widget.destroy();
webClient.remove();
};
// render the viewController in a fragment as they must be able to render correctly
// without being in the DOM
const fragment = document.createDocumentFragment();
await viewController.appendTo(fragment);
dom.prepend(actionManager, fragment, {
callbacks: [{ widget: viewController }],
in_DOM: true,
});
if (!params.doNotDisableAHref) {
[...viewController.el.getElementsByTagName('A')].forEach(elem => {
elem.addEventListener('click', ev => {
ev.preventDefault();
});
});
}
return viewController;
}
/**
* Get the target (fixture or body) of the document and adds event listeners
* to intercept custom or DOM events.
*
* @param {boolean} [debug=false] if true, the widget will be appended in
* the DOM. Also, RPCs and uncaught OdooEvent will be logged
* @returns {HTMLElement}
*/
function prepareTarget(debug = false) {
document.body.classList.toggle('debug', debug);
return debug ? document.body : document.getElementById('qunit-fixture');
}
export default {
createCalendarView,
createComponent,
createModel,
createParent,
createView,
prepareTarget,
};
@@ -1,9 +1,6 @@
/** @odoo-module alias=web.test_utils_dom **/
import concurrency from "web.concurrency";
import Widget from "web.Widget";
const { Component } = owl;
/**
* DOM Test Utils
@@ -192,21 +189,6 @@
return click(el, Object.assign({}, options, { first: true }));
}
/**
* Click on the last element of a list of elements. Note that if the list has
* only one visible element, we trigger an error. In that case, it is better to
* use the click helper instead.
*
* @param {string|EventTarget|EventTarget[]} el (if string: it is a (jquery) selector)
* @param {boolean} [options={}] click options
* @param {boolean} [options.allowInvisible=false] if true, clicks on the
* element event if it is invisible
* @returns {Promise}
*/
async function clickLast(el, options) {
return click(el, Object.assign({}, options, { last: true }));
}
/**
* Simulate a drag and drop operation between 2 jquery nodes: $el and $to.
* This is a crude simulation, with only the mousedown, mousemove and mouseup
@@ -314,94 +296,6 @@
return returnAfterNextAnimationFrame();
}
/**
* Helper method to retrieve a distinct item from a collection of elements defined
* by the given "selector" string. It can either be the index of the item or its
* inner text.
* @param {Element} el
* @param {string} selector
* @param {number | string} [elFinder=0]
* @returns {Element | null}
*/
function findItem(el, selector, elFinder = 0) {
const elements = [...getNode(el).querySelectorAll(selector)];
if (!elements.length) {
throw new Error(`No element found with selector "${selector}".`);
}
switch (typeof elFinder) {
case "number": {
const match = elements[elFinder];
if (!match) {
throw new Error(
`No element with selector "${selector}" at index ${elFinder}.`
);
}
return match;
}
case "string": {
const match = elements.find(
(el) => el.innerText.trim().toLowerCase() === elFinder.toLowerCase()
);
if (!match) {
throw new Error(
`No element with selector "${selector}" containing "${elFinder}".
`);
}
return match;
}
default: throw new Error(
`Invalid provided element finder: must be a number|string|function.`
);
}
}
/**
* Helper function used to extract an HTML EventTarget element from a given
* target. The extracted element will depend on the target type:
* - Component|Widget -> el
* - jQuery -> associated element (must have 1)
* - HTMLCollection (or similar) -> first element (must have 1)
* - string -> result of document.querySelector with string
* - else -> as is
* @private
* @param {(Component|Widget|jQuery|HTMLCollection|HTMLElement|string)} target
* @returns {EventTarget}
*/
function getNode(target) {
let nodes;
if (target instanceof Component || target instanceof Widget) {
nodes = [target.el];
} else if (typeof target === 'string') {
nodes = document.querySelectorAll(target);
} else if (target === jQuery) { // jQuery (or $)
nodes = [document.body];
} else if (target.length) { // jQuery instance, HTMLCollection or array
nodes = target;
} else {
nodes = [target];
}
if (nodes.length !== 1) {
throw new Error(`Found ${nodes.length} nodes instead of 1.`);
}
const node = nodes[0];
if (!node) {
throw new Error(`Expected a node and got ${node}.`);
}
if (!_isEventTarget(node)) {
throw new Error(`Expected node to be an instance of EventTarget and got ${node.constructor.name}.`);
}
return node;
}
/**
* Open the datepicker of a given element.
*
* @param {jQuery} $datepickerEl element to which a datepicker is attached
*/
async function openDatepicker($datepickerEl) {
return click($datepickerEl.find('.o_datepicker_input'));
}
/**
* Returns a promise that will be resolved after the nextAnimationFrame after
* the next tick
@@ -484,122 +378,11 @@
}
}
/**
* Simulate a keypress event for a given character
*
* @param {string} char the character, or 'ENTER'
* @returns {Promise}
*/
async function triggerKeypressEvent(char) {
let keycode;
if (char === 'Enter') {
keycode = $.ui.keyCode.ENTER;
} else if (char === "Tab") {
keycode = $.ui.keyCode.TAB;
} else {
keycode = char.charCodeAt(0);
}
return triggerEvent(document.body, 'keypress', {
key: char,
keyCode: keycode,
which: keycode,
});
}
/**
* simulate a mouse event with a custom event who add the item position. This is
* sometimes necessary because the basic way to trigger an event (such as
* $el.trigger('mousemove')); ) is too crude for some uses.
*
* @param {jQuery|EventTarget} $el
* @param {string} type a mouse event type, such as 'mousedown' or 'mousemove'
* @returns {Promise}
*/
async function triggerMouseEvent($el, type) {
const el = $el instanceof jQuery ? $el[0] : $el;
if (!el) {
throw new Error(`no target found to trigger MouseEvent`);
}
const rect = el.getBoundingClientRect();
// try to click around the center of the element, biased to the bottom
// right as chrome messes up when clicking on the top-left corner...
const left = rect.x + Math.ceil(rect.width / 2);
const top = rect.y + Math.ceil(rect.height / 2);
return triggerEvent(el, type, {which: 1, clientX: left, clientY: top});
}
/**
* simulate a mouse event with a custom event on a position x and y. This is
* sometimes necessary because the basic way to trigger an event (such as
* $el.trigger('mousemove')); ) is too crude for some uses.
*
* @param {integer} x
* @param {integer} y
* @param {string} type a mouse event type, such as 'mousedown' or 'mousemove'
* @returns {HTMLElement}
*/
async function triggerPositionalMouseEvent(x, y, type) {
const ev = document.createEvent("MouseEvent");
const el = document.elementFromPoint(x, y);
ev.initMouseEvent(
type,
true /* bubble */,
true /* cancelable */,
window, null,
x, y, x, y, /* coordinates */
false, false, false, false, /* modifier keys */
0 /*left button*/, null
);
el.dispatchEvent(ev);
return el;
}
/**
* Simulate a "TAP" (touch) event with a custom position x and y.
*
* @param {number} x
* @param {number} y
* @returns {HTMLElement}
*/
async function triggerPositionalTapEvents(x, y) {
const element = document.elementFromPoint(x, y);
const touch = new Touch({
identifier: 0,
target: element,
clientX: x,
clientY: y,
pageX: x,
pageY: y,
});
await triggerEvent(element, 'touchstart', {
touches: [touch],
targetTouches: [touch],
changedTouches: [touch],
});
await triggerEvent(element, 'touchmove', {
touches: [touch],
targetTouches: [touch],
changedTouches: [touch],
});
await triggerEvent(element, 'touchend', {
changedTouches: [touch],
});
return element;
}
export default {
click,
clickFirst,
clickLast,
dragAndDrop,
findItem,
getNode,
openDatepicker,
returnAfterNextAnimationFrame,
triggerEvent,
triggerEvents,
triggerKeypressEvent,
triggerMouseEvent,
triggerPositionalMouseEvent,
triggerPositionalTapEvents,
};
@@ -22,77 +22,6 @@
// Public functions
//-------------------------------------------------------------------------
/**
* Autofills the input of a many2one field and clicks on the "Create and Edit" option.
*
* @param {string} fieldName
* @param {string} text Used as default value for the record name
* @see clickM2OItem
*/
async function clickM2OCreateAndEdit(fieldName, text = "ABC") {
await clickOpenM2ODropdown(fieldName);
const match = document.querySelector(`.o_field_many2one[name=${fieldName}] input`);
await editInput(match, text);
return clickM2OItem(fieldName, "Create and Edit");
}
/**
* Click on the active (highlighted) selection in a m2o dropdown.
*
* @param {string} fieldName
* @param {[string]} selector if set, this will restrict the search for the m2o
* input
* @returns {Promise}
*/
async function clickM2OHighlightedItem(fieldName, selector) {
const m2oSelector = `${selector || ''} .o_field_many2one[name=${fieldName}] input`;
const $dropdown = $(m2oSelector).autocomplete('widget');
// clicking on an li (no matter which one), will select the focussed one
return testUtilsDom.click($dropdown[0].querySelector('li'));
}
/**
* Click on a menuitem in the m2o dropdown. This helper will target an element
* which contains some specific text. Note that it assumes that the dropdown
* is currently open.
*
* Example:
* testUtils.fields.many2one.clickM2OItem('partner_id', 'George');
*
* @param {string} fieldName
* @param {string} searchText
* @returns {Promise}
*/
async function clickM2OItem(fieldName, searchText) {
const m2oSelector = `.o_field_many2one[name=${fieldName}] input`;
const $dropdown = $(m2oSelector).autocomplete('widget');
const $target = $dropdown.find(`li:contains(${searchText})`).first();
if ($target.length !== 1 || !$target.is(':visible')) {
throw new Error('Menu item should be visible');
}
$target.mouseenter(); // This is NOT a mouseenter event. See jquery.js:5516 for more headaches.
return testUtilsDom.click($target);
}
/**
* Click to open the dropdown on a many2one
*
* @param {string} fieldName
* @param {[string]} selector if set, this will restrict the search for the m2o
* input
* @returns {Promise<HTMLInputElement>} the main many2one input
*/
async function clickOpenM2ODropdown(fieldName, selector) {
const m2oSelector = `${selector || ''} .o_field_many2one[name=${fieldName}] input`;
const matches = document.querySelectorAll(m2oSelector);
if (matches.length !== 1) {
throw new Error(`cannot open m2o: selector ${selector} has been found ${matches.length} instead of 1`);
}
await testUtilsDom.click(matches[0]);
return matches[0];
}
/**
* Sets the value of an element and then, trigger all specified events.
* Note that this helper also checks the unicity of the target.
@@ -133,51 +62,6 @@
return editAndTrigger(el, value, ['input']);
}
/**
* Sets the value of a select.
*
* Note that this helper also checks the unicity of the target.
*
* Example:
* testUtils.fields.editSelect($('selector'), 'somevalue');
*
* @param {jQuery|EventTarget} el should target an input, textarea or select
* @param {string|number} value
* @returns {Promise}
*/
function editSelect(el, value) {
return editAndTrigger(el, value, ['change']);
}
/**
* This helper is useful to test many2one fields. Here is what it does:
* - click to open the dropdown
* - enter a search string in the input
* - wait for the selection
* - click on the requested menuitem, or the active one by default
*
* Example:
* testUtils.fields.many2one.searchAndClickM2OItem('partner_id', {search: 'George'});
*
* @param {string} fieldName
* @param {[Object]} [options = {}]
* @param {[string]} [options.selector]
* @param {[string]} [options.search]
* @param {[string]} [options.item]
* @returns {Promise}
*/
async function searchAndClickM2OItem(fieldName, options = {}) {
const input = await clickOpenM2ODropdown(fieldName, options.selector);
if (options.search) {
await editInput(input, options.search);
}
if (options.item) {
return clickM2OItem(fieldName, options.item);
} else {
return clickM2OHighlightedItem(fieldName, options.selector);
}
}
/**
* Helper to trigger a key event on an element.
*
@@ -222,27 +106,8 @@
return triggerKey('down', $el, keyCode);
}
/**
* Helper to trigger a keyup event on an element.
*
* @param {jQuery} $el
* @param {number|string} keyCode @see triggerKey
* @returns {Promise}
*/
function triggerKeyup($el, keyCode) {
return triggerKey('up', $el, keyCode);
}
export default {
clickM2OCreateAndEdit,
clickM2OHighlightedItem,
clickM2OItem,
clickOpenM2ODropdown,
editAndTrigger,
editInput,
editSelect,
searchAndClickM2OItem,
triggerKey,
triggerKeydown,
triggerKeyup,
};
@@ -1,71 +0,0 @@
/** @odoo-module alias=web.test_utils_form **/
/**
* Form Test Utils
*
* This module defines various utility functions to help test form views.
*
* Note that all methods defined in this module are exported in the main
* testUtils file.
*/
import testUtilsDom from "web.test_utils_dom";
/**
* Clicks on the Edit button in a form view, to set it to edit mode. Note that
* it checks that the button is visible, so calling this method in edit mode
* will fail.
*
* @param {FormController} form
*/
function clickEdit(form) {
return testUtilsDom.click(form.$buttons.find('.o_form_button_edit'));
}
/**
* Clicks on the Save button in a form view. Note that this method checks that
* the Save button is visible.
*
* @param {FormController} form
*/
function clickSave(form) {
return testUtilsDom.click(form.$buttons.find('.o_form_button_save'));
}
/**
* Clicks on the Create button in a form view. Note that this method checks that
* the Create button is visible.
*
* @param {FormController} form
*/
function clickCreate(form) {
return testUtilsDom.click(form.$buttons.find('.o_form_button_create'));
}
/**
* Clicks on the Discard button in a form view. Note that this method checks that
* the Discard button is visible.
*
* @param {FormController} form
*/
function clickDiscard(form) {
return testUtilsDom.click(form.$buttons.find('.o_form_button_cancel'));
}
/**
* Reloads a form view.
*
* @param {FormController} form
* @param {[Object]} params given to the controller reload method
*/
function reload(form, params) {
return form.reload(params);
}
export default {
clickEdit: clickEdit,
clickSave: clickSave,
clickCreate: clickCreate,
clickDiscard: clickDiscard,
reload: reload,
};
@@ -1,25 +0,0 @@
/** @odoo-module alias=web.test_utils_graph **/
/**
* Graph Test Utils
*
* This module defines various utility functions to help test graph views.
*
* Note that all methods defined in this module are exported in the main
* testUtils file.
*/
/**
* Reloads a graph view.
*
* @param {GraphController} graph
* @param {[Object]} params given to the controller reload method
*/
function reload(graph, params) {
return graph.reload(params);
}
export default {
reload: reload,
};
@@ -1,99 +0,0 @@
/** @odoo-module alias=web.test_utils_kanban **/
/**
* Kanban Test Utils
*
* This module defines various utility functions to help testing kanban views.
*
* Note that all methods defined in this module are exported in the main
* testUtils file.
*/
import testUtilsDom from "web.test_utils_dom";
import testUtilsFields from "web.test_utils_fields";
/**
* Clicks on the Create button in a kanban view. Note that this method checks that
* the Create button is visible.
*
* @param {KanbanController} kanban
* @returns {Promise}
*/
function clickCreate(kanban) {
return testUtilsDom.click(kanban.$buttons.find('.o-kanban-button-new'));
}
/**
* Open the settings menu for a column (in a grouped kanban view)
*
* @param {jQuery} $column
* @returns {Promise}
*/
function toggleGroupSettings($column) {
var $dropdownToggler = $column.find('.o_kanban_config > a.dropdown-toggle');
if (!$dropdownToggler.is(':visible')) {
$dropdownToggler.css('display', 'block');
}
return testUtilsDom.click($dropdownToggler);
}
/**
* Edit a value in a quickcreate form view (this method assumes that the quick
* create feature is active, and a sub form view is open)
*
* @param {kanbanController} kanban
* @param {string|number} value
* @param {[string]} fieldName
* @returns {Promise}
*/
function quickCreate(kanban, value, fieldName) {
var additionalSelector = fieldName ? ('[name=' + fieldName + ']'): '';
var enterEvent = $.Event(
'keydown',
{
which: $.ui.keyCode.ENTER,
keyCode: $.ui.keyCode.ENTER,
}
);
return testUtilsFields.editAndTrigger(
kanban.$('.o_kanban_quick_create input' + additionalSelector),
value,
['input', enterEvent]
);
}
/**
* Reloads a kanban view.
*
* @param {KanbanController} kanban
* @param {[Object]} params given to the controller reload method
* @returns {Promise}
*/
function reload(kanban, params) {
return kanban.reload(params);
}
/**
* Open the setting dropdown of a kanban record. Note that the template of a
* kanban record is not standardized, so this method will fail if the template
* does not comply with the usual dom structure.
*
* @param {jQuery} $record
* @returns {Promise}
*/
function toggleRecordDropdown($record) {
var $dropdownToggler = $record.find('.o_dropdown_kanban > a.dropdown-toggle');
if (!$dropdownToggler.is(':visible')) {
$dropdownToggler.css('display', 'block');
}
return testUtilsDom.click($dropdownToggler);
}
export default {
clickCreate: clickCreate,
quickCreate: quickCreate,
reload: reload,
toggleGroupSettings: toggleGroupSettings,
toggleRecordDropdown: toggleRecordDropdown,
};
@@ -23,7 +23,6 @@ import session from "web.session";
import { patchWithCleanup, patchDate } from "@web/../tests/helpers/utils";
import { browser } from "@web/core/browser/browser";
import { assets } from "@web/core/assets";
import { processArch } from "@web/legacy/legacy_load_views";
import { Component } from "@odoo/owl";
import { uniqueId } from "@web/core/utils/functions";
@@ -41,12 +40,10 @@ const DebouncedField = basic_fields.DebouncedField;
* @private
* @param {Object} params
* @param {Bus} [params.bus]
* @param {boolean} [params.debug]
* @param {Object} [params.env]
* @param {Bus} [params.env.bus]
* @param {Object} [params.env.dataManager]
* @param {Object} [params.env.services]
* @param {Object[]} [params.favoriteFilters]
* @param {Object} [params.services]
* @param {Object} [params.session]
* @param {MockServer} [mockServer]
@@ -58,8 +55,6 @@ async function _getMockedOwlEnv(params, mockServer) {
const database = {parameters: params.translateParameters || {}};
// build the env
const favoriteFilters = params.favoriteFilters;
const debug = params.debug;
const services = {};
const env = Object.assign({}, params.env, {
_t: params.env && params.env._t || Object.assign((s => s), { database }),
@@ -248,35 +243,6 @@ function _observe(widget) {
// Public functions
//------------------------------------------------------------------------------
/**
* performs a get_view, and mocks the postprocessing done by the
* data_manager to return an equivalent structure.
*
* @param {MockServer} server
* @param {Object} params
* @param {string} params.model
* @returns {Object} an object with 3 keys: arch, fields and viewFields
*/
function getView(server, params) {
var view = server.getView(params);
const fields = server.fieldsGet(params.model);
// mock the structure produced by the DataManager
const models = { [params.model]: fields };
for (const modelName of view.models) {
models[modelName] = models[modelName] || server.fieldsGet(modelName);
}
const { arch, viewFields } = processArch(view.arch, view.type, params.model, models);
return {
arch,
fields,
model: view.model,
toolbar: view.toolbar,
type: view.type,
viewFields,
view_id: view.id,
};
}
/**
* intercepts an event bubbling up the widget hierarchy. The event intercepted
* must be a "custom event", i.e. an event generated by the method 'trigger_up'.
@@ -675,32 +641,11 @@ function unpatch(target) {
delete target.__patchID;
}
window.originalSetTimeout = window.setTimeout;
function patchSetTimeout() {
var original = window.setTimeout;
var self = this;
window.setTimeout = function (handler, delay) {
console.log("calling setTimeout on " + (handler.name || "some function") + "with delay of " + delay);
console.trace();
var handlerArguments = Array.prototype.slice.call(arguments, 1);
return original(function () {
handler.bind(self, handlerArguments)();
console.log('after doing the action of the setTimeout');
}, delay);
};
return function () {
window.setTimeout = original;
};
}
export default {
addMockEnvironment: addMockEnvironment,
getView: getView,
addMockEnvironmentOwl: addMockEnvironmentOwl,
intercept: intercept,
patchDate: legacyPatchDate,
patch: patch,
unpatch: unpatch,
patchSetTimeout: patchSetTimeout,
};
@@ -1,24 +0,0 @@
/** @odoo-module alias=web.test_utils_modal **/
/**
* Modal Test Utils
*
* This module defines various utility functions to help test pivot views.
*
* Note that all methods defined in this module are exported in the main
* testUtils file.
*/
import { _t } from "web.core";
import testUtilsDom from "web.test_utils_dom";
/**
* Click on a button in the footer of a modal (which contains a given string).
*
* @param {string} text (in english: this method will perform the translation)
*/
function clickButton(text) {
return testUtilsDom.click($(`.modal-footer button:contains(${_t(text)})`));
}
export default { clickButton };
@@ -1,54 +0,0 @@
/** @odoo-module alias=web.test_utils_pivot **/
import testUtilsDom from "web.test_utils_dom";
/**
* Pivot Test Utils
*
* This module defines various utility functions to help test pivot views.
*
* Note that all methods defined in this module are exported in the main
* testUtils file.
*/
/**
* Select a measure by clicking on the corresponding dropdown item (in the
* control panel 'Measure' submenu).
*
* Note that this method assumes that the dropdown menu is open.
* @see toggleMeasuresDropdown
*
* @param {PivotController} pivot
* @param {string} measure
*/
function clickMeasure(pivot, measure) {
return testUtilsDom.click(pivot.$buttons.find(`.dropdown-item[data-field=${measure}]`));
}
/**
* Open the 'Measure' dropdown menu (in the control panel)
*
* @see clickMeasure
*
* @param {PivotController} pivot
*/
function toggleMeasuresDropdown(pivot) {
return testUtilsDom.click(pivot.$buttons.filter('.btn-group:first').find('> button'));
}
/**
* Reloads a graph view.
*
* @param {PivotController} pivot
* @param {[Object]} params given to the controller reload method
*/
function reload(pivot, params) {
return pivot.reload(params);
}
export default {
clickMeasure: clickMeasure,
reload: reload,
toggleMeasuresDropdown: toggleMeasuresDropdown,
};
@@ -62,7 +62,7 @@
</div>`;
Parent.components = { ComponentAdapter };
const target = testUtils.prepareTarget();
const target = getFixture();
const parent = await mount(Parent, target);
assert.strictEqual(parent.el.innerHTML, '<div>Hello World!</div>');
@@ -92,7 +92,7 @@
</div>`;
Parent.components = { ComponentAdapter };
const target = testUtils.prepareTarget();
const target = getFixture();
const parent = await mount(Parent, target);
assert.strictEqual(parent.el.innerHTML, '<div>Hello World!</div>');
@@ -133,7 +133,7 @@
</div>`;
Parent.components = { ComponentAdapter };
const target = testUtils.prepareTarget();
const target = getFixture();
await mount(Parent, target);
});
@@ -167,7 +167,7 @@
</div>`;
Parent.components = { MyWidgetAdapter };
const target = testUtils.prepareTarget();
const target = getFixture();
const parent = await mount(Parent, target);
assert.strictEqual(parent.el.innerHTML, '<div>Hello World!</div>');
@@ -198,7 +198,7 @@
</div>`;
Parent.components = { ComponentAdapter };
const target = testUtils.prepareTarget();
const target = getFixture();
const parent = await mount(Parent, target);
assert.strictEqual(parent.el.innerHTML, '<div>Hello World!</div>');
@@ -245,7 +245,7 @@
</div>`;
Parent.components = { MyWidgetAdapter };
const target = testUtils.prepareTarget();
const target = getFixture();
const parent = await mount(Parent, target);
assert.strictEqual(parent.el.innerHTML, '<div>Hello World!</div>');
@@ -307,7 +307,7 @@
</div>`;
Parent.components = { AsyncComponent, MyWidgetAdapter };
const target = testUtils.prepareTarget();
const target = getFixture();
const parent = await mount(Parent, target);
assert.strictEqual(parent.el.innerHTML, '<div>Hi World!</div><div>Hello World!</div>');
@@ -353,7 +353,7 @@
</div>`;
Parent.components = { ComponentAdapter };
const target = testUtils.prepareTarget();
const target = getFixture();
const parent = await mount(Parent, target);
assert.verifySteps(['on_attach_callback']);
@@ -385,7 +385,7 @@
</div>`;
Parent.components = { ComponentAdapter };
const target = testUtils.prepareTarget();
const target = getFixture();
const parent = await mount(Parent, target);
assert.strictEqual(parent.el.innerHTML, '<div>widget</div><div>component</div>');
@@ -417,7 +417,7 @@
</div>`;
Parent.components = { ComponentAdapter };
const target = testUtils.prepareTarget();
const target = getFixture();
await mount(Parent, target);
widget.trigger_up('some-event', { value: 'a' });
@@ -452,7 +452,7 @@
},
});
const target = testUtils.prepareTarget();
const target = getFixture();
const parent = await mount(Parent, target, { env: owl.Component.env });
assert.strictEqual(parent.el.innerHTML, '<div></div>');
@@ -497,7 +497,7 @@
}
};
const target = testUtils.prepareTarget();
const target = getFixture();
await mount(Parent, target, { env });
});
@@ -524,47 +524,11 @@
session: { key: 'value' },
});
const target = testUtils.prepareTarget();
const target = getFixture();
await mount(Parent, target, { env: owl.Component.env });
cleanUp();
});
QUnit.test("sub widget that calls load_views", async function (assert) {
assert.expect(4);
const MyWidget = Widget.extend({
willStart: function () {
return this.loadViews('some_model', { x: 2 }, [[false, 'list']]);
},
});
class Parent extends LegacyComponent {
constructor() {
super(...arguments);
this.MyWidget = MyWidget;
}
}
Parent.template = xml`
<div>
<ComponentAdapter Component="MyWidget"/>
</div>`;
Parent.components = { ComponentAdapter };
const cleanUp = await addMockEnvironmentOwl(Parent, {
mockRPC: function (route, args) {
assert.strictEqual(route, '/web/dataset/call_kw/some_model');
assert.deepEqual(args.kwargs.context, { x: 2 });
assert.deepEqual(args.kwargs.views, [[false, 'list']]);
return Promise.resolve();
},
});
const target = testUtils.prepareTarget();
const parent = await mount(Parent, target, { env: owl.Component.env });
assert.strictEqual(parent.el.innerHTML, '<div></div>');
cleanUp();
});
QUnit.test("sub widgets in a t-if/t-else", async function (assert) {
assert.expect(3);
@@ -595,7 +559,7 @@
</div>`;
Parent.components = { ComponentAdapter };
const target = testUtils.prepareTarget();
const target = getFixture();
const parent = await mount(Parent, target);
assert.strictEqual(parent.el.innerHTML, '<div>Hi</div>');
@@ -639,7 +603,7 @@
</div>`;
Parent.components = { ComponentAdapter };
const target = testUtils.prepareTarget();
const target = getFixture();
const parent = await mount(Parent, target);
assert.strictEqual(parent.el.innerHTML, '<div>Hi</div>');
@@ -708,7 +672,7 @@
`;
Parent.components = { MyWidgetAdapter };
const target = testUtils.prepareTarget();
const target = getFixture();
const parent = await mount(Parent, target);
const widgetEl = myWidget.el;
@@ -779,7 +743,7 @@
`;
Parent.components = { MyWidgetAdapter };
const target = testUtils.prepareTarget();
const target = getFixture();
const parent = await mount(Parent, target);
assert.containsOnce(target, ".widget_id_0");
@@ -824,7 +788,7 @@
},
});
const target = testUtils.prepareTarget();
const target = getFixture();
const widget = new MyWidget();
await widget.appendTo(target);
@@ -862,7 +826,7 @@
},
});
const target = testUtils.prepareTarget();
const target = getFixture();
const widget = new MyWidget();
await widget.appendTo(target);
@@ -914,7 +878,7 @@
});
const widget = new MyWidget();
const target = testUtils.prepareTarget();
const target = getFixture();
const iframe = document.createElement("iframe");
target.appendChild(iframe);
await widget.appendTo(iframe.contentWindow.document.body);
@@ -955,7 +919,7 @@
}
});
const target = testUtils.prepareTarget();
const target = getFixture();
const widget = new MyWidget();
await widget.appendTo(target);
@@ -1017,7 +981,7 @@
}
});
const target = testUtils.prepareTarget();
const target = getFixture();
const widget = new MyWidget();
await widget.appendTo(target);
@@ -1079,7 +1043,7 @@
}
});
const target = testUtils.prepareTarget();
const target = getFixture();
const widget = new MyWidget();
const fragment = document.createElement("div");
await widget.appendTo(fragment);
@@ -1130,7 +1094,7 @@
MyComponent.template = xml`<div><MyChildComponent/></div>`;
MyComponent.components = { MyChildComponent };
const target = testUtils.prepareTarget();
const target = getFixture();
const widget = new ComponentWrapper(null, MyComponent, {});
await widget.mount(target);
@@ -1205,7 +1169,7 @@
MyComponent.template = xml`<div><MyChildComponent t-props="props" /></div>`;
MyComponent.components = { MyChildComponent };
const target = testUtils.prepareTarget();
const target = getFixture();
const widget = new ComponentWrapper(null, MyComponent, {
text: "takeNoMess"
});
@@ -1264,7 +1228,7 @@
MyComponent.template = xml`<div><MyChildComponent t-props="props" /></div>`;
MyComponent.components = { MyChildComponent };
const target = testUtils.prepareTarget();
const target = getFixture();
const widget = new ComponentWrapper(null, MyComponent, {
text: "takeNoMess"
});
@@ -1336,7 +1300,7 @@
},
});
const target = testUtils.prepareTarget();
const target = getFixture();
const widget = new MyWidget();
await widget.appendTo(target);
@@ -1368,7 +1332,7 @@
},
});
const target = testUtils.prepareTarget();
const target = getFixture();
const widget = new MyWidget();
await widget.appendTo(target);
assert.verifySteps([
@@ -1425,7 +1389,7 @@
},
});
const target = testUtils.prepareTarget();
const target = getFixture();
const widget = new MyWidget();
await widget.appendTo(target);
@@ -1454,7 +1418,7 @@
},
});
const target = testUtils.prepareTarget();
const target = getFixture();
const widget = new MyWidget();
await widget.appendTo(target);
@@ -1495,7 +1459,7 @@
}
});
const target = testUtils.prepareTarget();
const target = getFixture();
const parent = new Parent();
await parent.appendTo(target);
@@ -1569,7 +1533,7 @@
Parent.components = { ComponentAdapter };
const target = testUtils.prepareTarget();
const target = getFixture();
const parent = await mount(Parent, target);
assert.strictEqual(parent.el.innerHTML, '<div><span>Component</span></div>');
@@ -1633,7 +1597,7 @@
}
});
const target = testUtils.prepareTarget();
const target = getFixture();
const parent = new Parent();
await parent.appendTo(target);
@@ -3,7 +3,6 @@
import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { WebClient } from "@web/webclient/webclient";
import testUtils from "web.test_utils";
import { registerCleanup } from "../../helpers/cleanup";
import { makeTestEnv } from "../../helpers/mock_env";
import {
@@ -17,7 +16,7 @@ import {
editInput,
getNodesTextContent,
} from "../../helpers/utils";
import { pagerNext, toggleMenuItem, toggleSearchBarMenu } from "@web/../tests/search/helpers";
import { pagerNext, switchView, toggleMenuItem, toggleSearchBarMenu } from "@web/../tests/search/helpers";
import { session } from "@web/session";
import {
createWebClient,
@@ -121,8 +120,8 @@ QUnit.module("ActionManager", (hooks) => {
patchWithCleanup(session, { home_action_id: 1001 });
await createWebClient({ serverData });
await testUtils.nextTick(); // wait for the navbar to be updated
await testUtils.nextTick(); // wait for the action to be displayed
await nextTick(); // wait for the navbar to be updated
await nextTick(); // wait for the action to be displayed
assert.containsOnce(target, ".test_client_action");
assert.strictEqual(target.querySelector(".o_menu_brand").innerText, "App1");
@@ -196,8 +195,8 @@ QUnit.module("ActionManager", (hooks) => {
webClient.env.bus.trigger("test:hashchange", {
action: "HelloWorldTest",
});
await testUtils.nextTick();
await testUtils.nextTick();
await nextTick();
await nextTick();
assert.strictEqual(
$(target).find(".o_client_action_test").text(),
"Hello World",
@@ -215,7 +214,7 @@ QUnit.module("ActionManager", (hooks) => {
webClient.env.bus.trigger("test:hashchange", {
action: 1,
});
await testUtils.nextTick();
await nextTick();
await legacyExtraNextTick();
assert.containsOnce(target, ".o_control_panel");
assert.containsOnce(target, ".o_kanban_view");
@@ -237,8 +236,8 @@ QUnit.module("ActionManager", (hooks) => {
id: 2,
model: "partner",
});
await testUtils.nextTick();
await testUtils.nextTick();
await nextTick();
await nextTick();
assert.containsOnce(target, ".o_form_view");
assert.deepEqual(getBreadCrumbTexts(target), ["Second record"]);
assert.verifySteps(["/web/webclient/load_menus", "get_views", "web_read"]);
@@ -260,7 +259,7 @@ QUnit.module("ActionManager", (hooks) => {
Object.assign(browser.location, { hash });
await createWebClient({ serverData, mockRPC });
await testUtils.nextTick();
await nextTick();
assert.containsOnce(target, ".o_form_view");
assert.deepEqual(getBreadCrumbTexts(target), ["Second record"]);
assert.containsNone(target, ".o_menu_brand");
@@ -279,7 +278,7 @@ QUnit.module("ActionManager", (hooks) => {
model: "partner",
view_type: "form",
});
await testUtils.nextTick();
await nextTick();
await legacyExtraNextTick();
assert.containsOnce(target, ".o_form_view");
assert.verifySteps([
@@ -300,7 +299,7 @@ QUnit.module("ActionManager", (hooks) => {
action: 3,
view_type: "kanban",
});
await testUtils.nextTick();
await nextTick();
await legacyExtraNextTick();
assert.containsNone(target, ".o_list_view");
assert.containsOnce(target, ".o_kanban_view");
@@ -329,13 +328,13 @@ QUnit.module("ActionManager", (hooks) => {
id: 2,
view_type: "form",
});
await testUtils.nextTick();
await nextTick();
await legacyExtraNextTick();
assert.containsNone(target, ".o_list_view");
assert.containsOnce(target, ".o_form_view");
assert.deepEqual(getBreadCrumbTexts(target), ["Partners", "Second record"]);
// go back to List
await testUtils.dom.click($(target).find(".o_control_panel .breadcrumb a"));
await click(target.querySelector(".o_control_panel .breadcrumb a"));
assert.containsOnce(target, ".o_list_view");
assert.containsNone(target, ".o_form_view");
assert.verifySteps([
@@ -362,7 +361,7 @@ QUnit.module("ActionManager", (hooks) => {
"Second record",
]);
// go back to List
await testUtils.dom.click($(target).find(".o_control_panel .breadcrumb a:last"));
await click(target.querySelector(".o_control_panel .breadcrumb .o_back_button a"));
assert.deepEqual(getBreadCrumbTexts(target), ["Partners Action 4", "Partners"]);
});
@@ -401,7 +400,7 @@ QUnit.module("ActionManager", (hooks) => {
action: 3,
view_type: "kanban",
});
await testUtils.nextTick();
await nextTick();
await legacyExtraNextTick();
assert.containsNone(target, ".o_list_view");
assert.containsOnce(target, ".o_kanban_view");
@@ -411,7 +410,7 @@ QUnit.module("ActionManager", (hooks) => {
id: 4,
view_type: "form",
});
await testUtils.nextTick();
await nextTick();
await legacyExtraNextTick();
assert.containsNone(target, ".o_kanban_view");
assert.containsOnce(target, ".o_form_view");
@@ -437,7 +436,7 @@ QUnit.module("ActionManager", (hooks) => {
const webClient = await createWebClient({ serverData, mockRPC });
// execute action 3 and open the first record in a form view
await doAction(webClient, 3);
await testUtils.dom.click($(target).find(".o_list_view .o_data_cell:first"));
await click(target.querySelector(".o_list_view .o_data_cell"));
assert.containsOnce(target, ".o_form_view");
assert.deepEqual(getBreadCrumbTexts(target), ["Partners", "First record"]);
// switch to record 4
@@ -446,7 +445,7 @@ QUnit.module("ActionManager", (hooks) => {
id: 4,
view_type: "form",
});
await testUtils.nextTick();
await nextTick();
assert.containsOnce(target, ".o_form_view");
assert.deepEqual(getBreadCrumbTexts(target), ["Partners", "Fourth record"]);
// verify steps to ensure that the whole action hasn't been re-executed
@@ -485,7 +484,7 @@ QUnit.module("ActionManager", (hooks) => {
view_type: "list",
});
assert.verifySteps(["push_state"], "should have pushed the final state");
await testUtils.dom.click($(target).find("tr .o_data_cell:first"));
await click(target.querySelector("tr .o_data_cell"));
await legacyExtraNextTick();
currentHash = webClient.env.services.router.current.hash;
assert.deepEqual(currentHash, {
@@ -660,7 +659,7 @@ QUnit.module("ActionManager", (hooks) => {
await doAction(webClient, 3);
assert.containsOnce(target, ".o_list_view", "should now display the list view");
await testUtils.controlPanel.switchView(target, "kanban");
await switchView(target, "kanban");
await legacyExtraNextTick();
assert.containsOnce(target, ".o_kanban_view", "should now display the kanban view");