Skip to content

Commit a7c338d

Browse files
committed
[REF] web: Hoot - rename 'exact' to 'count'
This commit renames the 'exact' option given to queryX helpers to 'count', as to better reflect what it specifies, i.e. returning nodes if their count matches the given one. This commit also adds the ":count()" pseudo-class to be used in selectors, for consistency with other options which are also pseudo- classes. closes odoo#231355 X-original-commit: 8fcdcf0 Related: odoo/enterprise#97073 Signed-off-by: Michaël Mattiello (mcm) <mcm@odoo.com> Signed-off-by: Julien Mougenot (jum) <jum@odoo.com>
1 parent ad9b711 commit a7c338d

File tree

2 files changed

+53
-16
lines changed

2 files changed

+53
-16
lines changed

addons/web/static/lib/hoot-dom/helpers/dom.js

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,10 @@ import { waitUntil } from "./time";
5959
*
6060
* @typedef {{
6161
* contains?: string;
62+
* count?: number;
6263
* displayed?: boolean;
6364
* empty?: boolean;
6465
* eq?: number;
65-
* exact?: number;
6666
* first?: boolean;
6767
* focusable?: boolean;
6868
* has?: boolean;
@@ -374,6 +374,17 @@ function getWaitForNoneMessage() {
374374
return message;
375375
}
376376

377+
/**
378+
*
379+
* @param {number} count
380+
* @param {Parameters<NodeFilter>[0]} _node
381+
* @param {Parameters<NodeFilter>[1]} _i
382+
* @param {Parameters<NodeFilter>[2]} nodes
383+
*/
384+
function hasNodeCount(count, _node, _i, nodes) {
385+
return count === nodes.length;
386+
}
387+
377388
/**
378389
* @param {string} [char]
379390
*/
@@ -906,7 +917,10 @@ function _guardedQueryAll(target, options) {
906917
function _queryAll(target, options) {
907918
queryAllLevel++;
908919

909-
const { exact, root, ...modifiers } = options || {};
920+
const { count, root, ...modifiers } = options || {};
921+
if (count !== null && count !== undefined && (!$isInteger(count) || count <= 0)) {
922+
throw new HootDomError(`invalid 'count' option: should be a positive integer`);
923+
}
910924

911925
/** @type {Node[]} */
912926
let nodes = [];
@@ -954,7 +968,7 @@ function _queryAll(target, options) {
954968
const filteredNodes = applyFilters(modifierFilters, nodes);
955969

956970
// Register query message (if needed), and/or throw an error accordingly
957-
const message = registerQueryMessage(filteredNodes, exact);
971+
const message = registerQueryMessage(filteredNodes, count);
958972
if (message) {
959973
throw new HootDomError(message);
960974
}
@@ -969,7 +983,7 @@ function _queryAll(target, options) {
969983
* @param {QueryOptions} options
970984
*/
971985
function _queryOne(target, options) {
972-
return _guardedQueryAll(target, { ...options, exact: 1 })[0];
986+
return _guardedQueryAll(target, { ...options, count: 1 })[0];
973987
}
974988

975989
/**
@@ -1082,6 +1096,16 @@ const customPseudoClasses = new Map();
10821096

10831097
customPseudoClasses
10841098
.set("contains", makePatternBasedPseudoClass("contains", getNodeText))
1099+
.set("count", (strCount) => {
1100+
const count = $parseInt(strCount);
1101+
if (!$isInteger(count) || count <= 0) {
1102+
throw selectorError(
1103+
"count",
1104+
`expected count to be a positive integer (got ${strCount})`
1105+
);
1106+
}
1107+
return hasNodeCount.bind(null, count);
1108+
})
10851109
.set("displayed", () => isNodeDisplayed)
10861110
.set("empty", () => isEmpty)
10871111
.set("eq", (strIndex) => {
@@ -1865,6 +1889,8 @@ export function observe(target, callback) {
18651889
* * given *text* will be matched against:
18661890
* - an `<input>`, `<textarea>` or `<select>` element's **value**;
18671891
* - or any other element's **inner text**.
1892+
* - `:count`: return nodes if their count match the given *count*.
1893+
* If not matching, an error is thrown;
18681894
* - `:displayed`: matches nodes that are "displayed" (see {@link isDisplayed});
18691895
* - `:empty`: matches nodes that have an empty *content* (**value** or **inner text**);
18701896
* - `:eq(n)`: matches the *nth* node (0-based index);
@@ -1885,17 +1911,17 @@ export function observe(target, callback) {
18851911
* - `:visible`: matches nodes that are "visible" (see {@link isVisible});
18861912
*
18871913
* An `options` object can be specified to filter[1] the results:
1888-
* - `displayed`: whether the nodes must be "displayed" (see {@link isDisplayed});
1889-
* - `exact`: the exact number of nodes to match (throws an error if the number of
1914+
* - `count`: the exact number of nodes to match (throws an error if the number of
18901915
* nodes doesn't match);
1916+
* - `displayed`: whether the nodes must be "displayed" (see {@link isDisplayed});
18911917
* - `focusable`: whether the nodes must be "focusable" (see {@link isFocusable});
18921918
* - `root`: the root node to query the selector in (defaults to the current fixture);
18931919
* - `viewPort`: whether the nodes must be partially visible in the current viewport
18941920
* (see {@link isInViewPort});
18951921
* - `visible`: whether the nodes must be "visible" (see {@link isVisible}).
18961922
* * This option implies `displayed`
18971923
*
1898-
* [1] these filters (except for `exact` and `root`) achieve the same result as
1924+
* [1] these filters (except for `count` and `root`) achieve the same result as
18991925
* using their homonym pseudo-classes on the final group of the given selector
19001926
* string (e.g. ```queryAll`ul > li:visible`;``` = ```queryAll("ul > li", { visible: true })```).
19011927
*
@@ -1917,7 +1943,7 @@ export function observe(target, callback) {
19171943
* queryAll`#editor:shadow div`; // -> [div, div, ...] (inside shadow DOM)
19181944
* @example
19191945
* // with options
1920-
* queryAll(`div:first`, { exact: 1 }); // -> [div]
1946+
* queryAll(`div:first`, { count: 1 }); // -> [div]
19211947
* queryAll(`div`, { root: queryOne`iframe` }); // -> [div, div, ...]
19221948
* // redundant, but possible
19231949
* queryAll(`button:visible`, { visible: true }); // -> [button, button, ...]
@@ -2039,20 +2065,20 @@ export function queryFirst(target, options) {
20392065
}
20402066

20412067
/**
2042-
* Performs a {@link queryAll} with the given arguments, along with a forced `exact: 1`
2068+
* Performs a {@link queryAll} with the given arguments, along with a forced `count: 1`
20432069
* option to ensure only one node matches the given {@link Target}.
20442070
*
20452071
* The returned value is a single node instead of a list of nodes.
20462072
*
20472073
* @param {Target} target
2048-
* @param {Omit<QueryOptions, "exact">} [options]
2074+
* @param {Omit<QueryOptions, "count">} [options]
20492075
* @returns {Element}
20502076
*/
20512077
export function queryOne(target, options) {
20522078
[target, options] = parseRawArgs(arguments);
2053-
if ($isInteger(options?.exact)) {
2079+
if ($isInteger(options?.count)) {
20542080
throw new HootDomError(
2055-
`cannot call \`queryOne\` with 'exact'=${options.exact}: did you mean to use \`queryAll\`?`
2081+
`cannot call \`queryOne\` with 'count'=${options.count}: did you mean to use \`queryAll\`?`
20562082
);
20572083
}
20582084
return _queryOne(target, options);

addons/web/static/lib/hoot/tests/hoot-dom/dom.test.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,17 @@ describe(parseUrl(import.meta.url), () => {
496496
});
497497
});
498498

499+
test("query options", async () => {
500+
await mountForTest(FULL_HTML_TEMPLATE);
501+
502+
expect($$("input", { count: 2 })).toHaveLength(2);
503+
expect(() => $$("input", { count: 1 })).toThrow();
504+
505+
expect($$("option", { count: 6 })).toHaveLength(6);
506+
expect($$("option", { count: 3, root: "[name=title]" })).toHaveLength(3);
507+
expect(() => $$("option", { count: 6, root: "[name=title]" })).toThrow();
508+
});
509+
499510
test("advanced use cases", async () => {
500511
await mountForTest(FULL_HTML_TEMPLATE);
501512

@@ -864,7 +875,7 @@ describe(parseUrl(import.meta.url), () => {
864875
expect($1(".title:first")).toBe(getFixture().querySelector("header .title"));
865876

866877
expect(() => $1(".title")).toThrow();
867-
expect(() => $1(".title", { exact: 2 })).toThrow();
878+
expect(() => $1(".title", { count: 2 })).toThrow();
868879
});
869880

870881
test("queryRect", async () => {
@@ -902,10 +913,10 @@ describe(parseUrl(import.meta.url), () => {
902913

903914
// queryOne error messages
904915
expect(() => $1()).toThrow(`found 0 elements instead of 1`);
905-
expect(() => $$([], { exact: 18 })).toThrow(`found 0 elements instead of 18`);
916+
expect(() => $$([], { count: 18 })).toThrow(`found 0 elements instead of 18`);
906917
expect(() => $1("")).toThrow(`found 0 elements instead of 1: 0 matching ""`);
907-
expect(() => $$(".tralalero", { exact: -20 })).toThrow(
908-
`found 1 element instead of -20: 1 matching ".tralalero"`
918+
expect(() => $$(".tralalero", { count: -20 })).toThrow(
919+
`invalid 'count' option: should be a positive integer`
909920
);
910921
expect(() => $1`.tralalero:contains(Tralala):visible:scrollable:first`).toThrow(
911922
`found 0 elements instead of 1: 0 matching ".tralalero:contains(Tralala):visible:scrollable:first" (1 element with text "Tralala" > 1 visible element > 0 scrollable elements)`

0 commit comments

Comments
 (0)