diff --git a/lms/static/completion/js/ViewedEvent.js b/lms/static/completion/js/ViewedEvent.js index 1ad936f8a158..b2b9d5ff5bc4 100644 --- a/lms/static/completion/js/ViewedEvent.js +++ b/lms/static/completion/js/ViewedEvent.js @@ -109,7 +109,13 @@ export class ViewedEventTracker { constructor() { this.elementViewings = new Set(); this.handlers = []; - this.registerDomHandlers(); + if (window === window.parent) { + // Preview (legacy LMS frontend). + this.registerDomHandlers(); + } else { + // Learning MFE. + window.addEventListener('message', this.handleVisibilityMessage.bind(this)); + } } /** Add an element to track. */ @@ -121,7 +127,11 @@ export class ViewedEventTracker { (el, event) => this.callHandlers(el, event), ), ); - this.updateVisible(); + // Update visibility status immediately after adding the element (in case it's already visible). + // We don't need this for the Learning MFE because it will send a message once the iframe is loaded. + if (window === window.parent) { + this.updateVisible(); + } } /** Register a new handler to be called when an element has been viewed. */ @@ -177,4 +187,36 @@ export class ViewedEventTracker { handler(el, event); }); } + + /** Handle a unit.visibilityStatus message from the Learning MFE. */ + handleVisibilityMessage(event) { + if (event.data.type === 'unit.visibilityStatus') { + const { topPosition, viewportHeight } = event.data.data; + + this.elementViewings.forEach((elv) => { + const rect = elv.getBoundingRect(); + let visible = false; + + // Convert iframe-relative rect coordinates to be relative to the parent's viewport. + const elTopPosition = rect.top + topPosition; + const elBottomPosition = rect.bottom + topPosition; + + // Check if the element is visible in the parent's viewport. + if (elTopPosition < viewportHeight && elTopPosition >= 0) { + elv.markTopSeen(); + visible = true; + } + if (elBottomPosition <= viewportHeight && elBottomPosition > 0) { + elv.markBottomSeen(); + visible = true; + } + + if (visible) { + elv.handleVisible(); + } else { + elv.handleNotVisible(); + } + }); + } + } } diff --git a/openedx/features/enterprise_support/tests/test_utils.py b/openedx/features/enterprise_support/tests/test_utils.py index fbf2cccf9dbc..d693f6be72b6 100644 --- a/openedx/features/enterprise_support/tests/test_utils.py +++ b/openedx/features/enterprise_support/tests/test_utils.py @@ -269,10 +269,11 @@ def test_update_account_settings_context_for_enterprise(self, mock_get_fields): mock_get_fields.assert_called_once_with(user) assert expected_context == context + @ddt.data(settings.ENTERPRISE_READONLY_ACCOUNT_FIELDS, ['username', 'email', 'country']) @mock.patch('openedx.features.enterprise_support.utils.get_current_request') @mock.patch('openedx.features.enterprise_support.api.enterprise_customer_for_request') def test_get_enterprise_readonly_account_fields_no_sync_learner_profile_data( - self, mock_customer_for_request, mock_get_current_request, + self, readonly_fields, mock_customer_for_request, mock_get_current_request, ): mock_get_current_request.return_value = mock.Mock( GET={'enterprise_customer': 'some-uuid'}, @@ -284,7 +285,8 @@ def test_get_enterprise_readonly_account_fields_no_sync_learner_profile_data( } user = mock.Mock() - actual_fields = get_enterprise_readonly_account_fields(user) + with override_settings(ENTERPRISE_READONLY_ACCOUNT_FIELDS=readonly_fields): + actual_fields = get_enterprise_readonly_account_fields(user) assert set() == actual_fields mock_customer_for_request.assert_called_once_with(mock_get_current_request.return_value) mock_get_current_request.assert_called_once_with() diff --git a/openedx/features/enterprise_support/utils.py b/openedx/features/enterprise_support/utils.py index 4604b5c4fb00..6b007ebed57b 100644 --- a/openedx/features/enterprise_support/utils.py +++ b/openedx/features/enterprise_support/utils.py @@ -277,7 +277,7 @@ def get_enterprise_readonly_account_fields(user): # if user has no `UserSocialAuth` record then allow to edit `fullname` # whether the `sync_learner_profile_data` is enabled or disabled user_social_auth_record = _user_has_social_auth_record(user, enterprise_customer) - if not user_social_auth_record: + if not user_social_auth_record and 'name' in enterprise_readonly_account_fields: enterprise_readonly_account_fields.remove('name') sync_learner_profile_data = _get_sync_learner_profile_data(enterprise_customer)