first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,122 @@
@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe
Feature: Add H5P activity
In order to let students access a H5P package
As a teacher
I need to add H5P activity to a course
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/h5p:updatelibraries | Allow | editingteacher | System | |
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
@javascript
Scenario: Add an h5pactivity to a course
Given the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| intro | H5P activity Description |
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
When I am on the "Awesome H5P package" "h5pactivity activity" page
Then I should see "H5P activity Description"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I should see "Lorum ipsum"
And I should not see "Reuse"
And I should not see "Rights of use"
And I should not see "Embed"
@javascript
Scenario: Add an h5pactivity with download display option
Given the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| displayoptions | 12 |
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
When I am on the "Awesome H5P package" "h5pactivity activity" page
Then I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And "Reuse" "text" should exist in the ".h5p-actions" "css_element"
And I should not see "Rights of use"
And I should not see "Embed"
@javascript
Scenario: Add an h5pactivity with embed display option
Given the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| displayoptions | 10 |
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
When I am on the "Awesome H5P package" "h5pactivity activity" page
Then I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And "Reuse" "text" should not exist in the ".h5p-actions" "css_element"
And I should not see "Rights of use"
And I should see "Embed"
@javascript
Scenario: Add an h5pactivity with copyright display option using a content with copyright
Given the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| displayoptions | 6 |
| packagefilepath | h5p/tests/fixtures/guess-the-answer.h5p |
And I change window size to "large"
When I am on the "Awesome H5P package" "h5pactivity activity" page
Then I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And "Reuse" "text" should not exist in the ".h5p-actions" "css_element"
And I should see "Rights of use"
And I should not see "Embed"
And I click on "Rights of use" "button" in the ".h5p-actions" "css_element"
And I should see "Fruits"
And I should see "Attribution (CC BY) 4.0 International (CC BY 4.0)"
And I should see "H5P Author"
And I should see "https://h5p.org (Originator)"
And I should see "2000-2023"
And I should see "This is licence extras information added for testing purposes."
And I should see "Add metadata information, Another user, 01-11-23"
@javascript
Scenario: Add an h5pactivity with copyright display option using a content without copyright
Given the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| displayoptions | 6 |
| packagefilepath | h5p/tests/fixtures/ipsums.h5p |
When I am on the "Awesome H5P package" "h5pactivity activity" page
Then I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And "Reuse" "text" should not exist in the ".h5p-actions" "css_element"
And I should not see "Rights of use"
And I should not see "Embed"
@javascript
Scenario: Add an h5pactivity with all display options enabled
Given the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| displayoptions | 0 |
| packagefilepath | h5p/tests/fixtures/guess-the-answer.h5p |
When I am on the "Awesome H5P package" "h5pactivity activity" page
Then I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And "Reuse" "text" should exist in the ".h5p-actions" "css_element"
And I should see "Rights of use"
And I should see "Embed"
@@ -0,0 +1,82 @@
@mod @mod_h5pactivity @core_h5p
Feature: Attempts review settings.
In order to let users to review attempts
As a teacher
I need to have specific settings to let students access the attempts report
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| student2 | Student | 2 | student2@example.com |
| teacher1 | Teacher | 1 | teacher1@example.com |
| teacher2 | Teacher | 2 | teacher2@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| student2 | C1 | student |
| teacher1 | C1 | editingteacher |
| teacher2 | C1 | teacher |
Scenario Outline: Attempt review behaviour when accessing an H5P activity
Given the following "activity" exists:
| activity | h5pactivity |
| name | H5P package |
| intro | Test H5P description |
| course | C1 |
| idnumber | h5ppackage |
| enabletracking | <enabletracking> |
| reviewmode | <reviewmode> |
And the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | H5P package | 1 | compound | 0 | 2 | 6 | 1 | 0 |
| student1 | H5P package | 2 | compound | 2 | 2 | 4 | 1 | 1 |
| student2 | H5P package | 1 | compound | 1 | 2 | 8 | 1 | 0 |
When I am on the "H5P package" "h5pactivity activity" page logged in as <user>
Then "Attempts report" "link" should <attemptsreportlink> in current page administration
And I should <previewmode> "You are in preview mode."
And I should <attempttracking> "Attempt tracking is not enabled for this activity."
And I should <attempttrackingsettings> "You can enable it in Settings."
And I should <viewattempts> "View attempts (3)"
Examples:
| user | enabletracking | reviewmode | attemptsreportlink | previewmode | attempttracking | attempttrackingsettings | viewattempts |
| student1 | 1 | 1 | exist | not see | not see | not see | not see |
| student1 | 1 | 0 | not exist | not see | not see | not see | not see |
| student1 | 0 | 1 | not exist | not see | not see | not see | not see |
| teacher1 | 1 | 1 | exist | see | not see | not see | see |
| teacher1 | 1 | 0 | exist | see | not see | not see | see |
| teacher1 | 0 | 1 | not exist | see | see | see | not see |
| teacher2 | 0 | 1 | not exist | see | see | not see | not see |
| teacher2 | 1 | 1 | exist | see | not see | not see | see |
Scenario: View link behaviour
Given the following "activity" exists:
| activity | h5pactivity |
| name | H5P package |
| intro | Test H5P description |
| course | C1 |
| idnumber | h5ppackage |
| enabletracking | 1 |
| reviewmode | 1 |
When I am on the "H5P package" "h5pactivity activity" page logged in as teacher1
# The link is displayed with the correct number of attempts (in that case 0 because there are no attempts yet).
Then I should see "View attempts (0)"
Then I follow "View attempts (0)"
And I should not see "View user attempts"
But the following "mod_h5pactivity > attempt" exists:
| user | student1 |
| h5pactivity | H5P package |
| attempt | 1 |
| interactiontype | compound |
| rawscore | 2 |
| maxscore | 2 |
| duration | 4 |
| completion | 1 |
| success | 1 |
And I am on the "H5P package" "h5pactivity activity" page
And I follow "View attempts (1)"
And I should see "View (1)"
@@ -0,0 +1,104 @@
@mod @mod_h5pactivity @core_h5p @_switch_iframe
Feature: Content bank link in the activity settings form
In order to have direct access to the Content bank
As a teacher
I need to see a Content bank link in the activity settings
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
@javascript
Scenario: The content bank link should go to the course Content bank
When I log in as "teacher1"
And I add an h5pactivity activity to course "Course 1" section "1"
Then I should see "Use the content bank (opens in new window) to manage your H5P files"
And I click on "content bank (opens in new window)" "link" in the "General" "fieldset"
And I switch to a second window
And I should see "Content bank" in the "page-content" "region"
And I should see "filltheblanks.h5p" in the "page-content" "region"
And I close all opened windows
@javascript
Scenario: Content bank is not displayed if the user don't have access to the content bank
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/contentbank:access | Prevent | editingteacher | Course | C1 |
When I log in as "teacher1"
And I add an h5pactivity activity to course "Course 1" section "1"
Then I should not see "Use the content Bank (opens in new window) to manage your H5P files"
@javascript
Scenario: A different message should be displayed if the package file is a link to the content bank file
Given I log in as "admin"
And I add an h5pactivity activity to course "Course 1" section "1"
And I set the following fields to these values:
| Name | H5P package added with link to content bank |
| Description | Description |
And I click on "Add..." "button" in the "Package file" "form_row"
And I select "Content bank" repository in file picker
And I click on "filltheblanks.h5p" "file" in repository content area
And I click on "Link to the file" "radio"
And I click on "Select this file" "button"
And I click on "Save and display" "button"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I should see "Of which countries are Berlin, Washington, Beijing, Canberra and Brasilia the capitals?"
And I switch to the main frame
When I navigate to "Settings" in current page administration
Then I should not see "Use the content Bank (opens in new window) to manage your H5P files"
And I should see "Access the H5P file in the content bank (opens in a new window)."
And I follow "Access the H5P file in the content bank"
@javascript
Scenario: The content bank link should go to the course Content bank if the file is a copy to a content bank file
Given I log in as "admin"
And I add an h5pactivity activity to course "Course 1" section "1"
And I set the following fields to these values:
| Name | H5P package added with link to content bank |
| Description | Description |
And I click on "Add..." "button" in the "Package file" "form_row"
And I select "Content bank" repository in file picker
And I click on "filltheblanks.h5p" "file" in repository content area
And I click on "Make a copy of the file" "radio"
And I click on "Select this file" "button"
And I click on "Save and display" "button"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I should see "Of which countries are Berlin,"
And I switch to the main frame
When I navigate to "Settings" in current page administration
Then I should see "Use the content bank (opens in new window) to manage your H5P files"
@javascript
Scenario: The content bank link should go to the course Content bank if the file is referenced but to another repository
Given the following "user private file" exists:
| user | admin |
| filepath | h5p/tests/fixtures/guess-the-answer.h5p |
When I log in as "admin"
And I add an h5pactivity activity to course "Course 1" section "1"
And I set the following fields to these values:
| Name | H5P package added with link to content bank |
| Description | Description |
And I click on "Add..." "button" in the "Package file" "form_row"
And I select "Private files" repository in file picker
And I click on "guess-the-answer.h5p" "file" in repository content area
And I click on "Link to the file" "radio"
And I click on "Select this file" "button"
And I click on "Save and display" "button"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I should see "Which fruit is this?"
And I switch to the main frame
And I navigate to "Settings" in current page administration
Then I should see "Use the content bank (opens in new window) to manage your H5P files"
@@ -0,0 +1,50 @@
@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe @javascript
Feature: Set up attempt grading options into H5P activity
In order to use automatic grading in H5P activity
As a teacher
I need to be able to configure the attempt settings
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/h5p:updatelibraries | Allow | editingteacher | System | |
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add an h5pactivity activity to course "Course 1" section "1"
Scenario: Default values should have tracking and grading
When the field "Type" matches value "Point"
Then the "Grading method" "select" should be enabled
Scenario: Scale grading should not have a grading method.
When I set the following fields to these values:
| Name | Awesome H5P package |
| Type | Scale |
Then the "Grading method" "select" should be disabled
Scenario: None grading should not have a grading method.
When I set the following fields to these values:
| Name | Awesome H5P package |
| Type | None |
Then the "Grading method" "select" should be disabled
Scenario: Point grading should have a grading method.
When I set the following fields to these values:
| Name | Awesome H5P package |
| Type | Point |
Then the "Grading method" "select" should be enabled
Scenario: Disable tracking should make grading method disappear.
When I set the following fields to these values:
| Name | Awesome H5P package |
| Enable attempt tracking | No |
Then "Grading method" "field" should not be visible
@@ -0,0 +1,230 @@
@mod @mod_h5pactivity @core_h5p @core_xapi
Feature: Report different types of interactions.
In order to let users to review attempts
As a user
I need to view all valid interactions in the report
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
# This test is only about reporting, we don't need to specify any valid H5P file for it.
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| h5pactivity | H5P package | Test H5P description | C1 | h5ppackage |
Scenario: General success attempt information
Given the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | H5P package | 1 | compound | 2 | 2 | 4 | 1 | 1 |
When I am on the "H5P package" "h5pactivity activity" page logged in as student1
And I navigate to "Attempts report" in current page administration
And I follow "View report"
Then I should see "2 out of 2"
And I should see "Pass"
And I should see "4 seconds"
And I should see "This attempt is completed"
Scenario: General failed attempt statement
Given the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | H5P package | 1 | compound | 0 | 2 | 4 | 1 | 0 |
When I am on the "H5P package" "h5pactivity activity" page logged in as student1
And I navigate to "Attempts report" in current page administration
And I follow "View report"
Then I should see "0 out of 2"
And I should see "Fail"
And I should see "4 seconds"
And I should see "This attempt is completed"
Scenario: View a success choice statement
Given the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | H5P package | 1 | choice | 2 | 2 | 1 | 1 | 1 |
| student1 | H5P package | 1 | compound | 2 | 2 | 4 | 1 | 1 |
When I am on the "H5P package" "h5pactivity activity" page logged in as student1
And I navigate to "Attempts report" in current page administration
And I follow "View report"
Then I should see "Select the correct answers"
And "Correct answer" "icon" should exist in the "This is also a correct answer" "table_row"
And I should see "Score: 2 out of 2"
Scenario: View a failed choice statement
Given the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | H5P package | 1 | choice | 0 | 2 | 1 | 1 | 0 |
| student1 | H5P package | 1 | compound | 0 | 2 | 4 | 1 | 0 |
When I am on the "H5P package" "h5pactivity activity" page logged in as student1
And I navigate to "Attempts report" in current page administration
And I follow "View report"
Then I should see "Select the correct answers"
And "Incorrect answer" "icon" should exist in the "Another wrong answer" "table_row"
And I should see "Score: 0 out of 2"
Scenario: View a success matching statement
Given the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | H5P package | 1 | matching | 2 | 2 | 1 | 1 | 1 |
| student1 | H5P package | 1 | compound | 2 | 2 | 4 | 1 | 1 |
When I am on the "H5P package" "h5pactivity activity" page logged in as student1
And I navigate to "Attempts report" in current page administration
And I follow "View report"
Then I should see "Drag and Drop example 1"
And "Your answer is correct" "icon" should exist in the "Drop item A" "table_row"
And I should see "Score: 2 out of 2"
Scenario: View a failed matching statement
Given the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | H5P package | 1 | matching | 0 | 2 | 1 | 1 | 1 |
| student1 | H5P package | 1 | compound | 0 | 2 | 4 | 1 | 1 |
When I am on the "H5P package" "h5pactivity activity" page logged in as student1
And I navigate to "Attempts report" in current page administration
And I follow "View report"
Then I should see "Drag and Drop example 1"
And "Your answer is incorrect" "icon" should exist in the "Drop item A" "table_row"
And I should see "Score: 0 out of 2"
Scenario: View a success true-false statement
Given the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | H5P package | 1 | true-false | 2 | 2 | 1 | 1 | 1 |
| student1 | H5P package | 1 | compound | 2 | 2 | 4 | 1 | 1 |
When I am on the "H5P package" "h5pactivity activity" page logged in as student1
And I navigate to "Attempts report" in current page administration
And I follow "View report"
Then I should see "The correct answer is true"
And "Correct answer" "icon" should exist in the "True" "table_row"
And I should see "Score: 2 out of 2"
Scenario: View a failed true-false statement
Given the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | H5P package | 1 | true-false | 0 | 2 | 1 | 1 | 0 |
| student1 | H5P package | 1 | compound | 0 | 2 | 4 | 1 | 0 |
When I am on the "H5P package" "h5pactivity activity" page logged in as student1
And I navigate to "Attempts report" in current page administration
And I follow "View report"
Then I should see "The correct answer is true"
And "Incorrect answer" "icon" should exist in the "False" "table_row"
And I should see "Score: 0 out of 2"
Scenario: View a success fill-in statement
Given the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | H5P package | 1 | fill-in | 2 | 2 | 1 | 1 | 1 |
| student1 | H5P package | 1 | compound | 2 | 2 | 4 | 1 | 1 |
When I am on the "H5P package" "h5pactivity activity" page logged in as student1
And I navigate to "Attempts report" in current page administration
And I follow "View report"
Then I should see "This an example of missing word text"
And "Your answer is correct" "icon" should exist in the "Gap #1" "table_row"
And I should see "first" in the "Gap #1" "table_row"
And "Your answer is correct" "icon" should exist in the "Gap #2" "table_row"
And I should see "second" in the "Gap #2" "table_row"
And I should see "Score: 2 out of 2"
Scenario: View a failed fill-in statement
Given the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | H5P package | 1 | fill-in | 0 | 2 | 1 | 1 | 0 |
| student1 | H5P package | 1 | compound | 0 | 2 | 4 | 1 | 0 |
When I am on the "H5P package" "h5pactivity activity" page logged in as student1
And I navigate to "Attempts report" in current page administration
And I follow "View report"
Then I should see "This an example of missing word text"
And "Your answer is incorrect" "icon" should exist in the "Gap #1" "table_row"
And I should see "first" in the "Gap #1" "table_row"
And I should see "something" in the "Gap #1" "table_row"
And "Your answer is incorrect" "icon" should exist in the "Gap #2" "table_row"
And I should see "second" in the "Gap #2" "table_row"
And I should see "else" in the "Gap #2" "table_row"
And I should see "Score: 0 out of 2"
Scenario: View a success long-fill-in statement
Given the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | H5P package | 1 | long-fill-in | 2 | 2 | 1 | 1 | 1 |
| student1 | H5P package | 1 | compound | 2 | 2 | 4 | 1 | 1 |
When I am on the "H5P package" "h5pactivity activity" page logged in as student1
And I navigate to "Attempts report" in current page administration
And I follow "View report"
Then I should see "Please describe the novel The Hobbit"
And I should see "The Hobbit is book"
# Fill-in does not have a partial scope indicador, we only check the general one.
And I should see "2 out of 2"
Scenario: View a failed long-fill-in statement
Given the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | H5P package | 1 | long-fill-in | 0 | 2 | 1 | 1 | 0 |
| student1 | H5P package | 1 | compound | 0 | 2 | 4 | 1 | 0 |
When I am on the "H5P package" "h5pactivity activity" page logged in as student1
And I navigate to "Attempts report" in current page administration
And I follow "View report"
Then I should see "Please describe the novel The Hobbit"
And I should see "Who cares?"
# Fill-in does not have a partial scope indicador, we only check the general one.
And I should see "0 out of 2"
# The current H5P implementation does not send a complete sequencing interaction statement
# we check only the warning and the final result.
Scenario: View a success sequencing statement
Given the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | H5P package | 1 | sequencing | 2 | 2 | 1 | 1 | 1 |
| student1 | H5P package | 1 | compound | 2 | 2 | 4 | 1 | 1 |
When I am on the "H5P package" "h5pactivity activity" page logged in as student1
And I navigate to "Attempts report" in current page administration
And I follow "View report"
Then I should see "This interaction (sequencing) does not provide tracking information"
# Sequencing does not have a partial scope indicador, we only check the general one.
And I should see "2 out of 2"
# The current H5P implementation does not send a complete sequencing interaction statement
# we check only the warning and the final result.
Scenario: View a failed sequencing statement
Given the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | H5P package | 1 | sequencing | 2 | 2 | 1 | 1 | 0 |
| student1 | H5P package | 1 | compound | 0 | 2 | 4 | 1 | 0 |
When I am on the "H5P package" "h5pactivity activity" page logged in as student1
And I navigate to "Attempts report" in current page administration
And I follow "View report"
Then I should see "This interaction (sequencing) does not provide tracking information"
# Sequencing does not have a partial scope indicador, we only check the general one.
And I should see "0 out of 2"
# The current H5P implementation does not send a complete sequencing interaction statement
# we check only the warning and the final result.
Scenario: View a success other statement
Given the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | H5P package | 1 | other | 2 | 2 | 1 | 1 | 1 |
| student1 | H5P package | 1 | compound | 2 | 2 | 4 | 1 | 1 |
When I am on the "H5P package" "h5pactivity activity" page logged in as student1
And I navigate to "Attempts report" in current page administration
And I follow "View report"
Then I should see "This interaction (other) does not provide tracking information"
# Other does not have a partial scope indicador, we only check the general one.
And I should see "2 out of 2"
# The current H5P implementation does not send a complete sequencing interaction statement
# we check only the warning and the final result.
Scenario: View a failed other statement
Given the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | H5P package | 1 | other | 2 | 2 | 1 | 1 | 0 |
| student1 | H5P package | 1 | compound | 0 | 2 | 4 | 1 | 0 |
When I am on the "H5P package" "h5pactivity activity" page logged in as student1
And I navigate to "Attempts report" in current page administration
And I follow "View report"
Then I should see "This interaction (other) does not provide tracking information"
# Other does not have a partial scope indicador, we only check the general one.
And I should see "0 out of 2"
@@ -0,0 +1,40 @@
@mod @mod_h5pactivity
Feature: Duplicate and delete a h5pactivity
In order to quickly create and delete h5p activities
As a teacher
I need to duplicate or delete h5pactivity inside the same course
Background:
Given the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
Scenario: Duplicate and delete h5p activity
Given the following "activities" exist:
| activity | course | name | packagefilepath |
| h5pactivity | C1 | H5P Activity 1 | h5p/tests/fixtures/filltheblanks.h5p |
And I am on the "H5P Activity 1" "h5pactivity activity" page logged in as teacher1
# Initial confirmation that no error occurs when viewing h5p activity
And I should see "You are in preview mode."
And I am on "Course 1" course homepage with editing mode on
# Duplicate the h5p activity
When I duplicate "H5P Activity 1" activity
# Confirm that h5p activity was duplicated successfully
Then I should see "H5P Activity 1 (copy)"
And I am on the "H5P Activity 1 (copy)" "h5pactivity activity" page
# Confirm there are no errors when viewing duplicate h5p activity
And I should see "You are in preview mode."
And I am on the "Course 1" course page
# Delete the duplicate h5p activity
And I delete "H5P Activity 1 (copy)" activity
# Confirm duplicate was deleted successfully
And I should not see "H5P Activity 1 (copy)"
And I am on the "H5P Activity 1" "h5pactivity activity" page
# Confirm there are no errors on the original h5p activity after deleting the duplicate
And I should see "You are in preview mode."
@@ -0,0 +1,153 @@
@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe
Feature: Change grading options in an H5P activity
In order to let students do a H5P attempt
As a teacher
I need to define what students attempts are used for grading
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/h5p:updatelibraries | Allow | editingteacher | System | |
And the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| packagefilepath | h5p/tests/fixtures/multiple-choice-2-6.h5p |
And the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | Awesome H5P package | 1 | choice | 0 | 1 | 4 | 1 | 0 |
| student1 | Awesome H5P package | 2 | choice | 1 | 1 | 4 | 1 | 1 |
| student1 | Awesome H5P package | 3 | choice | 0 | 1 | 4 | 1 | 0 |
@javascript
Scenario: Default grading is max attempt grade
Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1
And I expand all fieldsets
And the field "Grading method" matches value "Highest grade"
And I click on "Save and return to course" "button"
When I navigate to "View > User report" in the course gradebook
And I click on "Student 1" in the "user" search widget
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Percentage |
| Awesome H5P package | 100.00 | 100.00 % |
@javascript
Scenario: Change setting to first attempt
Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1
When I set the following fields to these values:
| Grading method | First attempt |
And I click on "Save and return to course" "button"
And I navigate to "View > User report" in the course gradebook
And I click on "Student 1" in the "user" search widget
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Percentage |
| Awesome H5P package | 0.00 | 0.00 % |
@javascript
Scenario: Change setting to last attempt
Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1
When I set the following fields to these values:
| Grading method | Last attempt |
And I click on "Save and return to course" "button"
And I navigate to "View > User report" in the course gradebook
And I click on "Student 1" in the "user" search widget
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Percentage |
| Awesome H5P package | 0.00 | 0.00 % |
@javascript
Scenario: Change setting to average attempt
Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1
When I set the following fields to these values:
| Grading method | Average grade |
And I click on "Save and return to course" "button"
And I navigate to "View > User report" in the course gradebook
And I click on "Student 1" in the "user" search widget
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Percentage |
| Awesome H5P package | 33.33 | 33.33 % |
@javascript
Scenario: Change setting to manual grading
Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1
When I set the following fields to these values:
| Grading method | Don't calculate a grade |
And I click on "Save and return to course" "button"
And I navigate to "View > User report" in the course gradebook
And I click on "Student 1" in the "user" search widget
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Percentage |
| Awesome H5P package | - | - |
@javascript
Scenario: Disable tracking
Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1
When I set the following fields to these values:
| Enable attempt tracking | No |
And I click on "Save and return to course" "button"
And I navigate to "View > User report" in the course gradebook
And I click on "Student 1" in the "user" search widget
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Percentage |
| Awesome H5P package | - | - |
@javascript
Scenario: Reescale existing grades changing the maximum grade
# First we set to average and recalculate grades.
Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1
When I set the following fields to these values:
| Grading method | Average grade |
And I click on "Save and return to course" "button"
And I navigate to "View > User report" in the course gradebook
And I click on "Student 1" in the "user" search widget
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Range | Percentage |
| Awesome H5P package | 33.33 | 0100 | 33.33 % |
# Now we modify the maximum grade with rescaling.
And I am on the "Awesome H5P package" "h5pactivity activity editing" page
And I set the following fields to these values:
| Rescale existing grades | Yes |
| Maximum grade | 50 |
And I click on "Save and return to course" "button"
And I navigate to "View > User report" in the course gradebook
And I click on "Student 1" in the "user" search widget
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Range | Percentage |
| Awesome H5P package | 16.67 | 050 | 33.33 % |
@javascript
Scenario: Change maximum grade without rescaling grade
# First we set to average and recalculate grades.
Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1
When I set the following fields to these values:
| Grading method | Average grade |
And I click on "Save and return to course" "button"
And I navigate to "View > User report" in the course gradebook
And I click on "Student 1" in the "user" search widget
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Range | Percentage |
| Awesome H5P package | 33.33 | 0100 | 33.33 % |
# Now we modify the maximum grade with rescaling.
When I am on the "Awesome H5P package" "h5pactivity activity editing" page
And I set the following fields to these values:
| Rescale existing grades | No |
| Maximum grade | 50 |
And I click on "Save and return to course" "button"
And I navigate to "View > User report" in the course gradebook
And I click on "Student 1" in the "user" search widget
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Range | Percentage |
| Awesome H5P package | 33.33 | 050 | 66.67 % |
@@ -0,0 +1,41 @@
@mod @mod_h5pactivity @core_5hp
Feature: Control H5P activity availability for students
In order to restrict student access to H5P activity
As a teacher
I need to control the availability of the H5P activity
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "activities" exist:
| activity | course | name |
| h5pactivity | C1 | H5P Test |
@javascript
Scenario Outline: Restrict H5P activity access by date
Given I am on the "H5P Test" "h5pactivity activity editing" page logged in as teacher1
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I set the following fields to these values:
| Direction | from |
| x[day] | 1 |
| x[month] | 1 |
| x[year] | <year> |
And I press "Save and return to course"
When I am on the "Course 1" course page logged in as student1
Then I <fromvisibility> see "Available from"
Examples:
| year | fromvisibility |
| ## -1 year ## %Y ## | should not |
| ## +1 year ## %Y ## | should |
@@ -0,0 +1,55 @@
@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe @javascript @core_completion
Feature: View activity completion information in the h5p activity
In order to have visibility of h5p completion requirements
As a student
I need to be able to view my h5p completion progress
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Vinnie | Student1 | student1@example.com |
| teacher1 | Darrell | Teacher1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | enablecompletion | showcompletionconditions |
| Course 1 | C1 | 1 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| teacher1 | C1 | editingteacher |
And the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Music history |
| completion | 2 |
| completionview | 1 |
| completionusegrade | 1 |
| packagefilepath | h5p/tests/fixtures/filltheblanks.h5p |
Scenario: View automatic completion items
Given I change window size to "large"
And I am on the "Music history" "h5pactivity activity" page logged in as teacher1
# Teacher view.
And "Music history" should have the "View" completion condition
And "Music history" should have the "Receive a grade" completion condition
# Student view.
When I am on the "Music history" "h5pactivity activity" page logged in as student1
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
And I reload the page
Then the "View" completion condition of "Music history" is displayed as "done"
And the "Receive a grade" completion condition of "Music history" is displayed as "done"
Scenario: Use manual completion
Given I am on the "Music history" "h5pactivity activity editing" page logged in as teacher1
And I expand all fieldsets
And I set the field "Students must manually mark the activity as done" to "1"
And I press "Save and display"
# Teacher view.
And I am on the "Music history" "h5pactivity activity" page
And the manual completion button for "Music history" should be disabled
# Student view.
When I am on the "Music history" "h5pactivity activity" page logged in as student1
Then the manual completion button of "Music history" is displayed as "Mark as done"
And I toggle the manual completion state of "Music history"
And the manual completion button of "Music history" is displayed as "Done"
@@ -0,0 +1,66 @@
@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe @javascript @core_completion
Feature: Pass grade activity completion information in the h5p activity
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Vinnie | Student1 | student1@example.com |
| student2 | Vinnie | Student2 | student2@example.com |
| student3 | Vinnie | Student3 | student3@example.com |
| teacher1 | Darrell | Teacher1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category | enablecompletion |
| Course 1 | C1 | 0 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| student2 | C1 | student |
| student3 | C1 | student |
| teacher1 | C1 | editingteacher |
And the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Music history |
| completion | 2 |
| completionview | 1 |
| completionusegrade | 1 |
| completionpassgrade | 1 |
| gradepass | 25 |
| packagefilepath | h5p/tests/fixtures/filltheblanks.h5p |
Scenario: View automatic completion items
# Teacher view.
Given I change window size to "large"
And I am on the "Music history" "h5pactivity activity" page logged in as teacher1
And "Music history" should have the "View" completion condition
And "Music history" should have the "Receive a grade" completion condition
And "Music history" should have the "Receive a passing grade" completion condition
And I log out
# Student view.
When I am on the "Music history" "h5pactivity activity" page logged in as student1
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
And I reload the page
And I am on the "Music history" "h5pactivity activity" page logged in as student2
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Brasilia"
And I set the field with xpath "//input[contains(@aria-label,\"Blank input 2 of 4\")]" to "Washington"
And I set the field with xpath "//input[contains(@aria-label,\"Blank input 3 of 4\")]" to "Berlin"
And I set the field with xpath "//input[contains(@aria-label,\"Blank input 4 of 4\")]" to "Canberra"
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
And I switch to the main frame
And I reload the page
Then the "View" completion condition of "Music history" is displayed as "done"
And the "Receive a grade" completion condition of "Music history" is displayed as "done"
And the "Receive a passing grade" completion condition of "Music history" is displayed as "done"
And I log out
And I am on the "Music history" "h5pactivity activity" page logged in as student1
And the "View" completion condition of "Music history" is displayed as "done"
And the "Receive a grade" completion condition of "Music history" is displayed as "done"
And the "Receive a passing grade" completion condition of "Music history" is displayed as "failed"
And I am on the "Course 1" "course" page logged in as "teacher1"
And "Vinnie Student1" user has completed "Music history" activity
And "Vinnie Student2" user has completed "Music history" activity
And "Vinnie Student3" user has not completed "Music history" activity
@@ -0,0 +1,51 @@
@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe
Feature: Undeployed H5P activities packages should be available only to any user that can deploy packages.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| teacher2 | Teacher | 2 | teacher2@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| teacher2 | C1 | editingteacher |
| student1 | C1 | student |
# Make sure that the teacher2 can update libraries so it show the right info when.
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/h5p:updatelibraries | Allow | editingteacher | System | |
# Now create the activity as teacher1.
And the following "activities" exist:
| activity | course | name | username | packagefilepath |
| h5pactivity | C1 | Music history | teacher1 | h5p/tests/fixtures/filltheblanks.h5p |
And I log in as "admin"
And I navigate to "Users > Accounts > Browse list of users" in site administration
And I press "Delete" action in the "Teacher 1" report row
And I click on "Delete" "button" in the "Delete user" "dialogue"
And I should see "Deleted user Teacher 1"
@javascript
Scenario: In an H5P activity, as student I should not be able to deploy the package if not deployed by the teacher
beforehand. Then if a second teacher deploys the package, I can see it.
Given I am on the "Music history" "h5pactivity activity" page logged in as student1
And I switch to "h5p-player" class iframe
And "This file can't be displayed because it has been uploaded by a user without the required capability to deploy H5P content" "text" should exist
And I switch to the main frame
And I log out
# Then teacher2 will be allowed to deploy the package.
And I am on the "Music history" "h5pactivity activity" page logged in as teacher2
And I switch to "h5p-player" class iframe
When I switch to "h5p-iframe" class iframe
Then I should see "Of which countries are Berlin"
And I switch to the main frame
And I log out
# Now student1 should be able to see the package.
And I am on the "Music history" "h5pactivity activity" page logged in as student1
And I switch to "h5p-player" class iframe
When I switch to "h5p-iframe" class iframe
Then I should see "Of which countries are Berlin"
@@ -0,0 +1,100 @@
@mod @mod_h5pactivity @core_h5p
Feature: Teacher can control h5p activity grading setting
In order to set h5p activity grade
As a teacher
I need to be able to control h5p activity grading setting
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
| student2 | Student | 2 | student2@example.com |
| student3 | Student | 3 | student3@example.com |
| student4 | Student | 4 | student4@example.com |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
| student3 | C1 | student |
| student4 | C1 | student |
@javascript
Scenario: Verify that invalid grades given to students are not saved
Given the following "activities" exist:
| activity | name | course |
| h5pactivity | H5P point | C1 |
# Activity grade settings are not saved using generators so manual setting is necessary.
And I am on the "H5P point" "h5pactivity activity editing" page logged in as teacher1
And I set the following fields to these values:
| grade[modgrade_type] | Point |
| grade[modgrade_point] | 10 |
And I press "Save and return to course"
And I navigate to "View > Grader report" in the course gradebook
And I turn editing mode on
When I give the grade "50" to the user "Student 1" for the grade item "H5P point"
And I press "Save changes"
And I turn editing mode off
# Confirm that grades are not saved when grade entered is > maximum grade.
Then the following should exist in the "user-grades" table:
| -1- | -2- | -3- |
| Student 1 | student1@example.com | - |
| Student 2 | student2@example.com | - |
| Student 3 | student3@example.com | - |
| Student 4 | student4@example.com | - |
@javascript
Scenario: Verify that valid grades given to students are saved
Given the following "activities" exist:
| activity | name | course |
| h5pactivity | H5P point | C1 |
# Activity grade settings are not saved using generators so manual setting is necessary.
And I am on the "H5P point" "h5pactivity activity editing" page logged in as teacher1
And I set the following fields to these values:
| grade[modgrade_type] | Point |
| grade[modgrade_point] | 10 |
And I press "Save and return to course"
And I navigate to "View > Grader report" in the course gradebook
And I turn editing mode on
When I give the grade "10" to the user "Student 1" for the grade item "H5P point"
And I give the grade "5" to the user "Student 2" for the grade item "H5P point"
And I give the grade "0" to the user "Student 3" for the grade item "H5P point"
And I press "Save changes"
And I turn editing mode off
# Confirm that corresponding grades are stored for each student.
And the following should exist in the "user-grades" table:
| -1- | -2- | -3- |
| Student 1 | student1@example.com | 10 |
| Student 2 | student2@example.com | 5 |
| Student 3 | student3@example.com | 0 |
| Student 4 | student4@example.com | - |
@javascript
Scenario: Verify that scales given to students are saved
Given the following "activities" exist:
| activity | name | course |
| h5pactivity | H5P scale | C1 |
# Activity grade settings are not saved using generators so manual setting is necessary.
And I am on the "H5P scale" "h5pactivity activity editing" page logged in as teacher1
And I set the following fields to these values:
| grade[modgrade_type] | Scale |
| grade[modgrade_scale] | Default competence scale |
And I press "Save and return to course"
And I navigate to "View > Grader report" in the course gradebook
And I turn editing mode on
And I give the grade "Not yet competent" to the user "Student 1" for the grade item "H5P scale"
And I give the grade "Competent" to the user "Student 2" for the grade item "H5P scale"
And I give the grade "Competent" to the user "Student 4" for the grade item "H5P scale"
When I press "Save changes"
And I turn editing mode off
# Confirm that scale set for student is successfully saved.
Then the following should exist in the "user-grades" table:
| -1- | -2- | -3- |
| Student 1 | student1@example.com | Not yet competent |
| Student 2 | student2@example.com | Competent |
| Student 3 | student3@example.com | - |
| Student 4 | student4@example.com | Competent |
@@ -0,0 +1,51 @@
@mod @mod_h5pactivity @core_h5p
Feature: Teacher can reset H5P activity grades
As a teacher,
I should be able to reset H5P activity grades
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | One | teacher1@example.com |
| student1 | First | Student | student1@example.com |
| student2 | Second | Student | student2@example.com |
| student3 | Third | Student | student3@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
| student3 | C1 | student |
And the following "activities" exist:
| activity | course | name | grade[modgrade_type] | grade[modgrade_point] |
| h5pactivity | C1 | H5P Activity | point | 10 |
@javascript
Scenario:Teacher can reset H5P activity grades
Given I am on the "Course 1" course page logged in as teacher1
And I navigate to "View > Grader report" in the course gradebook
And I turn editing mode on
And I give the grade "7" to the user "First Student" for the grade item "H5P Activity"
And I give the grade "5" to the user "Second Student" for the grade item "H5P Activity"
And I give the grade "0" to the user "Third Student" for the grade item "H5P Activity"
And I press "Save changes"
# Confirm that grade was sucessfully saved
And I turn editing mode off
And I should see "7.00" in the "First Student" "table_row"
And I should see "5.00" in the "Second Student" "table_row"
And I should see "0.00" in the "Third Student" "table_row"
When I am on the "Course 1" "reset" page
And I expand all fieldsets
# Check `Delete all grades` in course reset page to reset grades
And I click on "Delete all grades" "checkbox"
And I press "Reset"
Then I should see "OK" in the "Gradebook" "table_row"
And I press "Continue"
# Confirm that previously saved grades are gone
And I navigate to "View > Grader report" in the course gradebook
And I should not see "7.00" in the "First Student" "table_row"
And I should not see "5.00" in the "Second Student" "table_row"
And I should not see "0.00" in the "Third Student" "table_row"
@@ -0,0 +1,194 @@
@mod @mod_h5pactivity @core_h5p @_switch_iframe
Feature: Inline editing H5P content
In order to edit an existing H5P activity file
As a teacher
I need to see the button and access to the H5P editor
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| teacher2 | Teacher | 2 | teacher2@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| teacher2 | C1 | editingteacher |
| student1 | C1 | student |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/h5p:updatelibraries | Allow | editingteacher | System | |
@javascript
Scenario: Add H5P activity using link to content bank file
Given the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| Course | C1 | contenttype_h5p | teacher1 | Greeting card | /h5p/tests/fixtures/greeting-card.h5p |
And I log in as "admin"
# Add the navigation block.
And I am on "Course 1" course homepage with editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
# Create an H5P activity with a link to the content-bank file.
And I add an h5pactivity activity to course "Course 1" section "1"
And I set the following fields to these values:
| Name | H5P package added as link to content bank |
| Description | Description |
And I click on "Add..." "button" in the "Package file" "form_row"
And I select "Content bank" repository in file picker
And I click on "Greeting card" "file" in repository content area
And I click on "Link to the file" "radio"
And I click on "Select this file" "button"
And I click on "Save and display" "button"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I should see "Hello world!"
And I switch to the main frame
# Modify the H5P content using the edit button (which opens the H5P editor).
And I follow "Edit H5P content"
And I should see "This content may be in use in other places."
And I switch to "h5p-editor-iframe" class iframe
And I set the field "Greeting text" to "It's a Wonderful Life!"
And I switch to the main frame
And I click on "Save changes" "button"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
# Check the H5P content has changed.
And I should not see "Hello world!"
And I should see "It's a Wonderful Life!"
And I switch to the main frame
# Check the H5P has also changed into the content bank.
And I am on "Course 1" course homepage
And I click on "Site pages" "list_item" in the "Navigation" "block"
And I click on "Content bank" "link" in the "Navigation" "block"
And I click on "Greeting card" "link"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I should not see "Hello world!"
And I should see "It's a Wonderful Life!"
And I switch to the main frame
And I log out
# Check teacher1 can see the Edit button (because she is the author of this file in the content bank).
And I am on the "H5P package added as link to content bank" "h5pactivity activity" page logged in as teacher1
And I should see "Edit H5P content"
And I log out
# Check teacher2 can't see the Edit button (because the file was created by the teacher1).
When I am on the "H5P package added as link to content bank" "h5pactivity activity" page logged in as teacher2
Then I should not see "Edit H5P content"
And I log out
# Check student1 can't see the Edit button.
And I am on the "H5P package added as link to content bank" "h5pactivity activity" page logged in as student1
And I should not see "Edit H5P content"
@javascript
Scenario: Add H5P activity using copy to content bank file
Given the following "contentbank content" exist:
| contextlevel | reference | contenttype | user | contentname | filepath |
| Course | C1 | contenttype_h5p | admin | Greeting card | /h5p/tests/fixtures/greeting-card.h5p |
And I log in as "admin"
# Add the navigation block.
And I am on "Course 1" course homepage with editing mode on
And the following config values are set as admin:
| unaddableblocks | | theme_boost|
And I add the "Navigation" block if not present
# Create an H5P activity with a copy to the content-bank file.
And I add an h5pactivity activity to course "Course 1" section "1"
And I set the following fields to these values:
| Name | H5P package added as copy to content bank |
| Description | Description |
And I click on "Add..." "button" in the "Package file" "form_row"
And I select "Content bank" repository in file picker
And I click on "Greeting card" "file" in repository content area
And I click on "Make a copy of the file" "radio"
And I click on "Select this file" "button"
And I click on "Save and display" "button"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I should see "Hello world!"
And I switch to the main frame
# Modify the H5P content using the edit button (which opens the H5P editor).
And I follow "Edit H5P content"
And I should not see "This content may be in use in other places."
And I switch to "h5p-editor-iframe" class iframe
And I set the field "Greeting text" to "The nightmare before Christmas"
And I switch to the main frame
And I click on "Save changes" "button"
# Check the H5P content has changed.
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I should not see "Hello world!"
And I should see "The nightmare before Christmas"
And I switch to the main frame
# Check the H5P has also changed into the content bank.
And I am on "Course 1" course homepage
And I click on "Site pages" "list_item" in the "Navigation" "block"
And I click on "Content bank" "link" in the "Navigation" "block"
And I click on "Greeting card" "link"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I should see "Hello world!"
And I should not see "The nightmare before Christmas"
And I switch to the main frame
And I log out
# Check teacher1 can see the Edit button (because the file is a copy).
And I am on the "H5P package added as copy to content bank" "h5pactivity activity" page logged in as teacher1
And I should see "Edit H5P content"
And I log out
# Check teacher2 can also see the Edit button (because the file is a copy).
When I am on the "H5P package added as copy to content bank" "h5pactivity activity" page logged in as teacher2
Then I should see "Edit H5P content"
And I log out
# Check student1 can't see the Edit button.
And I am on the "H5P package added as copy to content bank" "h5pactivity activity" page logged in as student1
And I should not see "Edit H5P content"
@javascript
Scenario: Add H5P activity using private user file
Given the following "user private file" exists:
| user | teacher1 |
| filepath | h5p/tests/fixtures/greeting-card.h5p |
# Create an H5P activity with a private user file.
When I log in as "teacher1"
And I add an h5pactivity activity to course "Course 1" section "1"
And I set the following fields to these values:
| Name | H5P package added as private user file |
| Description | Description |
And I click on "Add..." "button" in the "Package file" "form_row"
And I select "Private files" repository in file picker
And I click on "greeting-card.h5p" "file" in repository content area
And I click on "Link to the file" "radio"
And I click on "Select this file" "button"
And I click on "Save and display" "button"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I should see "Hello world!"
And I switch to the main frame
# Modify the H5P content using the edit button (which opens the H5P editor).
And I follow "Edit H5P content"
And I should see "This content may be in use in other places."
And I switch to "h5p-editor-iframe" class iframe
And I set the field "Greeting text" to "Little women"
And I switch to the main frame
And I click on "Save changes" "button"
# Check the H5P content has changed.
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I should not see "Hello world!"
And I should see "Little women"
And I switch to the main frame
And I log out
# Check admin can't see the Edit button (because the file belongs to teacher1).
And I am on the "H5P package added as private user file" "h5pactivity activity" page logged in as admin
And I should not see "Edit H5P content"
And I log out
# Check teacher2 can't see the Edit button (because the file belongs to teacher1).
When I am on the "H5P package added as private user file" "h5pactivity activity" page logged in as teacher2
Then I should not see "Edit H5P content"
And I log out
# Check student1 can't see the Edit button.
And I am on the "H5P package added as private user file" "h5pactivity activity" page logged in as student1
And I should not see "Edit H5P content"
@@ -0,0 +1,95 @@
@mod @mod_h5pactivity @core_h5p
Feature: Add H5P activity context locking
In order to let users access a H5P attempts
As a user
I need to access attempts reports even if no more users can submit attempts
Background:
Given the following config values are set as admin:
| contextlocking | 1 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
| student2 | Student | 2 | student2@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
# This test is only about reporting, we don't need to specify any valid H5P file for it.
And the following "activity" exists:
| activity | h5pactivity |
| name | H5P package |
| intro | Test H5P description |
| course | C1 |
| idnumber | h5ppackage |
And the following "mod_h5pactivity > attempt" exists:
| user | student1 |
| h5pactivity | H5P package |
| attempt | 1 |
| interactiontype | compound |
| rawscore | 2 |
| maxscore | 2 |
| duration | 4 |
| completion | 1 |
| success | 1 |
Scenario: Access participants report on a freeze context
Given the "h5ppackage" "Activity module" is context frozen
And I am on the "H5P package" "h5pactivity activity" page logged in as admin
When I navigate to "Attempts report" in current page administration
Then I should see "Student 1"
And I should see "View (1)" in the "Student 1" "table_row"
And I should see "Student 2"
And I should not see "Teacher 1"
Scenario: Access own attempts on a freeze context
Given the "h5ppackage" "Activity module" is context frozen
And I am on the "H5P package" "h5pactivity activity" page logged in as student1
When I navigate to "Attempts report" in current page administration
And I follow "View report"
Then I should see "Attempt #1: Student 1"
And I should see "This attempt is completed"
Scenario: Access participants report without any user with submit capability
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| mod/h5pactivity:submit | Prohibit | student | System | |
And I am on the "H5P package" "h5pactivity activity" page logged in as admin
When I navigate to "Attempts report" in current page administration
Then I should see "Student 1"
And I should see "View (1)" in the "Student 1" "table_row"
And I should see "Student 2"
And I should not see "Teacher 1"
Scenario: Access participant report to list students with submit capability but no view one
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| mod/h5pactivity:view | Prohibit | student | System | |
And I am on the "H5P package" "h5pactivity activity" page logged in as admin
When I navigate to "Attempts report" in current page administration
Then I should see "No participants to display"
Scenario: Access participant report but with no users with view or submit capability
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| mod/h5pactivity:submit | Prohibit | student | System | |
| mod/h5pactivity:view | Prohibit | student | System | |
And I am on the "H5P package" "h5pactivity activity" page logged in as admin
When I navigate to "Attempts report" in current page administration
Then I should see "No participants to display"
Scenario: Access participant report in a hidden activity
Given I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
And I click on "Hide" "link" in the "H5P package" activity
When I am on the "H5P package" "h5pactivity activity" page
And I navigate to "Attempts report" in current page administration
Then I should see "Student 1"
And I should see "View (1)"
And I should see "Student 2"
And I should not see "Teacher 1"
@@ -0,0 +1,69 @@
@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe @javascript
Feature: Users can see the H5P recent activity from the recent activity block
In order to quickly see the updates from H5P activity in my course
As a user
I need to be able to see the recent activity in the recent activity block
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
| student2 | Student | 2 | student2@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/h5p:updatelibraries | Allow | editingteacher | System | |
And the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| packagefilepath | h5p/tests/fixtures/multiple-choice-2-6.h5p |
And the following "blocks" exist:
| 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 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 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
Scenario: Student see only his own activity
Given I am on the "Course 1" course page logged in as student1
And I should see "H5P submitted:" in the "Recent activity" "block"
And I should see "Student 1" in the "Recent activity" "block"
And I should not see "Grade:" in the "Recent activity" "block"
And I should not see "Student 2" in the "Recent activity" "block"
When I click on "Full report of recent activity" "link"
Then I should see "H5P Awesome H5P package"
And I should see "Student 1 - "
And I should not see "Grade:"
And I should not see "Student 2 - "
Scenario: Teacher see each student activity
Given I am on the "Course 1" course page logged in as teacher1
And I should see "H5P submitted:" in the "Recent activity" "block"
And I should see "Student 1" in the "Recent activity" "block"
And I should not see "Grade:" in the "Recent activity" "block"
And I should see "Student 2" in the "Recent activity" "block"
When I click on "Full report of recent activity" "link"
Then I should see "H5P Awesome H5P package"
And I should see "Student 1 - "
And I should see "Grade:"
And I should see "Student 2 - "
@@ -0,0 +1,39 @@
@mod @mod_h5pactivity @core_h5p
Feature: Reporting information in the h5p activity
In order to let teachers view a report on the attempts made in the h5p activity
As a teacher
I can access the report page and see the attempts made by the students
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
| student2 | Student | 2 | student2@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
# This test is only about reporting, we don't need to specify any valid H5P file for it.
And the following "activity" exists:
| activity | h5pactivity |
| name | H5P package |
| intro | Test H5P description |
| course | C1 |
| idnumber | h5ppackage |
And the following "mod_h5pactivity > attempts" exist:
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
| student1 | H5P package | 1 | compound | 2 | 2 | 4 | 1 | 1 |
| student2 | H5P package | 1 | choice | 2 | 2 | 4 | 1 | 1 |
| student1 | H5P package | 2 | compound | 2 | 2 | 4 | 1 | 1 |
Scenario: Access the report page and check the label for the attempts and attempt headers
Given I am on the "H5P package" "h5pactivity activity" page logged in as teacher1
When I navigate to "Attempts report" in current page administration
Then I should see "View (2)" in the "Student 1" "table_row"
And I should see "View (1)" in the "Student 2" "table_row"
And I should see "Attempts (3)" in the "table" "table"
@@ -0,0 +1,49 @@
@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe @javascript
Feature: View fill the blanks attempt report
In order to let users to review a fill the blanks attempt
As a user
I need to view fill in interactions in the report
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
And the following config values are set as admin:
# No HTML should appear even with formatstringstriptags disabled.
| formatstringstriptags | 0 |
And the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| grademethod | 2 |
| packagefilepath | h5p/tests/fixtures/filltheblanks.h5p |
Scenario: View attempt in a fill the blanks content
Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
# Do an attempt.
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Brigadoon"
And I set the field with xpath "//input[contains(@aria-label,\"Blank input 2 of 4\")]" to "Emerald city"
And I set the field with xpath "//input[contains(@aria-label,\"Blank input 3 of 4\")]" to "Narnia"
And I set the field with xpath "//input[contains(@aria-label,\"Blank input 4 of 4\")]" to "Canberra"
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
And I switch to the main frame
And I reload the page
# Check attempt.
When I navigate to "Attempts report" in current page administration
And I follow "View report"
Then I should see "Of which countries are Berlin, Washington, Beijing, Canberra and Brasilia the capitals?"
And I should see "Brigadoon" in the "Brasilia" "table_row"
And "Your answer is incorrect" "icon" should exist in the "Brasilia" "table_row"
And I should see "Emerald city" in the "Washington" "table_row"
And I should see "Narnia" in the "Berlin" "table_row"
And "Your answer is incorrect" "icon" should exist in the "Berlin" "table_row"
And "Your answer is correct" "icon" should exist in the "Canberra" "table_row"
And I should not see "<p>"
@@ -0,0 +1,41 @@
@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe @javascript
Feature: View essay attempt report
In order to let users to review an essay attempt
As a user
I need to view long fill in interactions in the report
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
And the following config values are set as admin:
# No HTML should appear even with formatstringstriptags disabled.
| formatstringstriptags | 0 |
And the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| grademethod | 2 |
| packagefilepath | h5p/tests/fixtures/basic_essay.h5p |
Scenario: View attempt essay content
# Do an attempt.
Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
And I change window size to "large"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I set the field with xpath "//textarea" to "This is a smurfing smurf"
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
And I switch to the main frame
And I reload the page
# Check attempt.
When I navigate to "Attempts report" in current page administration
And I follow "View report"
Then I should see "This is a smurfing smurf"
And I should not see "<strong>smurf</strong>"
@@ -0,0 +1,160 @@
@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe @javascript
Feature: Users can save the current state of an H5P activity
In order to continue an H5P activity where I left
As a user
I need to be able to save the current state
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| student2 | Student | 2 | student2@example.com |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| student2 | C1 | student |
| teacher1 | C1 | editingteacher |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/h5p:updatelibraries | Allow | editingteacher | System | |
And the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| packagefilepath | h5p/tests/fixtures/filltheblanks.h5p |
Scenario: Content state is not saved when enablesavestate is disabled
Given the following config values are set as admin:
| enablesavestate | 0 | mod_h5pactivity|
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Narnia"
And I switch to the main frame
And I am on the "Course 1" course page
When I am on the "Awesome H5P package" "h5pactivity activity" page
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
Then the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" does not match value "Narnia"
Scenario: Content state is saved when enablesavestate is enabled
Given the following config values are set as admin:
| enablesavestate | 1 | mod_h5pactivity|
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Narnia"
And I switch to the main frame
And I am on the "Course 1" course page
When I am on the "Awesome H5P package" "h5pactivity activity" page
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
Then the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" matches value "Narnia"
Scenario: Content state is not saved for teachers when enablesavestate is enabled
Given the following config values are set as admin:
| enablesavestate | 1 | mod_h5pactivity|
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as teacher1
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Narnia"
And I switch to the main frame
And I am on the "Course 1" course page
When I am on the "Awesome H5P package" "h5pactivity activity" page
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
Then the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" does not match value "Narnia"
Scenario: Content state is reseted when content changes
Given the following config values are set as admin:
| enablesavestate | 1 | mod_h5pactivity|
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Narnia"
And I switch to the main frame
And I am on the "Course 1" course page
When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as admin
# Change the content.
And I follow "Edit H5P content"
And I switch to "h5p-editor-iframe" class iframe
And I set the field "Title" to "Capitals"
And I switch to the main frame
And I click on "Save changes" "button"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I should see "Check"
# Check the content state has been reseted.
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
Then I should see "Data Reset"
And I should see "This content has changed since you last used it."
And I click on "OK" "button"
And the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" does not match value "Narnia"
Scenario: Content state is not reseted when content edition is cancelled
Given the following config values are set as admin:
| enablesavestate | 1 | mod_h5pactivity|
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Narnia"
And I switch to the main frame
And I am on the "Course 1" course page
When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as admin
# Start content edition.
And I follow "Edit H5P content"
And I switch to "h5p-editor-iframe" class iframe
And I set the field "Title" to "Capitals"
And I switch to the main frame
And I click on "Cancel" "button"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I should see "Check"
# Check the content state hasn't been reseted.
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
And I should see "Awesome H5P package"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
Then I should not see "Data Reset"
And I should not see "This content has changed since you last used it."
And the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" matches value "Narnia"
Scenario: Content state is removed when an attempt is created
Given the following config values are set as admin:
| enablesavestate | 1 | mod_h5pactivity|
# Save state content for student2, to check this data is not removed when student1 finishes their attempt.
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student2
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Vallhonesta"
# Confirm the content state has been saved properly.
And I reload the page
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" matches value "Vallhonesta"
# Create an attempt for student1.
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
And I should not see "Attempts report"
When I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Narnia"
And I click on "Check" "button"
# Check the state content has been removed.
And I reload the page
Then I should see "Attempts report"
And I am on the "Awesome H5P package" "h5pactivity activity" page
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" does not match value "Narnia"
And I switch to the main frame
# Check the state content for student2 is still there.
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student2
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" matches value "Vallhonesta"
@@ -0,0 +1,86 @@
@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe
Feature: Do a H5P attempt
In order to let students do a H5P attempt
As a teacher
I need to list students attempts on various reports
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/h5p:updatelibraries | Allow | editingteacher | System | |
And the following "activity" exists:
| activity | h5pactivity |
| course | C1 |
| name | Awesome H5P package |
| packagefilepath | h5p/tests/fixtures/multiple-choice-2-6.h5p |
| grademethod | 2 |
Scenario: View an H5P as a teacher
When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as teacher1
Then I should see "You are in preview mode."
@javascript
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 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
And I am on the "Course 1" course page logged in as teacher1
And I navigate to course participants
And I follow "Student 1"
Then I follow "Today's logs"
And I should see "xAPI statement received"
@javascript
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 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"
# We need to wait 1 second here because, in very quick environments, the 2nd
# attempts happen too close to the 1st one and it's not sent properly. See MDL-76010.
And I wait "1" seconds
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"
# 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
# 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"
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"
# Again, the wait between 3rd and 4th attempt, to separate them a little bit.
And I wait "1" seconds
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
When I navigate to "Attempts report" in current page administration
And "1" row "Score" column of "table" table should contain "0"
And "2" row "Score" column of "table" table should contain "1"
And "3" row "Score" column of "table" table should contain "0"
And "4" row "Score" column of "table" table should contain "1"
And I am on the "Course 1" "grades > User report > View" page logged in as "teacher1"
And I click on "Student 1" in the "user" search widget
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Percentage |
| Awesome H5P package | 50.00 | 50.00 % |
@@ -0,0 +1,69 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Events test.
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\event;
use advanced_testcase;
use context_course;
use context_module;
/**
* H5P activity events test cases.
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_module_instance_list_viewed_test extends advanced_testcase {
/**
* Test course_module_instance_list_viewed event.
*/
public function test_course_module_instance_list_viewed(): void {
// There is no proper API to call to trigger this event, so what we are
// doing here is simply making sure that the events returns the right information.
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$params = [
'context' => context_course::instance($course->id)
];
$event = course_module_instance_list_viewed::create($params);
// Triggering and capturing the event.
$sink = $this->redirectEvents();
$event->trigger();
$events = $sink->get_events();
$this->assertCount(1, $events);
$event = reset($events);
// Checking that the event contains the expected values.
$this->assertInstanceOf('\mod_h5pactivity\event\course_module_instance_list_viewed', $event);
$this->assertEquals(context_course::instance($course->id), $event->get_context());
$this->assertEventContextNotUsed($event);
}
}
@@ -0,0 +1,73 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Events test.
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\event;
use advanced_testcase;
use context_course;
use context_module;
/**
* H5P activity events test cases.
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_module_viewed_test extends advanced_testcase {
/**
* Test course_module_viewed event.
*/
public function test_course_module_viewed(): void {
// There is no proper API to call to trigger this event, so what we are
// doing here is simply making sure that the events returns the right information.
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course->id]);
$params = [
'context' => context_module::instance($activity->cmid),
'objectid' => $activity->id
];
$event = course_module_viewed::create($params);
// Triggering and capturing the event.
$sink = $this->redirectEvents();
$event->trigger();
$events = $sink->get_events();
$this->assertCount(1, $events);
$event = reset($events);
// Checking that the event contains the expected values.
$this->assertInstanceOf('\mod_h5pactivity\event\course_module_viewed', $event);
$this->assertEquals(context_module::instance($activity->cmid), $event->get_context());
$this->assertEquals($activity->id, $event->objectid);
$this->assertEventContextNotUsed($event);
}
}
@@ -0,0 +1,147 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Events test.
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\event;
use advanced_testcase;
use moodle_url;
use coding_exception;
use context_module;
defined('MOODLE_INTERNAL') || die();
/**
* H5P activity events test cases.
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class report_viewed_test extends advanced_testcase {
/**
* Test report_viewed event.
*
* @dataProvider report_viewed_data
* @param bool $usea if a (instanceid) will be used in the event
* @param bool $useattemptid if attemptid will be used in the event
* @param bool $useuserid if user id will be used in the event
* @param bool $exception if exception is expected
*/
public function test_report_viewed(bool $usea, bool $useattemptid, bool $useuserid, bool $exception): void {
$this->resetAfterTest();
// Must be a non-guest user to create h5pactivities.
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course->id]);
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
// Create a user with 1 attempt.
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
$params = ['cmid' => $activity->cmid, 'userid' => $user->id];
$attempt = $generator->create_content($activity, $params);
$other = [];
$urlparams = [];
if ($usea) {
$other['instanceid'] = $activity->id;
$urlparams['a'] = $activity->id;
}
if ($useuserid) {
$other['userid'] = $user->id;
$urlparams['userid'] = $user->id;
}
if ($useattemptid) {
$other['attemptid'] = $attempt->id;
$urlparams['attemptid'] = $attempt->id;
}
$params = [
'context' => context_module::instance($activity->cmid),
'objectid' => $activity->id,
'other' => $other,
];
if ($exception) {
$this->expectException(coding_exception::class);
}
$event = report_viewed::create($params);
// Triggering and capturing the event.
$sink = $this->redirectEvents();
$event->trigger();
$events = $sink->get_events();
$this->assertCount(1, $events);
$event = reset($events);
// Checking that the event contains the expected values.
$this->assertInstanceOf('\mod_h5pactivity\event\report_viewed', $event);
$this->assertEquals(context_module::instance($activity->cmid), $event->get_context());
$this->assertEquals($activity->id, $event->objectid);
$eventurl = $event->get_url();
$url = new moodle_url('/mod/h5pactivity/report.php', $urlparams);
$this->assertTrue($eventurl->compare($url));
}
/**
* Data provider for data request creation tests.
*
* @return array
*/
public function report_viewed_data(): array {
return [
// Exception cases.
'Event withour other data (exception)' => [
false, false, false, true
],
'Event with only userid (exception)' => [
false, false, true, true
],
'Event with only attemptid (exception)' => [
false, true, false, true
],
'Event with attemptid and userid (exception)' => [
false, true, true, true
],
// Correct cases.
'Event with instance id' => [
true, false, false, false
],
'Event with instance id and attempt id' => [
true, false, true, false
],
'Event with instance id and userid' => [
true, true, false, false
],
'Event with instance id, user id and attemptid' => [
true, true, true, false
],
];
}
}
@@ -0,0 +1,77 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Events test.
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\event;
use advanced_testcase;
use context_module;
/**
* H5P activity events test cases.
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class statement_received_test extends advanced_testcase {
/**
* Test statement_recieved event.
*/
public function test_statement_received(): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
// Must be a non-guest user to create h5pactivities.
$this->setAdminUser();
// There is no proper API to call to trigger this event, so what we are
// doing here is simply making sure that the events returns the right information.
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course->id]);
$params = [
'context' => context_module::instance($activity->cmid),
'objectid' => $activity->id
];
$event = statement_received::create($params);
// Triggering and capturing the event.
$sink = $this->redirectEvents();
$event->trigger();
$events = $sink->get_events();
$this->assertCount(1, $events);
$event = reset($events);
// Checking that the event contains the expected values.
$this->assertInstanceOf('\mod_h5pactivity\event\statement_received', $event);
$this->assertEquals(context_module::instance($activity->cmid), $event->get_context());
$this->assertEquals($activity->id, $event->objectid);
$this->assertEventContextNotUsed($event);
}
}
+488
View File
@@ -0,0 +1,488 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* External function test for get_attempts.
*
* @package mod_h5pactivity
* @category external
* @since Moodle 3.9
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\external;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
use mod_h5pactivity\local\manager;
use core_external\external_api;
use externallib_advanced_testcase;
/**
* External function test for get_attempts.
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get_attempts_test extends externallib_advanced_testcase {
/**
* Test the behaviour of get_attempts.
*
* @dataProvider execute_data
* @param int $grademethod the activity grading method
* @param string $loginuser the user which calls the webservice
* @param string|null $participant the user to get the data
* @param bool $createattempts if the student user has attempts created
* @param int|null $count the expected number of attempts returned (null for exception)
*/
public function test_execute(int $grademethod, string $loginuser, ?string $participant,
bool $createattempts, ?int $count): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity',
['course' => $course, 'enabletracking' => 1, 'grademethod' => $grademethod]);
$manager = manager::create_from_instance($activity);
$cm = $manager->get_coursemodule();
// Prepare users: 1 teacher, 2 students, 1 unenroled user.
$users = [
'editingteacher' => $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'),
'student' => $this->getDataGenerator()->create_and_enrol($course, 'student'),
'other' => $this->getDataGenerator()->create_and_enrol($course, 'student'),
'noenrolled' => $this->getDataGenerator()->create_user(),
];
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
if ($createattempts) {
$user = $users['student'];
$params = ['cmid' => $cm->id, 'userid' => $user->id];
$generator->create_content($activity, $params);
$generator->create_content($activity, $params);
}
// Create another user with 2 attempts to validate no cross attempts are returned.
$user = $users['other'];
$params = ['cmid' => $cm->id, 'userid' => $user->id];
$generator->create_content($activity, $params);
$generator->create_content($activity, $params);
// Execute external method.
$this->setUser($users[$loginuser]);
$userids = ($participant) ? [$users[$participant]->id] : [];
$checkuserid = ($participant) ? $users[$participant]->id : $users[$loginuser]->id;
$result = get_attempts::execute($activity->id, $userids);
$result = external_api::clean_returnvalue(
get_attempts::execute_returns(),
$result
);
// Validate general structure.
$this->assertArrayHasKey('activityid', $result);
$this->assertArrayHasKey('usersattempts', $result);
$this->assertArrayHasKey('warnings', $result);
$this->assertEquals($activity->id, $result['activityid']);
if ($count === null) {
$this->assertCount(1, $result['warnings']);
$this->assertCount(0, $result['usersattempts']);
return;
}
$this->assertCount(0, $result['warnings']);
$this->assertCount(1, $result['usersattempts']);
$userattempts = $result['usersattempts'][0];
$this->assertEquals($checkuserid, $userattempts['userid']);
// Validate scored attempts.
if ($grademethod == manager::GRADEMANUAL || $grademethod == manager::GRADEAVERAGEATTEMPT || $count == 0) {
$this->assertArrayNotHasKey('scored', $userattempts);
} else {
$this->assertArrayHasKey('scored', $userattempts);
list($dbgrademethod, $title) = $manager->get_selected_attempt();
$this->assertEquals($grademethod, $dbgrademethod);
$this->assertEquals($grademethod, $userattempts['scored']['grademethod']);
$this->assertEquals($title, $userattempts['scored']['title']);
$this->assertCount(1, $userattempts['scored']['attempts']);
}
// Validate returned attempts.
$this->assertCount($count, $userattempts['attempts']);
foreach ($userattempts['attempts'] as $attempt) {
$this->assertArrayHasKey('id', $attempt);
$this->assertEquals($checkuserid, $attempt['userid']);
$this->assertArrayHasKey('timecreated', $attempt);
$this->assertArrayHasKey('timemodified', $attempt);
$this->assertArrayHasKey('attempt', $attempt);
$this->assertArrayHasKey('rawscore', $attempt);
$this->assertArrayHasKey('maxscore', $attempt);
$this->assertArrayHasKey('duration', $attempt);
$this->assertArrayHasKey('completion', $attempt);
$this->assertArrayHasKey('success', $attempt);
$this->assertArrayHasKey('scaled', $attempt);
}
}
/**
* Data provider for the test_execute tests.
*
* @return array
*/
public function execute_data(): array {
return [
// Teacher checking a user with attempts.
'Manual grade, Teacher asking participant with attempts' => [
manager::GRADEMANUAL, 'editingteacher', 'student', true, 2
],
'Highest grade, Teacher asking participant with attempts' => [
manager::GRADEHIGHESTATTEMPT, 'editingteacher', 'student', true, 2
],
'Average grade, Teacher asking participant with attempts' => [
manager::GRADEAVERAGEATTEMPT, 'editingteacher', 'student', true, 2
],
'Last grade, Teacher asking participant with attempts' => [
manager::GRADELASTATTEMPT, 'editingteacher', 'student', true, 2
],
'First grade, Teacher asking participant with attempts' => [
manager::GRADEFIRSTATTEMPT, 'editingteacher', 'student', true, 2
],
// Teacher checking a user without attempts.
'Manual grade, Teacher asking participant without attempts' => [
manager::GRADEMANUAL, 'editingteacher', 'student', false, 0
],
'Highest grade, Teacher asking participant without attempts' => [
manager::GRADEHIGHESTATTEMPT, 'editingteacher', 'student', false, 0
],
'Average grade, Teacher asking participant without attempts' => [
manager::GRADEAVERAGEATTEMPT, 'editingteacher', 'student', false, 0
],
'Last grade, Teacher asking participant without attempts' => [
manager::GRADELASTATTEMPT, 'editingteacher', 'student', false, 0
],
'First grade, Teacher asking participant without attempts' => [
manager::GRADEFIRSTATTEMPT, 'editingteacher', 'student', false, 0
],
// Student checking own attempts specifying userid.
'Manual grade, check same user attempts report with attempts' => [
manager::GRADEMANUAL, 'student', 'student', true, 2
],
'Highest grade, check same user attempts report with attempts' => [
manager::GRADEHIGHESTATTEMPT, 'student', 'student', true, 2
],
'Average grade, check same user attempts report with attempts' => [
manager::GRADEAVERAGEATTEMPT, 'student', 'student', true, 2
],
'Last grade, check same user attempts report with attempts' => [
manager::GRADELASTATTEMPT, 'student', 'student', true, 2
],
'First grade, check same user attempts report with attempts' => [
manager::GRADEFIRSTATTEMPT, 'student', 'student', true, 2
],
// Student checking own attempts.
'Manual grade, check own attempts report with attempts' => [
manager::GRADEMANUAL, 'student', null, true, 2
],
'Highest grade, check own attempts report with attempts' => [
manager::GRADEHIGHESTATTEMPT, 'student', null, true, 2
],
'Average grade, check own attempts report with attempts' => [
manager::GRADEAVERAGEATTEMPT, 'student', null, true, 2
],
'Last grade, check own attempts report with attempts' => [
manager::GRADELASTATTEMPT, 'student', null, true, 2
],
'First grade, check own attempts report with attempts' => [
manager::GRADEFIRSTATTEMPT, 'student', null, true, 2
],
// Student checking own report without attempts.
'Manual grade, check own attempts report without attempts' => [
manager::GRADEMANUAL, 'student', 'student', false, 0
],
'Highest grade, check own attempts report without attempts' => [
manager::GRADEHIGHESTATTEMPT, 'student', 'student', false, 0
],
'Average grade, check own attempts report without attempts' => [
manager::GRADEAVERAGEATTEMPT, 'student', 'student', false, 0
],
'Last grade, check own attempts report without attempts' => [
manager::GRADELASTATTEMPT, 'student', 'student', false, 0
],
'First grade, check own attempts report without attempts' => [
manager::GRADEFIRSTATTEMPT, 'student', 'student', false, 0
],
// Student trying to get another user attempts.
'Manual grade, student trying to stalk another student' => [
manager::GRADEMANUAL, 'student', 'other', false, null
],
'Highest grade, student trying to stalk another student' => [
manager::GRADEHIGHESTATTEMPT, 'student', 'other', false, null
],
'Average grade, student trying to stalk another student' => [
manager::GRADEAVERAGEATTEMPT, 'student', 'other', false, null
],
'Last grade, student trying to stalk another student' => [
manager::GRADELASTATTEMPT, 'student', 'other', false, null
],
'First grade, student trying to stalk another student' => [
manager::GRADEFIRSTATTEMPT, 'student', 'other', false, null
],
// Teacher trying to get a non enroled user attempts.
'Manual grade, teacher trying to get an non enrolled user attempts' => [
manager::GRADEMANUAL, 'editingteacher', 'noenrolled', false, null
],
'Highest grade, teacher trying to get an non enrolled user attempts' => [
manager::GRADEHIGHESTATTEMPT, 'editingteacher', 'noenrolled', false, null
],
'Average grade, teacher trying to get an non enrolled user attempts' => [
manager::GRADEAVERAGEATTEMPT, 'editingteacher', 'noenrolled', false, null
],
'Last grade, teacher trying to get an non enrolled user attempts' => [
manager::GRADELASTATTEMPT, 'editingteacher', 'noenrolled', false, null
],
'First grade, teacher trying to get an non enrolled user attempts' => [
manager::GRADEFIRSTATTEMPT, 'editingteacher', 'noenrolled', false, null
],
// Student trying to get a non enroled user attempts.
'Manual grade, student trying to get an non enrolled user attempts' => [
manager::GRADEMANUAL, 'student', 'noenrolled', false, null
],
'Highest grade, student trying to get an non enrolled user attempts' => [
manager::GRADEHIGHESTATTEMPT, 'student', 'noenrolled', false, null
],
'Average grade, student trying to get an non enrolled user attempts' => [
manager::GRADEAVERAGEATTEMPT, 'student', 'noenrolled', false, null
],
'Last grade, student trying to get an non enrolled user attempts' => [
manager::GRADELASTATTEMPT, 'student', 'noenrolled', false, null
],
'First grade, student trying to get an non enrolled user attempts' => [
manager::GRADEFIRSTATTEMPT, 'student', 'noenrolled', false, null
],
];
}
/**
* Test the behaviour of get_attempts when tracking is not enabled.
*
*/
public function test_execute_no_tracking(): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity',
['course' => $course, 'enabletracking' => 0]);
$manager = manager::create_from_instance($activity);
$cm = $manager->get_coursemodule();
// Prepare users: 1 teacher, 1 student.
$users = [
'editingteacher' => $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'),
'student' => $this->getDataGenerator()->create_and_enrol($course, 'student'),
];
// Execute external method.
$this->setUser($users['editingteacher']);
$result = get_attempts::execute($activity->id, [$users['student']->id]);
$result = external_api::clean_returnvalue(
get_attempts::execute_returns(),
$result
);
$this->assertCount(1, $result['warnings']);
$this->assertCount(0, $result['usersattempts']);
}
/**
* Test the behaviour of get_attempts when own review is not allowed.
*
*/
public function test_execute_no_own_review(): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity',
['course' => $course, 'enabletracking' => 1, 'reviewmode' => manager::REVIEWNONE]);
$manager = manager::create_from_instance($activity);
$cm = $manager->get_coursemodule();
// Prepare users: 1 student.
$users = [
'student' => $this->getDataGenerator()->create_and_enrol($course, 'student'),
];
// Execute external method.
$this->setUser($users['student']);
$result = get_attempts::execute($activity->id);
$result = external_api::clean_returnvalue(
get_attempts::execute_returns(),
$result
);
$this->assertCount(1, $result['warnings']);
$this->assertCount(0, $result['usersattempts']);
}
/**
* Test the behaviour of get_attempts getting more than one user at once.
*
* @dataProvider execute_multipleusers_data
* @param string $loginuser the user which calls the webservice
* @param string[] $participants the users to get the data
* @param string[] $warnings the expected users with warnings
* @param string[] $resultusers expected users in the resultusers
*/
public function test_execute_multipleusers(string $loginuser, array $participants,
array $warnings, array $resultusers): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity',
['course' => $course]);
$manager = manager::create_from_instance($activity);
$cm = $manager->get_coursemodule();
// Prepare users: 1 teacher, 2 students with attempts, 1 student without, 1 no enrolled.
$users = [
'editingteacher' => $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'),
'student1' => $this->getDataGenerator()->create_and_enrol($course, 'student'),
'student2' => $this->getDataGenerator()->create_and_enrol($course, 'student'),
'noattempts' => $this->getDataGenerator()->create_and_enrol($course, 'student'),
'noenrolled' => $this->getDataGenerator()->create_user(),
];
// Generate attempts (student1 with 1 attempt, student2 with 2).
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
$user = $users['student1'];
$params = ['cmid' => $cm->id, 'userid' => $user->id];
$generator->create_content($activity, $params);
$user = $users['student2'];
$params = ['cmid' => $cm->id, 'userid' => $user->id];
$generator->create_content($activity, $params);
$generator->create_content($activity, $params);
$countattempts = [
$users['editingteacher']->id => 0,
$users['student1']->id => 1,
$users['student2']->id => 2,
$users['noattempts']->id => 0,
$users['noenrolled']->id => 0,
];
// Execute external method.
$this->setUser($users[$loginuser]);
$userids = [];
foreach ($participants as $participant) {
$userids[] = $users[$participant]->id;
}
$result = get_attempts::execute($activity->id, $userids);
$result = external_api::clean_returnvalue(
get_attempts::execute_returns(),
$result
);
$this->assertCount(count($warnings), $result['warnings']);
$this->assertCount(count($resultusers), $result['usersattempts']);
$expectedwarnings = [];
foreach ($warnings as $warninguser) {
$id = $users[$warninguser]->id;
$expectedwarnings[$id] = $warninguser;
}
foreach ($result['warnings'] as $warning) {
$this->assertEquals('user', $warning['item']);
$this->assertEquals(1, $warning['warningcode']);
$this->assertArrayHasKey($warning['itemid'], $expectedwarnings);
}
$expectedusers = [];
foreach ($resultusers as $resultuser) {
$id = $users[$resultuser]->id;
$expectedusers[$id] = $resultuser;
}
foreach ($result['usersattempts'] as $usersattempts) {
$this->assertArrayHasKey('userid', $usersattempts);
$userid = $usersattempts['userid'];
$this->assertArrayHasKey($userid, $expectedusers);
$this->assertCount($countattempts[$userid], $usersattempts['attempts']);
if ($countattempts[$userid]) {
$this->assertArrayHasKey('scored', $usersattempts);
}
}
}
/**
* Data provider for the test_execute_multipleusers.
*
* @return array
*/
public function execute_multipleusers_data(): array {
return [
// Teacher checks.
'Teacher checking students with attempts' => [
'editingteacher', ['student1', 'student2'], [], ['student1', 'student2']
],
'Teacher checking one student with atempts and one not' => [
'editingteacher', ['student1', 'noattempts'], [], ['student1', 'noattempts']
],
'Teacher checking no students' => [
'editingteacher', [], [], ['editingteacher']
],
'Teacher checking one student and a no enrolled user' => [
'editingteacher', ['student1', 'noenrolled'], ['noenrolled'], ['student1']
],
// Student checks.
'Student checking self attempts and another user' => [
'student1', ['student1', 'student2'], ['student2'], ['student1']
],
'Student checking no students' => [
'student1', [], [], ['student1']
],
'Student checking self attempts and a no enrolled user' => [
'student1', ['student1', 'noenrolled'], ['noenrolled'], ['student1']
],
];
}
}
@@ -0,0 +1,201 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* External function test for get_h5pactivities_by_courses.
*
* @package mod_h5pactivity
* @category external
* @since Moodle 3.9
* @copyright 2020 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\external;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
use core_external\external_api;
use externallib_advanced_testcase;
use context_module;
/**
* External function test for get_h5pactivities_by_courses.
*
* @package mod_h5pactivity
* @copyright 2020 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get_h5pactivities_by_courses_test extends externallib_advanced_testcase {
/**
* Test test_get_h5pactivities_by_courses user student.
*/
public function test_get_h5pactivities_by_courses(): void {
global $CFG, $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Create 2 courses.
// Course 1 -> 2 activities with H5P files package without deploy.
// Course 2 -> 1 activity with H5P file package deployed.
$course1 = $this->getDataGenerator()->create_course();
$params = [
'course' => $course1->id,
'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/filltheblanks.h5p',
'introformat' => 1
];
$activities[] = $this->getDataGenerator()->create_module('h5pactivity', $params);
// Add filename and contextid to make easier the asserts.
$activities[0]->filename = 'filltheblanks.h5p';
$context = context_module::instance($activities[0]->cmid);
$activities[0]->contextid = $context->id;
$params = [
'course' => $course1->id,
'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/greeting-card.h5p',
'introformat' => 1
];
$activities[] = $this->getDataGenerator()->create_module('h5pactivity', $params);
// Add filename and contextid to make easier the asserts.
$activities[1]->filename = 'greeting-card.h5p';
$context = context_module::instance($activities[1]->cmid);
$activities[1]->contextid = $context->id;
$course2 = $this->getDataGenerator()->create_course();
$params = [
'course' => $course2->id,
'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/guess-the-answer.h5p',
'introformat' => 1
];
$activities[] = $this->getDataGenerator()->create_module('h5pactivity', $params);
$activities[2]->filename = 'guess-the-answer.h5p';
$context = context_module::instance($activities[2]->cmid);
$activities[2]->contextid = $context->id;
// Create a fake deploy H5P file.
$generator = $this->getDataGenerator()->get_plugin_generator('core_h5p');
$deployedfile = $generator->create_export_file($activities[2]->filename, $context->id, 'mod_h5pactivity', 'package');
// Create a user and enrol as student in both courses.
$user = $this->getDataGenerator()->create_user();
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
$maninstance1 = $DB->get_record('enrol', ['courseid' => $course1->id, 'enrol' => 'manual'], '*', MUST_EXIST);
$maninstance2 = $DB->get_record('enrol', ['courseid' => $course2->id, 'enrol' => 'manual'], '*', MUST_EXIST);
$manual = enrol_get_plugin('manual');
$manual->enrol_user($maninstance1, $user->id, $studentrole->id);
$manual->enrol_user($maninstance2, $user->id, $studentrole->id);
// Set admin settings.
set_config('enablesavestate', 1, 'mod_h5pactivity');
set_config('savestatefreq', 120, 'mod_h5pactivity');
// Check the activities returned by the first course.
$this->setUser($user);
$courseids = [$course1->id];
$result = get_h5pactivities_by_courses::execute($courseids);
$result = external_api::clean_returnvalue(get_h5pactivities_by_courses::execute_returns(), $result);
$this->assertCount(0, $result['warnings']);
$this->assertCount(2, $result['h5pactivities']);
$this->assert_activities($activities, $result);
$this->assertNotContains('deployedfile', $result['h5pactivities'][0]);
$this->assertNotContains('deployedfile', $result['h5pactivities'][1]);
$this->assertEquals(1, $result['h5pglobalsettings']['enablesavestate']);
$this->assertEquals(120, $result['h5pglobalsettings']['savestatefreq']);
// Call the external function without passing course id.
// Expected result, all the courses, course1 and course2.
$result = get_h5pactivities_by_courses::execute([]);
$result = external_api::clean_returnvalue(get_h5pactivities_by_courses::execute_returns(), $result);
$this->assertCount(0, $result['warnings']);
$this->assertCount(3, $result['h5pactivities']);
// We need to sort the $result by id.
// Because we are not sure how it is ordered with more than one course.
array_multisort(array_map(function($element) {
return $element['id'];
}, $result['h5pactivities']), SORT_ASC, $result['h5pactivities']);
$this->assert_activities($activities, $result);
$this->assertNotContains('deployedfile', $result['h5pactivities'][0]);
$this->assertNotContains('deployedfile', $result['h5pactivities'][1]);
// Only the activity from the second course has been deployed.
$this->assertEquals($deployedfile['filename'], $result['h5pactivities'][2]['deployedfile']['filename']);
$this->assertEquals($deployedfile['filepath'], $result['h5pactivities'][2]['deployedfile']['filepath']);
$this->assertEquals($deployedfile['filesize'], $result['h5pactivities'][2]['deployedfile']['filesize']);
$this->assertEquals($deployedfile['timemodified'], $result['h5pactivities'][2]['deployedfile']['timemodified']);
$this->assertEquals($deployedfile['mimetype'], $result['h5pactivities'][2]['deployedfile']['mimetype']);
$this->assertEquals($deployedfile['fileurl'], $result['h5pactivities'][2]['deployedfile']['fileurl']);
$this->assertEquals(1, $result['h5pglobalsettings']['enablesavestate']);
$this->assertEquals(120, $result['h5pglobalsettings']['savestatefreq']);
// Unenrol user from second course.
$manual->unenrol_user($maninstance2, $user->id);
// Remove the last activity from the array.
array_pop($activities);
// Disable save state.
set_config('enablesavestate', 0, 'mod_h5pactivity');
// Call the external function without passing course id.
$result = get_h5pactivities_by_courses::execute([]);
$result = external_api::clean_returnvalue(get_h5pactivities_by_courses::execute_returns(), $result);
$this->assertCount(0, $result['warnings']);
$this->assertCount(2, $result['h5pactivities']);
$this->assert_activities($activities, $result);
$this->assertEquals(0, $result['h5pglobalsettings']['enablesavestate']);
$this->assertNotContains('savestatefreq', $result['h5pglobalsettings']);
// Call for the second course we unenrolled the user from, expected warning.
$result = get_h5pactivities_by_courses::execute([$course2->id]);
$result = external_api::clean_returnvalue(get_h5pactivities_by_courses::execute_returns(), $result);
$this->assertCount(1, $result['warnings']);
$this->assertEquals('1', $result['warnings'][0]['warningcode']);
$this->assertEquals($course2->id, $result['warnings'][0]['itemid']);
}
/**
* Create a scenario to use into the tests.
*
* @param array $activities list of H5P activities.
* @param array $result list of H5P activities by WS.
* @return void
*/
protected function assert_activities(array $activities, array $result): void {
$total = count($result['h5pactivities']);
for ($i = 0; $i < $total; $i++) {
$this->assertEquals($activities[$i]->id, $result['h5pactivities'][$i]['id']);
$this->assertEquals($activities[$i]->course, $result['h5pactivities'][$i]['course']);
$this->assertEquals($activities[$i]->name, $result['h5pactivities'][$i]['name']);
$this->assertEquals($activities[$i]->timecreated, $result['h5pactivities'][$i]['timecreated']);
$this->assertEquals($activities[$i]->timemodified, $result['h5pactivities'][$i]['timemodified']);
$this->assertEquals($activities[$i]->intro, $result['h5pactivities'][$i]['intro']);
$this->assertEquals($activities[$i]->introformat, $result['h5pactivities'][$i]['introformat']);
$this->assertEquals([], $result['h5pactivities'][$i]['introfiles']);
$this->assertEquals($activities[$i]->grade, $result['h5pactivities'][$i]['grade']);
$this->assertEquals($activities[$i]->displayoptions, $result['h5pactivities'][$i]['displayoptions']);
$this->assertEquals($activities[$i]->enabletracking, $result['h5pactivities'][$i]['enabletracking']);
$this->assertEquals($activities[$i]->grademethod, $result['h5pactivities'][$i]['grademethod']);
$this->assertEquals($activities[$i]->cmid, $result['h5pactivities'][$i]['coursemodule']);
$this->assertEquals($activities[$i]->contextid, $result['h5pactivities'][$i]['context']);
$this->assertEquals($activities[$i]->filename, $result['h5pactivities'][$i]['package'][0]['filename']);
}
}
}
@@ -0,0 +1,137 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* External function test for get_h5pactivity_access_information.
*
* @package mod_h5pactivity
* @category external
* @since Moodle 3.9
* @copyright 2020 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\external;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
use dml_missing_record_exception;
use core_external\external_api;
use externallib_advanced_testcase;
/**
* External function test for get_h5pactivity_access_information.
*
* @package mod_h5pactivity
* @copyright 2020 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get_h5pactivity_access_information_test extends externallib_advanced_testcase {
/**
* Test the behaviour of get_h5pactivity_access_information().
*
* @dataProvider get_h5pactivity_access_information_data
* @param string $role user role in course
* @param int $enabletracking if tracking is enabled
* @param array $enabledcaps capabilities enabled
*/
public function test_get_h5pactivity_access_information(string $role, int $enabletracking, array $enabledcaps): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity',
[
'course' => $course,
'enabletracking' => $enabletracking
]
);
if ($role) {
$user = $this->getDataGenerator()->create_and_enrol($course, $role);
$this->setUser($user);
}
// Check the access information.
$result = get_h5pactivity_access_information::execute($activity->id);
$result = external_api::clean_returnvalue(get_h5pactivity_access_information::execute_returns(), $result);
$this->assertCount(0, $result['warnings']);
unset($result['warnings']);
// Check the values for capabilities.
foreach ($result as $capname => $capvalue) {
if (in_array($capname, $enabledcaps)) {
$this->assertTrue($capvalue);
} else {
$this->assertFalse($capvalue);
}
}
}
/**
* Data provider for get_h5pactivity_access_information.
*
* @return array
*/
public function get_h5pactivity_access_information_data(): array {
return [
'Admin, tracking enabled' => [
'', 1, ['canview', 'canreviewattempts', 'canaddinstance']
],
'Admin, tracking disabled' => [
'', 0, ['canview', 'canreviewattempts', 'canaddinstance']
],
'Student, tracking enabled' => [
'student', 1, ['canview', 'cansubmit']
],
'Student, tracking disabled' => [
'student', 0, ['canview']
],
'Teacher, tracking enabled' => [
'editingteacher', 1, [
'canview',
'canreviewattempts',
'canaddinstance'
]
],
'Teacher, tracking disabled' => [
'editingteacher', 0, [
'canview',
'canreviewattempts',
'canaddinstance'
]
],
];
}
/**
* Test dml_missing_record_exception in get_h5pactivity_access_information.
*/
public function test_dml_missing_record_exception(): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
// Call the WS using an unexisting h5pactivityid.
$this->expectException(dml_missing_record_exception::class);
$result = get_h5pactivity_access_information::execute(1);
}
}
+428
View File
@@ -0,0 +1,428 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* External function test for get_results.
*
* @package mod_h5pactivity
* @category external
* @since Moodle 3.9
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\external;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
use mod_h5pactivity\local\manager;
use core_external\external_api;
use externallib_advanced_testcase;
use dml_missing_record_exception;
/**
* External function test for get_results.
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get_results_test extends externallib_advanced_testcase {
/**
* Test the behaviour of get_results.
*
* @dataProvider execute_data
* @param int $enabletracking the activity tracking enable
* @param int $reviewmode the activity review mode
* @param string $loginuser the user which calls the webservice
* @param string|null $participant the user to get the data
* @param bool $createattempts if the student user has attempts created
* @param int|null $count the expected number of attempts returned (null for exception)
*/
public function test_execute(int $enabletracking, int $reviewmode, string $loginuser,
?string $participant, bool $createattempts, ?int $count): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity',
['course' => $course, 'enabletracking' => $enabletracking, 'reviewmode' => $reviewmode]);
$manager = manager::create_from_instance($activity);
$cm = $manager->get_coursemodule();
// Prepare users: 1 teacher, 1 student and 1 unenroled user.
$users = [
'editingteacher' => $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'),
'student' => $this->getDataGenerator()->create_and_enrol($course, 'student'),
'other' => $this->getDataGenerator()->create_and_enrol($course, 'student'),
];
$attempts = [];
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
if ($createattempts) {
$user = $users['student'];
$params = ['cmid' => $cm->id, 'userid' => $user->id];
$attempts['student'] = $generator->create_content($activity, $params);
}
// Create another 2 attempts for the user "other" to validate no cross attempts are returned.
$user = $users['other'];
$params = ['cmid' => $cm->id, 'userid' => $user->id];
$attempts['other'] = $generator->create_content($activity, $params);
// Execute external method.
$this->setUser($users[$loginuser]);
$attemptid = $attempts[$participant]->id ?? 0;
$result = get_results::execute($activity->id, [$attemptid]);
$result = external_api::clean_returnvalue(
get_results::execute_returns(),
$result
);
// Validate general structure.
$this->assertArrayHasKey('activityid', $result);
$this->assertArrayHasKey('attempts', $result);
$this->assertArrayHasKey('warnings', $result);
$this->assertEquals($activity->id, $result['activityid']);
if ($count === null) {
$this->assertCount(1, $result['warnings']);
$this->assertCount(0, $result['attempts']);
return;
}
$this->assertCount(0, $result['warnings']);
$this->assertCount(1, $result['attempts']);
// Validate attempt.
$attempt = $result['attempts'][0];
$this->assertEquals($attemptid, $attempt['id']);
// Validate results.
$this->assertArrayHasKey('results', $attempt);
$this->assertCount($count, $attempt['results']);
foreach ($attempt['results'] as $value) {
$this->assertEquals($attemptid, $value['attemptid']);
$this->assertArrayHasKey('subcontent', $value);
$this->assertArrayHasKey('rawscore', $value);
$this->assertArrayHasKey('maxscore', $value);
$this->assertArrayHasKey('duration', $value);
$this->assertArrayHasKey('track', $value);
if (isset($value['options'])) {
foreach ($value['options'] as $option) {
$this->assertArrayHasKey('description', $option);
$this->assertArrayHasKey('id', $option);
}
}
}
}
/**
* Data provider for the test_execute tests.
*
* @return array
*/
public function execute_data(): array {
return [
'Teacher reviewing an attempt' => [
1, manager::REVIEWCOMPLETION, 'editingteacher', 'student', true, 1
],
'Teacher try to review an inexistent attempt' => [
1, manager::REVIEWCOMPLETION, 'editingteacher', 'student', false, null
],
'Teacher reviewing attempt with student review mode off' => [
1, manager::REVIEWNONE, 'editingteacher', 'student', true, 1
],
'Student reviewing own attempt' => [
1, manager::REVIEWCOMPLETION, 'student', 'student', true, 1
],
'Student reviewing an inexistent attempt' => [
1, manager::REVIEWCOMPLETION, 'student', 'student', false, null
],
'Student reviewing own attempt with review mode off' => [
1, manager::REVIEWNONE, 'student', 'student', true, null
],
'Student try to stalk other student attempt' => [
1, manager::REVIEWCOMPLETION, 'student', 'other', false, null
],
'Teacher trying to review an attempt without tracking enabled' => [
0, manager::REVIEWNONE, 'editingteacher', 'student', true, null
],
'Student trying to review an attempt without tracking enabled' => [
0, manager::REVIEWNONE, 'editingteacher', 'student', true, null
],
'Student trying to stalk another student attempt without tracking enabled' => [
0, manager::REVIEWNONE, 'editingteacher', 'student', true, null
],
];
}
/**
* Test the behaviour of get_results.
*
* @dataProvider execute_multipleattempts_data
* @param string $loginuser the user which calls the webservice
* @param array $getattempts the attempts to get the data
* @param array $warnings warnigns expected
* @param array $reports data expected
*
*/
public function test_execute_multipleattempts(string $loginuser,
array $getattempts, array $warnings, array $reports): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$manager = manager::create_from_instance($activity);
$cm = $manager->get_coursemodule();
// Prepare users: 1 teacher, 2 student.
$users = [
'editingteacher' => $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'),
'student1' => $this->getDataGenerator()->create_and_enrol($course, 'student'),
'student2' => $this->getDataGenerator()->create_and_enrol($course, 'student'),
];
$attempts = [];
// Generate attempts for student 1 and 2.
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
$user = $users['student1'];
$params = ['cmid' => $cm->id, 'userid' => $user->id];
$attempts['student1_1'] = $generator->create_content($activity, $params);
$attempts['student1_2'] = $generator->create_content($activity, $params);
$user = $users['student2'];
$params = ['cmid' => $cm->id, 'userid' => $user->id];
$attempts['student2_1'] = $generator->create_content($activity, $params);
$attempts['student2_2'] = $generator->create_content($activity, $params);
// Execute external method.
$this->setUser($users[$loginuser]);
$attemptids = [];
foreach ($getattempts as $getattempt) {
$attemptids[] = $attempts[$getattempt]->id ?? 0;
}
$result = get_results::execute($activity->id, $attemptids);
$result = external_api::clean_returnvalue(
get_results::execute_returns(),
$result
);
// Validate general structure.
$this->assertArrayHasKey('activityid', $result);
$this->assertArrayHasKey('attempts', $result);
$this->assertArrayHasKey('warnings', $result);
$this->assertEquals($activity->id, $result['activityid']);
$this->assertCount(count($warnings), $result['warnings']);
$this->assertCount(count($reports), $result['attempts']);
// Validate warnings.
$expectedwarnings = [];
foreach ($warnings as $warningattempt) {
$id = $attempts[$warningattempt]->id ?? 0;
$expectedwarnings[$id] = $warningattempt;
}
foreach ($result['warnings'] as $warning) {
$this->assertEquals('h5pactivity_attempts', $warning['item']);
$this->assertEquals(1, $warning['warningcode']);
$this->assertArrayHasKey($warning['itemid'], $expectedwarnings);
}
// Validate attempts.
$expectedattempts = [];
foreach ($reports as $expectedattempt) {
$id = $attempts[$expectedattempt]->id;
$expectedattempts[$id] = $expectedattempt;
}
foreach ($result['attempts'] as $value) {
$this->assertArrayHasKey($value['id'], $expectedattempts);
}
}
/**
* Data provider for the test_execute_multipleattempts tests.
*
* @return array
*/
public function execute_multipleattempts_data(): array {
return [
// Teacher cases.
'Teacher reviewing students attempts' => [
'editingteacher', ['student1_1', 'student2_1'], [], ['student1_1', 'student2_1']
],
'Teacher reviewing invalid attempt' => [
'editingteacher', ['student1_1', 'invalid'], ['invalid'], ['student1_1']
],
'Teacher reviewing empty attempts list' => [
'editingteacher', [], [], []
],
// Student cases.
'Student reviewing own students attempts' => [
'student1', ['student1_1', 'student1_2'], [], ['student1_1', 'student1_2']
],
'Student reviewing invalid attempt' => [
'student1', ['student1_1', 'invalid'], ['invalid'], ['student1_1']
],
'Student reviewing trying to access another user attempts' => [
'student1', ['student1_1', 'student2_1'], ['student2_1'], ['student1_1']
],
'Student reviewing empty attempts list' => [
'student1', [], [], ['student1_1', 'student1_2']
],
];
}
/**
* Test the behaviour of get_results using mixed activityid.
*
* @dataProvider execute_mixactivities_data
* @param string $activityname the activity name to use
* @param string $attemptname the attempt name to use
* @param string $expectedwarnings expected warning attempt
* @param string $expectedattempt expected result attempt
*
*/
public function test_execute_mixactivities(string $activityname, string $attemptname,
string $expectedwarnings, string $expectedattempt): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create 2 courses.
$course1 = $this->getDataGenerator()->create_course();
$course2 = $this->getDataGenerator()->create_course();
// Prepare users: 1 teacher, 1 student.
$user = $this->getDataGenerator()->create_and_enrol($course1, 'student');
$this->getDataGenerator()->enrol_user($user->id, $course2->id, 'student');
// Create our base activity.
$activity11 = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course1]);
$manager11 = manager::create_from_instance($activity11);
$cm11 = $manager11->get_coursemodule();
// Create a second activity in the same course to check if the retuned attempt is the correct one.
$activity12 = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course1]);
$manager12 = manager::create_from_instance($activity12);
$cm12 = $manager12->get_coursemodule();
// Create a second activity on a different course.
$activity21 = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course2]);
$manager21 = manager::create_from_instance($activity21);
$cm21 = $manager21->get_coursemodule();
$activities = [
'11' => $activity11->id,
'12' => $activity12->id,
'21' => $activity21->id,
'inexistent' => 0,
];
// Generate attempts.
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
$params = ['cmid' => $cm11->id, 'userid' => $user->id];
$attempt11 = $generator->create_content($activity11, $params);
$params = ['cmid' => $cm12->id, 'userid' => $user->id];
$attempt12 = $generator->create_content($activity12, $params);
$params = ['cmid' => $cm21->id, 'userid' => $user->id];
$attempt21 = $generator->create_content($activity21, $params);
$attempts = [
'11' => $attempt11->id,
'12' => $attempt12->id,
'21' => $attempt21->id,
'inexistent' => 0,
];
if ($activityname == 'inexistent') {
$this->expectException(dml_missing_record_exception::class);
}
// Execute external method.
$this->setUser($user);
$attemptid = $attempts[$attemptname];
$result = get_results::execute($activities[$activityname], [$attemptid]);
$result = external_api::clean_returnvalue(
get_results::execute_returns(),
$result
);
// Validate general structure.
$this->assertArrayHasKey('activityid', $result);
$this->assertArrayHasKey('attempts', $result);
$this->assertArrayHasKey('warnings', $result);
if (empty($expectedwarnings)) {
$this->assertEmpty($result['warnings']);
} else {
$this->assertEquals('h5pactivity_attempts', $result['warnings'][0]['item']);
$this->assertEquals(1, $result['warnings'][0]['warningcode']);
$this->assertEquals($attempts[$expectedwarnings], $result['warnings'][0]['itemid']);
}
if (empty($expectedattempt)) {
$this->assertEmpty($result['attempts']);
} else {
$this->assertEquals($attempts[$expectedattempt], $result['attempts'][0]['id']);
}
}
/**
* Data provider for the test_execute_multipleattempts tests.
*
* @return array
*/
public function execute_mixactivities_data(): array {
return [
// Teacher cases.
'Correct activity id' => [
'11', '11', '', '11'
],
'Wrong activity id' => [
'21', '11', '11', ''
],
'Inexistent activity id' => [
'inexistent', '11', '', ''
],
'Inexistent attempt id' => [
'11', 'inexistent', 'inexistent', ''
],
];
}
}
@@ -0,0 +1,249 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* External function test for get_user_attempts.
*
* @package mod_h5pactivity
* @category external
* @since Moodle 3.11
* @copyright 2020 Ilya Tregubov <ilya@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\external;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
use mod_h5pactivity\local\manager;
use core_external\external_api;
use externallib_advanced_testcase;
/**
* External function test for get_user_attempts.
*
* @package mod_h5pactivity
* @copyright 2020 Ilya Tregubov <ilya@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get_user_attempts_test extends externallib_advanced_testcase {
/**
* Test the behaviour of get_user_attempts getting more than one user at once.
*
* @dataProvider execute_multipleusers_data
* @param string $loginuser the user which calls the webservice
* @param string[] $participants the users to get the data
* @param string[] $warnings the expected users with warnings
* @param string[] $resultusers expected users in the resultusers
*/
public function test_execute_multipleusers(string $loginuser, array $participants,
array $warnings, array $resultusers): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity',
['course' => $course]);
$manager = manager::create_from_instance($activity);
$cm = $manager->get_coursemodule();
$users = ['editingteacher' => $this->getDataGenerator()->create_and_enrol($course, 'editingteacher')];
// Prepare users.
foreach ($participants as $participant) {
if ($participant == 'noenrolled') {
$users[$participant] = $this->getDataGenerator()->create_user();
} else {
$users[$participant] = $this->getDataGenerator()->create_and_enrol($course, 'student');
}
}
// Generate attempts (student1 with 1 attempt, student2 with 2 etc).
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
$attemptcount = 1;
foreach ($users as $key => $user) {
if (($key == 'noattempts') || ($key == 'noenrolled') || ($key == 'editingteacher')) {
$countattempts[$user->id] = 0;
} else {
$params = ['cmid' => $cm->id, 'userid' => $user->id];
for ($i = 1; $i <= $attemptcount; $i++) {
$generator->create_content($activity, $params);
}
$countattempts[$user->id] = $attemptcount;
$attemptcount++;
}
}
// Execute external method.
$this->setUser($users[$loginuser]);
if ($loginuser == 'student1') {
$this->expectException('moodle_exception');
$this->expectExceptionMessage('h5pactivity:reviewattempts required view attempts' .
' of all enrolled users');
}
$result = get_user_attempts::execute($activity->id);
$result = external_api::clean_returnvalue(
get_user_attempts::execute_returns(),
$result
);
$this->assertCount(count($warnings), $result['warnings']);
// Teacher is excluded.
$this->assertCount(count($resultusers), $result['usersattempts']);
$expectedwarnings = [];
foreach ($warnings as $warninguser) {
$id = $users[$warninguser]->id;
$expectedwarnings[$id] = $warninguser;
}
foreach ($result['warnings'] as $warning) {
$this->assertEquals('user', $warning['item']);
$this->assertEquals(1, $warning['warningcode']);
$this->assertArrayHasKey($warning['itemid'], $expectedwarnings);
}
$expectedusers = [];
foreach ($resultusers as $resultuser) {
$id = $users[$resultuser]->id;
$expectedusers[$id] = $resultuser;
}
foreach ($result['usersattempts'] as $usersattempts) {
$this->assertArrayHasKey('userid', $usersattempts);
$userid = $usersattempts['userid'];
$this->assertArrayHasKey($userid, $expectedusers);
$this->assertCount($countattempts[$userid], $usersattempts['attempts']);
if ($countattempts[$userid]) {
$this->assertArrayHasKey('scored', $usersattempts);
}
}
}
/**
* Data provider for the test_execute_multipleusers.
*
* @return array
*/
public function execute_multipleusers_data(): array {
return [
// Teacher checks.
'Teacher checking students with attempts' => [
'editingteacher',
['student1', 'student2', 'student3', 'student4', 'student5'],
[],
['student1', 'student2', 'student3', 'student4', 'student5'],
],
'Teacher checking 2 students with atempts and one not' => [
'editingteacher',
['student1', 'student2', 'noattempts'],
[],
['student1', 'student2', 'noattempts'],
],
'Teacher checking no students' => [
'editingteacher',
[],
[],
[],
],
'Teacher checking one student and a no enrolled user' => [
'editingteacher',
['student1', 'noenrolled'],
[],
['student1'],
],
// Permission check.
'Student checking attempts and another user' => [
'student1',
['student1', 'student2'],
['student2'],
['student1'],
],
];
}
/**
* Data provider for {@see test_execute_with_sortorder}
*
* @return array[]
*/
public function execute_with_sortorder(): array {
return [
'Sort by id' => ['id', ['user01', 'user02']],
'Sort by id desc' => ['id desc', ['user02', 'user01']],
'Sort by id asc' => ['id asc', ['user01', 'user02']],
'Sort by firstname' => ['firstname', ['user01', 'user02']],
'Sort by firstname desc' => ['firstname desc', ['user02', 'user01']],
'Sort by firstname asc' => ['firstname asc', ['user01', 'user02']],
'Sort by lastname' => ['lastname', ['user02', 'user01']],
'Sort by lastname desc' => ['lastname desc', ['user01', 'user02']],
'Sort by lastname asc' => ['lastname asc', ['user02', 'user01']],
// Edge cases (should fall back to default).
'Sort by empty string' => ['', ['user01', 'user02']],
'Sort by invalid field' => ['invalid', ['user01', 'user02']],
];
}
/**
* Test external execute method with sortorder
*
* @param string $sortorder
* @param string[] $expectedorder
*
* @dataProvider execute_with_sortorder
*/
public function test_execute_with_sortorder(string $sortorder, array $expectedorder): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create course, module.
$course = $this->getDataGenerator()->create_course();
$module = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
// Couple of enrolled users in the course.
$users['user01'] = $this->getDataGenerator()->create_and_enrol($course, 'student', [
'username' => 'user01',
'firstname' => 'Adam',
'lastname' => 'Zebra',
]);
$users['user02'] = $this->getDataGenerator()->create_and_enrol($course, 'student', [
'username' => 'user02',
'firstname' => 'Zoe',
'lastname' => 'Apples',
]);
$result = external_api::clean_returnvalue(
get_user_attempts::execute_returns(),
get_user_attempts::execute($module->id, $sortorder)
);
// Map expected order of usernames to user IDs.
$expectedorderbyuserid = array_map(static function(string $username) use ($users): int {
return $users[$username]->id;
}, $expectedorder);
// The order should match the ordering of user attempt user IDs.
$this->assertEquals($expectedorderbyuserid, array_column($result['usersattempts'], 'userid'));
}
}
@@ -0,0 +1,117 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_h5pactivity\external;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
use mod_h5pactivity\local\manager;
use core_external\external_api;
use externallib_advanced_testcase;
/**
* External function test for log_report_viewed.
*
* @package mod_h5pactivity
* @category external
* @covers \mod_h5pactivity\external\log_report_viewed
* @since Moodle 3.11
* @copyright 2021 Ilya Tregubov <ilya@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class log_report_viewed_test extends externallib_advanced_testcase {
/**
* Test the behaviour of log_report_viewed.
*
* @dataProvider execute_data
* @param int $enabletracking the activity tracking enable
* @param int $reviewmode the activity review mode
* @param string $loginuser the user which calls the webservice
* @param string|null $participant the user to log the data
*/
public function test_execute(int $enabletracking, int $reviewmode, string $loginuser, ?string $participant): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Enrol users: 1 teacher, 1 student.
$users = [
'editingteacher' => $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'),
'student' => $this->getDataGenerator()->create_and_enrol($course, 'student'),
];
// Add h5p activity.
$activity = $this->getDataGenerator()->create_module('h5pactivity',
['course' => $course, 'enabletracking' => $enabletracking, 'reviewmode' => $reviewmode]);
// Create attempt for h5p activity.
$attempts = [];
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
$user = $users['student'];
$manager = manager::create_from_instance($activity);
$cm = $manager->get_coursemodule();
$params = ['cmid' => $cm->id, 'userid' => $user->id];
$attempts['student'] = $generator->create_content($activity, $params);
// Redirect events to the sink, so we can recover them later.
$sink = $this->redirectEvents();
// Execute external method.
$this->setUser($users[$loginuser]);
$attemptid = $attempts[$participant]->id ?? 0;
$result = log_report_viewed::execute($activity->id, $user->id, $attemptid);
$result = external_api::clean_returnvalue(
log_report_viewed::execute_returns(),
$result
);
// Validate general structure.
$this->assertArrayHasKey('status', $result);
$events = $sink->get_events();
$event = end($events);
// Check the event details are correct.
$this->assertInstanceOf('mod_h5pactivity\event\report_viewed', $event);
$this->assertEquals(\context_module::instance($cm->id), $event->get_context());
$this->assertEquals($cm->instance, $event->other['instanceid']);
$this->assertEquals($user->id, $event->other['userid']);
$this->assertEquals($attemptid, $event->other['attemptid']);
}
/**
* Data provider for the test_execute tests.
*
* @return array
*/
public static function execute_data(): array {
return [
'Student reviewing own attempt' => [
1, manager::REVIEWCOMPLETION, 'student', 'student'
],
'Teacher reviewing student attempts' => [
1, manager::REVIEWCOMPLETION, 'editingteacher', 'student'
],
];
}
}
+155
View File
@@ -0,0 +1,155 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* External function test for view_h5pactivity.
*
* @package mod_h5pactivity
* @category external
* @since Moodle 3.9
* @copyright 2020 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\external;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
use core_external\external_api;
use externallib_advanced_testcase;
use stdClass;
use context_module;
use course_modinfo;
/**
* External function test for view_h5pactivity.
*
* @package mod_h5pactivity
* @copyright 2020 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class view_h5pactivity_test extends externallib_advanced_testcase {
/**
* Test test_view_h5pactivity invalid id.
*/
public function test_view_h5pactivity_invalid_id(): void {
$this->resetAfterTest();
$this->setAdminUser();
$this->expectException('moodle_exception');
$result = view_h5pactivity::execute(0);
}
/**
* Test test_view_h5pactivity user not enrolled.
*/
public function test_view_h5pactivity_user_not_enrolled(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Setup scenario.
$scenario = $this->setup_scenario();
// Test not-enrolled user.
$usernotenrolled = self::getDataGenerator()->create_user();
$this->setUser($usernotenrolled);
$this->expectException('moodle_exception');
$result = view_h5pactivity::execute($scenario->h5pactivity->id);
}
/**
* Test test_view_h5pactivity user student.
*/
public function test_view_h5pactivity_user_student(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Setup scenario.
$scenario = $this->setup_scenario();
$cm = get_coursemodule_from_instance('h5pactivity', $scenario->h5pactivity->id);
$this->setUser($scenario->student);
// Trigger and capture the event.
$sink = $this->redirectEvents();
$result = view_h5pactivity::execute($scenario->h5pactivity->id);
$result = external_api::clean_returnvalue(view_h5pactivity::execute_returns(), $result);
$this->assertTrue($result['status']);
$events = $sink->get_events();
$this->assertCount(1, $events);
$event = array_shift($events);
// Checking that the event contains the expected values.
$this->assertInstanceOf('\mod_h5pactivity\event\course_module_viewed', $event);
$this->assertEquals($scenario->contextmodule, $event->get_context());
$h5pactivity = new \moodle_url('/mod/h5pactivity/view.php', array('id' => $cm->id));
$this->assertEquals($h5pactivity, $event->get_url());
$this->assertEventContextNotUsed($event);
$this->assertNotEmpty($event->get_name());
}
/**
* Test test_view_h5pactivity user missing capabilities.
*/
public function test_view_h5pactivity_user_missing_capabilities(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Setup scenario.
$scenario = $this->setup_scenario();
$studentrole = $DB->get_record('role', array('shortname' => 'student'));
// Test user with no capabilities.
// We need a explicit prohibit since this capability is only defined in authenticated user and guest roles.
assign_capability('mod/h5pactivity:view', CAP_PROHIBIT, $studentrole->id, $scenario->contextmodule->id);
// Empty all the caches that may be affected by this change.
accesslib_clear_all_caches_for_unit_testing();
course_modinfo::clear_instance_cache();
$this->setUser($scenario->student);
$this->expectException('moodle_exception');
$result = view_h5pactivity::execute($scenario->h5pactivity->id);
}
/**
* Create a scenario to use into the tests.
*
* @return stdClass $scenario
*/
protected function setup_scenario() {
$course = $this->getDataGenerator()->create_course();
$h5pactivity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
$contextmodule = context_module::instance($h5pactivity->cmid);
$scenario = new stdClass();
$scenario->contextmodule = $contextmodule;
$scenario->student = $student;
$scenario->h5pactivity = $h5pactivity;
return $scenario;
}
}
@@ -0,0 +1,52 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* H5P-related steps definitions.
*
* @package mod_h5pactivity
* @category test
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_mod_h5pactivity_generator extends behat_generator_base {
protected function get_creatable_entities(): array {
return [
'attempts' => [
'singular' => 'attempt',
'datagenerator' => 'attempt',
'required' => ['h5pactivity', 'user'],
'switchids' => ['h5pactivity' => 'h5pactivityid', 'user' => 'userid'],
],
];
}
/**
* Look up the id of a h5p from its name.
*
* @param string $h5pname the activity name, for example 'Test h5p'.
* @return int corresponding id
*/
protected function get_h5pactivity_id(string $h5pname): int {
global $DB;
if (!$id = $DB->get_field('h5pactivity', 'id', ['name' => $h5pname])) {
throw new Exception('There is no h5p activity with name "' . $h5pname);
}
return $id;
}
}
+597
View File
@@ -0,0 +1,597 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Data generator.
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use mod_h5pactivity\local\manager;
defined('MOODLE_INTERNAL') || die();
/**
* h5pactivity module data generator class.
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_h5pactivity_generator extends testing_module_generator {
/**
* Creates new h5pactivity module instance. By default it contains a short
* text file.
*
* @param array|stdClass $record data for module being generated. Requires 'course' key
* (an id or the full object). Also can have any fields from add module form.
* @param null|array $options general options for course module. Since 2.6 it is
* possible to omit this argument by merging options into $record
* @return stdClass record from module-defined table with additional field
* cmid (corresponding id in course_modules table)
*/
public function create_instance($record = null, array $options = null): stdClass {
global $CFG, $USER;
// Ensure the record can be modified without affecting calling code.
$record = (object)(array)$record;
// Fill in optional values if not specified.
if (!isset($record->packagefilepath)) {
$record->packagefilepath = $CFG->dirroot.'/h5p/tests/fixtures/h5ptest.zip';
} else if (strpos($record->packagefilepath, $CFG->dirroot) !== 0) {
$record->packagefilepath = "{$CFG->dirroot}/{$record->packagefilepath}";
}
if (!isset($record->grade)) {
$record->grade = 100;
}
if (!isset($record->displayoptions)) {
$factory = new \core_h5p\factory();
$core = $factory->get_core();
$config = \core_h5p\helper::decode_display_options($core);
$record->displayoptions = \core_h5p\helper::get_display_options($core, $config);
}
if (!isset($record->enabletracking)) {
$record->enabletracking = 1;
}
if (!isset($record->grademethod)) {
$record->grademethod = manager::GRADEHIGHESTATTEMPT;
}
if (!isset($record->reviewmode)) {
$record->reviewmode = manager::REVIEWCOMPLETION;
}
$globaluser = $USER;
if (!empty($record->username)) {
$user = core_user::get_user_by_username($record->username);
$this->set_user($user);
}
// The 'packagefile' value corresponds to the draft file area ID. If not specified, create from packagefilepath.
if (empty($record->packagefile)) {
if (!isloggedin() || isguestuser()) {
throw new coding_exception('H5P activity generator requires a current user');
}
if (!file_exists($record->packagefilepath)) {
throw new coding_exception("File {$record->packagefilepath} does not exist");
}
$usercontext = context_user::instance($USER->id);
// Pick a random context id for specified user.
$record->packagefile = file_get_unused_draft_itemid();
// Add actual file there.
$filerecord = [
'component' => 'user',
'filearea' => 'draft',
'contextid' => $usercontext->id,
'itemid' => $record->packagefile,
'filename' => basename($record->packagefilepath),
'filepath' => '/',
'userid' => $USER->id,
];
$fs = get_file_storage();
$fs->create_file_from_pathname($filerecord, $record->packagefilepath);
}
$instance = parent::create_instance($record, (array)$options);
$this->set_user($globaluser);
return $instance;
}
/**
* Creata a fake attempt
* @param stdClass $instance object returned from create_instance() call
* @param stdClass|array $record
* @return stdClass generated object
* @throws coding_exception if function is not implemented by module
*/
public function create_content($instance, $record = []) {
global $DB, $USER;
$currenttime = time();
$cmid = $record['cmid'];
$userid = $record['userid'] ?? $USER->id;
$conditions = ['h5pactivityid' => $instance->id, 'userid' => $userid];
$attemptnum = $DB->count_records('h5pactivity_attempts', $conditions) + 1;
$attempt = (object)[
'h5pactivityid' => $instance->id,
'userid' => $userid,
'timecreated' => $currenttime,
'timemodified' => $currenttime,
'attempt' => $attemptnum,
'rawscore' => 3,
'maxscore' => 5,
'completion' => 1,
'success' => 1,
'scaled' => 0.6,
];
$attempt->id = $DB->insert_record('h5pactivity_attempts', $attempt);
// Create 3 diferent tracking results.
$result = (object)[
'attemptid' => $attempt->id,
'subcontent' => '',
'timecreated' => $currenttime,
'interactiontype' => 'compound',
'description' => 'description for '.$userid,
'correctpattern' => '',
'response' => '',
'additionals' => '{"extensions":{"http:\/\/h5p.org\/x-api\/h5p-local-content-id":'.
$cmid.'},"contextExtensions":{}}',
'rawscore' => 3,
'maxscore' => 5,
'completion' => 1,
'success' => 1,
'scaled' => 0.6,
];
$DB->insert_record('h5pactivity_attempts_results', $result);
$result->subcontent = 'bd03477a-90a1-486d-890b-0657d6e80ffd';
$result->interactiontype = 'compound';
$result->response = '0[,]5[,]2[,]3';
$result->additionals = '{"choices":[{"id":"0","description":{"en-US":"Blueberry\n"}},'.
'{"id":"1","description":{"en-US":"Raspberry\n"}},{"id":"5","description":'.
'{"en-US":"Strawberry\n"}},{"id":"2","description":{"en-US":"Cloudberry\n"}},'.
'{"id":"3","description":{"en-US":"Halle Berry\n"}},'.
'{"id":"4","description":{"en-US":"Cocktail cherry\n"}}],'.
'"extensions":{"http:\/\/h5p.org\/x-api\/h5p-local-content-id":'.$cmid.
',"http:\/\/h5p.org\/x-api\/h5p-subContentId":"'.$result->interactiontype.
'"},"contextExtensions":{}}';
$result->rawscore = 1;
$result->scaled = 0.2;
$DB->insert_record('h5pactivity_attempts_results', $result);
$result->subcontent = '14fcc986-728b-47f3-915b-'.$userid;
$result->interactiontype = 'matching';
$result->correctpattern = '["0[.]1[,]1[.]0[,]2[.]2"]';
$result->response = '1[.]0[,]0[.]1[,]2[.]2';
$result->additionals = '{"source":[{"id":"0","description":{"en-US":"A berry"}}'.
',{"id":"1","description":{"en-US":"An orange berry"}},'.
'{"id":"2","description":{"en-US":"A red berry"}}],'.
'"target":[{"id":"0","description":{"en-US":"Cloudberry"}},'.
'{"id":"1","description":{"en-US":"Blueberry"}},'.
'{"id":"2","description":{"en-US":"Redcurrant\n"}}],'.
'"contextExtensions":{}}';
$result->rawscore = 2;
$result->scaled = 0.4;
$DB->insert_record('h5pactivity_attempts_results', $result);
return $attempt;
}
/**
* Create a H5P attempt.
*
* This method is user by behat generator.
*
* @param array $data the attempts data array
*/
public function create_attempt(array $data): void {
global $DB;
if (!isset($data['h5pactivityid'])) {
throw new coding_exception('Must specify h5pactivityid when creating a H5P attempt.');
}
if (!isset($data['userid'])) {
throw new coding_exception('Must specify userid when creating a H5P attempt.');
}
// Defaults.
$data['attempt'] = $data['attempt'] ?? 1;
$data['rawscore'] = $data['rawscore'] ?? 0;
$data['maxscore'] = $data['maxscore'] ?? 0;
$data['duration'] = $data['duration'] ?? 0;
$data['completion'] = $data['completion'] ?? 1;
$data['success'] = $data['success'] ?? 0;
$data['attemptid'] = $this->get_attempt_object($data);
// Check interaction type and create a valid record for it.
$data['interactiontype'] = $data['interactiontype'] ?? 'compound';
$method = 'get_attempt_result_' . str_replace('-', '', $data['interactiontype']);
if (!method_exists($this, $method)) {
throw new Exception("Cannot create a {$data['interactiontype']} interaction statement");
}
$this->insert_statement($data, $this->$method($data));
// If the activity has tracking enabled, try to recalculate grades.
$activity = $DB->get_record('h5pactivity', ['id' => $data['h5pactivityid']]);
if ($activity->enabletracking) {
h5pactivity_update_grades($activity, $data['userid']);
}
}
/**
* Get or create an H5P attempt using the data array.
*
* @param array $attemptinfo the generator provided data
* @return int the attempt id
*/
private function get_attempt_object($attemptinfo): int {
global $DB;
$result = $DB->get_record('h5pactivity_attempts', [
'userid' => $attemptinfo['userid'],
'h5pactivityid' => $attemptinfo['h5pactivityid'],
'attempt' => $attemptinfo['attempt'],
]);
if ($result) {
return $result->id;
}
return $this->new_user_attempt($attemptinfo);
}
/**
* Creates a user attempt.
*
* @param array $attemptinfo the current attempt information.
* @return int the h5pactivity_attempt ID
*/
private function new_user_attempt(array $attemptinfo): int {
global $DB;
$record = (object)[
'h5pactivityid' => $attemptinfo['h5pactivityid'],
'userid' => $attemptinfo['userid'],
'timecreated' => time(),
'timemodified' => time(),
'attempt' => $attemptinfo['attempt'],
'rawscore' => $attemptinfo['rawscore'],
'maxscore' => $attemptinfo['maxscore'],
'duration' => $attemptinfo['duration'],
'completion' => $attemptinfo['completion'],
'success' => $attemptinfo['success'],
];
if (empty($record->maxscore)) {
$record->scaled = 0;
} else {
$record->scaled = $record->rawscore / $record->maxscore;
}
return $DB->insert_record('h5pactivity_attempts', $record);
}
/**
* Insert a new statement into an attempt.
*
* If the interaction type is "compound" it will also update the attempt general result.
*
* @param array $attemptinfo the current attempt information
* @param array $statement the statement tracking information
* @return int the h5pactivity_attempt_result ID
*/
private function insert_statement(array $attemptinfo, array $statement): int {
global $DB;
$record = $statement + [
'attemptid' => $attemptinfo['attemptid'],
'interactiontype' => $attemptinfo['interactiontype'] ?? 'compound',
'timecreated' => time(),
'rawscore' => $attemptinfo['rawscore'],
'maxscore' => $attemptinfo['maxscore'],
'duration' => $attemptinfo['duration'],
'completion' => $attemptinfo['completion'],
'success' => $attemptinfo['success'],
];
$result = $DB->insert_record('h5pactivity_attempts_results', $record);
if ($record['interactiontype'] == 'compound') {
$attempt = (object)[
'id' => $attemptinfo['attemptid'],
'rawscore' => $record['rawscore'],
'maxscore' => $record['maxscore'],
'duration' => $record['duration'],
'completion' => $record['completion'],
'success' => $record['success'],
];
$DB->update_record('h5pactivity_attempts', $attempt);
}
return $result;
}
/**
* Generates a valid compound tracking result.
*
* @param array $attemptinfo the current attempt information.
* @return array with the required statement data
*/
private function get_attempt_result_compound(array $attemptinfo): array {
$additionals = (object)[
"extensions" => (object)[
"http://h5p.org/x-api/h5p-local-content-id" => 1,
],
"contextExtensions" => (object)[],
];
return [
'subcontent' => '',
'description' => '',
'correctpattern' => '',
'response' => '',
'additionals' => json_encode($additionals),
];
}
/**
* Generates a valid choice tracking result.
*
* @param array $attemptinfo the current attempt information.
* @return array with the required statement data
*/
private function get_attempt_result_choice(array $attemptinfo): array {
$response = ($attemptinfo['rawscore']) ? '1[,]0' : '2[,]3';
$additionals = (object)[
"choices" => [
(object)[
"id" => "3",
"description" => (object)[
"en-US" => "Another wrong answer\n",
],
],
(object)[
"id" => "2",
"description" => (object)[
"en-US" => "Wrong answer\n",
],
],
(object)[
"id" => "1",
"description" => (object)[
"en-US" => "This is also a correct answer\n",
],
],
(object)[
"id" => "0",
"description" => (object)[
"en-US" => "This is a correct answer\n",
],
],
],
"extensions" => (object)[
"http://h5p.org/x-api/h5p-local-content-id" => 1,
"http://h5p.org/x-api/h5p-subContentId" => "4367a919-ec47-43c9-b521-c22d9c0c0d8d",
],
"contextExtensions" => (object)[],
];
return [
'subcontent' => microtime(),
'description' => 'Select the correct answers',
'correctpattern' => '["1[,]0"]',
'response' => $response,
'additionals' => json_encode($additionals),
];
}
/**
* Generates a valid matching tracking result.
*
* @param array $attemptinfo the current attempt information.
* @return array with the required statement data
*/
private function get_attempt_result_matching(array $attemptinfo): array {
$response = ($attemptinfo['rawscore']) ? '0[.]0[,]1[.]1' : '1[.]0[,]0[.]1';
$additionals = (object)[
"source" => [
(object)[
"id" => "0",
"description" => (object)[
"en-US" => "Drop item A\n",
],
],
(object)[
"id" => "1",
"description" => (object)[
"en-US" => "Drop item B\n",
],
],
],
"target" => [
(object)[
"id" => "0",
"description" => (object)[
"en-US" => "Drop zone A\n",
],
],
(object)[
"id" => "1",
"description" => (object)[
"en-US" => "Drop zone B\n",
],
],
],
"extensions" => [
"http://h5p.org/x-api/h5p-local-content-id" => 1,
"http://h5p.org/x-api/h5p-subContentId" => "682f1c74-c819-4e9d-8c36-12d9dc5fcdbc",
],
"contextExtensions" => (object)[],
];
return [
'subcontent' => microtime(),
'description' => 'Drag and Drop example 1',
'correctpattern' => '["0[.]0[,]1[.]1"]',
'response' => $response,
'additionals' => json_encode($additionals),
];
}
/**
* Generates a valid fill-in tracking result.
*
* @param array $attemptinfo the current attempt information.
* @return array with the required statement data
*/
private function get_attempt_result_fillin(array $attemptinfo): array {
$response = ($attemptinfo['rawscore']) ? 'first[,]second' : 'something[,]else';
$additionals = (object)[
"extensions" => (object)[
"http://h5p.org/x-api/h5p-local-content-id" => 1,
"http://h5p.org/x-api/h5p-subContentId" => "1a3febd5-7edc-4336-8112-12756b945b62",
"https://h5p.org/x-api/case-sensitivity" => true,
"https://h5p.org/x-api/alternatives" => [
["first"],
["second"],
],
],
"contextExtensions" => (object)[
"https://h5p.org/x-api/h5p-reporting-version" => "1.1.0",
],
];
return [
'subcontent' => microtime(),
'description' => '<p>This an example of missing word text.</p>
<p>The first answer if "first": the first answer is __________.</p>
<p>The second is second is "second": the secons answer is __________</p>',
'correctpattern' => '["{case_matters=true}first[,]second"]',
'response' => $response,
'additionals' => json_encode($additionals),
];
}
/**
* Generates a valid true-false tracking result.
*
* @param array $attemptinfo the current attempt information.
* @return array with the required statement data
*/
private function get_attempt_result_truefalse(array $attemptinfo): array {
$response = ($attemptinfo['rawscore']) ? 'true' : 'false';
$additionals = (object)[
"extensions" => (object)[
"http://h5p.org/x-api/h5p-local-content-id" => 1,
"http://h5p.org/x-api/h5p-subContentId" => "5de9fb1e-aa03-4c9a-8cf0-3870b3f012ca",
],
"contextExtensions" => (object)[],
];
return [
'subcontent' => microtime(),
'description' => 'The correct answer is true.',
'correctpattern' => '["true"]',
'response' => $response,
'additionals' => json_encode($additionals),
];
}
/**
* Generates a valid long-fill-in tracking result.
*
* @param array $attemptinfo the current attempt information.
* @return array with the required statement data
*/
private function get_attempt_result_longfillin(array $attemptinfo): array {
$response = ($attemptinfo['rawscore']) ? 'The Hobbit is book' : 'Who cares?';
$additionals = (object)[
"extensions" => (object)[
"http://h5p.org/x-api/h5p-local-content-id" => 1,
"http://h5p.org/x-api/h5p-subContentId" => "5de9fb1e-aa03-4c9a-8cf0-3870b3f012ca",
],
"contextExtensions" => (object)[],
];
return [
'subcontent' => microtime(),
'description' => '<p>Please describe the novel The Hobbit',
'correctpattern' => '',
'response' => $response,
'additionals' => json_encode($additionals),
];
}
/**
* Generates a valid sequencing tracking result.
*
* @param array $attemptinfo the current attempt information.
* @return array with the required statement data
*/
private function get_attempt_result_sequencing(array $attemptinfo): array {
$response = ($attemptinfo['rawscore']) ? 'true' : 'false';
$additionals = (object)[
"extensions" => (object)[
"http://h5p.org/x-api/h5p-local-content-id" => 1,
"http://h5p.org/x-api/h5p-subContentId" => "5de9fb1e-aa03-4c9a-8cf0-3870b3f012ca",
],
"contextExtensions" => (object)[],
];
return [
'subcontent' => microtime(),
'description' => 'The correct answer is true.',
'correctpattern' => '["{case_matters=true}first[,]second"]',
'response' => $response,
'additionals' => json_encode($additionals),
];
}
/**
* Generates a valid other tracking result.
*
* @param array $attemptinfo the current attempt information.
* @return array with the required statement data
*/
private function get_attempt_result_other(array $attemptinfo): array {
$additionals = (object)[
"extensions" => (object)[
"http://h5p.org/x-api/h5p-local-content-id" => 1,
],
"contextExtensions" => (object)[],
];
return [
'subcontent' => microtime(),
'description' => '',
'correctpattern' => '',
'response' => '',
'additionals' => json_encode($additionals),
];
}
}
+353
View File
@@ -0,0 +1,353 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_h5pactivity;
use mod_h5pactivity\local\manager;
/**
* Genarator tests class for mod_h5pactivity.
*
* @package mod_h5pactivity
* @category test
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class generator_test extends \advanced_testcase {
/**
* Test on H5P activity creation.
*/
public function test_create_instance(): void {
global $DB, $CFG, $USER;
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
// Create one activity.
$this->assertFalse($DB->record_exists('h5pactivity', ['course' => $course->id]));
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$records = $DB->get_records('h5pactivity', ['course' => $course->id], 'id');
$this->assertEquals(15, $activity->displayoptions);
$this->assertEquals(1, count($records));
$this->assertTrue(array_key_exists($activity->id, $records));
// Create a second one with different name and dusplay options.
$params = [
'course' => $course->id, 'name' => 'Another h5pactivity', 'displayoptions' => 6,
'enabletracking' => 0, 'grademethod' => manager::GRADELASTATTEMPT,
];
$activity = $this->getDataGenerator()->create_module('h5pactivity', $params);
$records = $DB->get_records('h5pactivity', ['course' => $course->id], 'id');
$this->assertEquals(6, $activity->displayoptions);
$this->assertEquals(0, $activity->enabletracking);
$this->assertEquals(manager::GRADELASTATTEMPT, $activity->grademethod);
$this->assertEquals(manager::REVIEWCOMPLETION, $activity->reviewmode);
$this->assertEquals(2, count($records));
$this->assertEquals('Another h5pactivity', $records[$activity->id]->name);
// Examples of specifying the package file (do not validate anything, just check for exceptions).
// 1. As path to the file in filesystem.
$params = [
'course' => $course->id,
'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/filltheblanks.h5p'
];
$activity = $this->getDataGenerator()->create_module('h5pactivity', $params);
// 2. As file draft area id.
$fs = get_file_storage();
$params = [
'course' => $course->id,
'packagefile' => file_get_unused_draft_itemid()
];
$usercontext = \context_user::instance($USER->id);
$filerecord = ['component' => 'user', 'filearea' => 'draft',
'contextid' => $usercontext->id, 'itemid' => $params['packagefile'],
'filename' => 'singlescobasic.zip', 'filepath' => '/'];
$filepath = $CFG->dirroot.'/h5p/tests/fixtures/filltheblanks.h5p';
$fs->create_file_from_pathname($filerecord, $filepath);
$activity = $this->getDataGenerator()->create_module('h5pactivity', $params);
}
/**
* Test that a new H5P activity cannot be generated without a valid file
* other user.
*/
public function test_create_file_exception(): void {
global $CFG;
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
// Testing generator exceptions.
$params = [
'course' => $course->id,
'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/wrong_file_.xxx'
];
$this->expectException(\coding_exception::class);
$activity = $this->getDataGenerator()->create_module('h5pactivity', $params);
}
/**
* Test to create H5P attempts
*
* @dataProvider create_attempt_data
*
* @param array $tracks the attempt tracks objects
* @param int $attempts the final registered attempts
* @param int $results the final registered attempts results
* @param bool $exception if an exception is expected
*
*/
public function test_create_attempt(array $tracks, int $attempts, int $results, bool $exception): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
$this->assertEquals(0, $DB->count_records('h5pactivity_attempts'));
$this->assertEquals(0, $DB->count_records('h5pactivity_attempts_results'));
if ($exception) {
$this->expectException(\Exception::class);
}
foreach ($tracks as $track) {
$attemptinfo = [
'userid' => $user->id,
'h5pactivityid' => $activity->id,
'attempt' => $track['attempt'],
'interactiontype' => $track['interactiontype'],
'rawscore' => $track['rawscore'],
'maxscore' => $track['maxscore'],
'duration' => $track['duration'],
'completion' => $track['completion'],
'success' => $track['success'],
];
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
$generator->create_attempt($attemptinfo);
$this->assert_attempt_matches_info($attemptinfo);
}
$this->assertEquals($attempts, $DB->count_records('h5pactivity_attempts'));
$this->assertEquals($results, $DB->count_records('h5pactivity_attempts_results'));
}
/**
* Data provider for create attempt test.
*
* @return array
*/
public static function create_attempt_data(): array {
return [
'Compound statement' => [
[
[
'interactiontype' => 'compound', 'attempt' => 1, 'rawscore' => 2,
'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0
],
], 1, 1, false,
],
'Choice statement' => [
[
[
'interactiontype' => 'choice', 'attempt' => 1, 'rawscore' => 2,
'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0
],
], 1, 1, false,
],
'Matching statement' => [
[
[
'interactiontype' => 'matching', 'attempt' => 1, 'rawscore' => 2,
'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0
],
], 1, 1, false,
],
'Fill-in statement' => [
[
[
'interactiontype' => 'fill-in', 'attempt' => 1, 'rawscore' => 2,
'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0
],
], 1, 1, false,
],
'True-false statement' => [
[
[
'interactiontype' => 'true-false', 'attempt' => 1, 'rawscore' => 2,
'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0
],
], 1, 1, false,
],
'Long-fill-in statement' => [
[
[
'interactiontype' => 'long-fill-in', 'attempt' => 1, 'rawscore' => 2,
'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0
],
], 1, 1, false,
],
'Sequencing statement' => [
[
[
'interactiontype' => 'sequencing', 'attempt' => 1, 'rawscore' => 2,
'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0
],
], 1, 1, false,
],
'Other statement' => [
[
[
'interactiontype' => 'other', 'attempt' => 1, 'rawscore' => 2,
'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0
],
], 1, 1, false,
],
'No graded statement' => [
[
[
'interactiontype' => 'other', 'attempt' => 1, 'rawscore' => 0,
'maxscore' => 0, 'duration' => 1, 'completion' => 1, 'success' => 0
],
], 1, 1, false,
],
'Invalid statement type' => [
[
[
'interactiontype' => 'no-valid-statement-type', 'attempt' => 1, 'rawscore' => 2,
'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0
],
], 0, 0, true,
],
'Adding a second statement to attempt' => [
[
[
'interactiontype' => 'true-false', 'attempt' => 1, 'rawscore' => 2,
'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0
],
[
'interactiontype' => 'compound', 'attempt' => 1, 'rawscore' => 3,
'maxscore' => 3, 'duration' => 2, 'completion' => 1, 'success' => 0
],
], 1, 2, false,
],
'Creating two attempts' => [
[
[
'interactiontype' => 'compound', 'attempt' => 1, 'rawscore' => 2,
'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0
],
[
'interactiontype' => 'compound', 'attempt' => 2, 'rawscore' => 3,
'maxscore' => 3, 'duration' => 1, 'completion' => 1, 'success' => 0
],
], 2, 2, false,
],
];
}
/**
* Insert track into attempt, creating the attempt if necessary.
*
* @param array $attemptinfo the attempt track information
*/
private function assert_attempt_matches_info($attemptinfo): void {
global $DB;
$attempt = $DB->get_record('h5pactivity_attempts', [
'userid' => $attemptinfo['userid'],
'h5pactivityid' => $attemptinfo['h5pactivityid'],
'attempt' => $attemptinfo['attempt'],
]);
$this->assertEquals($attemptinfo['rawscore'], $attempt->rawscore);
$this->assertEquals($attemptinfo['maxscore'], $attempt->maxscore);
$this->assertEquals($attemptinfo['duration'], $attempt->duration);
$this->assertEquals($attemptinfo['completion'], $attempt->completion);
$this->assertEquals($attemptinfo['success'], $attempt->success);
$track = $DB->get_record('h5pactivity_attempts_results', [
'attemptid' => $attempt->id,
'interactiontype' => $attemptinfo['interactiontype'],
]);
$this->assertEquals($attemptinfo['rawscore'], $track->rawscore);
$this->assertEquals($attemptinfo['maxscore'], $track->maxscore);
$this->assertEquals($attemptinfo['duration'], $track->duration);
$this->assertEquals($attemptinfo['completion'], $track->completion);
$this->assertEquals($attemptinfo['success'], $track->success);
}
/**
* Test exceptions when creating an invalid attempt.
*
* @dataProvider create_attempt_exceptions_data
*
* @param bool $validmod if the activity id is provided
* @param bool $validuser if the user id is provided
*/
public function test_create_attempt_exceptions(bool $validmod, bool $validuser): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
$this->expectException(\coding_exception::class);
$attemptinfo = [
'attempt' => 1,
'interactiontype' => 'compound',
'rawscore' => 2,
'maxscore' => 1,
'duration' => 1,
'completion' => 1,
'success' => 0,
];
if ($validmod) {
$attemptinfo['h5pactivityid'] = $activity->id;
}
if ($validuser) {
$attemptinfo['userid'] = $user->id;
}
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
$generator->create_attempt($attemptinfo);
}
/**
* Data provider for data request creation tests.
*
* @return array
*/
public static function create_attempt_exceptions_data(): array {
return [
'Invalid user' => [true, false],
'Invalid activity' => [false, true],
'Invalid user and activity' => [false, false],
];
}
}
+429
View File
@@ -0,0 +1,429 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_h5pactivity;
use advanced_testcase;
use mod_h5pactivity\local\manager;
/**
* Unit tests for (some of) mod/h5pactivity/lib.php.
*
* @package mod_h5pactivity
* @copyright 2021 Ilya Tregubov <ilya@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class lib_test extends advanced_testcase {
/**
* Load required test libraries
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once("{$CFG->dirroot}/mod/h5pactivity/lib.php");
}
/**
* Test that h5pactivity_delete_instance removes data.
*
* @covers ::h5pactivity_delete_instance
*/
public function test_h5pactivity_delete_instance(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$this->setUser($user);
/** @var \mod_h5pactivity_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
/** @var \core_h5p_generator $h5pgenerator */
$h5pgenerator = $this->getDataGenerator()->get_plugin_generator('core_h5p');
// Add an attempt to the H5P activity.
$attemptinfo = [
'userid' => $user->id,
'h5pactivityid' => $activity->id,
'attempt' => 1,
'interactiontype' => 'compound',
'rawscore' => 2,
'maxscore' => 2,
'duration' => 1,
'completion' => 1,
'success' => 0,
];
$generator->create_attempt($attemptinfo);
// Add also a xAPI state to the H5P activity.
$filerecord = [
'contextid' => \context_module::instance($activity->cmid)->id,
'component' => 'mod_h5pactivity',
'filearea' => 'package',
'itemid' => 0,
'filepath' => '/',
'filename' => 'dummy.h5p',
'addxapistate' => true,
];
$h5pgenerator->generate_h5p_data(false, $filerecord);
// Check the H5P activity exists and the attempt has been created.
$this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id]));
$this->assertEquals(2, $DB->count_records('grade_items'));
$this->assertEquals(2, $DB->count_records('grade_grades'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
// Check nothing happens when given activity id doesn't exist.
h5pactivity_delete_instance($activity->id + 1);
$this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id]));
$this->assertEquals(2, $DB->count_records('grade_items'));
$this->assertEquals(2, $DB->count_records('grade_grades'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
// Check the H5P instance and its associated data is removed.
h5pactivity_delete_instance($activity->id);
$this->assertEmpty($DB->get_record('h5pactivity', ['id' => $activity->id]));
$this->assertEquals(1, $DB->count_records('grade_items'));
$this->assertEquals(1, $DB->count_records('grade_grades'));
$this->assertEquals(0, $DB->count_records('xapi_states'));
}
/**
* Test that assign_print_recent_activity shows ungraded submitted assignments.
*
* @covers ::h5pactivity_print_recent_activity
*/
public function test_print_recent_activity(): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
$activity = $this->getDataGenerator()->create_module('h5pactivity',
['course' => $course, 'enabletracking' => 1, 'grademethod' => manager::GRADEHIGHESTATTEMPT]);
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
$manager = manager::create_from_instance($activity);
$cm = $manager->get_coursemodule();
$user = $student;
$params = ['cmid' => $cm->id, 'userid' => $user->id];
$generator->create_content($activity, $params);
$this->setUser($student);
$this->expectOutputRegex('/submitted:/');
h5pactivity_print_recent_activity($course, true, time() - 3600);
}
/**
* Test that h5pactivity_print_recent_activity does not display any warnings when a custom fullname has been configured.
*
* @covers ::h5pactivity_print_recent_activity
*/
public function test_print_recent_activity_fullname(): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
$activity = $this->getDataGenerator()->create_module('h5pactivity',
['course' => $course, 'enabletracking' => 1, 'grademethod' => manager::GRADEHIGHESTATTEMPT]);
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
$manager = manager::create_from_instance($activity);
$cm = $manager->get_coursemodule();
$user = $student;
$params = ['cmid' => $cm->id, 'userid' => $user->id];
$generator->create_content($activity, $params);
$this->setUser($teacher);
$this->expectOutputRegex('/submitted:/');
set_config('fullnamedisplay', 'firstname, lastnamephonetic');
h5pactivity_print_recent_activity($course, false, time() - 3600);
}
/**
* Test that h5pactivity_get_recent_mod_activity fetches the h5pactivity correctly.
*
* @covers ::h5pactivity_get_recent_mod_activity
*/
public function test_h5pactivity_get_recent_mod_activity(): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
$activity = $this->getDataGenerator()->create_module('h5pactivity',
['course' => $course, 'enabletracking' => 1, 'grademethod' => manager::GRADEHIGHESTATTEMPT]);
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
$manager = manager::create_from_instance($activity);
$cm = $manager->get_coursemodule();
$user = $student;
$params = ['cmid' => $cm->id, 'userid' => $user->id];
$generator->create_content($activity, $params);
$index = 1;
$activities = [
$index => (object) [
'type' => 'h5pactivity',
'cmid' => $cm->id,
],
];
$this->setUser($teacher);
h5pactivity_get_recent_mod_activity($activities, $index, time() - HOURSECS, $course->id, $cm->id);
$activity = $activities[1];
$this->assertEquals("h5pactivity", $activity->type);
$this->assertEquals($student->id, $activity->user->id);
}
/**
* Test that h5pactivity_get_recent_mod_activity fetches activity correctly.
*
* @covers ::h5pactivity_fetch_recent_activity
*/
public function test_h5pactivity_fetch_recent_activity(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
// Create users and groups.
$students = array();
$groups = array();
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
for ($i = 1; $i < 6; $i++) {
$students[$i] = $this->getDataGenerator()->create_and_enrol($course, 'student');
$groups[$i] = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
}
$groups[$i] = $this->getDataGenerator()->create_group(array('courseid' => $course->id, 'participation' => 0));
// Update the course set the groupmode SEPARATEGROUPS and forced.
update_course((object)array('id' => $course->id, 'groupmode' => SEPARATEGROUPS, 'groupmodeforce' => true));
// Student 1 is in groups 1 and 3.
groups_add_member($groups[1], $students[1]);
groups_add_member($groups[3], $students[1]);
// Student 2 is in groups 1 and 2.
groups_add_member($groups[1], $students[2]);
groups_add_member($groups[2], $students[2]);
// Student 3 is only in group 3.
groups_add_member($groups[3], $students[3]);
// Student 4 is only in group 5 (non-participation).
groups_add_member($groups[6], $students[4]);
// Student 5 is not in any groups.
// Grader is only in group 3.
groups_add_member($groups[3], $teacher);
$timestart = time() - 1;
// Create h5pactivity.
$activity = $this->getDataGenerator()->create_module('h5pactivity',
['course' => $course->id, 'enabletracking' => 1, 'grademethod' => manager::GRADEHIGHESTATTEMPT,
'groupmode' => SEPARATEGROUPS]);
$teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
$cmcontext = \context_module::instance($activity->cmid);
assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $teacherrole->id, $cmcontext, true);
$manager = manager::create_from_instance($activity);
$cm = $manager->get_coursemodule();
// Create attempts.
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
foreach ($students as $student) {
$params = ['cmid' => $cm->id, 'userid' => $student->id];
$generator->create_content($activity, $params);
}
// Get all attempts.
$dbparams = [$timestart, $course->id, 'h5pactivity'];
$userfieldsapi = \core_user\fields::for_userpic();
$namefields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects;;
$sql = "SELECT h5pa.id, h5pa.timemodified, cm.id as cmid, $namefields
FROM {h5pactivity_attempts} h5pa
JOIN {h5pactivity} h5p ON h5p.id = h5pa.h5pactivityid
JOIN {course_modules} cm ON cm.instance = h5p.id
JOIN {modules} md ON md.id = cm.module
JOIN {user} u ON u.id = h5pa.userid
WHERE h5pa.timemodified > ?
AND h5p.course = ?
AND md.name = ?
ORDER BY h5pa.timemodified ASC";
$submissions = $DB->get_records_sql($sql, $dbparams);
$this->assertCount(count($students), $submissions);
// Fetch activity for student (only his own).
$this->setUser($students[1]);
$recentactivity = h5pactivity_fetch_recent_activity($submissions, $course->id);
$this->assertCount(1, $recentactivity);
$this->assertEquals($students[1]->id, $recentactivity[$students[1]->id]->userid);
// Fetch users group info for grader.
$this->setUser($teacher);
$recentactivity = h5pactivity_fetch_recent_activity($submissions, $course->id);
$this->assertCount(2, $recentactivity);
// Grader, Student 1 and 3 are in Group 3.
$this->assertEquals($students[1]->id, $recentactivity[$students[1]->id]->userid);
$this->assertEquals($students[3]->id, $recentactivity[$students[3]->id]->userid);
// Grader is in Group 2.
groups_remove_member($groups[3], $teacher);
groups_add_member($groups[2], $teacher);
get_fast_modinfo($course->id, 0, true);
$recentactivity = h5pactivity_fetch_recent_activity($submissions, $course->id);
$this->assertCount(1, $recentactivity);
// Grader, Student 2 are in Group 2.
$this->assertEquals($students[2]->id, $recentactivity[$students[2]->id]->userid);
// Grader is in Group 1.
groups_remove_member($groups[2], $teacher);
groups_add_member($groups[1], $teacher);
get_fast_modinfo($course->id, 0, true);
$recentactivity = h5pactivity_fetch_recent_activity($submissions, $course->id);
$this->assertCount(2, $recentactivity);
// Grader, Student 1 and 2 are in Group 1.
$this->assertEquals($students[1]->id, $recentactivity[$students[1]->id]->userid);
$this->assertEquals($students[2]->id, $recentactivity[$students[2]->id]->userid);
// Grader is in no group.
groups_remove_member($groups[1], $teacher);
get_fast_modinfo($course->id, 0, true);
$recentactivity = h5pactivity_fetch_recent_activity($submissions, $course->id);
// Student 4 and Student 5 have submissions, but they are not in a participation group, so they do not show up in recent
// activity for separate groups mode.
$this->assertCount(0, $recentactivity);
}
/**
* Test that h5pactivity_reset_userdata reset user data.
*
* @covers ::h5pactivity_reset_userdata
*/
public function test_h5pactivity_reset_userdata(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$this->setUser($user);
/** @var \mod_h5pactivity_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
/** @var \core_h5p_generator $h5pgenerator */
$h5pgenerator = $this->getDataGenerator()->get_plugin_generator('core_h5p');
// Add an attempt to the H5P activity.
$attemptinfo = [
'userid' => $user->id,
'h5pactivityid' => $activity->id,
'attempt' => 1,
'interactiontype' => 'compound',
'rawscore' => 2,
'maxscore' => 2,
'duration' => 1,
'completion' => 1,
'success' => 0,
];
$generator->create_attempt($attemptinfo);
// Add also a xAPI state to the H5P activity.
$filerecord = [
'contextid' => \context_module::instance($activity->cmid)->id,
'component' => 'mod_h5pactivity',
'filearea' => 'package',
'itemid' => 0,
'filepath' => '/',
'filename' => 'dummy.h5p',
'addxapistate' => true,
];
$h5pgenerator->generate_h5p_data(false, $filerecord);
// Check the H5P activity exists and the attempt has been created with the expected data.
$this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id]));
$this->assertEquals(2, $DB->count_records('grade_items'));
$this->assertEquals(2, $DB->count_records('grade_grades'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
// Check nothing happens when reset_h5pactivity is not set.
$data = new \stdClass();
h5pactivity_reset_userdata($data);
$this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id]));
$this->assertEquals(2, $DB->count_records('grade_items'));
$this->assertEquals(2, $DB->count_records('grade_grades'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
// Check nothing happens when reset_h5pactivity is not set.
$data = (object) [
'courseid' => $course->id,
];
h5pactivity_reset_userdata($data);
$this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id]));
$this->assertEquals(2, $DB->count_records('grade_items'));
$this->assertEquals(2, $DB->count_records('grade_grades'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
// Check nothing happens when the given course doesn't exist.
$data = (object) [
'reset_h5pactivity' => true,
'courseid' => $course->id + 1,
];
h5pactivity_reset_userdata($data);
$this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id]));
$this->assertEquals(2, $DB->count_records('grade_items'));
$this->assertEquals(2, $DB->count_records('grade_grades'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
// Check the H5P instance and its associated data is reset.
$data = (object) [
'reset_h5pactivity' => true,
'courseid' => $course->id,
];
h5pactivity_reset_userdata($data);
$this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id]));
$this->assertEquals(2, $DB->count_records('grade_items'));
$this->assertEquals(1, $DB->count_records('grade_grades'));
$this->assertEquals(0, $DB->count_records('xapi_states'));
}
}
@@ -0,0 +1,483 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* mod_h5pactivity attempt tests
*
* @package mod_h5pactivity
* @category test
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\local;
use \core_xapi\local\statement;
use \core_xapi\local\statement\item;
use \core_xapi\local\statement\item_agent;
use \core_xapi\local\statement\item_activity;
use \core_xapi\local\statement\item_definition;
use \core_xapi\local\statement\item_verb;
use \core_xapi\local\statement\item_result;
use core_xapi\test_helper;
use stdClass;
/**
* Attempt tests class for mod_h5pactivity.
*
* @package mod_h5pactivity
* @category test
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt_test extends \advanced_testcase {
/**
* Generate a scenario to run all tests.
* @return array course_modules, user record, course record
*/
private function generate_testing_scenario(): array {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$cm = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST);
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
return [$cm, $student, $course];
}
/**
* Test for create_attempt method.
*/
public function test_create_attempt(): void {
global $CFG, $DB;
require_once($CFG->dirroot.'/lib/xapi/tests/helper.php');
[$cm, $student, $course] = $this->generate_testing_scenario();
$student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Save the current state for this activity for student1 and student2 (before creating the first attempt).
$manager = manager::create_from_coursemodule($cm);
$this->setUser($student2);
test_helper::create_state([
'activity' => item_activity::create_from_id($manager->get_context()->id),
'component' => 'mod_h5pactivity',
], true);
$this->setUser($student);
test_helper::create_state([
'activity' => item_activity::create_from_id($manager->get_context()->id),
'component' => 'mod_h5pactivity',
], true);
$this->assertEquals(2, $DB->count_records('xapi_states'));
// Create first attempt.
$attempt = attempt::new_attempt($student, $cm);
$this->assertEquals($student->id, $attempt->get_userid());
$this->assertEquals($cm->instance, $attempt->get_h5pactivityid());
$this->assertEquals(1, $attempt->get_attempt());
$this->assertEquals(1, $DB->count_records('xapi_states'));
$this->assertEquals(0, $DB->count_records('xapi_states', ['userid' => $student->id]));
// Create a second attempt.
$attempt = attempt::new_attempt($student, $cm);
$this->assertEquals($student->id, $attempt->get_userid());
$this->assertEquals($cm->instance, $attempt->get_h5pactivityid());
$this->assertEquals(2, $attempt->get_attempt());
}
/**
* Test for last_attempt method
*/
public function test_last_attempt(): void {
list($cm, $student) = $this->generate_testing_scenario();
// Create first attempt.
$attempt = attempt::last_attempt($student, $cm);
$this->assertEquals($student->id, $attempt->get_userid());
$this->assertEquals($cm->instance, $attempt->get_h5pactivityid());
$this->assertEquals(1, $attempt->get_attempt());
$lastid = $attempt->get_id();
// Get last attempt.
$attempt = attempt::last_attempt($student, $cm);
$this->assertEquals($student->id, $attempt->get_userid());
$this->assertEquals($cm->instance, $attempt->get_h5pactivityid());
$this->assertEquals(1, $attempt->get_attempt());
$this->assertEquals($lastid, $attempt->get_id());
// Now force a new attempt.
$attempt = attempt::new_attempt($student, $cm);
$this->assertEquals($student->id, $attempt->get_userid());
$this->assertEquals($cm->instance, $attempt->get_h5pactivityid());
$this->assertEquals(2, $attempt->get_attempt());
$lastid = $attempt->get_id();
// Get last attempt.
$attempt = attempt::last_attempt($student, $cm);
$this->assertEquals($student->id, $attempt->get_userid());
$this->assertEquals($cm->instance, $attempt->get_h5pactivityid());
$this->assertEquals(2, $attempt->get_attempt());
$this->assertEquals($lastid, $attempt->get_id());
}
/**
* Test saving statements.
*
* @dataProvider save_statement_data
* @param string $subcontent subcontent identifier
* @param bool $hasdefinition generate definition
* @param bool $hasresult generate result
* @param array $results 0 => insert ok, 1 => maxscore, 2 => rawscore, 3 => count
*/
public function test_save_statement(string $subcontent, bool $hasdefinition, bool $hasresult, array $results): void {
list($cm, $student) = $this->generate_testing_scenario();
$attempt = attempt::new_attempt($student, $cm);
$this->assertEquals(0, $attempt->get_maxscore());
$this->assertEquals(0, $attempt->get_rawscore());
$this->assertEquals(0, $attempt->count_results());
$this->assertEquals(0, $attempt->get_duration());
$this->assertNull($attempt->get_completion());
$this->assertNull($attempt->get_success());
$this->assertFalse($attempt->get_scoreupdated());
$statement = $this->generate_statement($hasdefinition, $hasresult);
$result = $attempt->save_statement($statement, $subcontent);
$this->assertEquals($results[0], $result);
$this->assertEquals($results[1], $attempt->get_maxscore());
$this->assertEquals($results[2], $attempt->get_rawscore());
$this->assertEquals($results[3], $attempt->count_results());
$this->assertEquals($results[4], $attempt->get_duration());
$this->assertEquals($results[5], $attempt->get_completion());
$this->assertEquals($results[6], $attempt->get_success());
if ($results[5]) {
$this->assertTrue($attempt->get_scoreupdated());
} else {
$this->assertFalse($attempt->get_scoreupdated());
}
}
/**
* Data provider for data request creation tests.
*
* @return array
*/
public function save_statement_data(): array {
return [
'Statement without definition and result' => [
'', false, false, [false, 0, 0, 0, 0, null, null]
],
'Statement with definition but no result' => [
'', true, false, [false, 0, 0, 0, 0, null, null]
],
'Statement with result but no definition' => [
'', true, false, [false, 0, 0, 0, 0, null, null]
],
'Statement subcontent without definition and result' => [
'111-222-333', false, false, [false, 0, 0, 0, 0, null, null]
],
'Statement subcontent with definition but no result' => [
'111-222-333', true, false, [false, 0, 0, 0, 0, null, null]
],
'Statement subcontent with result but no definition' => [
'111-222-333', true, false, [false, 0, 0, 0, 0, null, null]
],
'Statement with definition, result but no subcontent' => [
'', true, true, [true, 2, 2, 1, 25, 1, 1]
],
'Statement with definition, result and subcontent' => [
'111-222-333', true, true, [true, 0, 0, 1, 0, null, null]
],
];
}
/**
* Test delete results from attempt.
*/
public function test_delete_results(): void {
list($cm, $student) = $this->generate_testing_scenario();
$attempt = $this->generate_full_attempt($student, $cm);
$attempt->delete_results();
$this->assertEquals(0, $attempt->count_results());
}
/**
* Test delete attempt.
*/
public function test_delete_attempt(): void {
global $DB;
list($cm, $student) = $this->generate_testing_scenario();
// Check no previous attempts are created.
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(0, $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(0, $count);
// Generate one attempt.
$attempt1 = $this->generate_full_attempt($student, $cm);
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(1, $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(2, $count);
// Generate a second attempt.
$attempt2 = $this->generate_full_attempt($student, $cm);
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(2, $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(4, $count);
// Delete the first attempt.
attempt::delete_attempt($attempt1);
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(1, $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(2, $count);
$this->assertEquals(2, $attempt2->count_results());
}
/**
* Test delete all attempts.
*
* @dataProvider delete_all_attempts_data
* @param bool $hasstudent if user is specificed
* @param int[] 0-3 => statements count results, 4-5 => totals
*/
public function test_delete_all_attempts(bool $hasstudent, array $results): void {
global $DB;
list($cm, $student, $course) = $this->generate_testing_scenario();
// For this test we need extra activity and student.
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$cm2 = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST);
$student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Check no previous attempts are created.
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(0, $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(0, $count);
// Generate some attempts attempt on both activities and students.
$attempts = [];
$attempts[] = $this->generate_full_attempt($student, $cm);
$attempts[] = $this->generate_full_attempt($student2, $cm);
$attempts[] = $this->generate_full_attempt($student, $cm2);
$attempts[] = $this->generate_full_attempt($student2, $cm2);
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(4, $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(8, $count);
// Delete all specified attempts.
$user = ($hasstudent) ? $student : null;
attempt::delete_all_attempts($cm, $user);
// Check data.
for ($assert = 0; $assert < 4; $assert++) {
$count = $attempts[$assert]->count_results();
$this->assertEquals($results[$assert], $count);
}
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals($results[4], $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals($results[5], $count);
}
/**
* Data provider for data request creation tests.
*
* @return array
*/
public function delete_all_attempts_data(): array {
return [
'Delete all attempts from activity' => [
false, [0, 0, 2, 2, 2, 4]
],
'Delete all attempts from user' => [
true, [0, 2, 2, 2, 3, 6]
],
];
}
/**
* Test set_score method.
*
*/
public function test_set_score(): void {
global $DB;
list($cm, $student, $course) = $this->generate_testing_scenario();
// Generate one attempt.
$attempt = $this->generate_full_attempt($student, $cm);
$dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]);
$this->assertEquals($dbattempt->rawscore, $attempt->get_rawscore());
$this->assertEquals(2, $dbattempt->rawscore);
$this->assertEquals($dbattempt->maxscore, $attempt->get_maxscore());
$this->assertEquals(2, $dbattempt->maxscore);
$this->assertEquals(1, $dbattempt->scaled);
// Set attempt score.
$attempt->set_score(5, 10);
$this->assertEquals(5, $attempt->get_rawscore());
$this->assertEquals(10, $attempt->get_maxscore());
$this->assertTrue($attempt->get_scoreupdated());
// Save new score into DB.
$attempt->save();
$dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]);
$this->assertEquals($dbattempt->rawscore, $attempt->get_rawscore());
$this->assertEquals(5, $dbattempt->rawscore);
$this->assertEquals($dbattempt->maxscore, $attempt->get_maxscore());
$this->assertEquals(10, $dbattempt->maxscore);
$this->assertEquals(0.5, $dbattempt->scaled);
}
/**
* Test set_duration method.
*
* @dataProvider basic_setters_data
* @param string $attribute the stribute to test
* @param int $oldvalue attribute old value
* @param int $newvalue attribute new expected value
*/
public function test_basic_setters(string $attribute, int $oldvalue, int $newvalue): void {
global $DB;
list($cm, $student, $course) = $this->generate_testing_scenario();
// Generate one attempt.
$attempt = $this->generate_full_attempt($student, $cm);
$setmethod = 'set_'.$attribute;
$getmethod = 'get_'.$attribute;
$dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]);
$this->assertEquals($dbattempt->$attribute, $attempt->$getmethod());
$this->assertEquals($oldvalue, $dbattempt->$attribute);
// Set attempt attribute.
$attempt->$setmethod($newvalue);
$this->assertEquals($newvalue, $attempt->$getmethod());
// Save new score into DB.
$attempt->save();
$dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]);
$this->assertEquals($dbattempt->$attribute, $attempt->$getmethod());
$this->assertEquals($newvalue, $dbattempt->$attribute);
// Set null $attribute.
$attempt->$setmethod(null);
$this->assertNull($attempt->$getmethod());
// Save new score into DB.
$attempt->save();
$dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]);
$this->assertEquals($dbattempt->$attribute, $attempt->$getmethod());
$this->assertNull($dbattempt->$attribute);
}
/**
* Data provider for testing basic setters.
*
* @return array
*/
public function basic_setters_data(): array {
return [
'Set attempt duration' => [
'duration', 25, 35
],
'Set attempt completion' => [
'completion', 1, 0
],
'Set attempt success' => [
'success', 1, 0
],
];
}
/**
* Generate a fake attempt with two results.
*
* @param stdClass $student a user record
* @param stdClass $cm a course_module record
* @return attempt
*/
private function generate_full_attempt($student, $cm): attempt {
$attempt = attempt::new_attempt($student, $cm);
$this->assertEquals(0, $attempt->get_maxscore());
$this->assertEquals(0, $attempt->get_rawscore());
$this->assertEquals(0, $attempt->count_results());
$statement = $this->generate_statement(true, true);
$saveok = $attempt->save_statement($statement, '');
$this->assertTrue($saveok);
$saveok = $attempt->save_statement($statement, '111-222-333');
$this->assertTrue($saveok);
$this->assertEquals(2, $attempt->count_results());
return $attempt;
}
/**
* Return a xAPI partial statement with object defined.
* @param bool $hasdefinition if has to include definition
* @param bool $hasresult if has to include results
* @return statement
*/
private function generate_statement(bool $hasdefinition, bool $hasresult): statement {
global $USER;
$statement = new statement();
$statement->set_actor(item_agent::create_from_user($USER));
$statement->set_verb(item_verb::create_from_id('http://adlnet.gov/expapi/verbs/completed'));
$definition = null;
if ($hasdefinition) {
$definition = item_definition::create_from_data((object)[
'interactionType' => 'compound',
'correctResponsesPattern' => '1',
]);
}
$statement->set_object(item_activity::create_from_id('something', $definition));
if ($hasresult) {
$statement->set_result(item_result::create_from_data((object)[
'completion' => true,
'success' => true,
'score' => (object) ['min' => 0, 'max' => 2, 'raw' => 2, 'scaled' => 1],
'duration' => 'PT25S',
]));
}
return $statement;
}
}
+360
View File
@@ -0,0 +1,360 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* mod_h5pactivity grader tests
*
* @package mod_h5pactivity
* @category test
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\local;
use grade_item;
use stdClass;
/**
* Grader tests class for mod_h5pactivity.
*
* @package mod_h5pactivity
* @category test
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class grader_test extends \advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setupBeforeClass(): void {
global $CFG;
require_once($CFG->libdir.'/gradelib.php');
}
/**
* Test for grade item delete.
*/
public function test_grade_item_delete(): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
$grader = new grader($activity);
// Force a user grade.
$this->generate_fake_attempt($activity, $user, 5, 10);
$grader->update_grades($user->id);
$gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id);
$this->assertNotEquals(0, count($gradeinfo->items));
$this->assertArrayHasKey($user->id, $gradeinfo->items[0]->grades);
$grader->grade_item_delete();
$gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id);
$this->assertEquals(0, count($gradeinfo->items));
}
/**
* Test for grade item update.
*
* @dataProvider grade_item_update_data
* @param int $newgrade new activity grade
* @param bool $reset if has to reset grades
* @param string $idnumber the new idnumber
*/
public function test_grade_item_update(int $newgrade, bool $reset, string $idnumber): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Force a user initial grade.
$grader = new grader($activity);
$this->generate_fake_attempt($activity, $user, 5, 10);
$grader->update_grades($user->id);
$gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id);
$this->assertNotEquals(0, count($gradeinfo->items));
$item = array_shift($gradeinfo->items);
$this->assertArrayHasKey($user->id, $item->grades);
$this->assertEquals(50, round($item->grades[$user->id]->grade));
// Module grade value determine the way gradebook acts. That means that the expected
// result depends on this value.
// - Grade > 0: regular max grade value.
// - Grade = 0: no grading is used (but grademax remains the same).
// - Grade < 0: a scaleid is used (value = -scaleid).
if ($newgrade > 0) {
$grademax = $newgrade;
$scaleid = null;
$usergrade = ($newgrade > 50) ? 50 : $newgrade;
} else if ($newgrade == 0) {
$grademax = 100;
$scaleid = null;
$usergrade = null; // No user grades expected.
} else if ($newgrade < 0) {
$scale = $this->getDataGenerator()->create_scale(array("scale" => "value1, value2, value3"));
$newgrade = -1 * $scale->id;
$grademax = 3;
$scaleid = $scale->id;
$usergrade = 3; // 50 value will ve converted to "value 3" on scale.
}
// Update grade item.
$activity->grade = $newgrade;
// In case a reset is need, usergrade will be empty.
if ($reset) {
$param = 'reset';
$usergrade = null;
} else {
// Individual user gradings will be tested as a subcall of update_grades.
$param = null;
}
$grader = new grader($activity, $idnumber);
$grader->grade_item_update($param);
// Check new grade item and grades.
$gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id);
$item = array_shift($gradeinfo->items);
$this->assertEquals($scaleid, $item->scaleid);
$this->assertEquals($grademax, $item->grademax);
$this->assertArrayHasKey($user->id, $item->grades);
if ($usergrade) {
$this->assertEquals($usergrade, round($item->grades[$user->id]->grade));
} else {
$this->assertEmpty($item->grades[$user->id]->grade);
}
if (!empty($idnumber)) {
$gradeitem = grade_item::fetch(['idnumber' => $idnumber, 'courseid' => $course->id]);
$this->assertInstanceOf('grade_item', $gradeitem);
}
}
/**
* Data provider for test_grade_item_update.
*
* @return array
*/
public function grade_item_update_data(): array {
return [
'Change idnumber' => [
100, false, 'newidnumber'
],
'Increase max grade to 110' => [
110, false, ''
],
'Decrease max grade to 80' => [
40, false, ''
],
'Decrease max grade to 40 (less than actual grades)' => [
40, false, ''
],
'Reset grades' => [
100, true, ''
],
'Disable grades' => [
0, false, ''
],
'Use scales' => [
-1, false, ''
],
'Use scales with reset' => [
-1, true, ''
],
];
}
/**
* Test for grade update.
*
* @dataProvider update_grades_data
* @param int $newgrade the new activity grade
* @param bool $all if has to be applied to all students or just to one
* @param int $completion 1 all student have the activity completed, 0 one have incompleted
* @param array $results expected results (user1 grade, user2 grade)
*/
public function test_update_grades(int $newgrade, bool $all, int $completion, array $results): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$user1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
$user2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Force a user initial grade.
$grader = new grader($activity);
$this->generate_fake_attempt($activity, $user1, 5, 10);
$this->generate_fake_attempt($activity, $user2, 3, 12, $completion);
$grader->update_grades();
$gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, [$user1->id, $user2->id]);
$this->assertNotEquals(0, count($gradeinfo->items));
$item = array_shift($gradeinfo->items);
$this->assertArrayHasKey($user1->id, $item->grades);
$this->assertArrayHasKey($user2->id, $item->grades);
$this->assertEquals(50, $item->grades[$user1->id]->grade);
// Uncompleted attempts does not generate grades.
if ($completion) {
$this->assertEquals(25, $item->grades[$user2->id]->grade);
} else {
$this->assertNull($item->grades[$user2->id]->grade);
}
// Module grade value determine the way gradebook acts. That means that the expected
// result depends on this value.
// - Grade > 0: regular max grade value.
// - Grade <= 0: no grade calculation is used (scale and no grading).
if ($newgrade < 0) {
$scale = $this->getDataGenerator()->create_scale(array("scale" => "value1, value2, value3"));
$activity->grade = -1 * $scale->id;
} else {
$activity->grade = $newgrade;
}
$userid = ($all) ? 0 : $user1->id;
$grader = new grader($activity);
$grader->update_grades($userid);
// Check new grade item and grades.
$gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, [$user1->id, $user2->id]);
$item = array_shift($gradeinfo->items);
$this->assertArrayHasKey($user1->id, $item->grades);
$this->assertArrayHasKey($user2->id, $item->grades);
$this->assertEquals($results[0], $item->grades[$user1->id]->grade);
$this->assertEquals($results[1], $item->grades[$user2->id]->grade);
}
/**
* Data provider for test_grade_item_update.
*
* @return array
*/
public function update_grades_data(): array {
return [
// Quantitative grade, all attempts completed.
'Same grademax, all users, all completed' => [
100, true, 1, [50, 25]
],
'Same grademax, one user, all completed' => [
100, false, 1, [50, 25]
],
'Increade max, all users, all completed' => [
200, true, 1, [100, 50]
],
'Increade max, one user, all completed' => [
200, false, 1, [100, 25]
],
'Decrease max, all users, all completed' => [
50, true, 1, [25, 12.5]
],
'Decrease max, one user, all completed' => [
50, false, 1, [25, 25]
],
// Quantitative grade, some attempts not completed.
'Same grademax, all users, not completed' => [
100, true, 0, [50, null]
],
'Same grademax, one user, not completed' => [
100, false, 0, [50, null]
],
'Increade max, all users, not completed' => [
200, true, 0, [100, null]
],
'Increade max, one user, not completed' => [
200, false, 0, [100, null]
],
'Decrease max, all users, not completed' => [
50, true, 0, [25, null]
],
'Decrease max, one user, not completed' => [
50, false, 0, [25, null]
],
// No grade (no grade will be used).
'No grade, all users, all completed' => [
0, true, 1, [null, null]
],
'No grade, one user, all completed' => [
0, false, 1, [null, null]
],
'No grade, all users, not completed' => [
0, true, 0, [null, null]
],
'No grade, one user, not completed' => [
0, false, 0, [null, null]
],
// Scale (grate item will updated but without regrading).
'Scale, all users, all completed' => [
-1, true, 1, [3, 3]
],
'Scale, one user, all completed' => [
-1, false, 1, [3, 3]
],
'Scale, all users, not completed' => [
-1, true, 0, [3, null]
],
'Scale, one user, not completed' => [
-1, false, 0, [3, null]
],
];
}
/**
* Create a fake attempt for a specific user.
*
* @param stdClass $activity activity instance record.
* @param stdClass $user user record
* @param int $rawscore score obtained
* @param int $maxscore attempt max score
* @param int $completion 1 for activity completed, 0 for not completed yet
* @return stdClass the attempt record
*/
private function generate_fake_attempt(stdClass $activity, stdClass $user,
int $rawscore, int $maxscore, int $completion = 1): stdClass {
global $DB;
$attempt = (object)[
'h5pactivityid' => $activity->id,
'userid' => $user->id,
'timecreated' => 10,
'timemodified' => 20,
'attempt' => 1,
'rawscore' => $rawscore,
'maxscore' => $maxscore,
'duration' => 2,
'completion' => $completion,
'success' => 0,
];
$attempt->scaled = $attempt->rawscore / $attempt->maxscore;
$attempt->id = $DB->insert_record('h5pactivity_attempts', $attempt);
return $attempt;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,226 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_h5pactivity\output\result;
use core_xapi\local\statement\item_result;
use mod_h5pactivity\local\manager;
/**
* Result test class for H5P activity.
*
* @package mod_h5pactivity
* @covers \mod_h5pactivity\output\result
* @category test
* @copyright 2023 Laurent David <laurent.david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class result_test extends \advanced_testcase {
/**
* Data provider for result export_options test
*
* @return array[]
*/
public static function result_data_provider(): array {
return [
'fill-in with case sensitive' => [
'result' => [
'interactiontype' => 'fill-in',
'description' => '<p>Fill in the missing words</p>
<p>Meow .... this is a __________</p>
<p>Bark... this is a __________</p>',
'correctpattern' => '["{case_matters=true}cat[,]dog"]',
'response' => 'Cat[,]dog',
'additionals' => '{"extensions":{"http:\\/\\/h5p.org\\/x-api\\/h5p-local-content-id":31,'
. '"https:\\/\\/h5p.org\\/x-api\\/case-sensitivity":true,'
. '"https:\\/\\/h5p.org\\/x-api\\/alternatives":[["cat"],["dog"]]},"contextExtensions":{}}',
],
'useranswers' => [
'Gap #1 - cat' => (object) ['answer' => 'Cat', 'incorrect' => true],
'Gap #2 - dog' => (object) ['answer' => 'dog', 'correct' => true],
],
],
'fill-in with case insensitive' => [
'result' => [
'interactiontype' => 'fill-in',
'description' => '<p>Fill in the missing words</p>
<p>Meow .... this is a __________</p>
<p>Bark... this is a __________</p>',
'correctpattern' => '["{case_matters=false}cat[,]dog"]',
'response' => 'Cat[,]dog',
'additionals' => '{"extensions":{"http:\\/\\/h5p.org\\/x-api\\/h5p-local-content-id":31,'
. '"https:\\/\\/h5p.org\\/x-api\\/case-sensitivity":false,'
. '"https:\\/\\/h5p.org\\/x-api\\/alternatives":[["cat"],["dog"]]},"contextExtensions":{}}',
],
'useranswers' => [
'Gap #1 - cat' => (object) ['answer' => 'Cat', 'correct' => true],
'Gap #2 - dog' => (object) ['answer' => 'dog', 'correct' => true],
],
],
'drag and drop' => [
'result' => [
'interactiontype' => 'matching',
'description' => 'Drag and Drop Test',
'correctpattern' => '["0[.]0[,]0[.]2[,]1[.]1[,]1[.]0"]',
'response' => '0[.]0[,]1[.]1[,]0[.]2[,]0[.]3',
'additionals' => '{"source":[{"id":"0","description":{"en-US":"Answer 1 (DZ1 and DZ2)\n"}},'
. '{"id":"1","description":{"en-US":"Anwser 2 (DZ2)\n"}},'
. '{"id":"2","description":{"en-US":"Anwser 3 (DZ1)\n"}},'
. '{"id":"3","description":{"en-US":"Anwser 4 (neither)\n"}}],'
. '"target":[{"id":"0","description":{"en-US":"Dropzone 1\n"}},'
. '{"id":"1","description":{"en-US":"Dropzone 2\n"}}],'
. '"extensions":{"http:\/\/h5p.org\/x-api\/h5p-local-content-id":41,'
. '"http:\/\/h5p.org\/x-api\/h5p-subContentId":"59590246-f16e-4855-8dd6-c80e892ef96b"},'
. '"contextExtensions":{}}',
],
'useranswers' => [
'Answer 1 (DZ1 and DZ2) - Dropzone 1, Dropzone 2' => (object) ['answer' => 'Dropzone 1', 'correct' => true],
'Anwser 2 (DZ2) - Dropzone 2' => (object) ['answer' => 'Dropzone 2', 'correct' => true],
'Anwser 3 (DZ1) - Dropzone 1' => (object) ['answer' => 'Dropzone 1', 'correct' => true],
'Anwser 4 (neither) - ' => (object) ['answer' => 'Dropzone 1', 'incorrect' => true],
],
],
'drag and drop with no answer' => [
'result' => [
'interactiontype' => 'matching',
'description' => 'Drag and Drop Test',
'correctpattern' => '["0[.]0[,]0[.]2[,]1[.]1[,]1[.]0"]',
'response' => '1[.]0[,]1[.]1',
'additionals' => '{"source":[{"id":"0","description":{"en-US":"Answer 1 (DZ1 and DZ2)\n"}},'
. '{"id":"1","description":{"en-US":"Anwser 2 (DZ2)\n"}},'
. '{"id":"2","description":{"en-US":"Anwser 3 (DZ1)\n"}},'
. '{"id":"3","description":{"en-US":"Anwser 4 (neither)\n"}}],'
. '"target":[{"id":"0","description":{"en-US":"Dropzone 1\n"}},'
. '{"id":"1","description":{"en-US":"Dropzone 2\n"}}],'
. '"extensions":{"http:\/\/h5p.org\/x-api\/h5p-local-content-id":41,'
. '"http:\/\/h5p.org\/x-api\/h5p-subContentId":"59590246-f16e-4855-8dd6-c80e892ef96b"},'
. '"contextExtensions":{}}',
],
'useranswers' => [
'Answer 1 (DZ1 and DZ2) - Dropzone 1, Dropzone 2' => (object) ['answer' => 'Dropzone 2', 'correct' => true],
'Anwser 2 (DZ2) - Dropzone 2' => (object) ['answer' => 'Dropzone 2', 'correct' => true],
'Anwser 3 (DZ1) - Dropzone 1' => (object) ['answer' => 'None', 'incorrect' => true],
'Anwser 4 (neither) - ' => (object) ['answer' => 'None', 'correct' => true],
],
],
'sort the paragraph text' => [
'result' => [
'interactiontype' => 'sequencing',
'description' => 'SortParagraphs',
'correctpattern' => '["0[,]1[,]2[,]3[,]4"]',
'response' => '0[,]1[,]3[,]2[,]4',
'additionals' => '{"choices":[{"id":0,"description":{"en":"First I wake up at 7.30 am\\n",'
. '"en-US":"First I wake up at 7.30 am\\n"}}'
. ',{"id":1,"description":{"en":"Next I get dressed\\n","en-US":"Next I get dressed\\n"}},'
. '{"id":2,"description":{"en":"Afterward I have breakfast\\n","en-US":"Afterward I have breakfast\\n"}},'
. '{"id":3,"description":{"en":"I brush my teeth\\n","en-US":"I brush my teeth\\n"}},'
. '{"id":4,"description":{"en":"Finally I go school\\n","en-US":"Finally I go school\\n"}}],'
.
'"extensions":{"http:\\/\\/h5p.org\\/x-api\\/h5p-local-content-id":39,'
. '"https:\\/\\/h5p.org\\/x-api\\/duplicates-interchangeable":1},"contextExtensions":{}}',
],
'useranswers' => [
'#1 - First I wake up at 7.30 am' => (object) ['answer' => 'Correct answer', 'pass' => true],
'#2 - Next I get dressed' => (object) ['answer' => 'Correct answer', 'pass' => true],
'#3 - Afterward I have breakfast' => (object) ['answer' => 'Incorrect answer', 'fail' => true],
'#4 - I brush my teeth' => (object) ['answer' => 'Incorrect answer', 'fail' => true],
'#5 - Finally I go school' => (object) ['answer' => 'Correct answer', 'pass' => true],
],
],
'sequencing images' => [
'result' => [
'interactiontype' => 'sequencing',
'description' => 'Order the planets from smallest to largest',
'correctpattern' => '["item_3[,]item_0[,]item_7[,]item_1[,]item_5[,]item_2[,]item_4[,]item_6"]',
'response' => 'item_0[,]item_1[,]item_3[,]item_2[,]item_5[,]item_4[,]item_6[,]item_7',
'additionals' => '{"choices":[{"id":"item_3","description":{"en-US":"Mercury"}},'
. '{"id":"item_0","description":{"en-US":"Mars"}},{"id":"item_7","description":{"en-US":"Earth"}},'
. '{"id":"item_1","description":{"en-US":"Venus"}},{"id":"item_5","description":{"en-US":"Uranus"}},'
. '{"id":"item_2","description":{"en-US":"Neptune"}},{"id":"item_4","description":{"en-US":"Saturn"}},'
. '{"id":"item_6","description":{"en-US":"Jupiter"}}],"extensions":'
. '{"http:\/\/h5p.org\/x-api\/h5p-local-content-id":43},"contextExtensions":{}}',
],
'useranswers' => [
'#1 - Mercury' => (object) ['answer' => 'Correct answer', 'pass' => true],
'#2 - Mars' => (object) ['answer' => 'Correct answer', 'pass' => true],
'#3 - Earth' => (object) ['answer' => 'Incorrect answer', 'fail' => true],
'#4 - Venus' => (object) ['answer' => 'Incorrect answer', 'fail' => true],
'#5 - Uranus' => (object) ['answer' => 'Incorrect answer', 'fail' => true],
'#6 - Neptune' => (object) ['answer' => 'Incorrect answer', 'fail' => true],
'#7 - Saturn' => (object) ['answer' => 'Correct answer', 'pass' => true],
'#8 - Jupiter' => (object) ['answer' => 'Correct answer', 'pass' => true],
],
],
];
}
/**
* Test result export_options
*
* @param array $providedresultdata
* @param array $expecteduseranswers
* @return void
* @dataProvider result_data_provider
* @covers \mod_h5pactivity\output\result::export_options
*/
public function test_result_options(array $providedresultdata, array $expecteduseranswers): void {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity',
['course' => $course]);
$manager = manager::create_from_instance($activity);
$cm = $manager->get_coursemodule();
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
$params = ['cmid' => $cm->id, 'userid' => $student->id];
$attempt = $generator->create_content($activity, $params);
$resultdata = [
'attemptid' => $attempt->id,
'subcontent' => '',
'timecreated' => time(),
'completion' => true,
'success' => true,
'score' => (object) ['min' => 0, 'max' => 2, 'raw' => 2, 'scaled' => 1],
'duration' => 'PT25S',
];
$resultdata = array_merge($resultdata, $providedresultdata);
$result = item_result::create_from_data((object) $resultdata);
$classname = "mod_h5pactivity\\output\\result\\{$providedresultdata['interactiontype']}";
$classname = str_replace('-', '', $classname);
$reflectionoutput = new \ReflectionClass($classname);
$constructor = $reflectionoutput->getConstructor();
$resultoutput = $reflectionoutput->newInstanceWithoutConstructor();
$constructor->invoke($resultoutput, $result->get_data());
$exportoptions = $reflectionoutput->getMethod('export_options');
$data = $exportoptions->invoke($resultoutput);
$useranswersdata = array_map(function($item) {
return $item->useranswer ?? null;
}, $data);
$keys = array_map(function($item) {
return $item->description . ' - ' . ($item->correctanswer->answer ?? '');
}, $data);
$useranswersdata = array_combine($keys, $useranswersdata);
$this->assertEquals($expecteduseranswers, $useranswersdata);
}
}
@@ -0,0 +1,342 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* mod_h5pactivity privacy tests
*
* @package mod_h5pactivity
* @category test
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\privacy;
use \core_privacy\local\request\approved_contextlist;
use \core_privacy\local\request\approved_userlist;
use \core_privacy\local\request\writer;
use \core_privacy\tests\provider_testcase;
use core_xapi\local\statement\item_activity;
use core_xapi\test_helper;
use stdClass;
/**
* Privacy tests class for mod_h5pactivity.
*
* @package mod_h5pactivity
* @category test
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_h5pactivity\privacy\provider
*/
class provider_test extends provider_testcase {
/** @var stdClass User without any attempt. */
protected $student0;
/** @var stdClass User with some attempt. */
protected $student1;
/** @var stdClass User with some attempt. */
protected $student2;
/** @var stdClass User with some attempt. */
protected $student3;
/** @var \context context_module of the H5P activity. */
protected $context;
/**
* Test getting the context for the user ID related to this plugin.
*/
public function test_get_contexts_for_userid(): void {
$this->resetAfterTest(true);
$this->setAdminUser();
$this->h5pactivity_setup_test_scenario_data();
// The student0 hasn't any attempt.
$contextlist = provider::get_contexts_for_userid($this->student0->id);
$this->assertCount(0, (array) $contextlist->get_contextids());
// The student1 has data in the mod_h5pactivity context.
$contextlist = provider::get_contexts_for_userid($this->student1->id);
$this->assertCount(1, (array) $contextlist->get_contextids());
$this->assertContainsEquals($this->context->id, $contextlist->get_contextids());
}
/**
* Test getting the user IDs for the context related to this plugin.
*/
public function test_get_users_in_context(): void {
$this->resetAfterTest(true);
$this->setAdminUser();
$this->h5pactivity_setup_test_scenario_data();
$component = 'mod_h5pactivity';
$userlist = new \core_privacy\local\request\userlist($this->context, $component);
provider::get_users_in_context($userlist);
// Students 1 and 2 have attempts in the H5P context, student 0 does not.
$this->assertCount(2, $userlist);
$expected = [$this->student1->id, $this->student2->id];
$actual = $userlist->get_userids();
sort($expected);
sort($actual);
$this->assertEquals($expected, $actual);
}
/**
* Test that data is exported correctly for this plugin.
*/
public function test_export_user_data(): void {
$this->resetAfterTest(true);
$this->setAdminUser();
$this->h5pactivity_setup_test_scenario_data();
$component = 'mod_h5pactivity';
// Validate exported data for student0 (without any attempt).
$this->setUser($this->student0);
$writer = writer::with_context($this->context);
$this->export_context_data_for_user($this->student0->id, $this->context, $component);
$subcontextattempt1 = [
get_string('myattempts', 'mod_h5pactivity'),
get_string('attempt', 'mod_h5pactivity'). " 1"
];
$data = $writer->get_data($subcontextattempt1);
$this->assertEmpty($data);
// Validate exported data for student1.
writer::reset();
$this->setUser($this->student1);
$writer = writer::with_context($this->context);
$this->assertFalse($writer->has_any_data());
$this->export_context_data_for_user($this->student1->id, $this->context, $component);
$data = $writer->get_data([]);
$this->assertEquals('H5P 1', $data->name);
$data = (array)$writer->get_data($subcontextattempt1);
$this->assertCount(1, $data);
$this->assertCount(3, (array) reset($data));
$subcontextattempt2 = [
get_string('myattempts', 'mod_h5pactivity'),
get_string('attempt', 'mod_h5pactivity'). " 2"
];
$data = (array)$writer->get_data($subcontextattempt2);
$this->assertCount(3, (array) reset($data));
// The student1 has only 1 tracked attempts.
$subcontextattempt3 = [
get_string('myattempts', 'mod_h5pactivity'),
get_string('attempt', 'mod_h5pactivity'). " 3"
];
$data = (array)$writer->get_data($subcontextattempt3);
$this->assertEmpty($data);
}
/**
* Test for provider::delete_data_for_all_users_in_context().
*/
public function test_delete_data_for_all_users_in_context(): void {
global $DB;
$this->resetAfterTest(true);
$this->setAdminUser();
$this->h5pactivity_setup_test_scenario_data(true);
// Check data before deletion.
$this->assertEquals(6, $DB->count_records('h5pactivity_attempts'));
$this->assertEquals(18, $DB->count_records('h5pactivity_attempts_results'));
$this->assertEquals(2, $DB->count_records('xapi_states'));
// Delete data based on the context.
provider::delete_data_for_all_users_in_context($this->context);
// After deletion, the attempts entries should have been deleted.
$this->assertEquals(0, $DB->count_records('h5pactivity_attempts'));
// After deletion, the results entries should have been deleted.
$this->assertEquals(0, $DB->count_records('h5pactivity_attempts_results'));
// After deletion, the xapi states should have been deleted.
$this->assertEquals(0, $DB->count_records('xapi_states'));
}
/**
* Test for provider::delete_data_for_user().
*/
public function test_delete_data_for_user(): void {
global $DB;
$this->resetAfterTest(true);
$this->setAdminUser();
$this->h5pactivity_setup_test_scenario_data(true);
$params = ['userid' => $this->student1->id];
// Check data before deletion.
$this->assertEquals(6, $DB->count_records('h5pactivity_attempts'));
$this->assertEquals(18, $DB->count_records('h5pactivity_attempts_results'));
$this->assertEquals(2, $DB->count_records('xapi_states'));
// Save student1 attempts ids.
$attemptsids = $DB->get_records_menu('h5pactivity_attempts', $params, '', 'attempt, id');
list($resultselect, $attemptids) = $DB->get_in_or_equal($attemptsids);
$resultselect = 'id ' . $resultselect;
$approvedcontextlist = new approved_contextlist($this->student1, 'h5pactivity', [$this->context->id]);
provider::delete_data_for_user($approvedcontextlist);
// After deletion, the h5pactivity_attempts entries for the first student should have been deleted.
$this->assertEquals(0, $DB->count_records('h5pactivity_attempts', $params));
$this->assertEquals(4, $DB->count_records('h5pactivity_attempts'));
// After deletion, the results entries for the first student should have been deleted.
$count = $DB->count_records_select('h5pactivity_attempts_results', $resultselect, $attemptids);
$this->assertEquals(0, $count);
$this->assertEquals(12, $DB->count_records('h5pactivity_attempts_results'));
// After deletion, the results entries for the first student should have been deleted.
$this->assertEquals(0, $DB->count_records('xapi_states', $params));
$this->assertEquals(1, $DB->count_records('xapi_states'));
// Confirm that the h5pactivity hasn't been removed.
$h5pactivitycount = $DB->get_records('h5pactivity');
$this->assertCount(1, (array) $h5pactivitycount);
// Delete track for student0 (nothing has to be removed).
$approvedcontextlist = new approved_contextlist($this->student0, 'h5pactivity', [$this->context->id]);
provider::delete_data_for_user($approvedcontextlist);
$this->assertEquals(4, $DB->count_records('h5pactivity_attempts'));
$this->assertEquals(12, $DB->count_records('h5pactivity_attempts_results'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
}
/**
* Test for provider::delete_data_for_users().
*/
public function test_delete_data_for_users(): void {
global $DB;
$component = 'mod_h5pactivity';
$this->resetAfterTest(true);
$this->setAdminUser();
// In this scenario we need a 3rd user to test batch deletion.
// Create student2 with 2 attempts.
$this->h5pactivity_setup_test_scenario_data(true);
// Check data before deletion.
$this->assertEquals(6, $DB->count_records('h5pactivity_attempts'));
$this->assertEquals(18, $DB->count_records('h5pactivity_attempts_results'));
$this->assertEquals(2, $DB->count_records('xapi_states'));
// Save student1 and student2 attempts ids.
$params1 = ['userid' => $this->student1->id];
$attempts1ids = $DB->get_records_menu('h5pactivity_attempts', $params1, '', 'attempt, id');
$params2 = ['userid' => $this->student2->id];
$attempts2ids = $DB->get_records_menu('h5pactivity_attempts', $params2, '', 'attempt, id');
list($resultselect, $attemptids) = $DB->get_in_or_equal(array_merge($attempts1ids, $attempts2ids));
$resultselect = 'id ' . $resultselect;
// Delete student 1 ans 2 data, retain student 3 data.
$approveduserids = [$this->student1->id, $this->student2->id];
$approvedlist = new approved_userlist($this->context, $component, $approveduserids);
provider::delete_data_for_users($approvedlist);
// After deletion, the h5pactivity_attempts entries for student1 and student2 should have been deleted.
$this->assertEquals(0, $DB->count_records('h5pactivity_attempts', $params1));
$this->assertEquals(0, $DB->count_records('h5pactivity_attempts', $params2));
$this->assertEquals(0, $DB->count_records('xapi_states', $params1));
$this->assertEquals(0, $DB->count_records('xapi_states', $params2));
$this->assertEquals(2, $DB->count_records('h5pactivity_attempts'));
// After deletion, the results entries for the first and second student should have been deleted.
$count = $DB->count_records_select('h5pactivity_attempts_results', $resultselect, $attemptids);
$this->assertEquals(0, $count);
$this->assertEquals(6, $DB->count_records('h5pactivity_attempts_results'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
// Confirm that the h5pactivity hasn't been removed.
$h5pactivitycount = $DB->get_records('h5pactivity');
$this->assertCount(1, (array) $h5pactivitycount);
// Delete results track for student0 (nothing has to be removed).
$approveduserids = [$this->student0->id];
$approvedlist = new approved_userlist($this->context, $component, $approveduserids);
provider::delete_data_for_users($approvedlist);
$this->assertEquals(2, $DB->count_records('h5pactivity_attempts'));
$this->assertEquals(6, $DB->count_records('h5pactivity_attempts_results'));
$this->assertEquals(1, $DB->count_records('xapi_states'));
}
/**
* Helper function to setup 3 users and 2 H5P attempts for student1 and student2.
* $this->student0 is always created without any attempt.
*
* @param bool $extrauser generate a 3rd user (default false).
*/
protected function h5pactivity_setup_test_scenario_data(bool $extrauser = false): void {
global $CFG, $USER;
require_once($CFG->dirroot.'/lib/xapi/tests/helper.php');
$generator = $this->getDataGenerator();
$course = $this->getDataGenerator()->create_course();
$params = ['course' => $course];
$activity = $this->getDataGenerator()->create_module('h5pactivity', $params);
$cm = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST);
$this->context = \context_module::instance($activity->cmid);
/** @var \mod_h5pactivity_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
// Create student0 without any attempt.
$this->student0 = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Create student1 with 2 attempts and 1 xapi state.
$this->student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
$params = ['cmid' => $cm->id, 'userid' => $this->student1->id];
$generator->create_content($activity, $params);
$generator->create_content($activity, $params);
$currentuser = $USER;
$this->setUser($this->student1);
test_helper::create_state([
'activity' => item_activity::create_from_id($this->context->id),
'component' => 'mod_h5pactivity',
], true);
$this->setUser($currentuser);
// Create student2 with 2 attempts.
$this->student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
$params = ['cmid' => $cm->id, 'userid' => $this->student2->id];
$generator->create_content($activity, $params);
$generator->create_content($activity, $params);
if ($extrauser) {
$this->student3 = $this->getDataGenerator()->create_and_enrol($course, 'student');
$params = ['cmid' => $cm->id, 'userid' => $this->student3->id];
$generator->create_content($activity, $params);
$generator->create_content($activity, $params);
// Add 1 xapi state.
$currentuser = $USER;
$this->setUser($this->student3);
test_helper::create_state([
'activity' => item_activity::create_from_id($this->context->id),
'component' => 'mod_h5pactivity',
], true);
$this->setUser($currentuser);
}
}
}
+220
View File
@@ -0,0 +1,220 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* mod_h5pactivity generator tests
*
* @package mod_h5pactivity
* @category test
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity;
use advanced_testcase;
use backup;
use backup_controller;
use backup_setting;
use restore_controller;
use restore_dbops;
use stdClass;
/**
* Genarator tests class for mod_h5pactivity.
*
* @package mod_h5pactivity
* @category test
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class restore_test extends advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setupBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
}
/**
* Test on H5P activity backup and restore.
*
* @dataProvider backup_restore_data
* @param bool $content if has to create attempts
* @param bool $userdata if backup have userdata
* @param array $result1 data to check on original course
* @param array $result2 data to check on resotred course
*/
public function test_backup_restore(bool $content, bool $userdata, array $result1, array $result2): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Create one activity.
$this->assertFalse($DB->record_exists('h5pactivity', ['course' => $course->id]));
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$cm = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST);
if ($content) {
$generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity');
$params = ['cmid' => $cm->id, 'userid' => $user->id];
$generator->create_content($activity, $params);
}
$this->assertEquals($result1[0], $DB->count_records('h5pactivity', ['course' => $course->id]));
$this->assertEquals($result1[1], $DB->count_records('h5pactivity_attempts', ['h5pactivityid' => $activity->id]));
$attemptid = $DB->get_field('h5pactivity_attempts', 'id', ['h5pactivityid' => $activity->id]);
$this->assertEquals($result1[2], $DB->count_records('h5pactivity_attempts_results', ['attemptid' => $attemptid]));
// Execute course backup and restore.
$newcourseid = $this->backup_and_restore($course, $userdata);
// Check original activity.
$this->assertEquals($result1[0], $DB->count_records('h5pactivity', ['course' => $course->id]));
$this->assertEquals($result1[1], $DB->count_records('h5pactivity_attempts', ['h5pactivityid' => $activity->id]));
$attempt = $DB->get_record('h5pactivity_attempts', ['h5pactivityid' => $activity->id]);
$attemptid = $attempt->id ?? 0;
$this->assertEquals($result1[2], $DB->count_records('h5pactivity_attempts_results', ['attemptid' => $attemptid]));
// Check original activity.
$this->assertEquals($result2[0], $DB->count_records('h5pactivity', ['course' => $newcourseid]));
$activity2 = $DB->get_record('h5pactivity', ['course' => $newcourseid]);
$this->assertEquals($result2[1], $DB->count_records('h5pactivity_attempts', ['h5pactivityid' => $activity2->id]));
$attempt2 = $DB->get_record('h5pactivity_attempts', ['h5pactivityid' => $activity2->id]);
$attempt2id = $attempt2->id ?? 0;
$this->assertEquals($result2[2], $DB->count_records('h5pactivity_attempts_results', ['attemptid' => $attempt2id]));
// Compare activities.
$this->assertEquals($newcourseid, $activity2->course);
$this->assertEquals($activity->name, $activity2->name);
$this->assertEquals($activity->intro, $activity2->intro);
$this->assertEquals($activity->introformat, $activity2->introformat);
$this->assertEquals($activity->grade, $activity2->grade);
$this->assertEquals($activity->displayoptions, $activity2->displayoptions);
$this->assertEquals($activity->enabletracking, $activity2->enabletracking);
$this->assertEquals($activity->grademethod, $activity2->grademethod);
// Compare attempts.
if ($content && $userdata) {
$this->assertEquals($activity2->id, $attempt2->h5pactivityid);
$this->assertEquals($attempt->userid, $attempt2->userid);
$this->assertEquals($attempt->timecreated, $attempt2->timecreated);
$this->assertEquals($attempt->timemodified, $attempt2->timemodified);
$this->assertEquals($attempt->attempt, $attempt2->attempt);
$this->assertEquals($attempt->rawscore, $attempt2->rawscore);
$this->assertEquals($attempt->maxscore, $attempt2->maxscore);
$this->assertEquals($attempt->duration, $attempt2->duration);
$this->assertEquals($attempt->completion, $attempt2->completion);
$this->assertEquals($attempt->success, $attempt2->success);
// Compare results.
$results = $DB->get_records('h5pactivity_attempts_results', ['attemptid' => $attempt->id]);
foreach ($results as $result) {
$result2 = $DB->get_record('h5pactivity_attempts_results', [
'subcontent' => $result->subcontent, 'attemptid' => $attempt2->id
]);
$this->assertNotFalse($result2);
$this->assertEquals($result->timecreated, $result2->timecreated);
$this->assertEquals($result->interactiontype, $result2->interactiontype);
$this->assertEquals($result->description, $result2->description);
$this->assertEquals($result->correctpattern, $result2->correctpattern);
$this->assertEquals($result->response, $result2->response);
$this->assertEquals($result->additionals, $result2->additionals);
$this->assertEquals($result->rawscore, $result2->rawscore);
$this->assertEquals($result->maxscore, $result2->maxscore);
$this->assertEquals($result->duration, $result2->duration);
$this->assertEquals($result->completion, $result2->completion);
$this->assertEquals($result->success, $result2->success);
}
}
}
/**
* Data provider for test_backup_restore.
*
* @return array
*/
public function backup_restore_data(): array {
return [
'Activity attempts and restore with userdata' => [
true, true, [1, 1, 3], [1, 1, 3]
],
'No activity attempts and restore with userdata' => [
false, true, [1, 0, 0], [1, 0, 0]
],
'Activity attempts and restore with no userdata' => [
true, false, [1, 1, 3], [1, 0, 0]
],
'No activity attempts and restore with no userdata' => [
false, false, [1, 0, 0], [1, 0, 0]
],
];
}
/**
* Backs a course up and restores it.
*
* @param stdClass $srccourse Course object to backup
* @param bool $userdata if the backup must be with user data
* @return int ID of newly restored course
*/
private function backup_and_restore(stdClass $srccourse, bool $userdata): int {
global $USER, $CFG;
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
// Turn off file logging, otherwise it can't delete the file (Windows).
$CFG->backup_file_logger_level = backup::LOG_NONE;
// Do backup with default settings. MODE_IMPORT means it will just
// create the directory and not zip it.
$bc = new backup_controller(backup::TYPE_1COURSE, $srccourse->id,
backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
$USER->id);
$bc->get_plan()->get_setting('users')->set_status(backup_setting::NOT_LOCKED);
$bc->get_plan()->get_setting('users')->set_value($userdata);
$backupid = $bc->get_backupid();
$bc->execute_plan();
$bc->destroy();
// Do restore to new course with default settings.
$newcourseid = restore_dbops::create_new_course(
$srccourse->fullname, $srccourse->shortname . '_2', $srccourse->category
);
$rc = new restore_controller($backupid, $newcourseid,
backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
backup::TARGET_NEW_COURSE);
$rc->get_plan()->get_setting('users')->set_status(backup_setting::NOT_LOCKED);
$rc->get_plan()->get_setting('users')->set_value($userdata);
$this->assertTrue($rc->execute_precheck());
$rc->execute_plan();
$rc->destroy();
return $newcourseid;
}
}
+474
View File
@@ -0,0 +1,474 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_h5pactivity\xapi;
use \core_xapi\local\statement;
use \core_xapi\local\statement\item_agent;
use \core_xapi\local\statement\item_activity;
use \core_xapi\local\statement\item_definition;
use \core_xapi\local\statement\item_verb;
use \core_xapi\local\statement\item_result;
use context_module;
use core_xapi\test_helper;
use stdClass;
/**
* Attempt tests class for mod_h5pactivity.
*
* @package mod_h5pactivity
* @category test
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \mod_h5pactivity\xapi\handler
*/
final class handler_test extends \advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot.'/lib/xapi/tests/helper.php');
}
/**
* Generate a valid scenario for each tests.
*
* @return stdClass an object with all scenario data in it
*/
private function generate_testing_scenario(): stdClass {
$this->resetAfterTest();
$this->setAdminUser();
$data = new stdClass();
$data->course = $this->getDataGenerator()->create_course();
// Generate 2 users, one enroled into course and one not.
$data->student = $this->getDataGenerator()->create_and_enrol($data->course, 'student');
$data->otheruser = $this->getDataGenerator()->create_user();
// H5P activity.
$data->activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $data->course]);
$data->context = context_module::instance($data->activity->cmid);
$data->xapihandler = handler::create('mod_h5pactivity');
$this->assertNotEmpty($data->xapihandler);
$this->assertInstanceOf('\mod_h5pactivity\xapi\handler', $data->xapihandler);
$this->setUser($data->student);
return $data;
}
/**
* Test for xapi_handler with valid statements.
*/
public function test_xapi_handler(): void {
global $DB;
$data = $this->generate_testing_scenario();
$xapihandler = $data->xapihandler;
$context = $data->context;
$student = $data->student;
$otheruser = $data->otheruser;
// Check we have 0 entries in the attempts tables.
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(0, $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(0, $count);
$statements = $this->generate_statements($context, $student);
// Insert first statement.
$event = $xapihandler->statement_to_event($statements[0]);
$this->assertNotNull($event);
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(1, $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(1, $count);
// Insert second statement.
$event = $xapihandler->statement_to_event($statements[1]);
$this->assertNotNull($event);
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(1, $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(2, $count);
// Insert again first statement.
$event = $xapihandler->statement_to_event($statements[0]);
$this->assertNotNull($event);
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(2, $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(3, $count);
// Insert again second statement.
$event = $xapihandler->statement_to_event($statements[1]);
$this->assertNotNull($event);
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(2, $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(4, $count);
}
/**
* Testing wrong statements scenarios.
*
* @dataProvider xapi_handler_errors_data
* @param bool $hasverb valid verb
* @param bool $hasdefinition generate definition
* @param bool $hasresult generate result
* @param bool $hascontext valid context
* @param bool $hasuser valid user
* @param bool $generateattempt if generates an empty attempt
*/
public function test_xapi_handler_errors(bool $hasverb, bool $hasdefinition, bool $hasresult,
bool $hascontext, bool $hasuser, bool $generateattempt): void {
global $DB, $CFG;
$data = $this->generate_testing_scenario();
$xapihandler = $data->xapihandler;
$context = $data->context;
$student = $data->student;
$otheruser = $data->otheruser;
// Check we have 0 entries in the attempts tables.
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(0, $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(0, $count);
$statement = new statement();
if ($hasverb) {
$statement->set_verb(item_verb::create_from_id('http://adlnet.gov/expapi/verbs/completed'));
} else {
$statement->set_verb(item_verb::create_from_id('cook'));
}
$definition = null;
if ($hasdefinition) {
$definition = item_definition::create_from_data((object)[
'interactionType' => 'compound',
'correctResponsesPattern' => '1',
]);
}
if ($hascontext) {
$statement->set_object(item_activity::create_from_id($context->id, $definition));
} else {
$statement->set_object(item_activity::create_from_id('paella', $definition));
}
if ($hasresult) {
$statement->set_result(item_result::create_from_data((object)[
'completion' => true,
'success' => true,
'score' => (object) ['min' => 0, 'max' => 2, 'raw' => 2, 'scaled' => 1],
]));
}
if ($hasuser) {
$statement->set_actor(item_agent::create_from_user($student));
} else {
$statement->set_actor(item_agent::create_from_user($otheruser));
}
$event = $xapihandler->statement_to_event($statement);
$this->assertNull($event);
// No enties should be generated.
$count = $DB->count_records('h5pactivity_attempts');
$attempts = ($generateattempt) ? 1 : 0;
$this->assertEquals($attempts, $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(0, $count);
}
/**
* Data provider for data request creation tests.
*
* @return array
*/
public static function xapi_handler_errors_data(): array {
return [
// Invalid Definitions and results possibilities.
'Invalid definition and result' => [
true, false, false, true, true, false
],
'Invalid result' => [
true, true, false, true, true, false
],
'Invalid definition (generate empty attempt)' => [
true, false, true, true, true, true
],
// Invalid verb possibilities.
'Invalid verb, definition and result' => [
false, false, false, true, true, false
],
'Invalid verb and result' => [
false, true, false, true, true, false
],
'Invalid verb and definition' => [
false, false, true, true, true, false
],
// Invalid context possibilities.
'Invalid definition, result and context' => [
true, false, false, false, true, false
],
'Invalid result and context' => [
true, true, false, false, true, false
],
'Invalid definition and context' => [
true, false, true, false, true, false
],
'Invalid verb, definition result and context' => [
false, false, false, false, true, false
],
'Invalid verb, result and context' => [
false, true, false, false, true, false
],
'Invalid verb, definition and context' => [
false, false, true, false, true, false
],
// Invalid user possibilities.
'Invalid definition, result and user' => [
true, false, false, true, false, false
],
'Invalid result and user' => [
true, true, false, true, false, false
],
'Invalid definition and user' => [
true, false, true, true, false, false
],
'Invalid verb, definition, result and user' => [
false, false, false, true, false, false
],
'Invalid verb, result and user' => [
false, true, false, true, false, false
],
'Invalid verb, definition and user' => [
false, false, true, true, false, false
],
'Invalid definition, result, context and user' => [
true, false, false, false, false, false
],
'Invalid result, context and user' => [
true, true, false, false, false, false
],
'Invalid definition, context and user' => [
true, false, true, false, false, false
],
'Invalid verb, definition, result, context and user' => [
false, false, false, false, false, false
],
'Invalid verb, result, context and user' => [
false, true, false, false, false, false
],
'Invalid verb, definition, context and user' => [
false, false, true, false, false, false
],
];
}
/**
* Test xapi_handler stored statements.
*/
public function test_stored_statements(): void {
global $DB;
$data = $this->generate_testing_scenario();
$xapihandler = $data->xapihandler;
$context = $data->context;
$student = $data->student;
$otheruser = $data->otheruser;
$activity = $data->activity;
// Check we have 0 entries in the attempts tables.
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(0, $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(0, $count);
$statements = $this->generate_statements($context, $student);
// Insert statements.
$stored = $xapihandler->process_statements($statements);
$this->assertCount(2, $stored);
$this->assertEquals(true, $stored[0]);
$this->assertEquals(true, $stored[1]);
$count = $DB->count_records('h5pactivity_attempts');
$this->assertEquals(1, $count);
$count = $DB->count_records('h5pactivity_attempts_results');
$this->assertEquals(2, $count);
// Validate stored data.
$attempts = $DB->get_records('h5pactivity_attempts');
$attempt = array_shift($attempts);
$statement = $statements[0];
$data = $statement->get_result()->get_data();
$this->assertEquals(1, $attempt->attempt);
$this->assertEquals($student->id, $attempt->userid);
$this->assertEquals($activity->id, $attempt->h5pactivityid);
$this->assertEquals($data->score->raw, $attempt->rawscore);
$this->assertEquals($data->score->max, $attempt->maxscore);
$this->assertEquals($statement->get_result()->get_duration(), $attempt->duration);
$this->assertEquals($data->completion, $attempt->completion);
$this->assertEquals($data->success, $attempt->success);
$results = $DB->get_records('h5pactivity_attempts_results');
foreach ($results as $result) {
$statement = (empty($result->subcontent)) ? $statements[0] : $statements[1];
$xapiresult = $statement->get_result()->get_data();
$xapiobject = $statement->get_object()->get_data();
$this->assertEquals($attempt->id, $result->attemptid);
$this->assertEquals($xapiobject->definition->interactionType, $result->interactiontype);
$this->assertEquals($xapiresult->score->raw, $result->rawscore);
$this->assertEquals($xapiresult->score->max, $result->maxscore);
$this->assertEquals($statement->get_result()->get_duration(), $result->duration);
$this->assertEquals($xapiresult->completion, $result->completion);
$this->assertEquals($xapiresult->success, $result->success);
}
}
/**
* Returns a basic xAPI statements simulating a H5P content.
*
* @param context_module $context activity context
* @param stdClass $user user record
* @return statement[] array of xAPI statements
*/
private function generate_statements(context_module $context, stdClass $user): array {
$statements = [];
$statement = new statement();
$statement->set_actor(item_agent::create_from_user($user));
$statement->set_verb(item_verb::create_from_id('http://adlnet.gov/expapi/verbs/completed'));
$definition = item_definition::create_from_data((object)[
'interactionType' => 'compound',
'correctResponsesPattern' => '1',
]);
$statement->set_object(item_activity::create_from_id($context->id, $definition));
$statement->set_result(item_result::create_from_data((object)[
'completion' => true,
'success' => true,
'score' => (object) ['min' => 0, 'max' => 2, 'raw' => 2, 'scaled' => 1],
'duration' => 'PT25S',
]));
$statements[] = $statement;
$statement = new statement();
$statement->set_actor(item_agent::create_from_user($user));
$statement->set_verb(item_verb::create_from_id('http://adlnet.gov/expapi/verbs/completed'));
$definition = item_definition::create_from_data((object)[
'interactionType' => 'matching',
'correctResponsesPattern' => '1',
]);
$statement->set_object(item_activity::create_from_id($context->id.'?subContentId=111-222-333', $definition));
$statement->set_result(item_result::create_from_data((object)[
'completion' => true,
'success' => true,
'score' => (object) ['min' => 0, 'max' => 1, 'raw' => 0, 'scaled' => 0],
'duration' => 'PT20S',
]));
$statements[] = $statement;
return $statements;
}
/**
* Test validate_state method.
*/
public function test_validate_state(): void {
global $DB;
$this->resetAfterTest();
/** @var \core_h5p_generator $generator */
$generator = $this->getDataGenerator()->get_plugin_generator('core_h5p');
// Create a valid H5P activity with a valid xAPI state.
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
$this->setUser($user);
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$coursecontext = \context_course::instance($course->id);
$activitycontext = \context_module::instance($activity->cmid);
$component = 'mod_h5pactivity';
$filerecord = [
'contextid' => $activitycontext->id,
'component' => $component,
'filearea' => 'package',
'itemid' => 0,
'filepath' => '/',
'filename' => 'dummy.h5p',
'addxapistate' => true,
];
$generator->generate_h5p_data(false, $filerecord);
$handler = handler::create($component);
// Change the method visibility for validate_state in order to test it.
$method = new \ReflectionMethod(handler::class, 'validate_state');
// The activity id should be numeric.
$state = test_helper::create_state(['activity' => item_activity::create_from_id('AA')]);
$result = $method->invoke($handler, $state);
$this->assertFalse($result);
// The activity id should exist.
$state = test_helper::create_state();
$result = $method->invoke($handler, $state);
$this->assertFalse($result);
// The given activity should be H5P activity.
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course]);
$state = test_helper::create_state([
'activity' => item_activity::create_from_id($forum->cmid),
]);
$result = $method->invoke($handler, $state);
$this->assertFalse($result);
// Tracking should be enabled for the H5P activity.
$state = test_helper::create_state([
'activity' => item_activity::create_from_id($activitycontext->id),
'component' => $component,
]);
$result = $method->invoke($handler, $state);
$this->assertTrue($result);
// So, when tracking is disabled, the state won't be considered valid.
$activity2 = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course, 'enabletracking' => 0]);
$activitycontext2 = \context_module::instance($activity2->cmid);
$state = test_helper::create_state([
'activity' => item_activity::create_from_id($activitycontext2->id),
'component' => $component,
]);
$result = $method->invoke($handler, $state);
$this->assertFalse($result);
// The user should have permission to submit.
$studentrole = $DB->get_record('role', array('shortname' => 'student'));
assign_capability('mod/h5pactivity:submit', CAP_PROHIBIT, $studentrole->id, $coursecontext->id);
// Empty all the caches that may be affected by this change.
accesslib_clear_all_caches_for_unit_testing();
\course_modinfo::clear_instance_cache();
$state = test_helper::create_state([
'activity' => item_activity::create_from_id($activitycontext->id),
'component' => $component,
]);
$result = $method->invoke($handler, $state);
$this->assertFalse($result);
}
}