fix: send XBlock visibility status to the LMS (#1491)
This commit is contained in:
1
package-lock.json
generated
1
package-lock.json
generated
@@ -34,6 +34,7 @@
|
||||
"husky": "7.0.4",
|
||||
"joi": "^17.11.0",
|
||||
"js-cookie": "3.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"postcss-loader": "^8.1.1",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"husky": "7.0.4",
|
||||
"joi": "^17.11.0",
|
||||
"js-cookie": "3.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"postcss-loader": "^8.1.1",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
@@ -85,6 +86,49 @@ const useIFrameBehavior = ({
|
||||
|
||||
useEventListener('message', receiveMessage);
|
||||
|
||||
// Send visibility status to the iframe. It's used to mark XBlocks as viewed.
|
||||
React.useEffect(() => {
|
||||
if (!hasLoaded) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const iframeElement = document.getElementById(elementId);
|
||||
if (!iframeElement || !iframeElement.contentWindow) {
|
||||
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);
|
||||
|
||||
// Clean up event listeners on unmount.
|
||||
return () => {
|
||||
window.removeEventListener('scroll', throttledUpdateVisibility);
|
||||
window.removeEventListener('resize', throttledUpdateVisibility);
|
||||
};
|
||||
}, [hasLoaded, elementId]);
|
||||
|
||||
/**
|
||||
* onLoad *should* only fire after everything in the iframe has finished its own load events.
|
||||
* Which means that the plugin.resize message (which calls setHasLoaded above) will have fired already
|
||||
|
||||
@@ -30,6 +30,11 @@ jest.mock('react-redux', () => ({
|
||||
useDispatch: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('lodash', () => ({
|
||||
...jest.requireActual('lodash'),
|
||||
throttle: jest.fn((fn) => fn),
|
||||
}));
|
||||
|
||||
jest.mock('./useLoadBearingHook', () => jest.fn());
|
||||
|
||||
jest.mock('@edx/frontend-platform/logging', () => ({
|
||||
@@ -64,7 +69,10 @@ const dispatch = jest.fn();
|
||||
useDispatch.mockReturnValue(dispatch);
|
||||
|
||||
const postMessage = jest.fn();
|
||||
const frame = { contentWindow: { postMessage } };
|
||||
const frame = {
|
||||
contentWindow: { postMessage },
|
||||
getBoundingClientRect: jest.fn(() => ({ top: 100 })),
|
||||
};
|
||||
const mockGetElementById = jest.fn(() => frame);
|
||||
const testHash = '#test-hash';
|
||||
|
||||
@@ -87,6 +95,10 @@ describe('useIFrameBehavior hook', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
state.mock();
|
||||
global.document.getElementById = mockGetElementById;
|
||||
global.window.addEventListener = jest.fn();
|
||||
global.window.removeEventListener = jest.fn();
|
||||
global.window.innerHeight = 800;
|
||||
});
|
||||
afterEach(() => {
|
||||
state.resetVals();
|
||||
@@ -265,6 +277,53 @@ describe('useIFrameBehavior hook', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('visibility tracking', () => {
|
||||
it('sets up visibility tracking after iframe has loaded', () => {
|
||||
state.mockVals({ ...defaultStateVals, hasLoaded: true });
|
||||
useIFrameBehavior(props);
|
||||
|
||||
const effects = getEffects([true, props.elementId], React);
|
||||
expect(effects.length).toEqual(2);
|
||||
effects[0](); // Execute the visibility tracking effect.
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
config.LMS_BASE_URL,
|
||||
);
|
||||
});
|
||||
it('does not set up visibility tracking before iframe has loaded', () => {
|
||||
state.mockVals({ ...defaultStateVals, hasLoaded: false });
|
||||
useIFrameBehavior(props);
|
||||
|
||||
const effects = getEffects([false, props.elementId], React);
|
||||
expect(effects).toBeNull();
|
||||
|
||||
expect(global.window.addEventListener).not.toHaveBeenCalled();
|
||||
expect(postMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
it('cleans up event listeners on unmount', () => {
|
||||
state.mockVals({ ...defaultStateVals, hasLoaded: true });
|
||||
useIFrameBehavior(props);
|
||||
|
||||
const effects = getEffects([true, props.elementId], React);
|
||||
const cleanup = effects[0](); // Execute the effect and get the cleanup function.
|
||||
cleanup(); // Call the cleanup function.
|
||||
|
||||
expect(global.window.removeEventListener).toHaveBeenCalledTimes(2);
|
||||
expect(global.window.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function));
|
||||
expect(global.window.removeEventListener).toHaveBeenCalledWith('resize', expect.any(Function));
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
describe('handleIFrameLoad', () => {
|
||||
|
||||
Reference in New Issue
Block a user