From 870263001eb883342f0045ed9117d5bff2cbf4d9 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Sat, 5 Apr 2025 18:09:46 +0200 Subject: [PATCH] fix: ensure iframe visibility tracking is triggered on load The previous implementation had a race condition that sometimes prevented XBlocks from being marked as viewed. Users had to scroll or resize the window to trigger visibility tracking instead of having it happen once content loads. --- .../Unit/hooks/useIFrameBehavior.test.js | 28 +++++++++----- .../sequence/Unit/hooks/useIFrameBehavior.ts | 38 ++++++++++--------- 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js b/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js index 35d59b4d..34e56c22 100644 --- a/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js +++ b/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js @@ -280,7 +280,7 @@ describe('useIFrameBehavior hook', () => { }); }); describe('visibility tracking', () => { - it('sets up visibility tracking after iframe has loaded', () => { + it('sets up visibility tracking after iframe loads', () => { mockState({ ...defaultStateVals, hasLoaded: true }); renderHook(() => useIFrameBehavior(props)); @@ -288,15 +288,9 @@ describe('useIFrameBehavior hook', () => { expect(global.window.addEventListener).toHaveBeenCalledTimes(2); expect(global.window.addEventListener).toHaveBeenCalledWith('scroll', expect.any(Function)); expect(global.window.addEventListener).toHaveBeenCalledWith('resize', expect.any(Function)); - // Initial visibility update. - expect(postMessage).toHaveBeenCalledWith( - { - type: 'unit.visibilityStatus', - data: { - topPosition: 100, - viewportHeight: 800, - }, - }, + // Initial visibility update is handled by the `handleIFrameLoad` method. + expect(postMessage).not.toHaveBeenCalledWith( + expect.objectContaining({ type: 'unit.visibilityStatus' }), config.LMS_BASE_URL, ); }); @@ -362,6 +356,20 @@ describe('useIFrameBehavior hook', () => { window.onmessage(event); expect(dispatch).toHaveBeenCalledWith(processEvent(event.data, fetchCourse)); }); + it('updates initial iframe visibility on load', () => { + const { result } = renderHook(() => useIFrameBehavior(props)); + result.current.handleIFrameLoad(); + expect(postMessage).toHaveBeenCalledWith( + { + type: 'unit.visibilityStatus', + data: { + topPosition: 100, + viewportHeight: 800, + }, + }, + config.LMS_BASE_URL, + ); + }); }); it('forwards handleIframeLoad, showError, and hasLoaded from state fields', () => { mockState(stateVals); diff --git a/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.ts b/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.ts index 9e63015b..4a882da3 100644 --- a/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.ts +++ b/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.ts @@ -102,6 +102,23 @@ const useIFrameBehavior = ({ useEventListener('message', receiveMessage); // Send visibility status to the iframe. It's used to mark XBlocks as viewed. + const updateIframeVisibility = () => { + const iframeElement = document.getElementById(elementId) as HTMLIFrameElement | null; + const rect = iframeElement?.getBoundingClientRect(); + const visibleInfo = { + type: 'unit.visibilityStatus', + data: { + topPosition: rect?.top, + viewportHeight: window.innerHeight, + }, + }; + iframeElement?.contentWindow?.postMessage( + visibleInfo, + `${getConfig().LMS_BASE_URL}`, + ); + }; + + // Set up visibility tracking event listeners. React.useEffect(() => { if (!hasLoaded) { return undefined; @@ -112,27 +129,9 @@ const useIFrameBehavior = ({ return undefined; } - const updateIframeVisibility = () => { - const rect = iframeElement.getBoundingClientRect(); - const visibleInfo = { - type: 'unit.visibilityStatus', - data: { - topPosition: rect.top, - viewportHeight: window.innerHeight, - }, - }; - iframeElement?.contentWindow?.postMessage( - visibleInfo, - `${getConfig().LMS_BASE_URL}`, - ); - }; - // Throttle the update function to prevent it from sending too many messages to the iframe. const throttledUpdateVisibility = throttle(updateIframeVisibility, 100); - // Update the visibility of the iframe in case the element is already visible. - updateIframeVisibility(); - // Add event listeners to update the visibility of the iframe when the window is scrolled or resized. window.addEventListener('scroll', throttledUpdateVisibility); window.addEventListener('resize', throttledUpdateVisibility); @@ -167,6 +166,9 @@ const useIFrameBehavior = ({ dispatch(processEvent(e.data, fetchCourse)); } }; + + // Update the visibility of the iframe in case the element is already visible. + updateIframeVisibility(); }; React.useEffect(() => {