diff --git a/libs/widget-lib/src/widgetIframeLoading.test.ts b/libs/widget-lib/src/widgetIframeLoading.test.ts index 004dcdace2..2b48501565 100644 --- a/libs/widget-lib/src/widgetIframeLoading.test.ts +++ b/libs/widget-lib/src/widgetIframeLoading.test.ts @@ -54,6 +54,41 @@ describe('widgetIframeLoading error UI styling', () => { expect(document.head.querySelectorAll(`style[data-${ERROR_CLASS}]`).length).toBeLessThanOrEqual(1) }) + + it('shows a loading state on the reload button while the retry is in progress', () => { + const { container, iframe } = setup() + failIframe(iframe) + + const reloadBtn = container.querySelector(`.${ERROR_CLASS} button`) + expect(reloadBtn?.disabled).toBe(false) + + reloadBtn?.click() + + // The panel stays mounted and the button is disabled (cursor:progress) while the retry runs. + expect(container.querySelector(`.${ERROR_CLASS}`)).not.toBeNull() + expect(reloadBtn?.disabled).toBe(true) + }) + + it('removes the error panel and reveals the iframe once the widget is ready', () => { + const { container, iframe, state } = setup() + failIframe(iframe) + expect(container.querySelector(`.${ERROR_CLASS}`)).not.toBeNull() + expect(iframe.style.display).toBe('none') + + state.onWidgetReady() + + expect(container.querySelector(`.${ERROR_CLASS}`)).toBeNull() + expect(iframe.style.display).toBe('') + }) + + it('keeps a single error panel across repeated failures on the same widget', () => { + const { container, iframe } = setup() + + failIframe(iframe) + failIframe(iframe) + + expect(container.querySelectorAll(`.${ERROR_CLASS}`).length).toBe(1) + }) }) function setup(): { container: HTMLElement; iframe: HTMLIFrameElement; state: LoadingState } { diff --git a/libs/widget-lib/src/widgetIframeLoading.ts b/libs/widget-lib/src/widgetIframeLoading.ts index ebb7a70ee7..88b64b4c79 100644 --- a/libs/widget-lib/src/widgetIframeLoading.ts +++ b/libs/widget-lib/src/widgetIframeLoading.ts @@ -19,22 +19,34 @@ export function widgetIframeLoading( let cancelled = false let timeout: number | null = null + function removeLoadingErrorContainer(): void { + const existing = container.getElementsByClassName(WIDGET_LOADING_ERROR_CLASS)[0] + if (existing) container.removeChild(existing) + } + function onLoadingError(): void { onWidgetLoadingError?.() iframe.style.display = 'none' ensureLoadingErrorStyles() + // Replace any previous error/loading panel so a repeated failure doesn't stack panels. + removeLoadingErrorContainer() + const loadingContainer = document.createElement('div') const reloadBtn = document.createElement('button') const errorText = document.createElement('p') reloadBtn.innerText = 'Reload' reloadBtn.addEventListener('click', () => { - container.removeChild(loadingContainer) + // Keep the panel mounted and switch it to a loading state so the retry is visibly in + // progress; it's cleared by onWidgetReady on success or replaced by onLoadingError on + // another failure. + reloadBtn.disabled = true + errorText.innerText = 'Loading…' + destroy(true) iframe.src = 'about:blank' - iframe.style.display = '' setTimeout(() => { iframe.src = originalSrc @@ -69,8 +81,9 @@ export function widgetIframeLoading( onWidgetReady() { if (timeout) clearTimeout(timeout) - const loadingContainer = document.getElementsByClassName(WIDGET_LOADING_ERROR_CLASS)[0] - if (loadingContainer) container.removeChild(loadingContainer) + // Reveal the iframe again — it is hidden while an error/retry panel is shown. + iframe.style.display = '' + removeLoadingErrorContainer() }, cancelWidgetLoading() { cancelled = true