Skip to content

Commit 448bd20

Browse files
committed
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.
1 parent e6f7588 commit 448bd20

File tree

2 files changed

+38
-28
lines changed

2 files changed

+38
-28
lines changed

src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -280,23 +280,17 @@ describe('useIFrameBehavior hook', () => {
280280
});
281281
});
282282
describe('visibility tracking', () => {
283-
it('sets up visibility tracking after iframe has loaded', () => {
283+
it('sets up visibility tracking after iframe loads', () => {
284284
mockState({ ...defaultStateVals, hasLoaded: true });
285285

286286
renderHook(() => useIFrameBehavior(props));
287287

288288
expect(global.window.addEventListener).toHaveBeenCalledTimes(2);
289289
expect(global.window.addEventListener).toHaveBeenCalledWith('scroll', expect.any(Function));
290290
expect(global.window.addEventListener).toHaveBeenCalledWith('resize', expect.any(Function));
291-
// Initial visibility update.
292-
expect(postMessage).toHaveBeenCalledWith(
293-
{
294-
type: 'unit.visibilityStatus',
295-
data: {
296-
topPosition: 100,
297-
viewportHeight: 800,
298-
},
299-
},
291+
// Initial visibility update is handled by the `handleIFrameLoad` method.
292+
expect(postMessage).not.toHaveBeenCalledWith(
293+
expect.objectContaining({ type: 'unit.visibilityStatus' }),
300294
config.LMS_BASE_URL,
301295
);
302296
});
@@ -362,6 +356,20 @@ describe('useIFrameBehavior hook', () => {
362356
window.onmessage(event);
363357
expect(dispatch).toHaveBeenCalledWith(processEvent(event.data, fetchCourse));
364358
});
359+
it('updates initial iframe visibility on load', () => {
360+
const { result } = renderHook(() => useIFrameBehavior(props));
361+
result.current.handleIFrameLoad();
362+
expect(postMessage).toHaveBeenCalledWith(
363+
{
364+
type: 'unit.visibilityStatus',
365+
data: {
366+
topPosition: 100,
367+
viewportHeight: 800,
368+
},
369+
},
370+
config.LMS_BASE_URL,
371+
);
372+
});
365373
});
366374
it('forwards handleIframeLoad, showError, and hasLoaded from state fields', () => {
367375
mockState(stateVals);

src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,23 @@ const useIFrameBehavior = ({
102102
useEventListener('message', receiveMessage);
103103

104104
// Send visibility status to the iframe. It's used to mark XBlocks as viewed.
105+
const updateIframeVisibility = () => {
106+
const iframeElement = document.getElementById(elementId) as HTMLIFrameElement | null;
107+
const rect = iframeElement?.getBoundingClientRect();
108+
const visibleInfo = {
109+
type: 'unit.visibilityStatus',
110+
data: {
111+
topPosition: rect?.top,
112+
viewportHeight: window.innerHeight,
113+
},
114+
};
115+
iframeElement?.contentWindow?.postMessage(
116+
visibleInfo,
117+
`${getConfig().LMS_BASE_URL}`,
118+
);
119+
};
120+
121+
// Set up visibility tracking event listeners.
105122
React.useEffect(() => {
106123
if (!hasLoaded) {
107124
return undefined;
@@ -112,27 +129,9 @@ const useIFrameBehavior = ({
112129
return undefined;
113130
}
114131

115-
const updateIframeVisibility = () => {
116-
const rect = iframeElement.getBoundingClientRect();
117-
const visibleInfo = {
118-
type: 'unit.visibilityStatus',
119-
data: {
120-
topPosition: rect.top,
121-
viewportHeight: window.innerHeight,
122-
},
123-
};
124-
iframeElement?.contentWindow?.postMessage(
125-
visibleInfo,
126-
`${getConfig().LMS_BASE_URL}`,
127-
);
128-
};
129-
130132
// Throttle the update function to prevent it from sending too many messages to the iframe.
131133
const throttledUpdateVisibility = throttle(updateIframeVisibility, 100);
132134

133-
// Update the visibility of the iframe in case the element is already visible.
134-
updateIframeVisibility();
135-
136135
// Add event listeners to update the visibility of the iframe when the window is scrolled or resized.
137136
window.addEventListener('scroll', throttledUpdateVisibility);
138137
window.addEventListener('resize', throttledUpdateVisibility);
@@ -167,6 +166,9 @@ const useIFrameBehavior = ({
167166
dispatch(processEvent(e.data, fetchCourse));
168167
}
169168
};
169+
170+
// Update the visibility of the iframe in case the element is already visible.
171+
updateIframeVisibility();
170172
};
171173

172174
React.useEffect(() => {

0 commit comments

Comments
 (0)