diff --git a/h5p/js/embed.js b/h5p/js/embed.js index 72ce9aa8bb5..6013e31c047 100644 --- a/h5p/js/embed.js +++ b/h5p/js/embed.js @@ -226,6 +226,48 @@ document.onreadystatechange = async() => { const Pending = await getPendingClass(); var resizePending = new Pending('core_h5p/iframe:resize'); + // Track when the embedded H5P content is fully attached. + const contentLoadedPending = new Pending('core_h5p/iframe:contentLoaded'); + let contentLoaded = false; + const markContentLoaded = function() { + const body = iFrame?.contentDocument?.body; + const hasContent = body && body.querySelector('.h5p-container, .h5p-content'); + + // Check that H5P instance is actually ready with proper initialization + if (contentLoaded || !hasContent || !H5P?.instances?.[0]) { + return false; + } + + contentLoaded = true; + contentLoadedPending.resolve(); + H5PEmbedCommunicator.send('contentLoaded'); + return true; + }; + + // If the content is already there, mark it immediately. + markContentLoaded(); + + // Observe the iframe document for the first appearance of the H5P container. + if (!contentLoaded && iFrame.contentDocument?.body) { + const contentObserver = new MutationObserver(function() { + if (markContentLoaded()) { + contentObserver.disconnect(); + } + }); + contentObserver.observe(iFrame.contentDocument.body, {childList: true, subtree: true}); + } + + // Extended fallback timeout to ensure pending resolves even if content detection fails. + if (!contentLoaded) { + setTimeout(function() { + if (!contentLoaded) { + contentLoaded = true; + contentLoadedPending.resolve(); + H5PEmbedCommunicator.send('contentLoaded'); + } + }, 1000); + } + H5P.on(instance, 'resize', function() { if (H5P.isFullscreen) { return; // Skip iframe resize. @@ -249,6 +291,8 @@ document.onreadystatechange = async() => { }, 150); }); + H5P.externalDispatcher.on('domChanged', markContentLoaded); + // Get emitted xAPI data. H5P.externalDispatcher.on('xAPI', function(event) { statementPosted = false; diff --git a/lib/tests/behat/behat_general.php b/lib/tests/behat/behat_general.php index eacea366669..31fd79d7aaf 100644 --- a/lib/tests/behat/behat_general.php +++ b/lib/tests/behat/behat_general.php @@ -203,6 +203,8 @@ class behat_general extends behat_base { $this->execute_js_on_node($iframe, "{{ELEMENT}}.name = '{$iframename}';"); } $context->getSession()->switchToIFrame($iframename); + // Wait until the iframe is completely loaded and all pending operations complete. + $context->getSession()->wait(behat_base::get_extended_timeout(), behat_base::PAGE_READY_JS); // If no exception we are done. return true; @@ -211,6 +213,66 @@ class behat_general extends behat_base { ); } + /** + * Wait until the specified iframe is interactable (visible, sized, and not occluded). + * + * @Given /^I wait until "(?P(?:[^"]|\\")*)" iframe is interactable$/ + * @Given /^I wait until "(?P(?:[^"]|\\")*)" class iframe is interactable$/ + * @param string $name The name or class of the iframe + */ + public function wait_until_iframe_interactable(string $name): void { + if (!$this->running_javascript()) { + throw new DriverException( + 'iFrame interactability checks are disabled in scenarios without Javascript support', + ); + } + $this->spin( + function ($context) use ($name) { + $iframe = $context->find('iframe', $name); + $scrolljs = '{{ELEMENT}}.scrollIntoView({behavior: "auto", block: "center", inline: "center"});'; + $this->execute_js_on_node($iframe, $scrolljs); + $this->ensure_node_is_visible($iframe); + + // Check that the iframe is not occluded at its center point. + $iframexpath = $iframe->getXpath(); + $js = <<evaluate_script($js); + }, + behat_base::get_extended_timeout() + ); + } + + /** + * Wait until the specified iframe is interactable (visible, sized, and not occluded) and switche to it. + * + * @Given /^I wait until "(?P(?:[^"]|\\")*)" iframe is interactable and switch to it$/ + * @Given /^I wait until "(?P(?:[^"]|\\")*)" class iframe is interactable and switch to it$/ + * @param string $name The name of the iframe + */ + public function wait_until_iframe_interactable_and_switch_to(string $name): void { + $this->wait_until_iframe_interactable($name); + $this->switch_to_iframe($name); + } + /** * Switches to the main Moodle frame. * diff --git a/mod/h5pactivity/tests/behat/recent_activity.feature b/mod/h5pactivity/tests/behat/recent_activity.feature index a8b562c462f..2aa48bc21f2 100644 --- a/mod/h5pactivity/tests/behat/recent_activity.feature +++ b/mod/h5pactivity/tests/behat/recent_activity.feature @@ -30,16 +30,14 @@ Feature: Users can see the H5P recent activity from the recent activity block | blockname | contextlevel | reference | pagetypepattern | defaultregion | | recent_activity | Course | C1 | course-view-* | side-pre | And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1 - # The H5P content needs some time to be displayed (so better to wait for 1 second to avoid random errors). - And I switch to "h5p-player" class iframe - And I switch to "h5p-iframe" class iframe + And I wait until "h5p-player" iframe is interactable and switch to it + And I wait until "h5p-iframe" iframe is interactable and switch to it And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element" And I click on "Check" "button" in the ".h5p-question-buttons" "css_element" And I switch to the main frame And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student2 - # The H5P content needs some time to be displayed (so better to wait for 1 second to avoid random errors). - And I switch to "h5p-player" class iframe - And I switch to "h5p-iframe" class iframe + And I wait until "h5p-player" iframe is interactable and switch to it + And I wait until "h5p-iframe" iframe is interactable and switch to it And I click on "Correct one" "text" in the ".h5p-question-content" "css_element" And I click on "Check" "button" in the ".h5p-question-buttons" "css_element" And I switch to the main frame diff --git a/mod/h5pactivity/tests/behat/sending_attempt.feature b/mod/h5pactivity/tests/behat/sending_attempt.feature index affa0fb3565..8e756944cb0 100644 --- a/mod/h5pactivity/tests/behat/sending_attempt.feature +++ b/mod/h5pactivity/tests/behat/sending_attempt.feature @@ -34,8 +34,8 @@ Feature: Do a H5P attempt Scenario: Do an attempt and check on course log report When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1 And I should not see "You are in preview mode." - And I switch to "h5p-player" class iframe - And I switch to "h5p-iframe" class iframe + And I wait until "h5p-player" iframe is interactable and switch to it + And I wait until "h5p-iframe" iframe is interactable and switch to it And I click on "Correct one" "text" in the ".h5p-question-content" "css_element" And I click on "Check" "button" in the ".h5p-question-buttons" "css_element" And I switch to the main frame @@ -49,8 +49,8 @@ Feature: Do a H5P attempt Scenario: Do various attempts and check them with the attempts and user grades reports Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1 And I should not see "You are in preview mode." - And I switch to "h5p-player" class iframe - And I switch to "h5p-iframe" class iframe + And I wait until "h5p-player" iframe is interactable and switch to it + And I wait until "h5p-iframe" iframe is interactable and switch to it And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element" And I click on "Check" "button" in the ".h5p-question-buttons" "css_element" And I click on "Retry" "button" in the ".h5p-question-buttons" "css_element" @@ -62,8 +62,8 @@ Feature: Do a H5P attempt # H5P does not allow to Retry if the user checks the correct answer, we need to refresh the page. And I switch to the main frame And I reload the page - And I switch to "h5p-player" class iframe - And I switch to "h5p-iframe" class iframe + And I wait until "h5p-player" iframe is interactable and switch to it + And I wait until "h5p-iframe" iframe is interactable and switch to it # Because of the steps above, the 2nd and 3rd attempts are enough "separated" and we don't # need to add any wait here. And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element"