[FIX] web: CP concurence issue with custom delay

Commit 62ebc97bd8 introduced the
possibility to customise the debounce delay associated with a command
palette search based on namespace. This introduced a concurrency bug
when changing namespace.

Problem:
Let's say we have 2 namespaces with very different debounce delays.
For example, # has 2000 and @ has 0.
1. A user enter "#"
2. He changes his mind before the end of the search. He deletes "#"
and enter "@".
3. The search associated with "@" is resolved before the search for "#".
The associated results are displayed.
4. The search associated with "#" is resolved. The associated results
are displayed.
5. The user does not get the results related to his last search which
are those of "@".

Solution:
When we change the namespace, we will cancel the current search function.

Part-of: odoo/odoo#82773
This commit is contained in:
FrancoisGe
2022-03-08 11:03:35 +00:00
parent 8a5721bc7d
commit b2058b9942
2 changed files with 82 additions and 12 deletions
@@ -271,20 +271,18 @@ export class CommandPalette extends Component {
debounceSearch(value) {
const { namespace } = this.processSearchValue(value);
if (!this.debounceSearchByNamespace[namespace]) {
const namespaceConfig = this.configByNamespace[namespace] || {};
if (namespaceConfig.debounceDelay) {
this.debounceSearchByNamespace[namespace] = debounce.apply(this, [
this.search,
namespaceConfig.debounceDelay,
]);
} else {
this.debounceSearchByNamespace[namespace] = this.defaultDebounceSearch;
const namespaceConfig = this.configByNamespace[namespace] || {};
if (this.namespace !== namespace) {
if (this.lastDebounceSearch) {
this.lastDebounceSearch.cancel();
}
this.lastDebounceSearch = debounce(
(value) => this.search(value),
namespaceConfig.debounceDelay || 0
);
this.namespace = namespace;
}
const debounceSearch =
this.debounceSearchByNamespace[namespace] || this.defaultDebounceSearch;
this.searchValuePromise = debounceSearch.apply(this, [value]).catch(() => {
this.searchValuePromise = this.lastDebounceSearch(value).catch(() => {
this.searchValuePromise = null;
});
}
@@ -181,6 +181,78 @@ QUnit.test("custom debounce delay", async (assert) => {
assert.verifySteps(["0", "200", "100", "0"]);
});
QUnit.test("concurrency with custom debounce delay", async (assert) => {
const def = makeDeferred();
patchWithCleanup(browser, {
clearTimeout: () => {},
setTimeout: async (later, delay) => {
if (delay === 200) {
await def;
}
later();
},
});
await mount(TestComponent, target, { env });
const configByNamespace = {
"@": {
debounceDelay: 200,
},
"#": {
debounceDelay: 100,
},
};
const action = () => {};
const providers = [
{
namespace: "@",
provide: () => [
{
name: "Command@",
action,
},
],
},
{
namespace: "#",
provide: () => [
{
name: "Command#",
action,
},
],
},
];
const config = {
configByNamespace,
providers,
};
env.services.dialog.add(CommandPaletteDialog, {
config,
});
await nextTick();
assert.containsOnce(target, ".o_command_palette");
assert.containsNone(target, ".o_command");
await editSearchBar("@");
await nextTick();
assert.deepEqual(
[...target.querySelectorAll(".o_command")].map((el) => el.textContent),
[]
);
await editSearchBar("#");
await nextTick();
assert.deepEqual(
[...target.querySelectorAll(".o_command")].map((el) => el.textContent),
["Command#"]
);
def.resolve();
await nextTick();
assert.deepEqual(
[...target.querySelectorAll(".o_command")].map((el) => el.textContent),
["Command#"]
);
});
QUnit.test("custom placeholder", async (assert) => {
await mount(TestComponent, target, { env });
const config = {