diff --git a/addons/web_editor/static/src/js/editor/image_processing.js b/addons/web_editor/static/src/js/editor/image_processing.js index 8ecd07370f3..1faca26309b 100644 --- a/addons/web_editor/static/src/js/editor/image_processing.js +++ b/addons/web_editor/static/src/js/editor/image_processing.js @@ -349,12 +349,29 @@ export async function applyModifications(img, dataOptions = {}) { * @param {String} src URL of the image to load * @param {HTMLImageElement} [img] img element in which to load the image * @returns {Promise} Promise that resolves to the loaded img + * or a placeholder image if the src is not found. */ export function loadImage(src, img = new Image()) { + const handleImage = (source, resolve, reject) => { + img.addEventListener("load", () => resolve(img), {once: true}); + img.addEventListener("error", reject, {once: true}); + img.src = source; + }; + // The server will return a placeholder image with the following src. + const placeholderHref = "/web/image/__odoo__unknown__src__/"; + return new Promise((resolve, reject) => { - img.addEventListener('load', () => resolve(img), {once: true}); - img.addEventListener('error', reject, {once: true}); - img.src = src; + fetch(src) + .then(response => { + if (!response.ok) { + src = placeholderHref; + } + handleImage(src, resolve, reject); + }) + .catch(error => { + src = placeholderHref; + handleImage(src, resolve, reject); + }); }); } diff --git a/addons/website/static/tests/tours/snippet_images_wall.js b/addons/website/static/tests/tours/snippet_images_wall.js index 3212cfbff67..2f4885b1747 100644 --- a/addons/website/static/tests/tours/snippet_images_wall.js +++ b/addons/website/static/tests/tours/snippet_images_wall.js @@ -2,6 +2,24 @@ import wTourUtils from "@website/js/tours/tour_utils"; +const wallRaceConditionClass = "image_wall_race_condition"; +const preventRaceConditionSteps = [{ + content: "Wait a few ms to avoid race condition", + // Ensure the class is remove from previous call of those steps + trigger: `body:not(.${wallRaceConditionClass})`, + run() { + setTimeout(() => { + document.body.classList.add(wallRaceConditionClass); + }, 500); + } +}, { + content: "Check the race condition class is added after a few ms", + trigger: `body.${wallRaceConditionClass}`, + run() { + document.body.classList.remove(wallRaceConditionClass); + } +}]; + const selectSignImageStep = { content: "Click on image 14", extra_trigger: ".o_we_customize_panel:not(:has(.snippet-option-GalleryElement))", @@ -9,7 +27,13 @@ const selectSignImageStep = { }; // Without reselecting the image, the tour manages to click on the // move button before the active image is updated. -const reselectSignImageSteps = [{ + +// We need to wait a few ms before clicking on the footer because after +// clicking on reposition option, there may be a delay during the click on +// another block would be ignored. +const reselectSignImageSteps = [ + ...preventRaceConditionSteps, +{ content: "Select footer", trigger: "iframe footer", }, selectSignImageStep];