Merge branch 'MDL-85658-405' of https://github.com/HuongNV13/moodle into MOODLE_405_STABLE

This commit is contained in:
Mihail Geshoski
2026-02-16 13:47:58 +08:00
4 changed files with 116 additions and 12 deletions
+44
View File
@@ -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;
+62
View File
@@ -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_name_string>(?:[^"]|\\")*)" iframe is interactable$/
* @Given /^I wait until "(?P<iframe_name_string>(?:[^"]|\\")*)" 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 = <<<EOF
(function() {
const xpath = "{$iframexpath}";
const el = document.evaluate(
xpath,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
if (!el) { return false; }
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) { return false; }
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
const target = document.elementFromPoint(x, y);
return target === el || el.contains(target);
})()
EOF;
return (bool) $this->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_name_string>(?:[^"]|\\")*)" iframe is interactable and switch to it$/
* @Given /^I wait until "(?P<iframe_name_string>(?:[^"]|\\")*)" 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.
*
@@ -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
@@ -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"