[REF] mail, *: tests: refactor contains to further remove jQuery
* = account, base_iban, bus, calendar, crm_livechat, hr, hr_holidays,
im_livechat, mrp, project, sms, snailmail, test_mail,
test_mail_full, web, website_livechat, website_slides
Add support in `contains` for most operations that we use in tests.
Remove return value from `contains`.
Move into `web` module.
Remove import/export chains, directly import from correct module.
closes odoo/odoo#134652
Related: odoo/enterprise#47064
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
@@ -479,6 +479,7 @@ This module provides the core of the Odoo Web Client.
|
||||
'web/static/tests/main.js',
|
||||
'web/static/tests/mock_server_tests.js',
|
||||
'web/static/tests/setup.js',
|
||||
'web/static/tests/utils.js',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'web/static/tests/env_tests.js',
|
||||
|
||||
@@ -128,7 +128,7 @@ QUnit.module("Components", (hooks) => {
|
||||
|
||||
// Click on label
|
||||
assert.verifySteps([]);
|
||||
await click(target, ".o-checkbox > .form-check-label", true);
|
||||
await click(target, ".o-checkbox > .form-check-label", { skipVisibilityCheck: true });
|
||||
assert.notOk(target.querySelector(".o-checkbox input").checked);
|
||||
assert.verifySteps(["false"]);
|
||||
|
||||
|
||||
@@ -53,7 +53,9 @@ QUnit.module("Components", ({ beforeEach }) => {
|
||||
Object.assign(xhr, {
|
||||
upload: new window.EventTarget(),
|
||||
open() {},
|
||||
send(data) { customSend && customSend(data); },
|
||||
send(data) {
|
||||
customSend && customSend(data);
|
||||
},
|
||||
});
|
||||
return xhr;
|
||||
},
|
||||
@@ -132,7 +134,7 @@ QUnit.module("Components", ({ beforeEach }) => {
|
||||
fileUploadService.uploads[1].xhr.dispatchEvent(new Event("abort"));
|
||||
},
|
||||
});
|
||||
await click(target, ".o-file-upload-progress-bar-abort", true);
|
||||
await click(target, ".o-file-upload-progress-bar-abort", { skipVisibilityCheck: true });
|
||||
assert.containsNone(target, ".file_upload");
|
||||
});
|
||||
|
||||
@@ -149,10 +151,16 @@ QUnit.module("Components", ({ beforeEach }) => {
|
||||
progressEvent.total = 500000000;
|
||||
fileUploadService.uploads[1].xhr.upload.dispatchEvent(progressEvent);
|
||||
await nextTick();
|
||||
assert.strictEqual(target.querySelector(".file_upload_progress_text_left").textContent, "Uploading... (50%)");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".file_upload_progress_text_left").textContent,
|
||||
"Uploading... (50%)"
|
||||
);
|
||||
progressEvent.loaded = 350000000;
|
||||
fileUploadService.uploads[1].xhr.upload.dispatchEvent(progressEvent);
|
||||
await nextTick();
|
||||
assert.strictEqual(target.querySelector(".file_upload_progress_text_right").textContent, "(350/500MB)");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".file_upload_progress_text_right").textContent,
|
||||
"(350/500MB)"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2526,6 +2526,11 @@ export async function makeMockServer(serverData, mockRPC) {
|
||||
const { loadJS, loadCSS } = assets;
|
||||
patchWithCleanup(assets, {
|
||||
async loadJS(resource) {
|
||||
if (resource === "/web/static/lib/stacktracejs/stacktrace.js") {
|
||||
// Bypass `mockRPC` for the stracktrace.js lib to avoid infinite loop if there
|
||||
// is an error inside the `mockRPC` call.
|
||||
return loadJS(resource);
|
||||
}
|
||||
let res = await mockRPC(resource, {});
|
||||
if (res === undefined) {
|
||||
res = await loadJS(resource);
|
||||
|
||||
@@ -445,14 +445,18 @@ export async function triggerScroll(
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
export function click(el, selector, skipVisibilityCheck = false) {
|
||||
if (el.disabled) {
|
||||
export function click(
|
||||
el,
|
||||
selector,
|
||||
{ mouseEventInit = {}, skipDisabledCheck = false, skipVisibilityCheck = false } = {}
|
||||
) {
|
||||
if (!skipDisabledCheck && el.disabled) {
|
||||
throw new Error("Can't click on a disabled button");
|
||||
}
|
||||
return triggerEvents(
|
||||
el,
|
||||
selector,
|
||||
["pointerdown", "mousedown", "focus", "pointerup", "mouseup", "click"],
|
||||
["pointerdown", "mousedown", "focus", "pointerup", "mouseup", ["click", mouseEventInit]],
|
||||
{ skipVisibilityCheck }
|
||||
);
|
||||
}
|
||||
@@ -987,7 +991,7 @@ export async function clickOpenedDropdownItem(target, fieldName, itemContent) {
|
||||
if (indexToClick === -1) {
|
||||
throw new Error(`The element '${itemContent}' does not exist in the dropdown`);
|
||||
}
|
||||
await click(dropdownItems[indexToClick], null, "click");
|
||||
await click(dropdownItems[indexToClick]);
|
||||
}
|
||||
|
||||
export async function selectDropdownItem(target, fieldName, itemContent) {
|
||||
|
||||
@@ -0,0 +1,550 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { registerCleanup } from "@web/../tests/helpers/cleanup";
|
||||
import {
|
||||
click as webClick,
|
||||
getFixture,
|
||||
makeDeferred,
|
||||
triggerEvents as webTriggerEvents,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { file } from "@web/../tests/legacy/helpers/test_utils";
|
||||
import { createFile as createFile2 } from "@web/../tests/legacy/helpers/test_utils_file";
|
||||
const { inputFiles: webInputFiles } = file;
|
||||
|
||||
export const createFile = createFile2;
|
||||
|
||||
/**
|
||||
* Create a fake object 'dataTransfer', linked to some files,
|
||||
* which is passed to drag and drop events.
|
||||
*
|
||||
* @param {Object[]} files
|
||||
* @returns {Object}
|
||||
*/
|
||||
function createFakeDataTransfer(files) {
|
||||
return {
|
||||
dropEffect: "all",
|
||||
effectAllowed: "all",
|
||||
files,
|
||||
items: [],
|
||||
types: ["Files"],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then clicks on it.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
* @param {boolean} [options.shiftKey]
|
||||
*/
|
||||
export async function click(selector, options = {}) {
|
||||
const { shiftKey } = options;
|
||||
delete options.shiftKey;
|
||||
await contains(selector, { click: { shiftKey }, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then drags `files` on it.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {Object[]} files
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function dragenterFiles(selector, files, options) {
|
||||
await contains(selector, { dragenterFiles: files, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then drops `files` on it.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {Object[]} files
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function dropFiles(selector, files, options) {
|
||||
await contains(selector, { dropFiles: files, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then inputs `files` on it.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {Object[]} files
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function inputFiles(selector, files, options) {
|
||||
await contains(selector, { inputFiles: files, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then pastes `files` on it.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {Object[]} files
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function pasteFiles(selector, files, options) {
|
||||
await contains(selector, { pasteFiles: files, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then focuses on it.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function focus(selector, options) {
|
||||
await contains(selector, { setFocus: true, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then inserts the given `content`.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {string} content
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
* @param {boolean} [options.replace=false]
|
||||
*/
|
||||
export async function insertText(selector, content, options = {}) {
|
||||
const { replace = false } = options;
|
||||
delete options.replace;
|
||||
await contains(selector, { ...options, insertText: { content, replace } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then sets its `scrollTop` to the given value.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {number|"bottom"} scrollTop
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function scroll(selector, scrollTop, options) {
|
||||
await contains(selector, { setScroll: scrollTop, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until exactly one element matching the given `selector` is present in
|
||||
* `options.target` and then triggers `event` on it.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {string[]} events
|
||||
* @param {ContainsOptions} [options] forwarded to `contains`
|
||||
*/
|
||||
export async function triggerEvents(selector, events, options) {
|
||||
await contains(selector, { triggerEvents: events, ...options });
|
||||
}
|
||||
|
||||
let hasUsedContainsPositively = false;
|
||||
QUnit.testStart(() => (hasUsedContainsPositively = false));
|
||||
/**
|
||||
* @typedef {Object} ContainsOptions
|
||||
* @property {[string, ContainsOptions]} [after] if provided, the found element(s) must be after the
|
||||
* element matched by this param.
|
||||
* @property {[string, ContainsOptions]} [before] if provided, the found element(s) must be before the
|
||||
* element matched by this param.
|
||||
* @property {Object} [click] if provided, clicks on the first found element
|
||||
* @property {[string, ContainsOptions]} [contains] if provided, a sub-contains check will be applied
|
||||
* having the result of the main contains as target. The first param is the sub-selector, and the
|
||||
* secound param are options to forward to the sub-call. Having sub-contains only makes sense with
|
||||
* a count of 1.
|
||||
* @property {Array<[string, ContainsOptions]>} [containsMulti] similar than contains param but with
|
||||
* multiple rules that have to match at the same time.
|
||||
* @property {number} [count=1] numbers of elements to be found to declare the contains check
|
||||
* as successful. Elements are counted after applying all other filters.
|
||||
* @property {Object[]} [dragenterFiles] if provided, drags the given files on the found element
|
||||
* @property {Object[]} [dropFiles] if provided, drops the given files on the found element
|
||||
* @property {Object[]} [inputFiles] if provided, inputs the given files on the found element
|
||||
* @property {{content:string, replace:boolean}} [insertText] if provided, adds to (or replace) the
|
||||
* value of the first found element by the given content.
|
||||
* @property {[string, ContainsOptions]} [parent] if provided, the found element(s) must have as
|
||||
* parent the node matching the parent parameter.
|
||||
* @property {Object[]} [pasteFiles] if provided, pastes the given files on the found element
|
||||
* @property {number|"bottom"} [scroll] if provided, the scrollTop of the found element(s)
|
||||
* must match.
|
||||
* Note: when using one of the scrollTop options, it is advised to ensure the height is not going
|
||||
* to change soon, by checking with a preceding contains that all the expected elements are in DOM.
|
||||
* @property {boolean} [setFocus] if provided, focuses the first found element.
|
||||
* @property {number|"bottom"} [setScroll] if provided, sets the scrollTop on the first found
|
||||
* element.
|
||||
* @property {HTMLElement} [target=getFixture()]
|
||||
* @property {string[]} [triggerEvents] if provided, triggers the given events on the found element
|
||||
* @property {string} [text] if provided, the textContent of the found element(s) must match.
|
||||
* @property {string} [value] if provided, the input value of the found element(s) must match.
|
||||
* Note: value changes are not observed directly, another mutation must happen to catch them.
|
||||
*/
|
||||
class Contains {
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @param {ContainsOptions} [options={}]
|
||||
*/
|
||||
constructor(selector, options = {}) {
|
||||
this.selector = selector;
|
||||
this.options = options;
|
||||
this.options.count ??= 1;
|
||||
this.options.targetParam = this.options.target;
|
||||
this.options.target ??= getFixture();
|
||||
let selectorMessage = `${this.options.count} of "${this.selector}"`;
|
||||
if (this.options.targetParam) {
|
||||
selectorMessage = `${selectorMessage} inside a specific target`;
|
||||
}
|
||||
if (this.options.parent) {
|
||||
selectorMessage = `${selectorMessage} inside a specific parent`;
|
||||
}
|
||||
if (this.options.contains) {
|
||||
selectorMessage = `${selectorMessage} with a specified sub-contains`;
|
||||
}
|
||||
if (this.options.containsMulti) {
|
||||
selectorMessage = `${selectorMessage} with multiple specified sub-contains`;
|
||||
}
|
||||
if (this.options.text !== undefined) {
|
||||
selectorMessage = `${selectorMessage} with text "${this.options.text}"`;
|
||||
}
|
||||
if (this.options.value !== undefined) {
|
||||
selectorMessage = `${selectorMessage} with value "${this.options.value}"`;
|
||||
}
|
||||
if (this.options.scroll !== undefined) {
|
||||
selectorMessage = `${selectorMessage} with scroll "${this.options.scroll}"`;
|
||||
}
|
||||
if (this.options.after !== undefined) {
|
||||
selectorMessage = `${selectorMessage} after a specified element`;
|
||||
}
|
||||
if (this.options.before !== undefined) {
|
||||
selectorMessage = `${selectorMessage} before a specified element`;
|
||||
}
|
||||
this.selectorMessage = selectorMessage;
|
||||
this.options.containsMulti ??= [];
|
||||
if (this.options.contains !== undefined) {
|
||||
this.options.containsMulti.push(this.options.contains);
|
||||
}
|
||||
if (this.options.count) {
|
||||
hasUsedContainsPositively = true;
|
||||
} else if (!hasUsedContainsPositively) {
|
||||
throw new Error(
|
||||
`Starting a test with "contains" of count 0 for selector "${this.selector}" is useless because it might immediately resolve. Start the test by checking that an expected element actually exists.`
|
||||
);
|
||||
}
|
||||
/** @type {string} */
|
||||
this.successMessage = undefined;
|
||||
/** @type {function} */
|
||||
this.executeError = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts this contains check, either immediately resolving if there is a
|
||||
* match, or registering appropriate listeners and waiting until there is a
|
||||
* match or a timeout (resolving or rejecting respectively).
|
||||
*
|
||||
* Success or failure messages will be logged with QUnit as well.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
run() {
|
||||
this.done = false;
|
||||
this.def = makeDeferred();
|
||||
this.scrollListeners = new Set();
|
||||
this.onScroll = () => this.runOnce("after scroll");
|
||||
if (!this.runOnce("immediately")) {
|
||||
this.timer = setTimeout(
|
||||
() => this.runOnce("Timeout of 5 seconds", { crashOnFail: true }),
|
||||
5000
|
||||
);
|
||||
this.observer = new MutationObserver((mutations) => {
|
||||
try {
|
||||
this.runOnce("after mutations");
|
||||
} catch (e) {
|
||||
this.def.reject(e); // prevents infinite loop in case of programming error
|
||||
}
|
||||
});
|
||||
this.observer.observe(document.body, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
registerCleanup(() => {
|
||||
if (!this.done) {
|
||||
this.runOnce("Test ended", { crashOnFail: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.def;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs this contains check once, immediately returning the result (or
|
||||
* undefined), and possibly resolving or rejecting the main promise
|
||||
* (and printing QUnit log) depending on options.
|
||||
* If undefined is returned it means the check was not successful.
|
||||
*
|
||||
* @param {string} whenMessage
|
||||
* @param {Object} [options={}]
|
||||
* @param {boolean} [options.crashOnFail=false]
|
||||
* @param {boolean} [options.executeOnSuccess=true]
|
||||
* @returns {HTMLElement[]|undefined}
|
||||
*/
|
||||
runOnce(whenMessage, { crashOnFail = false, executeOnSuccess = true } = {}) {
|
||||
const res = this.select();
|
||||
if (res?.length === this.options.count || crashOnFail) {
|
||||
// clean before doing anything else to avoid infinite loop due to side effects
|
||||
this.observer?.disconnect();
|
||||
clearTimeout(this.timer);
|
||||
for (const el of this.scrollListeners ?? []) {
|
||||
el.removeEventListener("scroll", this.onScroll);
|
||||
}
|
||||
this.done = true;
|
||||
}
|
||||
if (res?.length === this.options.count) {
|
||||
this.successMessage = `Found ${this.selectorMessage} (${whenMessage})`;
|
||||
if (executeOnSuccess) {
|
||||
this.executeAction(res[0]);
|
||||
}
|
||||
return res;
|
||||
} else {
|
||||
this.executeError = () => {
|
||||
let message = `Failed to find ${this.selectorMessage} (${whenMessage}).`;
|
||||
message = res
|
||||
? `${message} Found ${res.length} instead.`
|
||||
: `${message} Parent not found.`;
|
||||
if (this.parentContains) {
|
||||
if (this.parentContains.successMessage) {
|
||||
QUnit.assert.ok(true, this.parentContains.successMessage);
|
||||
} else {
|
||||
this.parentContains.executeError();
|
||||
}
|
||||
}
|
||||
QUnit.assert.ok(false, message);
|
||||
this.def?.reject(new Error(message));
|
||||
for (const childContains of this.childrenContains || []) {
|
||||
if (childContains.successMessage) {
|
||||
QUnit.assert.ok(true, childContains.successMessage);
|
||||
} else {
|
||||
childContains.executeError();
|
||||
}
|
||||
}
|
||||
};
|
||||
if (crashOnFail) {
|
||||
this.executeError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the action(s) given to this constructor on the found element,
|
||||
* prints the success messages, and resolves the main deferred.
|
||||
|
||||
* @param {HTMLElement} el
|
||||
*/
|
||||
executeAction(el) {
|
||||
let message = this.successMessage;
|
||||
if (this.options.click) {
|
||||
message = `${message} and clicked it`;
|
||||
webClick(el, undefined, {
|
||||
mouseEventInit: this.options.click,
|
||||
skipDisabledCheck: true,
|
||||
skipVisibilityCheck: true,
|
||||
});
|
||||
}
|
||||
if (this.options.dragenterFiles) {
|
||||
message = `${message} and dragged ${this.options.dragenterFiles.length} file(s)`;
|
||||
const ev = new Event("dragenter", { bubbles: true });
|
||||
Object.defineProperty(ev, "dataTransfer", {
|
||||
value: createFakeDataTransfer(this.options.dragenterFiles),
|
||||
});
|
||||
el.dispatchEvent(ev);
|
||||
}
|
||||
if (this.options.dropFiles) {
|
||||
message = `${message} and dropped ${this.options.dropFiles.length} file(s)`;
|
||||
const ev = new Event("drop", { bubbles: true });
|
||||
Object.defineProperty(ev, "dataTransfer", {
|
||||
value: createFakeDataTransfer(this.options.dropFiles),
|
||||
});
|
||||
el.dispatchEvent(ev);
|
||||
}
|
||||
if (this.options.inputFiles) {
|
||||
message = `${message} and inputted ${this.options.inputFiles.length} file(s)`;
|
||||
webInputFiles(el, this.options.inputFiles);
|
||||
}
|
||||
if (this.options.insertText !== undefined) {
|
||||
message = `${message} and inserted text "${this.options.insertText.content}" (replace: ${this.options.insertText.replace})`;
|
||||
el.focus();
|
||||
if (this.options.insertText.replace) {
|
||||
el.value = "";
|
||||
el.dispatchEvent(new window.KeyboardEvent("keydown", { key: "Backspace" }));
|
||||
el.dispatchEvent(new window.KeyboardEvent("keyup", { key: "Backspace" }));
|
||||
el.dispatchEvent(new window.InputEvent("input"));
|
||||
el.dispatchEvent(new window.InputEvent("change"));
|
||||
}
|
||||
for (const char of this.options.insertText.content) {
|
||||
el.value += char;
|
||||
el.dispatchEvent(new window.KeyboardEvent("keydown", { key: char }));
|
||||
el.dispatchEvent(new window.KeyboardEvent("keyup", { key: char }));
|
||||
el.dispatchEvent(new window.InputEvent("input"));
|
||||
el.dispatchEvent(new window.InputEvent("change"));
|
||||
}
|
||||
}
|
||||
if (this.options.pasteFiles) {
|
||||
message = `${message} and pasted ${this.options.pasteFiles.length} file(s)`;
|
||||
const ev = new Event("paste", { bubbles: true });
|
||||
Object.defineProperty(ev, "clipboardData", {
|
||||
value: createFakeDataTransfer(this.options.pasteFiles),
|
||||
});
|
||||
el.dispatchEvent(ev);
|
||||
}
|
||||
if (this.options.setFocus) {
|
||||
message = `${message} and focused it`;
|
||||
el.focus();
|
||||
}
|
||||
if (this.options.setScroll !== undefined) {
|
||||
message = `${message} and set scroll to "${this.options.setScroll}"`;
|
||||
el.scrollTop =
|
||||
this.options.setScroll === "bottom" ? el.scrollHeight : this.options.setScroll;
|
||||
}
|
||||
if (this.options.triggerEvents) {
|
||||
message = `${message} and triggered "${this.options.triggerEvents.join(", ")}" events`;
|
||||
webTriggerEvents(el, null, this.options.triggerEvents, {
|
||||
skipVisibilityCheck: true,
|
||||
});
|
||||
}
|
||||
if (this.parentContains) {
|
||||
QUnit.assert.ok(true, this.parentContains.successMessage);
|
||||
}
|
||||
QUnit.assert.ok(true, message);
|
||||
for (const childContains of this.childrenContains) {
|
||||
QUnit.assert.ok(true, childContains.successMessage);
|
||||
}
|
||||
this.def?.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the found element(s) according to this constructor setup.
|
||||
* If undefined is returned it means the parent cannot be found
|
||||
*
|
||||
* @returns {HTMLElement[]|undefined}
|
||||
*/
|
||||
select() {
|
||||
const target = this.selectParent();
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
/** @type HTMLElement[] */
|
||||
let baseRes;
|
||||
try {
|
||||
baseRes = [...target.querySelectorAll(this.selector)];
|
||||
} catch (error) {
|
||||
if (error.message.includes("Failed to execute 'querySelectorAll'")) {
|
||||
// keep jquery for backwards compatibility until all tests are converted
|
||||
baseRes = [...$(target).find(this.selector)];
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/** @type {Contains[]} */
|
||||
this.childrenContains = [];
|
||||
const res = baseRes.filter((el, currentIndex) => {
|
||||
let condition =
|
||||
(this.options.text === undefined || el.textContent.trim() === this.options.text) &&
|
||||
(this.options.value === undefined || el.value === this.options.value) &&
|
||||
(this.options.scroll === undefined ||
|
||||
(this.options.scroll === "bottom"
|
||||
? Math.abs(el.scrollHeight - el.clientHeight - el.scrollTop) <= 1
|
||||
: Math.abs(el.scrollTop - this.options.scroll) <= 1));
|
||||
for (const param of this.options.containsMulti) {
|
||||
const childContains = new Contains(param[0], { ...param[1], target: el });
|
||||
if (
|
||||
!childContains.runOnce(`as child of el ${currentIndex + 1})`, {
|
||||
executeOnSuccess: false,
|
||||
})
|
||||
) {
|
||||
condition = false;
|
||||
}
|
||||
this.childrenContains.push(childContains);
|
||||
}
|
||||
if (condition && this.options.after) {
|
||||
// avoid checking if condition already failed to avoid uncessary slowness
|
||||
const afterContains = new Contains(this.options.after[0], {
|
||||
...this.options.after[1],
|
||||
target,
|
||||
});
|
||||
const afterEl = afterContains.runOnce(`as "after"`, {
|
||||
executeOnSuccess: false,
|
||||
})?.[0];
|
||||
if (
|
||||
!afterEl ||
|
||||
!(el.compareDocumentPosition(afterEl) & Node.DOCUMENT_POSITION_PRECEDING)
|
||||
) {
|
||||
condition = false;
|
||||
}
|
||||
this.childrenContains.push(afterContains);
|
||||
}
|
||||
if (condition && this.options.before) {
|
||||
// avoid checking if condition already failed to avoid uncessary slowness
|
||||
const beforeContains = new Contains(this.options.before[0], {
|
||||
...this.options.before[1],
|
||||
target,
|
||||
});
|
||||
const beforeEl = beforeContains.runOnce(`as "before"`, {
|
||||
executeOnSuccess: false,
|
||||
})?.[0];
|
||||
if (
|
||||
!beforeEl ||
|
||||
!(el.compareDocumentPosition(beforeEl) & Node.DOCUMENT_POSITION_FOLLOWING)
|
||||
) {
|
||||
condition = false;
|
||||
}
|
||||
this.childrenContains.push(beforeContains);
|
||||
}
|
||||
return condition;
|
||||
});
|
||||
if (
|
||||
this.options.scroll !== undefined &&
|
||||
this.scrollListeners &&
|
||||
baseRes.length === this.options.count &&
|
||||
res.length !== this.options.count
|
||||
) {
|
||||
for (const el of baseRes) {
|
||||
if (!this.scrollListeners.has(el)) {
|
||||
this.scrollListeners.add(el);
|
||||
el.addEventListener("scroll", this.onScroll);
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the found element that should act as the target (parent) for the
|
||||
* main selector.
|
||||
* If undefined is returned it means the parent cannot be found.
|
||||
*
|
||||
* @returns {HTMLElement|undefined}
|
||||
*/
|
||||
selectParent() {
|
||||
if (this.options.parent) {
|
||||
this.parentContains = new Contains(this.options.parent[0], {
|
||||
...this.options.parent[1],
|
||||
target: this.options.target,
|
||||
});
|
||||
return this.parentContains.runOnce(`as parent`, { executeOnSuccess: false })?.[0];
|
||||
}
|
||||
return this.options.target;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until `count` elements matching the given `selector` are present in
|
||||
* `options.target`.
|
||||
*
|
||||
* @param {string} selector
|
||||
* @param {ContainsOptions} [options]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function contains(selector, options) {
|
||||
await new Contains(selector, options).run();
|
||||
}
|
||||
@@ -408,7 +408,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
assert.containsNone(target, ".o_field_many2many_tags_avatar .o_field_many2many_selection");
|
||||
|
||||
const o_kanban_record = target.querySelector(".o_kanban_record:nth-child(2)");
|
||||
await click(o_kanban_record, ".o_field_tags > .o_m2m_avatar_empty", true);
|
||||
await click(o_kanban_record, ".o_field_tags > .o_m2m_avatar_empty");
|
||||
const popover = document.querySelector(".o-overlay-container");
|
||||
assert.strictEqual(
|
||||
document.activeElement,
|
||||
@@ -484,7 +484,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
}
|
||||
},
|
||||
});
|
||||
await click(target, ".o_kanban_record:first-child .o_quick_assign", true);
|
||||
await click(target, ".o_kanban_record:first-child .o_quick_assign");
|
||||
// add and directly remove an item
|
||||
await click(target, ".o_popover .o-autocomplete--dropdown-item:first-child");
|
||||
await click(target, ".o_popover .o_tag .o_delete");
|
||||
@@ -555,7 +555,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
</templates>
|
||||
</kanban>`,
|
||||
});
|
||||
await click(target, ".o_kanban_record:first-child .o_quick_assign", true);
|
||||
await click(target, ".o_kanban_record:first-child .o_quick_assign");
|
||||
// add and directly close the dropdown
|
||||
await triggerEvent(target, null, "keydown", { key: "Tab" });
|
||||
await triggerEvent(document.activeElement, null, "keydown", { key: "Enter" });
|
||||
|
||||
@@ -339,7 +339,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
},
|
||||
});
|
||||
assert.containsNone(target, ".o_kanban_record .fa-star");
|
||||
await click(target.querySelector(".o_priority a.o_priority_star.fa-star-o"), null, true);
|
||||
await click(target.querySelector(".o_priority a.o_priority_star.fa-star-o"));
|
||||
assert.verifySteps(['web_save [[1],{"selection":"1"}]']);
|
||||
assert.containsOnce(target, ".o_kanban_record .fa-star");
|
||||
|
||||
@@ -348,7 +348,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
".o_control_panel_main_buttons .d-none.d-xl-inline-flex .o-kanban-button-new"
|
||||
);
|
||||
await click(target, ".o_kanban_quick_create .o_kanban_add");
|
||||
await click(target.querySelector(".o_priority a.o_priority_star.fa-star-o"), null, true);
|
||||
await click(target.querySelector(".o_priority a.o_priority_star.fa-star-o"));
|
||||
assert.verifySteps(['web_save [[6],{"selection":"1"}]']);
|
||||
assert.containsN(target, ".o_kanban_record .fa-star", 2);
|
||||
});
|
||||
|
||||
@@ -2283,7 +2283,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
assertFolded([false, false, false, true]);
|
||||
|
||||
// now, create a new property, it must unfold the last group
|
||||
await click(target, ".o_field_property_add button", { skipVisibilityCheck: true });
|
||||
await click(target, ".o_field_property_add button");
|
||||
assert.deepEqual(getGroups(), [
|
||||
[
|
||||
["SEPARATOR 2", "property_2"],
|
||||
|
||||
@@ -11003,7 +11003,9 @@ QUnit.module("Views", (hooks) => {
|
||||
|
||||
assert.containsNone(getCard(0), "img", "Initially there is no image.");
|
||||
|
||||
await click(document.body, ".modal .o_kanban_cover_image img", true);
|
||||
await click(document.body, ".modal .o_kanban_cover_image img", {
|
||||
skipVisibilityCheck: true,
|
||||
});
|
||||
await click(document.body, ".modal .btn-primary:first-child");
|
||||
|
||||
assert.containsOnce(target, 'img[data-src*="/web/image/1"]');
|
||||
|
||||
Reference in New Issue
Block a user