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,63 @@
<?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_scorm\backup;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . "/phpunit/classes/restore_date_testcase.php");
/**
* Restore date tests.
*
* @package mod_scorm
* @copyright 2017 onwards Ankit Agarwal <ankit.agrr@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class restore_date_test extends \restore_date_testcase {
public function test_restore_dates(): void {
global $DB, $USER;
$time = 10000;
list($course, $scorm) = $this->create_course_and_module('scorm', ['timeopen' => $time, 'timeclose' => $time]);
$scoes = scorm_get_scoes($scorm->id);
$sco = array_shift($scoes);
scorm_insert_track($USER->id, $scorm->id, $sco->id, 4, 'cmi.core.score.raw', 10);
// We do not want second differences to fail our test because of execution delays.
$DB->set_field('scorm_scoes_value', 'timemodified', $time);
// Do backup and restore.
$newcourseid = $this->backup_and_restore($course);
$newscorm = $DB->get_record('scorm', ['course' => $newcourseid]);
$this->assertFieldsNotRolledForward($scorm, $newscorm, ['timemodified']);
$props = ['timeopen', 'timeclose'];
$this->assertFieldsRolledForward($scorm, $newscorm, $props);
$sql = "SELECT *
FROM {scorm_scoes_value} v
JOIN {scorm_attempt} a ON a.id = v.attemptid
WHERE a.scormid = ?";
$tracks = $DB->get_records_sql($sql, [$newscorm->id]);
foreach ($tracks as $track) {
$this->assertEquals($time, $track->timemodified);
}
}
}
+37
View File
@@ -0,0 +1,37 @@
@mod @mod_scorm @_file_upload @_switch_iframe
Feature: Add scorm activity
In order to let students access a scorm package
As a teacher
I need to add scorm activity to a course
@javascript
Scenario: Add a scorm activity to a course
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 |
When I log in as "teacher1"
And I add a scorm activity to course "Course 1" section "1"
And I set the following fields to these values:
| Name | Awesome SCORM package |
| Description | Description |
And I upload "mod/scorm/tests/packages/singlesco_scorm12.zip" file to "Package file" filemanager
And I click on "Save and display" "button"
Then I should see "Awesome SCORM package"
And I should see "Enter"
And I should see "Preview"
And I log out
And I am on the "Awesome SCORM package" "scorm activity" page logged in as student1
And I should see "Enter"
And I press "Enter"
And I switch to "scorm_object" iframe
And I should see "Not implemented yet"
And I switch to the main frame
And I am on "Course 1" course homepage
@@ -0,0 +1,161 @@
@mod @mod_scorm @_file_upload @_switch_iframe
Feature: Scorm multi-sco completion
In order to let students access a scorm package
As a teacher
I need to add scorm activity to a course
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 "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| enablecompletion | 1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
@javascript
Scenario: Test completion with a single sco completion.
Given the following "activity" exists:
| activity | scorm |
| course | C1 |
| name | Basic Multi-sco SCORM package |
| completion | 2 |
# Add requirements
| completionstatusallscos | 0 |
| packagefilepath | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12.zip |
| completionstatusrequired | 4 |
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page logged in as student1
And I should see "Enter"
And I press "Enter"
And I switch to "scorm_object" iframe
And I should see "Play of the game"
And I switch to the main frame
And I follow "Exit activity"
And I wait until the page is ready
Then I should see "Basic Multi-sco SCORM package" in the "page" "region"
And I am on homepage
And I log out
And I am on the "Course 1" course page logged in as teacher1
Then "Student 1" user has completed "Basic Multi-sco SCORM package" activity
@javascript
Scenario: Test completion with all scos and correct sco load on re-entry.
Given the following "activity" exists:
| activity | scorm |
| course | C1 |
| name | ADV Multi-sco SCORM package |
| completion | 2 |
# Add requirements
| packagefilepath | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12.zip |
| completionstatusallscos | 1 |
And I am on the "ADV Multi-sco SCORM package" "scorm activity" page logged in as student1
And I should see "Enter"
And I press "Enter"
And I switch to "scorm_object" iframe
And I should see "Play of the game"
And I switch to the main frame
And I follow "Exit activity"
And I wait until the page is ready
Then I should see "ADV Multi-sco SCORM package" in the "page" "region"
And I am on homepage
And I log out
And I am on the "Course 1" course page logged in as teacher1
Then "Student 1" user has not completed "ADV Multi-sco SCORM package" activity
And I log out
And I am on the "ADV Multi-sco SCORM package" "scorm activity" page logged in as student1
And I should see "Enter"
And I press "Enter"
And I switch to "scorm_object" iframe
And I should see "Par"
And I switch to the main frame
And I click on "Keeping Score" "list_item"
And I switch to "scorm_object" iframe
And I should see "Scoring"
And I switch to the main frame
And I click on "Other Scoring Systems" "list_item"
And I switch to "scorm_object" iframe
And I should see "Other Scoring Systems"
And I switch to the main frame
And I click on "The Rules of Golf" "list_item"
And I switch to "scorm_object" iframe
And I should see "The Rules of Golf"
And I switch to the main frame
And I click on "Playing Golf Quiz" "list_item"
And I switch to "scorm_object" iframe
And I should see "Knowledge Check"
And I switch to the main frame
And I click on "Taking Care of the Course" "list_item"
And I switch to "scorm_object" iframe
And I should see "Etiquette - Care For the Course"
And I switch to the main frame
And I click on "Avoiding Distraction" "list_item"
And I switch to "scorm_object" iframe
And I should see "Etiquette - Avoiding Distraction"
And I switch to the main frame
And I click on "Playing Politely" "list_item"
And I switch to "scorm_object" iframe
And I should see "Etiquette - Playing the Game"
And I switch to the main frame
And I click on "Etiquette Quiz" "list_item"
And I switch to "scorm_object" iframe
And I should see "Knowledge Check"
And I switch to the main frame
And I click on "Handicapping Overview" "list_item"
And I switch to "scorm_object" iframe
And I should see "Handicapping"
And I switch to the main frame
And I click on "Calculating a Handicap" "list_item"
And I switch to "scorm_object" iframe
And I should see "Calculating a Handicap"
And I switch to the main frame
And I click on "Calculating a Handicapped Score" "list_item"
And I switch to "scorm_object" iframe
And I should see "Calculating a Score"
And I switch to the main frame
And I click on "Handicapping Example" "list_item"
And I switch to "scorm_object" iframe
And I should see "Calculating a Score"
And I switch to the main frame
And I click on "Handicapping Quiz" "list_item"
And I switch to "scorm_object" iframe
And I should see "Knowledge Check"
And I switch to the main frame
And I click on "How to Have Fun Playing Golf" "list_item"
And I switch to "scorm_object" iframe
And I should see "How to Have Fun Golfing"
And I switch to the main frame
And I click on "How to Make Friends Playing Golf" "list_item"
And I switch to "scorm_object" iframe
And I should see "How to Make Friends on the Golf Course"
And I switch to the main frame
And I click on "Having Fun Quiz" "list_item"
And I switch to "scorm_object" iframe
And I should see "Knowledge Check"
And I switch to the main frame
And I follow "Exit activity"
And I wait until the page is ready
Then I should see "ADV Multi-sco SCORM package" in the "page" "region"
And I log out
And I am on the "Course 1" course page logged in as teacher1
And "Student 1" user has completed "ADV Multi-sco SCORM package" activity
@@ -0,0 +1,115 @@
@mod @mod_scorm @core_completion
Feature: Scorm activity default completion
In order to make easier for teachers to set completion conditions
As a teacher or admin
I need to be able to set default condition completion
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| enablecompletion | 1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
@javascript
Scenario: Completion conditions when there is no default set at site or course level.
Given I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I press "Add an activity or resource"
When I click on "Add a new SCORM" "link" in the "Add an activity or resource" "dialogue"
And I expand all fieldsets
Then the field "None" matches value "1"
And the field "Add requirements" matches value "0"
And I set the field "Add requirements" to "1"
And the field "completionstatusrequired[4]" matches value "1"
And the field "completionstatusrequired[2]" matches value "0"
@javascript
Scenario: Completion conditions when default completion is set at site level but not at course level.
Given I log in as "admin"
And I navigate to "Courses > Default settings > Default activity completion" in site administration
And I click on "Expand SCORM" "button"
And I set the following fields to these values:
| id_completion_scorm_2 | 1 |
| completionview_scorm | 1 |
| completionstatusrequired_scorm[4] | 0 |
| completionstatusrequired_scorm[2] | 0 |
And I click on "Save changes" "button" in the "[data-region='activitycompletion-scorm']" "css_element"
And I am on "Course 1" course homepage with editing mode on
And I press "Add an activity or resource"
When I click on "Add a new SCORM" "link" in the "Add an activity or resource" "dialogue"
And I expand all fieldsets
Then the field "None" matches value "0"
And the field "Add requirements" matches value "1"
And the field "completionview" matches value "1"
And the field "completionstatusrequired[4]" matches value "0"
And the field "completionstatusrequired[2]" matches value "0"
@javascript
Scenario: Completion conditions when default completion is set at course level but not at site level.
Given I am on the "Course 1" course page logged in as teacher1
And I navigate to "Course completion" in current page administration
And I set the field "Course completion tertiary navigation" to "Default activity completion"
And I click on "Expand SCORM" "button"
And I set the following fields to these values:
| id_completion_scorm_2 | 1 |
| completionview_scorm | 1 |
| completionstatusrequired_scorm[4] | 0 |
| completionstatusrequired_scorm[2] | 0 |
And I click on "Save changes" "button" in the "[data-region='activitycompletion-scorm']" "css_element"
And I am on "Course 1" course homepage with editing mode on
And I press "Add an activity or resource"
When I click on "Add a new SCORM" "link" in the "Add an activity or resource" "dialogue"
And I expand all fieldsets
Then the field "None" matches value "0"
And the field "Add requirements" matches value "1"
And the field "completionview" matches value "1"
And the field "completionstatusrequired[4]" matches value "0"
And the field "completionstatusrequired[2]" matches value "0"
@javascript
Scenario: Completion conditions when default completion is set at site and course level.
Given I log in as "admin"
And I navigate to "Courses > Default settings > Default activity completion" in site administration
And I click on "Expand SCORM" "button"
And I set the following fields to these values:
| id_completion_scorm_2 | 1 |
| completionview_scorm | 1 |
| completionstatusrequired_scorm[4] | 0 |
| completionstatusrequired_scorm[2] | 0 |
And I click on "Save changes" "button" in the "[data-region='activitycompletion-scorm']" "css_element"
And the following "core_completion > Course defaults" exist:
| course | module | completion |
| C1 | scorm | 1 |
And I am on "Course 1" course homepage with editing mode on
And I press "Add an activity or resource"
When I click on "Add a new SCORM" "link" in the "Add an activity or resource" "dialogue"
And I expand all fieldsets
Then the field "None" matches value "0"
And the field "Students must manually mark the activity as done" matches value "1"
And the field "Add requirements" matches value "0"
@javascript
Scenario: Completion conditions when 'Passed' is marked as default but 'Completed' is unmarked.
Given I log in as "admin"
And I navigate to "Courses > Default settings > Default activity completion" in site administration
And I click on "Expand SCORM" "button"
And I set the following fields to these values:
| id_completion_scorm_2 | 1 |
| completionview_scorm | 1 |
| completionstatusrequired_scorm[4] | 0 |
| completionstatusrequired_scorm[2] | 1 |
And I click on "Save changes" "button" in the "[data-region='activitycompletion-scorm']" "css_element"
And I am on "Course 1" course homepage with editing mode on
And I press "Add an activity or resource"
When I click on "Add a new SCORM" "link" in the "Add an activity or resource" "dialogue"
And I expand all fieldsets
Then the field "None" matches value "0"
And the field "Add requirements" matches value "1"
And the field "completionview" matches value "1"
And the field "completionstatusrequired[4]" matches value "0"
And the field "completionstatusrequired[2]" matches value "1"
+66
View File
@@ -0,0 +1,66 @@
@mod @mod_scorm
Feature: Viewing scorm reports in separate and visible groups mode
In order to view reports for large courses
As a teacher
I need to filter the users on the reports page by group
Background:
And the following "courses" exist:
| fullname | shortname |
| Test Course 1 | C1 |
And the following "groups" exist:
| name | course | idnumber | participation |
| Group 1 | C1 | G1 | 1 |
| Group 2 | C1 | G2 | 1 |
| Group 3 | C1 | G3 | 0 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | TeacherG1 | 1 | teacher1@example.com |
| noneditor1 | NoneditorG1 | 1 | noneditor1@example.com |
| noneditor2 | NoneditorNone | 2 | noneditor2@example.com |
| user1 | User1G1 | 1 | user1@example.com |
| user2 | User2G2 | 2 | user2@example.com |
| user3 | User3None | 3 | user3@example.com |
| user4 | User4NPgroup | 4 | user4@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| noneditor1 | C1 | teacher |
| noneditor2 | C1 | teacher |
| user1 | C1 | student |
| user2 | C1 | student |
| user3 | C1 | student |
| user4 | C1 | student |
And the following "group members" exist:
| user | group |
| teacher1 | G1 |
| noneditor1 | G1 |
| user1 | G1 |
| user2 | G2 |
| user4 | G3 |
And the following "activities" exist:
| activity | course | name | packagefilepath | forcenewattempt | idnumber | groupmode |
| scorm | C1 | Separate scorm | mod/scorm/tests/packages/singlescobasic.zip | 0 | scorm1 | 1 |
| scorm | C1 | Visible scorm | mod/scorm/tests/packages/singlescobasic.zip | 0 | scorm2 | 2 |
Scenario Outline: Editing teachers should see all groups on the Reports page. Non-editing teachers should see just their own
groups in Separate groups mode, all groups in Visible groups mode.
Given I am on the "<scorm>" "scorm activity" page logged in as "<user>"
And I navigate to "Reports" in current page administration
Then I <all> see "All participants"
And I <G1> see "Group 1"
And I <G2> see "Group 2"
And I should not see "Group 3"
And I <user1> see "User1G1"
And I <user2> see "User2G2"
And I <user3> see "User3None"
And I <user4> see "User4NPgroup"
Examples:
| scorm | user | all | G1 | G2 | user1 | user2 | user3 | user4 |
| scorm1 | teacher1 | should | should | should | should | should | should | should |
| scorm1 | noneditor1 | should not | should | should not | should | should not | should not | should not |
| scorm1 | noneditor2 | should | should not | should not | should | should | should | should |
| scorm2 | teacher1 | should | should | should | should | should | should | should |
| scorm2 | noneditor1 | should | should | should | should | should not | should not | should not |
| scorm2 | noneditor2 | should | should | should | should | should not | should not | should not |
+34
View File
@@ -0,0 +1,34 @@
@mod @mod_scorm @_file_upload @_switch_iframe
Feature: Check a SCORM package with missing Organisational structure.
@javascript
Scenario: Add a scorm activity to a course
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 |
When I log in as "teacher1"
And I add a scorm activity to course "Course 1" section "1"
And I set the following fields to these values:
| Name | MissingOrg SCORM package |
| Description | Description |
| ID number | Missingorg |
And I upload "mod/scorm/tests/packages/singlescobasic_missingorg.zip" file to "Package file" filemanager
And I click on "Save and display" "button"
Then I should see "MissingOrg SCORM package"
And I should see "Enter"
And I should see "Preview"
And I log out
And I am on the "Missingorg" Activity page logged in as student1
And I should see "Enter"
And I press "Enter"
And I switch to "scorm_object" iframe
And I switch to "contentFrame" iframe
And I should see "Play of the game"
@@ -0,0 +1,304 @@
@mod @mod_scorm @_file_upload @_switch_iframe
Feature: Scorm multi-sco review mode.
Check review mode and attempt handling.
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 |
@javascript
Scenario: Test review mode with a single sco completion.
When the following "activities" exist:
| activity | course | name | packagefilepath | forcenewattempt |
| scorm | C1 | Basic Multi-sco SCORM package | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12.zip | 0 |
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page logged in as student1
And I should see "Enter"
And I press "Enter"
And I switch to "scorm_object" iframe
And I should see "Play of the game"
And I switch to the main frame
And I follow "Exit activity"
And I wait until the page is ready
And I should see "Basic Multi-sco SCORM package"
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page
And I should see "Enter"
And I press "Enter"
Then I should not see "Review mode"
@javascript
Scenario: Test review mode with all scos completed.
When the following "activities" exist:
| activity | course | name | packagefilepath | forcenewattempt |
| scorm | C1 | ADV Multi-sco SCORM package | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12.zip | 0 |
And I am on the "ADV Multi-sco SCORM package" "scorm activity" page logged in as student1
And I should see "Enter"
And I press "Enter"
And I switch to "scorm_object" iframe
And I should see "Play of the game"
And I switch to the main frame
And I click on "Par?" "list_item"
And I switch to "scorm_object" iframe
And I should see "Par"
And I switch to the main frame
And I click on "Keeping Score" "list_item"
And I switch to "scorm_object" iframe
And I should see "Scoring"
And I switch to the main frame
And I click on "Other Scoring Systems" "list_item"
And I switch to "scorm_object" iframe
And I should see "Other Scoring Systems"
And I switch to the main frame
And I click on "The Rules of Golf" "list_item"
And I switch to "scorm_object" iframe
And I should see "The Rules of Golf"
And I switch to the main frame
And I click on "Playing Golf Quiz" "list_item"
And I switch to "scorm_object" iframe
And I should see "Knowledge Check"
And I switch to the main frame
And I click on "Taking Care of the Course" "list_item"
And I switch to "scorm_object" iframe
And I should see "Etiquette - Care For the Course"
And I switch to the main frame
And I click on "Avoiding Distraction" "list_item"
And I switch to "scorm_object" iframe
And I should see "Etiquette - Avoiding Distraction"
And I switch to the main frame
And I click on "Playing Politely" "list_item"
And I switch to "scorm_object" iframe
And I should see "Etiquette - Playing the Game"
And I switch to the main frame
And I click on "Etiquette Quiz" "list_item"
And I switch to "scorm_object" iframe
And I should see "Knowledge Check"
And I switch to the main frame
And I click on "Handicapping Overview" "list_item"
And I switch to "scorm_object" iframe
And I should see "Handicapping"
And I switch to the main frame
And I click on "Calculating a Handicap" "list_item"
And I switch to "scorm_object" iframe
And I should see "Calculating a Handicap"
And I switch to the main frame
And I click on "Calculating a Handicapped Score" "list_item"
And I switch to "scorm_object" iframe
And I should see "Calculating a Score"
And I switch to the main frame
And I click on "Handicapping Example" "list_item"
And I switch to "scorm_object" iframe
And I should see "Calculating a Score"
And I switch to the main frame
And I click on "Handicapping Quiz" "list_item"
And I switch to "scorm_object" iframe
And I should see "Knowledge Check"
And I switch to the main frame
And I click on "How to Have Fun Playing Golf" "list_item"
And I switch to "scorm_object" iframe
And I should see "How to Have Fun Golfing"
And I switch to the main frame
And I click on "How to Make Friends Playing Golf" "list_item"
And I switch to "scorm_object" iframe
And I should see "How to Make Friends on the Golf Course"
And I switch to the main frame
And I click on "Having Fun Quiz" "list_item"
And I switch to "scorm_object" iframe
And I should see "Knowledge Check"
And I switch to the main frame
And I follow "Exit activity"
And I wait until the page is ready
And I should see "ADV Multi-sco SCORM package"
And I am on the "ADV Multi-sco SCORM package" "scorm activity" page
And I should see "Enter"
And I press "Enter"
Then I should see "Review mode"
@javascript
Scenario: Test force completed set to Always.
When the following "activities" exist:
| activity | course | name | packagefilepath | forcenewattempt |
| scorm | C1 | Basic Multi-sco SCORM package | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip | 2 |
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page logged in as student1
And I should see "Enter"
And I press "Enter"
And I switch to "scorm_object" iframe
And I should see "Play of the game"
And I switch to the main frame
And I follow "Exit activity"
And I wait until the page is ready
And I should see "Basic Multi-sco SCORM package"
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page
And I should see "Enter"
And I should not see "Start a new attempt"
And I press "Enter"
Then I should not see "Review mode"
And I switch to "scorm_object" iframe
And I should see "Play of the game"
@javascript
Scenario: Test force completed set to when previous complete/passed/failed.
When the following "activities" exist:
| activity | course | name | packagefilepath | forcenewattempt |
| scorm | C1 | Basic Multi-sco SCORM package | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip | 1 |
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page logged in as student1
And I should see "Enter"
And I press "Enter"
And I switch to "scorm_object" iframe
And I should see "Play of the game"
And I switch to the main frame
And I follow "Exit activity"
And I wait until the page is ready
And I should see "Basic Multi-sco SCORM package"
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page
And I should see "Enter"
And I should not see "Start a new attempt"
And I press "Enter"
And I should not see "Review mode"
And I switch to "scorm_object" iframe
And I should see "Par"
And I switch to the main frame
And I click on "Keeping Score" "list_item"
And I switch to "scorm_object" iframe
And I should see "Scoring"
And I switch to the main frame
And I click on "Other Scoring Systems" "list_item"
And I switch to "scorm_object" iframe
And I should see "Other Scoring Systems"
And I switch to the main frame
And I click on "The Rules of Golf" "list_item"
And I switch to "scorm_object" iframe
And I should see "The Rules of Golf"
And I switch to the main frame
And I click on "Playing Golf Quiz" "list_item"
And I switch to "scorm_object" iframe
And I should see "Knowledge Check"
And I switch to the main frame
And I click on "How to Have Fun Playing Golf" "list_item"
And I switch to "scorm_object" iframe
And I should see "How to Have Fun Golfing"
And I switch to the main frame
And I click on "How to Make Friends Playing Golf" "list_item"
And I switch to "scorm_object" iframe
And I should see "How to Make Friends on the Golf Course"
And I switch to the main frame
And I click on "Having Fun Quiz" "list_item"
And I switch to "scorm_object" iframe
And I should see "Knowledge Check"
And I switch to the main frame
And I follow "Exit activity"
And I wait until the page is ready
And I should see "Basic Multi-sco SCORM package"
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page
And I should see "Enter"
And I press "Enter"
Then I should not see "Review mode"
And I switch to "scorm_object" iframe
And I should see "Play of the game"
@javascript
Scenario: Test force completed set to Always and student skipview
When the following "activities" exist:
| activity | course | name | packagefilepath | forcenewattempt | skipview |
| scorm | C1 | Basic Multi-sco SCORM package | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip | 2 | 2 |
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page logged in as student1
And I switch to "scorm_object" iframe
And I should see "Play of the game"
And I switch to the main frame
And I follow "Exit activity"
And I wait until the page is ready
And I should see "Basic Multi-sco SCORM package"
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page
Then I should not see "Review mode"
And I switch to "scorm_object" iframe
And I should see "Play of the game"
@javascript
Scenario: Test force completed set to when previous complete/passed/failed.
When the following "activities" exist:
| activity | course | name | packagefilepath | forcenewattempt | skipview |
| scorm | C1 | Basic Multi-sco SCORM package | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip | 1 | 2 |
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page logged in as student1
And I switch to "scorm_object" iframe
And I should see "Play of the game"
And I switch to the main frame
And I follow "Exit activity"
And I wait until the page is ready
And I should see "Basic Multi-sco SCORM package"
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page
And I should not see "Review mode"
And I switch to "scorm_object" iframe
And I should see "Par"
And I switch to the main frame
And I click on "Keeping Score" "list_item"
And I switch to "scorm_object" iframe
And I should see "Scoring"
And I switch to the main frame
And I click on "Other Scoring Systems" "list_item"
And I switch to "scorm_object" iframe
And I should see "Other Scoring Systems"
And I switch to the main frame
And I click on "The Rules of Golf" "list_item"
And I switch to "scorm_object" iframe
And I should see "The Rules of Golf"
And I switch to the main frame
And I click on "Playing Golf Quiz" "list_item"
And I switch to "scorm_object" iframe
And I should see "Knowledge Check"
And I switch to the main frame
And I click on "How to Have Fun Playing Golf" "list_item"
And I switch to "scorm_object" iframe
And I should see "How to Have Fun Golfing"
And I switch to the main frame
And I click on "How to Make Friends Playing Golf" "list_item"
And I switch to "scorm_object" iframe
And I should see "How to Make Friends on the Golf Course"
And I switch to the main frame
And I click on "Having Fun Quiz" "list_item"
And I switch to "scorm_object" iframe
And I should see "Knowledge Check"
And I switch to the main frame
And I follow "Exit activity"
And I wait until the page is ready
And I should see "Basic Multi-sco SCORM package"
And I am on the "Basic Multi-sco SCORM package" "scorm activity" page
Then I should not see "Review mode"
And I switch to "scorm_object" iframe
And I should see "Play of the game"
@@ -0,0 +1,46 @@
@mod @mod_scorm @_file_upload @_switch_iframe @_alert
Feature: Confirm progress gets saved on unload events
In order to let students access a scorm package
As a teacher
I need to add scorm activity to a course
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 I change window size to "large"
@javascript
Scenario: Test progress gets saved correctly when the user navigates away from the scorm activity
Given the following "activity" exists:
| activity | scorm |
| course | C1 |
| name | Runtime Basic Calls SCORM 2004 3rd Edition package |
| packagefilepath | mod/scorm/tests/packages/RuntimeBasicCalls_SCORM20043rdEdition.zip |
When I log in as "student1"
And I am on "Course 1" course homepage
And I am on the "Runtime Basic Calls SCORM 2004 3rd Edition package" "scorm activity" page
Then I should see "Enter"
And I press "Enter"
And I switch to "scorm_object" iframe
And I press "Next"
And I press "Next"
And I switch to "contentFrame" iframe
And I should see "Scoring"
And I switch to the main frame
And I am on the "Runtime Basic Calls SCORM 2004 3rd Edition package" "scorm activity" page
And I should see "Enter"
And I click on "Enter" "button" confirming the dialogue
And I switch to "scorm_object" iframe
And I switch to "contentFrame" iframe
And I should see "Scoring"
And I switch to the main frame
# Go away from the scorm to stop background requests
And I am on homepage
@@ -0,0 +1,151 @@
@mod @mod_scorm @core_completion @_file_upload @_switch_iframe
Feature: View activity completion in the SCORM activity
In order to have visibility of scorm completion requirements
As a student
I need to be able to view my scorm 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 |
| Course 1 | C1 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| teacher1 | C1 | editingteacher |
And the following "activity" exists:
| activity | scorm |
| course | C1 |
| name | Music history |
| completion | 2 |
| completionstatusallscos | 0 |
# Add requirements
| packagefilepath | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip |
| completionstatusrequired | 6 |
| completionscorerequired | 3 |
| completionstatusrequired | 6 |
| completionstatusallscos | 1 |
| maxattempt | 1 |
| completionview | 1 |
| completionusegrade | 1 |
@javascript
Scenario: View automatic completion items as a teacher
Given I am on the "Music history" "scorm activity" page logged in as teacher1
Then "Music history" should have the "View" completion condition
And "Music history" should have the "Receive a score of 3 or more" completion condition
And "Music history" should have the "Do all parts of this activity" completion condition
And "Music history" should have the "Receive a grade" completion condition
And "Music history" should have the "Complete or pass the activity" completion condition
@javascript
Scenario: Any grade and Passing grade options are hidden
Given I am on the "Music history" "scorm activity" page logged in as teacher1
When I navigate to "Settings" in current page administration
And I expand all fieldsets
And the field "completionusegrade" matches value "1"
Then I should not see "Any grade"
And I should not see "Passing grade"
@javascript
Scenario: View automatic completion items as a student
Given I am on the "Music history" "scorm activity" page logged in as student1
# We need a little taller window because Firefox is, apparently, unable to auto-scroll within
# an iframe, so we need to ensure that the "Save changes" button is visible in the viewport.
And I change window size to "1366x968"
And the "View" completion condition of "Music history" is displayed as "todo"
And the "Receive a score of 3 or more" completion condition of "Music history" is displayed as "todo"
And the "Do all parts of this activity" completion condition of "Music history" is displayed as "todo"
And the "Receive a grade" completion condition of "Music history" is displayed as "todo"
And the "Complete or pass the activity" completion condition of "Music history" is displayed as "todo"
And I press "Enter"
And I switch to the main frame
And I click on "Par?" "list_item"
And I switch to "scorm_object" iframe
And I wait until the page is ready
And I switch to the main frame
And I click on "Keeping Score" "list_item"
And I switch to "scorm_object" iframe
And I wait until the page is ready
And I switch to the main frame
And I click on "Other Scoring Systems" "list_item"
And I switch to "scorm_object" iframe
And I wait until the page is ready
And I switch to the main frame
And I click on "The Rules of Golf" "list_item"
And I switch to "scorm_object" iframe
And I wait until the page is ready
And I switch to the main frame
And I click on "Playing Golf Quiz" "list_item"
And I switch to "scorm_object" iframe
And I wait until the page is ready
And I click on "[id='question_com.scorm.golfsamples.interactions.playing_1_1']" "css_element"
And I press "Submit Answers"
And I wait until "Score: 20" "text" exists
And I switch to the main frame
And I click on "How to Have Fun Playing Golf" "list_item"
And I switch to "scorm_object" iframe
And I wait until the page is ready
And I switch to the main frame
And I click on "How to Make Friends Playing Golf" "list_item"
And I switch to "scorm_object" iframe
And I wait until the page is ready
And I switch to the main frame
And I click on "Having Fun Quiz" "list_item"
And I switch to "scorm_object" iframe
And I wait until the page is ready
And I click on "[id='question_com.scorm.golfsamples.interactions.fun_1_False']" "css_element"
And I press "Submit Answers"
And I wait until "Score: 33" "text" exists
And I switch to the main frame
# We need to get some time till the last item is marked as done (or it won't be ready in slow databases).
# This could be a pause of a few seconds (working ok in super-slow oracle docker database), but re-visiting
# any of the pages seems to be doing the work too under that very same slow environment.
And I click on "Par?" "list_item"
And I switch to "scorm_object" iframe
And I wait until the page is ready
And I switch to the main frame
When I am on the "Music history" "scorm activity" page
Then the "View" completion condition of "Music history" is displayed as "done"
# Conditions that are not possible to achieve (eg score below requirement but all attempts used) are marked as failed.
And the "Receive a score of 3 or more" completion condition of "Music history" is displayed as "failed"
And the "Do all parts of this activity" 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 "Complete or pass the activity" completion condition of "Music history" is displayed as "done"
@javascript
Scenario: Use manual completion
Given I am on the "Music history" "scorm activity" page logged in as teacher1
And I navigate to "Settings" in current page administration
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 the manual completion button for "Music history" should be disabled
And I log out
# Student view.
When I am on the "Music history" "scorm 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"
@javascript
Scenario: Required minimum score should be greater than zero
Given I am on the "Music history" "scorm activity" page logged in as teacher1
And I navigate to "Settings" in current page administration
And I expand all fieldsets
When I set the field "completionscorerequired" to "0"
And I click on "Save and display" "button"
Then I should see "Minimum score must be greater than 0."
And "Enter" "button" should not exist
And I set the field "completionscorerequired" to "-1"
And I click on "Save and display" "button"
And I should see "Minimum score must be greater than 0."
And "Enter" "button" should not exist
And I set the field "completionscorerequired" to "5"
And I click on "Save and display" "button"
And I should not see "Minimum score must be greater than 0."
And "Enter" "button" should exist
@@ -0,0 +1,44 @@
@mod @mod_scorm
Feature: Scorm availability
In order to control when a SCORM activity is available to students
As a teacher
I need be able to set availability dates for the SCORM
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 "activities" exist:
| activity | course | name | packagefilepath | timeopen | timeclose |
| scorm | C1 | Past SCORM | mod/scorm/tests/packages/singlesco_scorm12.zip | ##yesterday## | ##yesterday## |
| scorm | C1 | Current SCORM | mod/scorm/tests/packages/singlesco_scorm12.zip | ##yesterday## | ##tomorrow## |
| scorm | C1 | Future SCORM | mod/scorm/tests/packages/singlesco_scorm12.zip | ##tomorrow## | ##tomorrow## |
Scenario: Scorm activity with dates in the past should not be available.
When I am on the "Past SCORM" "scorm activity" page logged in as "student1"
Then the activity date in "Past SCORM" should contain "Opened:"
And the activity date in "Past SCORM" should contain "##yesterday noon##%A, %d %B %Y, %I:%M##"
And the activity date in "Past SCORM" should contain "Closed:"
And the activity date in "Past SCORM" should contain "##yesterday noon##%A, %d %B %Y, %I:%M##"
And "Enter" "button" should not exist
And I should not see "Preview"
And I am on the "Current SCORM" "scorm activity" page
And the activity date in "Current SCORM" should contain "Opened:"
And the activity date in "Current SCORM" should contain "##yesterday noon##%A, %d %B %Y, %I:%M##"
And the activity date in "Current SCORM" should contain "Closes:"
And the activity date in "Current SCORM" should contain "##tomorrow noon##%A, %d %B %Y, %I:%M##"
And "Enter" "button" should exist
And I should see "Preview"
And I am on the "Future SCORM" "scorm activity" page
And the activity date in "Future SCORM" should contain "Opens:"
And the activity date in "Future SCORM" should contain "##tomorrow noon##%A, %d %B %Y, %I:%M##"
And the activity date in "Future SCORM" should contain "Closes:"
And the activity date in "Future SCORM" should contain "##tomorrow noon##%A, %d %B %Y, %I:%M##"
And "Enter" "button" should not exist
And I should not see "Preview"
@@ -0,0 +1,58 @@
@mod @mod_scorm
Feature: Scorm display options
In order to set how Scorm is displayed
As a teacher
I need to be able to choose from Scorm package display options
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | One | teacher1@example.com |
| student1 | Student | One | student1@example.com |
And the following "courses" exist:
| fullname | shortname | format | activitytype |
| Course 1 | C1 | topics | |
| Course 2 | C2 | singleactivity | scorm |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
| teacher1 | C2 | editingteacher |
| student1 | C2 | student |
@javascript
Scenario Outline: Teacher can change to various Scorm package display options
Given the following "activities" exist:
| activity | course | name | packagefilepath | hidetoc | nav |
| scorm | C1 | C1 Scorm 1 | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12.zip | <toc> | <shownavigation> |
And I am on the "C1 Scorm 1" "scorm activity" page logged in as teacher1
When I press "Preview"
# Confirm TOC display
# Used css_element to check > and < button display in TOC since similar buttons also exist in navigation
Then I <tocdisplay> see "Golf Explained - Minimum Run-time Calls"
And "[title='Show']" "css_element" <showdisplay> exist
And "[title='Hide']" "css_element" <hidedisplay> exist
# Confirm TOC dropdown display
And "scoid" "select" <tocdropdown> exist
# Confirm the navigation display
And "[id='scorm_nav']" "css_element" <navbar> exist
Examples:
| toc | shownavigation | tocdisplay | showdisplay | hidedisplay | tocdropdown | navbar |
| 1 | | should not | should | should not | should not | should not |
| 2 | 1 | should | should | should not | should | should not |
| 0 | 0 | should | should not | should | should not | should not |
| 0 | 1 | should | should not | should | should not | should |
Scenario: Student can exit Scorm activity in single activity course format
Given the following "activities" exist:
| activity | course | name | packagefilepath | popup |
| scorm | C2 | C2 Scorm 1 | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12.zip | 0 |
And I am on the "C2 Scorm 1" "scorm activity" page logged in as student1
And I press "Enter"
When I click on "Exit activity" "link"
# Confirm that student can exit activity
Then "Preview" "button" should exist
And "Enter" "button" should exist
And "Exit activity" "link" should not exist
And I should not see "Golf Explained - Minimum Run-time Calls"
@@ -0,0 +1,38 @@
@mod @mod_scorm
Feature: Scorm with no calendar capabilites
In order to allow work effectively
As a teacher
I need to be able to create SCORM activities even when I cannot edit calendar events
Background:
Given the following "courses" exist:
| fullname | shortname | category | groupmode |
| Course 1 | C1 | 0 | 1 |
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 |
And the following "activity" exists:
| activity | scorm |
| course | C1 |
| name | Test scorm name |
| timeopen | ##first day of January 2017## |
| timeclose | ##first day of February 2017## |
| packagefilepath | mod/scorm/tests/packages/singlesco_scorm12.zip |
And I log in as "admin"
And I am on the "Course 1" "permissions" page
And I override the system permissions of "Teacher" role with:
| capability | permission |
| moodle/calendar:manageentries | Prohibit |
And I log out
@javascript @_file_upload @_switch_iframe
Scenario: Editing a scorm activity without calendar permission
Given I am on the "Test scorm name" "scorm activity editing" page logged in as teacher1
When I set the following fields to these values:
| id_timeopen_year | 2018 |
| id_timeclose_year | 2018 |
And I press "Save and return to course"
Then I should see "Test scorm name"
+376
View File
@@ -0,0 +1,376 @@
<?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/>.
declare(strict_types = 1);
namespace mod_scorm;
use advanced_testcase;
use cm_info;
use coding_exception;
use mod_scorm\completion\custom_completion;
use moodle_exception;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . '/completionlib.php');
require_once($CFG->dirroot.'/mod/scorm/locallib.php');
/**
* Class for unit testing mod_scorm/custom_completion.
*
* @package mod_scorm
* @copyright 2021 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class custom_completion_test extends advanced_testcase {
/**
* Data provider for get_state().
*
* @return array[]
*/
public function get_state_provider(): array {
// Prepare various reusable user scorm track data used to mock various completion states/requirements.
$completionincomplete = (object) [
'id' => 1,
'scoid' => 1,
'element' => 'cmi.completion_status',
'value' => 'incomplete',
];
$completionpassed = (object) [
'id' => 1,
'scoid' => 1,
'element' => 'cmi.completion_status',
'value' => 'passed',
];
$completioncompleted = (object) [
'id' => 1,
'scoid' => 2,
'element' => 'cmi.success_status',
'value' => 'completed',
];
$completionscorefail = (object) [
'id' => 1,
'scoid' => 1,
'element' => 'cmi.score.raw',
'value' => '20',
];
$completionscorepass = (object) [
'id' => 1,
'scoid' => 1,
'element' => 'cmi.score.raw',
'value' => '100',
];
return [
'Undefined completion requirement' => [
'somenonexistentrule', COMPLETION_ENABLED, [$completionincomplete], 0, null, coding_exception::class
],
'Completion status requirement not available' => [
'completionstatusrequired', COMPLETION_DISABLED, [$completionincomplete], 0, null, moodle_exception::class
],
'Completion status Passed required, user has no completion status recorded' => [
'completionstatusrequired', 2, [], 0, COMPLETION_INCOMPLETE, null
],
'Completion status Passed required, user has not passed, can make another attempt' => [
'completionstatusrequired', 2, [$completionincomplete], 0, COMPLETION_INCOMPLETE, null
],
'Completion status Passed required, user has passed' => [
'completionstatusrequired', 2, [$completionpassed], 0, COMPLETION_COMPLETE, null
],
'Completion status Completed required, user has not completed, can make another attempt' => [
'completionstatusrequired', 4, [$completionincomplete], 2, COMPLETION_INCOMPLETE, null
],
'Completion status Completed required, user has completed' => [
'completionstatusrequired', 4, [$completioncompleted], 1, COMPLETION_COMPLETE, null
],
'Completion status Passed or Completed required, user has only completed, can make another attempt' => [
'completionstatusrequired', 6, [$completioncompleted], 0, COMPLETION_COMPLETE, null
],
'Completion status Passed or Completed required, user has completed and passed' => [
'completionstatusrequired', 6, [$completionpassed, $completioncompleted], 0, COMPLETION_COMPLETE, null
],
'Completion status Passed or Completed required, user has not passed or completed, but has another attempt' => [
'completionstatusrequired', 6, [$completionincomplete], 2, COMPLETION_INCOMPLETE, null
],
'Completion status Passed or Completed required, user has used all attempts, but not passed or completed' => [
'completionstatusrequired', 6, [$completionincomplete], 1, COMPLETION_COMPLETE_FAIL, null
],
'Completion status Passed required, user has used all attempts and completed, but not passed' => [
'completionstatusrequired', 2, [$completioncompleted], 1, COMPLETION_COMPLETE_FAIL, null
],
'Completion status Completed required, user has used all attempts, but not completed' => [
'completionstatusrequired', 4, [$completionincomplete], 1, COMPLETION_COMPLETE_FAIL, null
],
'Completion status Passed or Completed required, user has used all attempts, but not passed' => [
'completionstatusrequired', 6, [$completionincomplete, $completioncompleted], 2, COMPLETION_COMPLETE, null
],
'Completion score required, user has no score' => [
'completionscorerequired', 80, [], 0, COMPLETION_INCOMPLETE, null
],
'Completion score required, user score does not meet requirement, can make another attempt' => [
'completionscorerequired', 80, [$completionscorefail], 0, COMPLETION_INCOMPLETE, null
],
'Completion score required, user has used all attempts, but not reached the score' => [
'completionscorerequired', 80, [$completionscorefail], 1, COMPLETION_COMPLETE_FAIL, null
],
'Completion score required, user score meets requirement' => [
'completionscorerequired', 80, [$completionscorepass], 0, COMPLETION_COMPLETE, null
],
'Completion of all scos required, user has not completed, can make another attempt' => [
'completionstatusallscos', 1, [$completionincomplete, $completioncompleted], 3, COMPLETION_INCOMPLETE, null
],
'Completion of all scos required, user has completed' => [
'completionstatusallscos', 1, [$completionpassed, $completioncompleted], 2, COMPLETION_COMPLETE, null
],
'Completion of all scos required, user has used all attempts, but not completed all scos' => [
'completionstatusallscos', 1, [$completionincomplete, $completioncompleted], 2, COMPLETION_COMPLETE_FAIL, null
],
];
}
/**
* Test for get_state().
*
* @dataProvider get_state_provider
* @param string $rule The custom completion condition.
* @param int $rulevalue The custom completion rule value.
* @param array $uservalue The relevant record database mock data recorded against the user for the rule.
* @param int $maxattempts The number of attempts the activity allows (0 = unlimited).
* @param int|null $status Expected completion status for the rule.
* @param string|null $exception Expected exception.
*/
public function test_get_state(string $rule, int $rulevalue, array $uservalue, int $maxattempts, ?int $status,
?string $exception): void {
global $DB;
if (!is_null($exception)) {
$this->expectException($exception);
}
// Custom completion rule data for cm_info::customdata.
$customdataval = [
'customcompletionrules' => [
$rule => $rulevalue
]
];
// Build a mock cm_info instance.
$mockcminfo = $this->getMockBuilder(cm_info::class)
->disableOriginalConstructor()
->onlyMethods(['__get'])
->getMock();
// Mock the return of the magic getter method when fetching the cm_info object's
// customdata and instance values.
$mockcminfo->expects($this->any())
->method('__get')
->will($this->returnValueMap([
['customdata', $customdataval],
['instance', 1],
]));
// Mock the DB call fetching user's SCORM track data.
$DB = $this->createMock(get_class($DB));
$DB->expects($this->atMost(1))
->method('get_records_sql')
->willReturn($uservalue);
// For completed all scos tests, mock the DB call that fetches the sco IDs.
if ($rule === 'completionstatusallscos') {
$returnscos = [];
foreach ($uservalue as $data) {
$returnscos[$data->scoid] = (object) ['id' => $data->scoid];
}
$DB->expects($this->atMost(1))
->method('get_records')
->willReturn($returnscos);
}
// Anything not complete will check if attempts have been exhausted, mock the DB calls for that check.
if ($status != COMPLETION_COMPLETE) {
$mockscorm = (object) [
'id' => 1,
'version' => SCORM_13,
'grademethod' => GRADESCOES,
'maxattempt' => $maxattempts,
];
$DB->expects($this->atMost(1))
->method('get_record')
->willReturn($mockscorm);
$DB->expects($this->atMost(1))
->method('count_records_sql')
->willReturn(count($uservalue));
}
$customcompletion = new custom_completion($mockcminfo, 2);
$this->assertEquals($status, $customcompletion->get_state($rule));
}
/**
* Test for get_defined_custom_rules().
*/
public function test_get_defined_custom_rules(): void {
$expectedrules = [
'completionstatusrequired',
'completionscorerequired',
'completionstatusallscos',
];
$definedrules = custom_completion::get_defined_custom_rules();
$this->assertCount(3, $definedrules);
foreach ($definedrules as $definedrule) {
$this->assertContains($definedrule, $expectedrules);
}
}
/**
* Test for get_defined_custom_rule_descriptions().
*/
public function test_get_custom_rule_descriptions(): void {
// Get defined custom rules.
$rules = custom_completion::get_defined_custom_rules();
// Build a mock cm_info instance.
$mockcminfo = $this->getMockBuilder(cm_info::class)
->disableOriginalConstructor()
->onlyMethods(['__get'])
->getMock();
// Instantiate a custom_completion object using the mocked cm_info.
$customcompletion = new custom_completion($mockcminfo, 1);
// Get custom rule descriptions.
$ruledescriptions = $customcompletion->get_custom_rule_descriptions();
// Confirm that defined rules and rule descriptions are consistent with each other.
$this->assertEquals(count($rules), count($ruledescriptions));
foreach ($rules as $rule) {
$this->assertArrayHasKey($rule, $ruledescriptions);
}
}
/**
* Test for is_defined().
*/
public function test_is_defined(): void {
// Build a mock cm_info instance.
$mockcminfo = $this->getMockBuilder(cm_info::class)
->disableOriginalConstructor()
->getMock();
$customcompletion = new custom_completion($mockcminfo, 1);
// All rules are defined.
$this->assertTrue($customcompletion->is_defined('completionstatusrequired'));
$this->assertTrue($customcompletion->is_defined('completionscorerequired'));
$this->assertTrue($customcompletion->is_defined('completionstatusallscos'));
// Undefined rule is not found.
$this->assertFalse($customcompletion->is_defined('somerandomrule'));
}
/**
* Data provider for test_get_available_custom_rules().
*
* @return array[]
*/
public function get_available_custom_rules_provider(): array {
return [
'Completion status enabled only' => [
[
'completionstatusrequired' => 4,
'completionscorerequired' => COMPLETION_DISABLED,
'completionstatusallscos' => COMPLETION_DISABLED,
],
['completionstatusrequired'],
],
'Completion score enabled only' => [
[
'completionstatusrequired' => COMPLETION_DISABLED,
'completionscorerequired' => 80,
'completionstatusallscos' => COMPLETION_DISABLED,
],
['completionscorerequired'],
],
'Completion status and all scos completed both enabled' => [
[
'completionstatusrequired' => 2,
'completionscorerequired' => COMPLETION_DISABLED,
'completionstatusallscos' => COMPLETION_ENABLED,
],
['completionstatusrequired', 'completionstatusallscos'],
],
'Completion status and score both enabled' => [
[
'completionstatusrequired' => COMPLETION_ENABLED,
'completionscorerequired' => 80,
'completionstatusallscos' => COMPLETION_DISABLED,
],
['completionstatusrequired', 'completionscorerequired'],
],
'All custom completion conditions enabled' => [
[
'completionstatusrequired' => 6,
'completionscorerequired' => 80,
'completionstatusallscos' => COMPLETION_ENABLED,
],
['completionstatusrequired', 'completionscorerequired', 'completionstatusallscos'],
],
];
}
/**
* Test for get_available_custom_rules().
*
* @dataProvider get_available_custom_rules_provider
* @param array $completionrulesvalues
* @param array $expected
*/
public function test_get_available_custom_rules(array $completionrulesvalues, array $expected): void {
$customcompletionrules = [
'customcompletionrules' => $completionrulesvalues,
];
// Build a mock cm_info instance.
$mockcminfo = $this->getMockBuilder(cm_info::class)
->disableOriginalConstructor()
->onlyMethods(['__get'])
->getMock();
// Mock the return of magic getter for the customdata attribute.
$mockcminfo->expects($this->any())
->method('__get')
->with('customdata')
->willReturn($customcompletionrules);
$customcompletion = new custom_completion($mockcminfo, 1);
$this->assertEquals($expected, $customcompletion->get_available_custom_rules());
}
}
+123
View File
@@ -0,0 +1,123 @@
<?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/>.
/**
* Contains unit tests for mod_scorm\dates.
*
* @package mod_scorm
* @category test
* @copyright 2021 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
namespace mod_scorm;
use advanced_testcase;
use cm_info;
use core\activity_dates;
/**
* Class for unit testing mod_scorm\dates.
*
* @copyright 2021 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class dates_test extends advanced_testcase {
/**
* Data provider for get_dates_for_module().
* @return array[]
*/
public function get_dates_for_module_provider(): array {
$now = time();
$before = $now - DAYSECS;
$earlier = $before - DAYSECS;
$after = $now + DAYSECS;
$later = $after + DAYSECS;
return [
'without any dates' => [
null, null, []
],
'only with opening time' => [
$after, null, [
['label' => 'Opens:', 'timestamp' => $after, 'dataid' => 'timeopen'],
]
],
'only with closing time' => [
null, $after, [
['label' => 'Closes:', 'timestamp' => $after, 'dataid' => 'timeclose'],
]
],
'with both times' => [
$after, $later, [
['label' => 'Opens:', 'timestamp' => $after, 'dataid' => 'timeopen'],
['label' => 'Closes:', 'timestamp' => $later, 'dataid' => 'timeclose'],
]
],
'between the dates' => [
$before, $after, [
['label' => 'Opened:', 'timestamp' => $before, 'dataid' => 'timeopen'],
['label' => 'Closes:', 'timestamp' => $after, 'dataid' => 'timeclose'],
]
],
'dates are past' => [
$earlier, $before, [
['label' => 'Opened:', 'timestamp' => $earlier, 'dataid' => 'timeopen'],
['label' => 'Closed:', 'timestamp' => $before, 'dataid' => 'timeclose'],
]
],
];
}
/**
* Test for get_dates_for_module().
*
* @dataProvider get_dates_for_module_provider
* @param int|null $timeopen The 'available from' value of the scorm.
* @param int|null $timeclose The 'available to' value of the scorm.
* @param array $expected The expected value of calling get_dates_for_module()
*/
public function test_get_dates_for_module(?int $timeopen, ?int $timeclose, array $expected): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($user->id, $course->id);
$data = ['course' => $course->id];
if ($timeopen) {
$data['timeopen'] = $timeopen;
}
if ($timeclose) {
$data['timeclose'] = $timeclose;
}
$this->setAdminUser();
$scorm = $this->getDataGenerator()->create_module('scorm', $data);
$this->setUser($user);
$cm = get_coursemodule_from_instance('scorm', $scorm->id);
// Make sure we're using a cm_info object.
$cm = cm_info::create($cm);
$dates = activity_dates::get_dates_for_module($cm, (int) $user->id);
$this->assertEquals($expected, $dates);
}
}
+373
View File
@@ -0,0 +1,373 @@
<?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/>.
/**
* This file contains tests for scorm events.
*
* @package mod_scorm
* @copyright 2013 onwards Ankit Agarwal
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_scorm\event;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/scorm/locallib.php');
require_once($CFG->dirroot . '/mod/scorm/lib.php');
/**
* Test class for various events related to Scorm.
*
* @package mod_scorm
* @copyright 2013 onwards Ankit Agarwal
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class events_test extends \advanced_testcase {
/** @var stdClass store course object */
protected $eventcourse;
/** @var stdClass store user object */
protected $eventuser;
/** @var stdClass store scorm object */
protected $eventscorm;
/** @var stdClass store course module object */
protected $eventcm;
protected function setUp(): void {
$this->setAdminUser();
$this->eventcourse = $this->getDataGenerator()->create_course();
$this->eventuser = $this->getDataGenerator()->create_user();
$record = new \stdClass();
$record->course = $this->eventcourse->id;
$this->eventscorm = $this->getDataGenerator()->create_module('scorm', $record);
$this->eventcm = get_coursemodule_from_instance('scorm', $this->eventscorm->id);
}
/**
* Tests for attempt deleted event
*/
public function test_attempt_deleted_event(): void {
global $USER;
$this->resetAfterTest();
scorm_insert_track(2, $this->eventscorm->id, 1, 4, 'cmi.core.score.raw', 10);
$sink = $this->redirectEvents();
scorm_delete_attempt(2, $this->eventscorm, 4);
$events = $sink->get_events();
$sink->close();
$event = reset($events);
// Verify data.
$this->assertCount(3, $events);
$this->assertInstanceOf('\mod_scorm\event\attempt_deleted', $event);
$this->assertEquals($USER->id, $event->userid);
$this->assertEquals(\context_module::instance($this->eventcm->id), $event->get_context());
$this->assertEquals(4, $event->other['attemptid']);
$this->assertEquals(2, $event->relateduserid);
$this->assertEventContextNotUsed($event);
// Test event validations.
$this->expectException(\coding_exception::class);
\mod_scorm\event\attempt_deleted::create(array(
'contextid' => 5,
'relateduserid' => 2
));
}
/**
* Tests for interactions viewed validations.
*/
public function test_interactions_viewed_event_validations(): void {
$this->resetAfterTest();
try {
\mod_scorm\event\interactions_viewed::create(array(
'context' => \context_module::instance($this->eventcm->id),
'courseid' => $this->eventcourse->id,
'other' => array('attemptid' => 2)
));
$this->fail("Event validation should not allow \\mod_scorm\\event\\interactions_viewed to be triggered without
other['instanceid']");
} catch (\Exception $e) {
$this->assertInstanceOf('coding_exception', $e);
}
try {
\mod_scorm\event\interactions_viewed::create(array(
'context' => \context_module::instance($this->eventcm->id),
'courseid' => $this->eventcourse->id,
'other' => array('instanceid' => 2)
));
$this->fail("Event validation should not allow \\mod_scorm\\event\\interactions_viewed to be triggered without
other['attemptid']");
} catch (\Exception $e) {
$this->assertInstanceOf('coding_exception', $e);
}
}
/**
* Tests for tracks viewed event validations.
*/
public function test_tracks_viewed_event_validations(): void {
$this->resetAfterTest();
try {
\mod_scorm\event\tracks_viewed::create(array(
'context' => \context_module::instance($this->eventcm->id),
'courseid' => $this->eventcourse->id,
'other' => array('attemptid' => 2, 'scoid' => 2)
));
$this->fail("Event validation should not allow \\mod_scorm\\event\\tracks_viewed to be triggered without
other['instanceid']");
} catch (\Exception $e) {
$this->assertInstanceOf('coding_exception', $e);
}
try {
\mod_scorm\event\tracks_viewed::create(array(
'context' => \context_module::instance($this->eventcm->id),
'courseid' => $this->eventcourse->id,
'other' => array('instanceid' => 2, 'scoid' => 2)
));
$this->fail("Event validation should not allow \\mod_scorm\\event\\tracks_viewed to be triggered without
other['attemptid']");
} catch (\Exception $e) {
$this->assertInstanceOf('coding_exception', $e);
}
try {
\mod_scorm\event\tracks_viewed::create(array(
'context' => \context_module::instance($this->eventcm->id),
'courseid' => $this->eventcourse->id,
'other' => array('attemptid' => 2, 'instanceid' => 2)
));
$this->fail("Event validation should not allow \\mod_scorm\\event\\tracks_viewed to be triggered without
other['scoid']");
} catch (\Exception $e) {
$this->assertInstanceOf('coding_exception', $e);
}
}
/**
* Tests for userreport viewed event validations.
*/
public function test_user_report_viewed_event_validations(): void {
$this->resetAfterTest();
try {
\mod_scorm\event\user_report_viewed::create(array(
'context' => \context_module::instance($this->eventcm->id),
'courseid' => $this->eventcourse->id,
'other' => array('attemptid' => 2)
));
$this->fail("Event validation should not allow \\mod_scorm\\event\\user_report_viewed to be triggered without
other['instanceid']");
} catch (\Exception $e) {
$this->assertInstanceOf('coding_exception', $e);
}
try {
\mod_scorm\event\user_report_viewed::create(array(
'context' => \context_module::instance($this->eventcm->id),
'courseid' => $this->eventcourse->id,
'other' => array('instanceid' => 2)
));
$this->fail("Event validation should not allow \\mod_scorm\\event\\user_report_viewed to be triggered without
other['attemptid']");
} catch (\Exception $e) {
$this->assertInstanceOf('coding_exception', $e);
}
}
/**
* dataProvider for test_scoreraw_submitted_event().
*/
public function get_scoreraw_submitted_event_provider() {
return array(
// SCORM 1.2.
// - cmi.core.score.raw.
'cmi.core.score.raw => 100' => array('cmi.core.score.raw', '100'),
'cmi.core.score.raw => 90' => array('cmi.core.score.raw', '90'),
'cmi.core.score.raw => 50' => array('cmi.core.score.raw', '50'),
'cmi.core.score.raw => 10' => array('cmi.core.score.raw', '10'),
// Check an edge case (PHP empty() vs isset()): score value equals to '0'.
'cmi.core.score.raw => 0' => array('cmi.core.score.raw', '0'),
// SCORM 1.3 AKA 2004.
// - cmi.score.raw.
'cmi.score.raw => 100' => array('cmi.score.raw', '100'),
'cmi.score.raw => 90' => array('cmi.score.raw', '90'),
'cmi.score.raw => 50' => array('cmi.score.raw', '50'),
'cmi.score.raw => 10' => array('cmi.score.raw', '10'),
// Check an edge case (PHP empty() vs isset()): score value equals to '0'.
'cmi.score.raw => 0' => array('cmi.score.raw', '0'),
);
}
/**
* dataProvider for test_scoreraw_submitted_event_validations().
*/
public function get_scoreraw_submitted_event_validations() {
return array(
'scoreraw_submitted => missing cmielement' => array(
null, '50',
"Event validation should not allow \\mod_scorm\\event\\scoreraw_submitted " .
"to be triggered without other['cmielement']",
'Coding error detected, it must be fixed by a programmer: ' .
"The 'cmielement' must be set in other."
),
'scoreraw_submitted => missing cmivalue' => array(
'cmi.core.score.raw', null,
"Event validation should not allow \\mod_scorm\\event\\scoreraw_submitted " .
"to be triggered without other['cmivalue']",
'Coding error detected, it must be fixed by a programmer: ' .
"The 'cmivalue' must be set in other."
),
'scoreraw_submitted => wrong CMI element' => array(
'cmi.core.lesson_status', '50',
"Event validation should not allow \\mod_scorm\\event\\scoreraw_submitted " .
'to be triggered with a CMI element not representing a raw score',
'Coding error detected, it must be fixed by a programmer: ' .
"The 'cmielement' must represents a valid CMI raw score (cmi.core.lesson_status)."
),
);
}
/**
* Tests for score submitted event validations.
*
* @dataProvider get_scoreraw_submitted_event_validations
*
* @param string $cmielement a valid CMI raw score element
* @param string $cmivalue a valid CMI raw score value
* @param string $failmessage the message used to fail the test in case of missing to violate a validation rule
* @param string $excmessage the exception message when violating the validations rules
*/
public function test_scoreraw_submitted_event_validations($cmielement, $cmivalue, $failmessage, $excmessage): void {
$this->resetAfterTest();
try {
$data = array(
'context' => \context_module::instance($this->eventcm->id),
'courseid' => $this->eventcourse->id,
'other' => array('attemptid' => 2)
);
if ($cmielement != null) {
$data['other']['cmielement'] = $cmielement;
}
if ($cmivalue != null) {
$data['other']['cmivalue'] = $cmivalue;
}
\mod_scorm\event\scoreraw_submitted::create($data);
$this->fail($failmessage);
} catch (\Exception $e) {
$this->assertInstanceOf('coding_exception', $e);
$this->assertEquals($excmessage, $e->getMessage());
}
}
/**
* dataProvider for test_status_submitted_event().
*/
public function get_status_submitted_event_provider() {
return array(
// SCORM 1.2.
// 1. Status: cmi.core.lesson_status.
'cmi.core.lesson_status => passed' => array('cmi.core.lesson_status', 'passed'),
'cmi.core.lesson_status => completed' => array('cmi.core.lesson_status', 'completed'),
'cmi.core.lesson_status => failed' => array('cmi.core.lesson_status', 'failed'),
'cmi.core.lesson_status => incomplete' => array('cmi.core.lesson_status', 'incomplete'),
'cmi.core.lesson_status => browsed' => array('cmi.core.lesson_status', 'browsed'),
'cmi.core.lesson_status => not attempted' => array('cmi.core.lesson_status', 'not attempted'),
// SCORM 1.3 AKA 2004.
// 1. Completion status: cmi.completion_status.
'cmi.completion_status => completed' => array('cmi.completion_status', 'completed'),
'cmi.completion_status => incomplete' => array('cmi.completion_status', 'incomplete'),
'cmi.completion_status => not attempted' => array('cmi.completion_status', 'not attempted'),
'cmi.completion_status => unknown' => array('cmi.completion_status', 'unknown'),
// 2. Success status: cmi.success_status.
'cmi.success_status => passed' => array('cmi.success_status', 'passed'),
'cmi.success_status => failed' => array('cmi.success_status', 'failed'),
'cmi.success_status => unknown' => array('cmi.success_status', 'unknown')
);
}
/**
* dataProvider for test_status_submitted_event_validations().
*/
public function get_status_submitted_event_validations() {
return array(
'status_submitted => missing cmielement' => array(
null, 'passed',
"Event validation should not allow \\mod_scorm\\event\\status_submitted " .
"to be triggered without other['cmielement']",
'Coding error detected, it must be fixed by a programmer: ' .
"The 'cmielement' must be set in other."
),
'status_submitted => missing cmivalue' => array(
'cmi.core.lesson_status', null,
"Event validation should not allow \\mod_scorm\\event\\status_submitted " .
"to be triggered without other['cmivalue']",
'Coding error detected, it must be fixed by a programmer: ' .
"The 'cmivalue' must be set in other."
),
'status_submitted => wrong CMI element' => array(
'cmi.core.score.raw', 'passed',
"Event validation should not allow \\mod_scorm\\event\\status_submitted " .
'to be triggered with a CMI element not representing a valid CMI status element',
'Coding error detected, it must be fixed by a programmer: ' .
"The 'cmielement' must represents a valid CMI status element (cmi.core.score.raw)."
),
'status_submitted => wrong CMI value' => array(
'cmi.core.lesson_status', 'blahblahblah',
"Event validation should not allow \\mod_scorm\\event\\status_submitted " .
'to be triggered with a CMI element not representing a valid CMI status',
'Coding error detected, it must be fixed by a programmer: ' .
"The 'cmivalue' must represents a valid CMI status value (blahblahblah)."
),
);
}
/**
* Tests for status submitted event validations.
*
* @dataProvider get_status_submitted_event_validations
*
* @param string $cmielement a valid CMI status element
* @param string $cmivalue a valid CMI status value
* @param string $failmessage the message used to fail the test in case of missing to violate a validation rule
* @param string $excmessage the exception message when violating the validations rules
*/
public function test_status_submitted_event_validations($cmielement, $cmivalue, $failmessage, $excmessage): void {
$this->resetAfterTest();
try {
$data = array(
'context' => \context_module::instance($this->eventcm->id),
'courseid' => $this->eventcourse->id,
'other' => array('attemptid' => 2)
);
if ($cmielement != null) {
$data['other']['cmielement'] = $cmielement;
}
if ($cmivalue != null) {
$data['other']['cmivalue'] = $cmivalue;
}
\mod_scorm\event\status_submitted::create($data);
$this->fail($failmessage);
} catch (\Exception $e) {
$this->assertInstanceOf('coding_exception', $e);
$this->assertEquals($excmessage, $e->getMessage());
}
}
}
+967
View File
@@ -0,0 +1,967 @@
<?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_scorm;
use core_external\external_api;
use externallib_advanced_testcase;
use mod_scorm_external;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
require_once($CFG->dirroot . '/mod/scorm/lib.php');
/**
* SCORM module external functions tests
*
* @package mod_scorm
* @category external
* @copyright 2015 Juan Leyva <juan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 3.0
*/
class externallib_test extends externallib_advanced_testcase {
/** @var \stdClass course record. */
protected \stdClass $course;
/** @var \stdClass activity record. */
protected \stdClass $scorm;
/** @var \core\context\module context instance. */
protected \core\context\module $context;
/** @var \stdClass */
protected \stdClass $cm;
/** @var \stdClass user record. */
protected \stdClass $student;
/** @var \stdClass user record. */
protected \stdClass $teacher;
/** @var \stdClass a fieldset object, false or exception if error not found. */
protected \stdClass $studentrole;
/** @var \stdClass a fieldset object, false or exception if error not found. */
protected \stdClass $teacherrole;
/**
* Set up for every test
*/
public function setUp(): void {
global $DB, $CFG;
$this->resetAfterTest();
$this->setAdminUser();
$CFG->enablecompletion = 1;
// Setup test data.
$this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
$this->scorm = $this->getDataGenerator()->create_module('scorm', array('course' => $this->course->id),
array('completion' => 2, 'completionview' => 1));
$this->context = \context_module::instance($this->scorm->cmid);
$this->cm = get_coursemodule_from_instance('scorm', $this->scorm->id);
// Create users.
$this->student = self::getDataGenerator()->create_user();
$this->teacher = self::getDataGenerator()->create_user();
// Users enrolments.
$this->studentrole = $DB->get_record('role', array('shortname' => 'student'));
$this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
$this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual');
$this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual');
}
/**
* Test view_scorm
*/
public function test_view_scorm(): void {
global $DB;
// Test invalid instance id.
try {
mod_scorm_external::view_scorm(0);
$this->fail('Exception expected due to invalid mod_scorm instance id.');
} catch (\moodle_exception $e) {
$this->assertEquals('invalidrecord', $e->errorcode);
}
// Test not-enrolled user.
$user = self::getDataGenerator()->create_user();
$this->setUser($user);
try {
mod_scorm_external::view_scorm($this->scorm->id);
$this->fail('Exception expected due to not enrolled user.');
} catch (\moodle_exception $e) {
$this->assertEquals('requireloginerror', $e->errorcode);
}
// Test user with full capabilities.
$this->studentrole = $DB->get_record('role', array('shortname' => 'student'));
$this->getDataGenerator()->enrol_user($user->id, $this->course->id, $this->studentrole->id);
// Trigger and capture the event.
$sink = $this->redirectEvents();
$result = mod_scorm_external::view_scorm($this->scorm->id);
$result = external_api::clean_returnvalue(mod_scorm_external::view_scorm_returns(), $result);
$events = $sink->get_events();
$this->assertCount(1, $events);
$event = array_shift($events);
// Checking that the event contains the expected values.
$this->assertInstanceOf('\mod_scorm\event\course_module_viewed', $event);
$this->assertEquals($this->context, $event->get_context());
$moodleurl = new \moodle_url('/mod/scorm/view.php', array('id' => $this->cm->id));
$this->assertEquals($moodleurl, $event->get_url());
$this->assertEventContextNotUsed($event);
$this->assertNotEmpty($event->get_name());
}
/**
* Test get scorm attempt count
*/
public function test_mod_scorm_get_scorm_attempt_count_own_empty(): void {
// Set to the student user.
self::setUser($this->student);
// Retrieve my attempts (should be 0).
$result = mod_scorm_external::get_scorm_attempt_count($this->scorm->id, $this->student->id);
$result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_attempt_count_returns(), $result);
$this->assertEquals(0, $result['attemptscount']);
}
public function test_mod_scorm_get_scorm_attempt_count_own_with_complete(): void {
// Set to the student user.
self::setUser($this->student);
// Create attempts.
$scoes = scorm_get_scoes($this->scorm->id);
$sco = array_shift($scoes);
scorm_insert_track($this->student->id, $this->scorm->id, $sco->id, 1, 'cmi.core.lesson_status', 'completed');
scorm_insert_track($this->student->id, $this->scorm->id, $sco->id, 2, 'cmi.core.lesson_status', 'completed');
$result = mod_scorm_external::get_scorm_attempt_count($this->scorm->id, $this->student->id);
$result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_attempt_count_returns(), $result);
$this->assertEquals(2, $result['attemptscount']);
}
public function test_mod_scorm_get_scorm_attempt_count_own_incomplete(): void {
// Set to the student user.
self::setUser($this->student);
// Create a complete attempt, and an incomplete attempt.
$scoes = scorm_get_scoes($this->scorm->id);
$sco = array_shift($scoes);
scorm_insert_track($this->student->id, $this->scorm->id, $sco->id, 1, 'cmi.core.lesson_status', 'completed');
scorm_insert_track($this->student->id, $this->scorm->id, $sco->id, 2, 'cmi.core.credit', '0');
$result = mod_scorm_external::get_scorm_attempt_count($this->scorm->id, $this->student->id, true);
$result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_attempt_count_returns(), $result);
$this->assertEquals(1, $result['attemptscount']);
}
public function test_mod_scorm_get_scorm_attempt_count_others_as_teacher(): void {
// As a teacher.
self::setUser($this->teacher);
// Create a completed attempt for student.
$scoes = scorm_get_scoes($this->scorm->id);
$sco = array_shift($scoes);
scorm_insert_track($this->student->id, $this->scorm->id, $sco->id, 1, 'cmi.core.lesson_status', 'completed');
// I should be able to view the attempts for my students.
$result = mod_scorm_external::get_scorm_attempt_count($this->scorm->id, $this->student->id);
$result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_attempt_count_returns(), $result);
$this->assertEquals(1, $result['attemptscount']);
}
public function test_mod_scorm_get_scorm_attempt_count_others_as_student(): void {
// Create a second student.
$student2 = self::getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($student2->id, $this->course->id, $this->studentrole->id, 'manual');
// As a student.
self::setUser($student2);
// I should not be able to view the attempts of another student.
$this->expectException(\required_capability_exception::class);
mod_scorm_external::get_scorm_attempt_count($this->scorm->id, $this->student->id);
}
public function test_mod_scorm_get_scorm_attempt_count_invalid_instanceid(): void {
// As student.
self::setUser($this->student);
// Test invalid instance id.
$this->expectException(\moodle_exception::class);
mod_scorm_external::get_scorm_attempt_count(0, $this->student->id);
}
public function test_mod_scorm_get_scorm_attempt_count_invalid_userid(): void {
// As student.
self::setUser($this->student);
$this->expectException(\moodle_exception::class);
mod_scorm_external::get_scorm_attempt_count($this->scorm->id, -1);
}
/**
* Test get scorm scoes
*/
public function test_mod_scorm_get_scorm_scoes(): void {
global $DB;
$this->resetAfterTest(true);
// Create users.
$student = self::getDataGenerator()->create_user();
$teacher = self::getDataGenerator()->create_user();
// Create courses to add the modules.
$course = self::getDataGenerator()->create_course();
// First scorm, dates restriction.
$record = new \stdClass();
$record->course = $course->id;
$record->timeopen = time() + DAYSECS;
$record->timeclose = $record->timeopen + DAYSECS;
$scorm = self::getDataGenerator()->create_module('scorm', $record);
// Set to the student user.
self::setUser($student);
// Users enrolments.
$studentrole = $DB->get_record('role', array('shortname' => 'student'));
$teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
$this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual');
$this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id, 'manual');
// Retrieve my scoes, warning!.
try {
mod_scorm_external::get_scorm_scoes($scorm->id);
$this->fail('Exception expected due to invalid dates.');
} catch (\moodle_exception $e) {
$this->assertEquals('notopenyet', $e->errorcode);
}
$scorm->timeopen = time() - DAYSECS;
$scorm->timeclose = time() - HOURSECS;
$DB->update_record('scorm', $scorm);
try {
mod_scorm_external::get_scorm_scoes($scorm->id);
$this->fail('Exception expected due to invalid dates.');
} catch (\moodle_exception $e) {
$this->assertEquals('expired', $e->errorcode);
}
// Retrieve my scoes, user with permission.
self::setUser($teacher);
$result = mod_scorm_external::get_scorm_scoes($scorm->id);
$result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_scoes_returns(), $result);
$this->assertCount(2, $result['scoes']);
$this->assertCount(0, $result['warnings']);
$scoes = scorm_get_scoes($scorm->id);
$sco = array_shift($scoes);
$sco->extradata = array();
$this->assertEquals((array) $sco, $result['scoes'][0]);
$sco = array_shift($scoes);
$sco->extradata = array();
$sco->extradata[] = array(
'element' => 'isvisible',
'value' => $sco->isvisible
);
$sco->extradata[] = array(
'element' => 'parameters',
'value' => $sco->parameters
);
unset($sco->isvisible);
unset($sco->parameters);
// Sort the array (if we don't sort tests will fails for Postgres).
usort($result['scoes'][1]['extradata'], function($a, $b) {
return strcmp($a['element'], $b['element']);
});
$this->assertEquals((array) $sco, $result['scoes'][1]);
// Use organization.
$organization = 'golf_sample_default_org';
$result = mod_scorm_external::get_scorm_scoes($scorm->id, $organization);
$result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_scoes_returns(), $result);
$this->assertCount(1, $result['scoes']);
$this->assertEquals($organization, $result['scoes'][0]['organization']);
$this->assertCount(0, $result['warnings']);
// Test invalid instance id.
try {
mod_scorm_external::get_scorm_scoes(0);
$this->fail('Exception expected due to invalid instance id.');
} catch (\moodle_exception $e) {
$this->assertEquals('invalidrecord', $e->errorcode);
}
}
/**
* Test get scorm scoes (with a complex SCORM package)
*/
public function test_mod_scorm_get_scorm_scoes_complex_package(): void {
global $CFG;
// As student.
self::setUser($this->student);
$record = new \stdClass();
$record->course = $this->course->id;
$record->packagefilepath = $CFG->dirroot.'/mod/scorm/tests/packages/complexscorm.zip';
$scorm = self::getDataGenerator()->create_module('scorm', $record);
$result = mod_scorm_external::get_scorm_scoes($scorm->id);
$result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_scoes_returns(), $result);
$this->assertCount(9, $result['scoes']);
$this->assertCount(0, $result['warnings']);
$expectedscoes = array();
$scoreturnstructure = mod_scorm_external::get_scorm_scoes_returns();
$scoes = scorm_get_scoes($scorm->id);
foreach ($scoes as $sco) {
$sco->extradata = array();
foreach ($sco as $element => $value) {
// Add the extra data to the extradata array and remove the object element.
if (!isset($scoreturnstructure->keys['scoes']->content->keys[$element])) {
$sco->extradata[] = array(
'element' => $element,
'value' => $value
);
unset($sco->{$element});
}
}
$expectedscoes[] = (array) $sco;
}
$this->assertEquals($expectedscoes, $result['scoes']);
}
/*
* Test get scorm user data
*/
public function test_mod_scorm_get_scorm_user_data(): void {
global $DB;
$this->resetAfterTest(true);
// Create users.
$student1 = self::getDataGenerator()->create_user();
$teacher = self::getDataGenerator()->create_user();
// Set to the student user.
self::setUser($student1);
// Create courses to add the modules.
$course = self::getDataGenerator()->create_course();
// First scorm.
$record = new \stdClass();
$record->course = $course->id;
$scorm = self::getDataGenerator()->create_module('scorm', $record);
// Users enrolments.
$studentrole = $DB->get_record('role', array('shortname' => 'student'));
$teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
$this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id, 'manual');
$this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id, 'manual');
// Create attempts.
$scoes = scorm_get_scoes($scorm->id);
$sco = array_shift($scoes);
scorm_insert_track($student1->id, $scorm->id, $sco->id, 1, 'cmi.core.lesson_status', 'completed');
scorm_insert_track($student1->id, $scorm->id, $sco->id, 1, 'cmi.core.score.raw', '80');
scorm_insert_track($student1->id, $scorm->id, $sco->id, 2, 'cmi.core.lesson_status', 'completed');
$result = mod_scorm_external::get_scorm_user_data($scorm->id, 1);
$result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_user_data_returns(), $result);
$this->assertCount(2, $result['data']);
// Find our tracking data.
$found = 0;
foreach ($result['data'] as $scodata) {
foreach ($scodata['userdata'] as $userdata) {
if ($userdata['element'] == 'cmi.core.lesson_status' and $userdata['value'] == 'completed') {
$found++;
}
if ($userdata['element'] == 'cmi.core.score.raw' and $userdata['value'] == '80') {
$found++;
}
}
}
$this->assertEquals(2, $found);
// Test invalid instance id.
try {
mod_scorm_external::get_scorm_user_data(0, 1);
$this->fail('Exception expected due to invalid instance id.');
} catch (\moodle_exception $e) {
$this->assertEquals('invalidrecord', $e->errorcode);
}
}
/**
* Test insert scorm tracks
*/
public function test_mod_scorm_insert_scorm_tracks(): void {
global $DB;
$this->resetAfterTest(true);
// Create users.
$student = self::getDataGenerator()->create_user();
// Create courses to add the modules.
$course = self::getDataGenerator()->create_course();
// First scorm, dates restriction.
$record = new \stdClass();
$record->course = $course->id;
$record->timeopen = time() + DAYSECS;
$record->timeclose = $record->timeopen + DAYSECS;
$scorm = self::getDataGenerator()->create_module('scorm', $record);
// Get a SCO.
$scoes = scorm_get_scoes($scorm->id);
$sco = array_shift($scoes);
// Tracks.
$tracks = array();
$tracks[] = array(
'element' => 'cmi.core.lesson_status',
'value' => 'completed'
);
$tracks[] = array(
'element' => 'cmi.core.score.raw',
'value' => '80'
);
// Set to the student user.
self::setUser($student);
// Users enrolments.
$studentrole = $DB->get_record('role', array('shortname' => 'student'));
$this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual');
// Exceptions first.
try {
mod_scorm_external::insert_scorm_tracks($sco->id, 1, $tracks);
$this->fail('Exception expected due to dates');
} catch (\moodle_exception $e) {
$this->assertEquals('notopenyet', $e->errorcode);
}
$scorm->timeopen = time() - DAYSECS;
$scorm->timeclose = time() - HOURSECS;
$DB->update_record('scorm', $scorm);
try {
mod_scorm_external::insert_scorm_tracks($sco->id, 1, $tracks);
$this->fail('Exception expected due to dates');
} catch (\moodle_exception $e) {
$this->assertEquals('expired', $e->errorcode);
}
// Test invalid instance id.
try {
mod_scorm_external::insert_scorm_tracks(0, 1, $tracks);
$this->fail('Exception expected due to invalid sco id.');
} catch (\moodle_exception $e) {
$this->assertEquals('cannotfindsco', $e->errorcode);
}
$scorm->timeopen = 0;
$scorm->timeclose = 0;
$DB->update_record('scorm', $scorm);
// Retrieve my tracks.
$result = mod_scorm_external::insert_scorm_tracks($sco->id, 1, $tracks);
$result = external_api::clean_returnvalue(mod_scorm_external::insert_scorm_tracks_returns(), $result);
$this->assertCount(0, $result['warnings']);
$sql = "SELECT v.id
FROM {scorm_scoes_value} v
JOIN {scorm_attempt} a ON a.id = v.attemptid
WHERE a.userid = :userid AND a.attempt = :attempt AND a.scormid = :scormid AND v.scoid = :scoid";
$params = ['userid' => $student->id, 'scoid' => $sco->id, 'scormid' => $scorm->id, 'attempt' => 1];
$trackids = $DB->get_records_sql($sql, $params);
// We use asort here to prevent problems with ids ordering.
$expectedkeys = array_keys($trackids);
$this->assertEquals(asort($expectedkeys), asort($result['trackids']));
}
/**
* Test get scorm sco tracks
*/
public function test_mod_scorm_get_scorm_sco_tracks(): void {
global $DB;
$this->resetAfterTest(true);
// Create users.
$student = self::getDataGenerator()->create_user();
$otherstudent = self::getDataGenerator()->create_user();
$teacher = self::getDataGenerator()->create_user();
// Set to the student user.
self::setUser($student);
// Create courses to add the modules.
$course = self::getDataGenerator()->create_course();
// First scorm.
$record = new \stdClass();
$record->course = $course->id;
$scorm = self::getDataGenerator()->create_module('scorm', $record);
// Users enrolments.
$studentrole = $DB->get_record('role', array('shortname' => 'student'));
$teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
$this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual');
$this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id, 'manual');
// Create attempts.
$scoes = scorm_get_scoes($scorm->id);
$sco = array_shift($scoes);
scorm_insert_track($student->id, $scorm->id, $sco->id, 1, 'cmi.core.lesson_status', 'completed');
scorm_insert_track($student->id, $scorm->id, $sco->id, 1, 'cmi.core.score.raw', '80');
scorm_insert_track($student->id, $scorm->id, $sco->id, 2, 'cmi.core.lesson_status', 'completed');
$result = mod_scorm_external::get_scorm_sco_tracks($sco->id, $student->id, 1);
$result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_sco_tracks_returns(), $result);
// 7 default elements + 2 custom ones.
$this->assertCount(9, $result['data']['tracks']);
$this->assertEquals(1, $result['data']['attempt']);
$this->assertCount(0, $result['warnings']);
// Find our tracking data.
$found = 0;
foreach ($result['data']['tracks'] as $userdata) {
if ($userdata['element'] == 'cmi.core.lesson_status' and $userdata['value'] == 'completed') {
$found++;
}
if ($userdata['element'] == 'cmi.core.score.raw' and $userdata['value'] == '80') {
$found++;
}
}
$this->assertEquals(2, $found);
// Try invalid attempt.
$result = mod_scorm_external::get_scorm_sco_tracks($sco->id, $student->id, 10);
$result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_sco_tracks_returns(), $result);
$this->assertCount(0, $result['data']['tracks']);
$this->assertEquals(10, $result['data']['attempt']);
$this->assertCount(1, $result['warnings']);
$this->assertEquals('notattempted', $result['warnings'][0]['warningcode']);
// Capabilities check.
try {
mod_scorm_external::get_scorm_sco_tracks($sco->id, $otherstudent->id);
$this->fail('Exception expected due to invalid instance id.');
} catch (\required_capability_exception $e) {
$this->assertEquals('nopermissions', $e->errorcode);
}
self::setUser($teacher);
// Ommit the attempt parameter, the function should calculate the last attempt.
$result = mod_scorm_external::get_scorm_sco_tracks($sco->id, $student->id);
$result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_sco_tracks_returns(), $result);
// 7 default elements + 1 custom one.
$this->assertCount(8, $result['data']['tracks']);
$this->assertEquals(2, $result['data']['attempt']);
// Test invalid instance id.
try {
mod_scorm_external::get_scorm_sco_tracks(0, 1);
$this->fail('Exception expected due to invalid instance id.');
} catch (\moodle_exception $e) {
$this->assertEquals('cannotfindsco', $e->errorcode);
}
// Invalid user.
try {
mod_scorm_external::get_scorm_sco_tracks($sco->id, 0);
$this->fail('Exception expected due to invalid instance id.');
} catch (\moodle_exception $e) {
$this->assertEquals('invaliduser', $e->errorcode);
}
}
/*
* Test get scorms by courses
*/
public function test_mod_scorm_get_scorms_by_courses(): void {
global $DB;
$this->resetAfterTest(true);
// Create users.
$student = self::getDataGenerator()->create_user();
$teacher = self::getDataGenerator()->create_user();
// Set to the student user.
self::setUser($student);
// Create courses to add the modules.
$course1 = self::getDataGenerator()->create_course();
$course2 = self::getDataGenerator()->create_course();
// First scorm.
$record = new \stdClass();
$record->introformat = FORMAT_HTML;
$record->course = $course1->id;
$record->hidetoc = 2;
$record->displayattemptstatus = 2;
$record->skipview = 2;
$scorm1 = self::getDataGenerator()->create_module('scorm', $record);
// Second scorm.
$record = new \stdClass();
$record->introformat = FORMAT_HTML;
$record->course = $course2->id;
$scorm2 = self::getDataGenerator()->create_module('scorm', $record);
$studentrole = $DB->get_record('role', array('shortname' => 'student'));
$teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
// Users enrolments.
$this->getDataGenerator()->enrol_user($student->id, $course1->id, $studentrole->id, 'manual');
$this->getDataGenerator()->enrol_user($teacher->id, $course1->id, $teacherrole->id, 'manual');
// Execute real Moodle enrolment as we'll call unenrol() method on the instance later.
$enrol = enrol_get_plugin('manual');
$enrolinstances = enrol_get_instances($course2->id, true);
foreach ($enrolinstances as $courseenrolinstance) {
if ($courseenrolinstance->enrol == "manual") {
$instance2 = $courseenrolinstance;
break;
}
}
$enrol->enrol_user($instance2, $student->id, $studentrole->id);
$returndescription = mod_scorm_external::get_scorms_by_courses_returns();
// Test open/close dates.
$timenow = time();
$scorm1->timeopen = $timenow - DAYSECS;
$scorm1->timeclose = $timenow - HOURSECS;
$DB->update_record('scorm', $scorm1);
$result = mod_scorm_external::get_scorms_by_courses(array($course1->id));
$result = external_api::clean_returnvalue($returndescription, $result);
// Test default SCORM settings.
$this->assertCount(1, $result['options']);
$this->assertEquals('scormstandard', $result['options'][0]['name']);
$this->assertEquals(0, $result['options'][0]['value']);
$this->assertCount(1, $result['warnings']);
// Only 'id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles'.
$this->assertCount(8, $result['scorms'][0]);
$this->assertEquals('expired', $result['warnings'][0]['warningcode']);
$scorm1->timeopen = $timenow + DAYSECS;
$scorm1->timeclose = $scorm1->timeopen + DAYSECS;
$DB->update_record('scorm', $scorm1);
// Set the SCORM config values.
set_config('scormstandard', 1, 'scorm');
$result = mod_scorm_external::get_scorms_by_courses(array($course1->id));
$result = external_api::clean_returnvalue($returndescription, $result);
// Test SCORM settings.
$this->assertCount(1, $result['options']);
$this->assertEquals('scormstandard', $result['options'][0]['name']);
$this->assertEquals(1, $result['options'][0]['value']);
$this->assertCount(1, $result['warnings']);
// Only 'id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles'.
$this->assertCount(8, $result['scorms'][0]);
$this->assertEquals('notopenyet', $result['warnings'][0]['warningcode']);
// Reset times.
$scorm1->timeopen = 0;
$scorm1->timeclose = 0;
$DB->update_record('scorm', $scorm1);
// Create what we expect to be returned when querying the two courses.
// First for the student user.
$expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'lang', 'version', 'maxgrade',
'grademethod', 'whatgrade', 'maxattempt', 'forcecompleted', 'forcenewattempt', 'lastattemptlock',
'displayattemptstatus', 'displaycoursestructure', 'sha1hash', 'md5hash', 'revision', 'launch',
'skipview', 'hidebrowse', 'hidetoc', 'nav', 'navpositionleft', 'navpositiontop', 'auto',
'popup', 'width', 'height', 'timeopen', 'timeclose', 'packagesize',
'packageurl', 'scormtype', 'reference');
// Add expected coursemodule and data.
$scorm1->coursemodule = $scorm1->cmid;
$scorm1->section = 0;
$scorm1->visible = true;
$scorm1->groupmode = 0;
$scorm1->groupingid = 0;
$scorm1->lang = '';
$scorm2->coursemodule = $scorm2->cmid;
$scorm2->section = 0;
$scorm2->visible = true;
$scorm2->groupmode = 0;
$scorm2->groupingid = 0;
$scorm2->lang = '';
// SCORM size. The same package is used in both SCORMs.
$scormcontext1 = \context_module::instance($scorm1->cmid);
$scormcontext2 = \context_module::instance($scorm2->cmid);
$fs = get_file_storage();
$packagefile = $fs->get_file($scormcontext1->id, 'mod_scorm', 'package', 0, '/', $scorm1->reference);
$packagesize = $packagefile->get_filesize();
$packageurl1 = \moodle_url::make_webservice_pluginfile_url(
$scormcontext1->id, 'mod_scorm', 'package', 0, '/', $scorm1->reference)->out(false);
$packageurl2 = \moodle_url::make_webservice_pluginfile_url(
$scormcontext2->id, 'mod_scorm', 'package', 0, '/', $scorm2->reference)->out(false);
$scorm1->packagesize = $packagesize;
$scorm1->packageurl = $packageurl1;
$scorm2->packagesize = $packagesize;
$scorm2->packageurl = $packageurl2;
// Forced to boolean as it is returned as PARAM_BOOL.
$protectpackages = (bool)get_config('scorm', 'protectpackagedownloads');
$expected1 = array('protectpackagedownloads' => $protectpackages);
$expected2 = array('protectpackagedownloads' => $protectpackages);
foreach ($expectedfields as $field) {
// Since we return the fields used as boolean as PARAM_BOOL instead PARAM_INT we need to force casting here.
// From the returned fields definition we obtain the type expected for the field.
if (empty($returndescription->keys['scorms']->content->keys[$field]->type)) {
continue;
}
$fieldtype = $returndescription->keys['scorms']->content->keys[$field]->type;
if ($fieldtype == PARAM_BOOL) {
$expected1[$field] = (bool) $scorm1->{$field};
$expected2[$field] = (bool) $scorm2->{$field};
} else {
$expected1[$field] = $scorm1->{$field};
$expected2[$field] = $scorm2->{$field};
}
}
$expected1['introfiles'] = [];
$expected2['introfiles'] = [];
$expectedscorms = array();
$expectedscorms[] = $expected2;
$expectedscorms[] = $expected1;
// Call the external function passing course ids.
$result = mod_scorm_external::get_scorms_by_courses(array($course2->id, $course1->id));
$result = external_api::clean_returnvalue($returndescription, $result);
$this->assertEquals($expectedscorms, $result['scorms']);
// Call the external function without passing course id.
$result = mod_scorm_external::get_scorms_by_courses();
$result = external_api::clean_returnvalue($returndescription, $result);
$this->assertEquals($expectedscorms, $result['scorms']);
// Unenrol user from second course and alter expected scorms.
$enrol->unenrol_user($instance2, $student->id);
array_shift($expectedscorms);
// Call the external function without passing course id.
$result = mod_scorm_external::get_scorms_by_courses();
$result = external_api::clean_returnvalue($returndescription, $result);
$this->assertEquals($expectedscorms, $result['scorms']);
// Call for the second course we unenrolled the user from, expected warning.
$result = mod_scorm_external::get_scorms_by_courses(array($course2->id));
$this->assertCount(1, $result['options']);
$this->assertCount(1, $result['warnings']);
$this->assertEquals('1', $result['warnings'][0]['warningcode']);
$this->assertEquals($course2->id, $result['warnings'][0]['itemid']);
// Now, try as a teacher for getting all the additional fields.
self::setUser($teacher);
$additionalfields = array('updatefreq', 'timemodified', 'options',
'completionstatusrequired', 'completionscorerequired', 'completionstatusallscos',
'autocommit', 'section', 'visible', 'groupmode', 'groupingid');
foreach ($additionalfields as $field) {
$fieldtype = $returndescription->keys['scorms']->content->keys[$field]->type;
if ($fieldtype == PARAM_BOOL) {
$expectedscorms[0][$field] = (bool) $scorm1->{$field};
} else {
$expectedscorms[0][$field] = $scorm1->{$field};
}
}
$result = mod_scorm_external::get_scorms_by_courses();
$result = external_api::clean_returnvalue($returndescription, $result);
$this->assertEquals($expectedscorms, $result['scorms']);
// Even with the SCORM closed in time teacher should retrieve the info.
$scorm1->timeopen = $timenow - DAYSECS;
$scorm1->timeclose = $timenow - HOURSECS;
$DB->update_record('scorm', $scorm1);
$expectedscorms[0]['timeopen'] = $scorm1->timeopen;
$expectedscorms[0]['timeclose'] = $scorm1->timeclose;
$result = mod_scorm_external::get_scorms_by_courses();
$result = external_api::clean_returnvalue($returndescription, $result);
$this->assertEquals($expectedscorms, $result['scorms']);
// Admin also should get all the information.
self::setAdminUser();
$result = mod_scorm_external::get_scorms_by_courses(array($course1->id));
$result = external_api::clean_returnvalue($returndescription, $result);
$this->assertEquals($expectedscorms, $result['scorms']);
}
/**
* Test launch_sco
*/
public function test_launch_sco(): void {
global $DB;
// Test invalid instance id.
try {
mod_scorm_external::launch_sco(0);
$this->fail('Exception expected due to invalid mod_scorm instance id.');
} catch (\moodle_exception $e) {
$this->assertEquals('invalidrecord', $e->errorcode);
}
// Test not-enrolled user.
$user = self::getDataGenerator()->create_user();
$this->setUser($user);
try {
mod_scorm_external::launch_sco($this->scorm->id);
$this->fail('Exception expected due to not enrolled user.');
} catch (\moodle_exception $e) {
$this->assertEquals('requireloginerror', $e->errorcode);
}
// Test user with full capabilities.
$this->setUser($this->student);
// Trigger and capture the event.
$sink = $this->redirectEvents();
$scoes = scorm_get_scoes($this->scorm->id);
foreach ($scoes as $sco) {
// Find launchable SCO.
if ($sco->launch != '') {
break;
}
}
$result = mod_scorm_external::launch_sco($this->scorm->id, $sco->id);
$result = external_api::clean_returnvalue(mod_scorm_external::launch_sco_returns(), $result);
$events = $sink->get_events();
$this->assertCount(3, $events);
$event = array_pop($events);
// Checking that the event contains the expected values.
$this->assertInstanceOf('\mod_scorm\event\sco_launched', $event);
$this->assertEquals($this->context, $event->get_context());
$moodleurl = new \moodle_url('/mod/scorm/player.php', array('cm' => $this->cm->id, 'scoid' => $sco->id));
$this->assertEquals($moodleurl, $event->get_url());
$this->assertEventContextNotUsed($event);
$this->assertNotEmpty($event->get_name());
$event = array_shift($events);
$this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
// Check completion status.
$completion = new \completion_info($this->course);
$completiondata = $completion->get_data($this->cm);
$this->assertEquals(COMPLETION_VIEWED, $completiondata->completionstate);
// Invalid SCO.
try {
mod_scorm_external::launch_sco($this->scorm->id, -1);
$this->fail('Exception expected due to invalid SCO id.');
} catch (\moodle_exception $e) {
$this->assertEquals('cannotfindsco', $e->errorcode);
}
}
/**
* Test mod_scorm_get_scorm_access_information.
*/
public function test_mod_scorm_get_scorm_access_information(): void {
global $DB;
$this->resetAfterTest(true);
$student = self::getDataGenerator()->create_user();
$course = self::getDataGenerator()->create_course();
// Create the scorm.
$record = new \stdClass();
$record->course = $course->id;
$scorm = self::getDataGenerator()->create_module('scorm', $record);
$context = \context_module::instance($scorm->cmid);
$studentrole = $DB->get_record('role', array('shortname' => 'student'));
$this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual');
self::setUser($student);
$result = mod_scorm_external::get_scorm_access_information($scorm->id);
$result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_access_information_returns(), $result);
// Check default values for capabilities.
$enabledcaps = array('canskipview', 'cansavetrack', 'canviewscores');
unset($result['warnings']);
foreach ($result as $capname => $capvalue) {
if (in_array($capname, $enabledcaps)) {
$this->assertTrue($capvalue);
} else {
$this->assertFalse($capvalue);
}
}
// Now, unassign one capability.
unassign_capability('mod/scorm:viewscores', $studentrole->id);
array_pop($enabledcaps);
accesslib_clear_all_caches_for_unit_testing();
$result = mod_scorm_external::get_scorm_access_information($scorm->id);
$result = external_api::clean_returnvalue(mod_scorm_external::get_scorm_access_information_returns(), $result);
unset($result['warnings']);
foreach ($result as $capname => $capvalue) {
if (in_array($capname, $enabledcaps)) {
$this->assertTrue($capvalue);
} else {
$this->assertFalse($capvalue);
}
}
}
}
+88
View File
@@ -0,0 +1,88 @@
<?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_scorm;
defined('MOODLE_INTERNAL') || die;
// Make sure the code being tested is accessible.
global $CFG;
require_once($CFG->dirroot . '/mod/scorm/locallib.php'); // Include the code to test.
/**
* Unit tests for scorm_formatduration function from locallib.php
*
* @package mod_scorm
* @category test
* @copyright 2009 Dan Marsden
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class formatduration_test extends \basic_testcase {
public function test_scorm2004_format(): void {
$stryears = get_string('years');
$strmonths = trim(get_string('nummonths'));
$strdays = get_string('days');
$strhours = get_string('hours');
$strminutes = get_string('minutes');
$strseconds = get_string('seconds');
$suts = array(1 => 'PT001H012M0043.12S', 2 => 'PT15.3S', 3 => 'P01Y02M5DT0H7M', 4 => 'P0Y0M0DT0H1M00.00S',
5 => 'P1YT15M00.01S', 6 => 'P0Y0M0DT0H0M0.0S', 7 => 'P1MT4M0.30S', 8 => 'PT', 9 => 'P1DT2H3S', 10 => 'P4M');
$validates = array(1 => "1 $strhours 12 $strminutes 43.12 $strseconds",
2 => "15.3 $strseconds",
3 => "1 $stryears 2 $strmonths 5 $strdays 7 $strminutes ",
4 => "1 $strminutes ",
5 => "1 $stryears 15 $strminutes 0.01 $strseconds",
6 => '',
7 => "1 $strmonths 4 $strminutes 0.30 $strseconds",
8 => '',
9 => "1 $strdays 2 $strhours 3 $strseconds",
10 => "4 $strmonths ");
foreach ($suts as $key => $sut) {
$formatted = scorm_format_duration($sut);
$this->assertEquals($formatted, $validates[$key]);
}
}
public function test_scorm12_format(): void {
$stryears = get_string('years');
$strmonths = trim(get_string('nummonths'));
$strdays = get_string('days');
$strhours = get_string('hours');
$strminutes = get_string('minutes');
$strseconds = get_string('seconds');
$suts = array(1 => '00:00:00', 2 => '1:2:3', 3 => '12:34:56.78', 4 => '00:12:00.03', 5 => '01:00:23', 6 => '00:12:34.00',
7 => '00:01:02.03', 8 => '00:00:00.1', 9 => '1:23:00', 10 => '2:00:00');
$validates = array(1 => '',
2 => "1 $strhours 2 $strminutes 3 $strseconds",
3 => "12 $strhours 34 $strminutes 56.78 $strseconds",
4 => "12 $strminutes 0.03 $strseconds",
5 => "1 $strhours 23 $strseconds",
6 => "12 $strminutes 34 $strseconds",
7 => "1 $strminutes 2.03 $strseconds",
8 => "0.1 $strseconds",
9 => "1 $strhours 23 $strminutes ",
10 => "2 $strhours ");
foreach ($suts as $key => $sut) {
$formatted = scorm_format_duration($sut);
$this->assertEquals($formatted, $validates[$key]);
}
}
public function test_non_datetime(): void {
}
}
+103
View File
@@ -0,0 +1,103 @@
<?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_scorm data generator.
*
* @package mod_scorm
* @category test
* @copyright 2013 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* mod_scorm data generator class.
*
* @package mod_scorm
* @category test
* @copyright 2013 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_scorm_generator extends testing_module_generator {
public function create_instance($record = null, array $options = null) {
global $CFG, $USER;
require_once($CFG->dirroot.'/mod/scorm/lib.php');
require_once($CFG->dirroot.'/mod/scorm/locallib.php');
$cfgscorm = get_config('scorm');
// Add default values for scorm.
$record = (array)$record + array(
'scormtype' => SCORM_TYPE_LOCAL,
'packagefile' => '',
'packageurl' => '',
'updatefreq' => SCORM_UPDATE_NEVER,
'popup' => 0,
'width' => $cfgscorm->framewidth,
'height' => $cfgscorm->frameheight,
'skipview' => $cfgscorm->skipview,
'hidebrowse' => $cfgscorm->hidebrowse,
'displaycoursestructure' => $cfgscorm->displaycoursestructure,
'hidetoc' => $cfgscorm->hidetoc,
'nav' => $cfgscorm->nav,
'navpositionleft' => $cfgscorm->navpositionleft,
'navpositiontop' => $cfgscorm->navpositiontop,
'displayattemptstatus' => $cfgscorm->displayattemptstatus,
'timeopen' => 0,
'timeclose' => 0,
'grademethod' => GRADESCOES,
'maxgrade' => $cfgscorm->maxgrade,
'maxattempt' => $cfgscorm->maxattempt,
'whatgrade' => $cfgscorm->whatgrade,
'forcenewattempt' => $cfgscorm->forcenewattempt,
'lastattemptlock' => $cfgscorm->lastattemptlock,
'forcecompleted' => $cfgscorm->forcecompleted,
'masteryoverride' => $cfgscorm->masteryoverride,
'auto' => $cfgscorm->auto
);
if (empty($record['packagefilepath'])) {
$record['packagefilepath'] = $CFG->dirroot.'/mod/scorm/tests/packages/singlescobasic.zip';
}
if (strpos($record['packagefilepath'], $CFG->dirroot) !== 0) {
$record['packagefilepath'] = "{$CFG->dirroot}/{$record['packagefilepath']}";
}
// The 'packagefile' value corresponds to the draft file area ID. If not specified, create from packagefilepath.
if (empty($record['packagefile']) && $record['scormtype'] === SCORM_TYPE_LOCAL) {
if (!isloggedin() || isguestuser()) {
throw new coding_exception('Scorm 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 = array('component' => 'user', 'filearea' => 'draft',
'contextid' => $usercontext->id, 'itemid' => $record['packagefile'],
'filename' => basename($record['packagefilepath']), 'filepath' => '/');
$fs = get_file_storage();
$fs->create_file_from_pathname($filerecord, $record['packagefilepath']);
}
return parent::create_instance($record, (array)$options);
}
}
+69
View File
@@ -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/>.
namespace mod_scorm;
/**
* Genarator tests class for mod_scorm.
*
* @package mod_scorm
* @category test
* @copyright 2013 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class generator_test extends \advanced_testcase {
public function test_create_instance(): void {
global $DB, $CFG, $USER;
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$this->assertFalse($DB->record_exists('scorm', array('course' => $course->id)));
$scorm = $this->getDataGenerator()->create_module('scorm', array('course' => $course));
$records = $DB->get_records('scorm', array('course' => $course->id), 'id');
$this->assertEquals(1, count($records));
$this->assertTrue(array_key_exists($scorm->id, $records));
$params = array('course' => $course->id, 'name' => 'Another scorm');
$scorm = $this->getDataGenerator()->create_module('scorm', $params);
$records = $DB->get_records('scorm', array('course' => $course->id), 'id');
$this->assertEquals(2, count($records));
$this->assertEquals('Another scorm', $records[$scorm->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 = array(
'course' => $course->id,
'packagefilepath' => $CFG->dirroot.'/mod/scorm/tests/packages/singlescobasic.zip'
);
$scorm = $this->getDataGenerator()->create_module('scorm', $params);
// 2. As file draft area id.
$fs = get_file_storage();
$params = array(
'course' => $course->id,
'packagefile' => file_get_unused_draft_itemid()
);
$usercontext = \context_user::instance($USER->id);
$filerecord = array('component' => 'user', 'filearea' => 'draft',
'contextid' => $usercontext->id, 'itemid' => $params['packagefile'],
'filename' => 'singlescobasic.zip', 'filepath' => '/');
$fs->create_file_from_pathname($filerecord, $CFG->dirroot.'/mod/scorm/tests/packages/singlescobasic.zip');
$scorm = $this->getDataGenerator()->create_module('scorm', $params);
}
}
+908
View File
@@ -0,0 +1,908 @@
<?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/>.
/**
* SCORM module library functions tests
*
* @package mod_scorm
* @category test
* @copyright 2015 Juan Leyva <juan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 3.0
*/
namespace mod_scorm;
use mod_scorm_get_completion_active_rule_descriptions;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
require_once($CFG->dirroot . '/mod/scorm/lib.php');
/**
* SCORM module library functions tests
*
* @package mod_scorm
* @category test
* @copyright 2015 Juan Leyva <juan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 3.0
*/
class lib_test extends \advanced_testcase {
/** @var \stdClass course record. */
protected \stdClass $course;
/** @var \stdClass activity record. */
protected \stdClass $scorm;
/** @var \core\context\module context instance. */
protected \core\context\module $context;
/** @var \stdClass */
protected \stdClass $cm;
/** @var \stdClass user record. */
protected \stdClass $student;
/** @var \stdClass user record. */
protected \stdClass $teacher;
/** @var \stdClass a fieldset object, false or exception if error not found. */
protected \stdClass $studentrole;
/** @var \stdClass a fieldset object, false or exception if error not found. */
protected \stdClass $teacherrole;
/**
* Set up for every test
*/
public function setUp(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
// Setup test data.
$this->course = $this->getDataGenerator()->create_course();
$this->scorm = $this->getDataGenerator()->create_module('scorm', array('course' => $this->course->id));
$this->context = \context_module::instance($this->scorm->cmid);
$this->cm = get_coursemodule_from_instance('scorm', $this->scorm->id);
// Create users.
$this->student = self::getDataGenerator()->create_user();
$this->teacher = self::getDataGenerator()->create_user();
// Users enrolments.
$this->studentrole = $DB->get_record('role', array('shortname' => 'student'));
$this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
$this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual');
$this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual');
}
/** Test scorm_check_mode
*
* @return void
*/
public function test_scorm_check_mode(): void {
global $CFG;
$newattempt = 'on';
$attempt = 1;
$mode = 'normal';
scorm_check_mode($this->scorm, $newattempt, $attempt, $this->student->id, $mode);
$this->assertEquals('off', $newattempt);
$scoes = scorm_get_scoes($this->scorm->id);
$sco = array_pop($scoes);
scorm_insert_track($this->student->id, $this->scorm->id, $sco->id, 1, 'cmi.core.lesson_status', 'completed');
$newattempt = 'on';
scorm_check_mode($this->scorm, $newattempt, $attempt, $this->student->id, $mode);
$this->assertEquals('on', $newattempt);
// Now do the same with a SCORM 2004 package.
$record = new \stdClass();
$record->course = $this->course->id;
$record->packagefilepath = $CFG->dirroot.'/mod/scorm/tests/packages/RuntimeBasicCalls_SCORM20043rdEdition.zip';
$scorm13 = $this->getDataGenerator()->create_module('scorm', $record);
$newattempt = 'on';
$attempt = 1;
$mode = 'normal';
scorm_check_mode($scorm13, $newattempt, $attempt, $this->student->id, $mode);
$this->assertEquals('off', $newattempt);
$scoes = scorm_get_scoes($scorm13->id);
$sco = array_pop($scoes);
scorm_insert_track($this->student->id, $scorm13->id, $sco->id, 1, 'cmi.completion_status', 'completed');
$newattempt = 'on';
$attempt = 1;
$mode = 'normal';
scorm_check_mode($scorm13, $newattempt, $attempt, $this->student->id, $mode);
$this->assertEquals('on', $newattempt);
}
/**
* Test scorm_view
* @return void
*/
public function test_scorm_view(): void {
global $CFG;
// Trigger and capture the event.
$sink = $this->redirectEvents();
scorm_view($this->scorm, $this->course, $this->cm, $this->context);
$events = $sink->get_events();
$this->assertCount(1, $events);
$event = array_shift($events);
// Checking that the event contains the expected values.
$this->assertInstanceOf('\mod_scorm\event\course_module_viewed', $event);
$this->assertEquals($this->context, $event->get_context());
$url = new \moodle_url('/mod/scorm/view.php', array('id' => $this->cm->id));
$this->assertEquals($url, $event->get_url());
$this->assertEventContextNotUsed($event);
$this->assertNotEmpty($event->get_name());
}
/**
* Test scorm_get_availability_status and scorm_require_available
* @return void
*/
public function test_scorm_check_and_require_available(): void {
global $DB;
$this->setAdminUser();
// User override case.
$this->scorm->timeopen = time() + DAYSECS;
$this->scorm->timeclose = time() - DAYSECS;
list($status, $warnings) = scorm_get_availability_status($this->scorm, true, $this->context);
$this->assertEquals(true, $status);
$this->assertCount(0, $warnings);
// Now check with a student.
list($status, $warnings) = scorm_get_availability_status($this->scorm, true, $this->context, $this->student->id);
$this->assertEquals(false, $status);
$this->assertCount(2, $warnings);
$this->assertArrayHasKey('notopenyet', $warnings);
$this->assertArrayHasKey('expired', $warnings);
$this->assertEquals(userdate($this->scorm->timeopen), $warnings['notopenyet']);
$this->assertEquals(userdate($this->scorm->timeclose), $warnings['expired']);
// Reset the scorm's times.
$this->scorm->timeopen = $this->scorm->timeclose = 0;
// Set to the student user.
self::setUser($this->student);
// Usual case.
list($status, $warnings) = scorm_get_availability_status($this->scorm, false);
$this->assertEquals(true, $status);
$this->assertCount(0, $warnings);
// SCORM not open.
$this->scorm->timeopen = time() + DAYSECS;
list($status, $warnings) = scorm_get_availability_status($this->scorm, false);
$this->assertEquals(false, $status);
$this->assertCount(1, $warnings);
// SCORM closed.
$this->scorm->timeopen = 0;
$this->scorm->timeclose = time() - DAYSECS;
list($status, $warnings) = scorm_get_availability_status($this->scorm, false);
$this->assertEquals(false, $status);
$this->assertCount(1, $warnings);
// SCORM not open and closed.
$this->scorm->timeopen = time() + DAYSECS;
list($status, $warnings) = scorm_get_availability_status($this->scorm, false);
$this->assertEquals(false, $status);
$this->assertCount(2, $warnings);
// Now additional checkings with different parameters values.
list($status, $warnings) = scorm_get_availability_status($this->scorm, true, $this->context);
$this->assertEquals(false, $status);
$this->assertCount(2, $warnings);
// SCORM not open.
$this->scorm->timeopen = time() + DAYSECS;
$this->scorm->timeclose = 0;
list($status, $warnings) = scorm_get_availability_status($this->scorm, true, $this->context);
$this->assertEquals(false, $status);
$this->assertCount(1, $warnings);
// SCORM closed.
$this->scorm->timeopen = 0;
$this->scorm->timeclose = time() - DAYSECS;
list($status, $warnings) = scorm_get_availability_status($this->scorm, true, $this->context);
$this->assertEquals(false, $status);
$this->assertCount(1, $warnings);
// SCORM not open and closed.
$this->scorm->timeopen = time() + DAYSECS;
list($status, $warnings) = scorm_get_availability_status($this->scorm, true, $this->context);
$this->assertEquals(false, $status);
$this->assertCount(2, $warnings);
// As teacher now.
self::setUser($this->teacher);
// SCORM not open and closed.
$this->scorm->timeopen = time() + DAYSECS;
list($status, $warnings) = scorm_get_availability_status($this->scorm, false);
$this->assertEquals(false, $status);
$this->assertCount(2, $warnings);
// Now, we use the special capability.
// SCORM not open and closed.
$this->scorm->timeopen = time() + DAYSECS;
list($status, $warnings) = scorm_get_availability_status($this->scorm, true, $this->context);
$this->assertEquals(true, $status);
$this->assertCount(0, $warnings);
// Check exceptions does not broke anything.
scorm_require_available($this->scorm, true, $this->context);
// Now, expect exceptions.
$this->expectException('moodle_exception');
$this->expectExceptionMessage(get_string("notopenyet", "scorm", userdate($this->scorm->timeopen)));
// Now as student other condition.
self::setUser($this->student);
$this->scorm->timeopen = 0;
$this->scorm->timeclose = time() - DAYSECS;
$this->expectException('moodle_exception');
$this->expectExceptionMessage(get_string("expired", "scorm", userdate($this->scorm->timeclose)));
scorm_require_available($this->scorm, false);
}
/**
* Test scorm_get_last_completed_attempt
*
* @return void
*/
public function test_scorm_get_last_completed_attempt(): void {
$this->assertEquals(1, scorm_get_last_completed_attempt($this->scorm->id, $this->student->id));
}
public function test_scorm_core_calendar_provide_event_action_open(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a scorm activity.
$scorm = $this->getDataGenerator()->create_module('scorm', array('course' => $course->id,
'timeopen' => time() - DAYSECS, 'timeclose' => time() + DAYSECS));
// Create a calendar event.
$event = $this->create_action_event($course->id, $scorm->id, SCORM_EVENT_TYPE_OPEN);
// Only students see scorm events.
$this->setUser($this->student);
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Decorate action event.
$actionevent = mod_scorm_core_calendar_provide_event_action($event, $factory);
// Confirm the event was decorated.
$this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
$this->assertEquals(get_string('enter', 'scorm'), $actionevent->get_name());
$this->assertInstanceOf('moodle_url', $actionevent->get_url());
$this->assertEquals(1, $actionevent->get_item_count());
$this->assertTrue($actionevent->is_actionable());
}
public function test_scorm_core_calendar_provide_event_action_closed(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a scorm activity.
$scorm = $this->getDataGenerator()->create_module('scorm', array('course' => $course->id,
'timeclose' => time() - DAYSECS));
// Create a calendar event.
$event = $this->create_action_event($course->id, $scorm->id, SCORM_EVENT_TYPE_OPEN);
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Decorate action event.
$actionevent = mod_scorm_core_calendar_provide_event_action($event, $factory);
// No event on the dashboard if module is closed.
$this->assertNull($actionevent);
}
public function test_scorm_core_calendar_provide_event_action_open_in_future(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a scorm activity.
$scorm = $this->getDataGenerator()->create_module('scorm', array('course' => $course->id,
'timeopen' => time() + DAYSECS));
// Create a calendar event.
$event = $this->create_action_event($course->id, $scorm->id, SCORM_EVENT_TYPE_OPEN);
// Only students see scorm events.
$this->setUser($this->student);
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Decorate action event.
$actionevent = mod_scorm_core_calendar_provide_event_action($event, $factory);
// Confirm the event was decorated.
$this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
$this->assertEquals(get_string('enter', 'scorm'), $actionevent->get_name());
$this->assertInstanceOf('moodle_url', $actionevent->get_url());
$this->assertEquals(1, $actionevent->get_item_count());
$this->assertFalse($actionevent->is_actionable());
}
public function test_scorm_core_calendar_provide_event_action_with_different_user_as_admin(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a scorm activity.
$scorm = $this->getDataGenerator()->create_module('scorm', array('course' => $course->id,
'timeopen' => time() + DAYSECS));
// Create a calendar event.
$event = $this->create_action_event($course->id, $scorm->id, SCORM_EVENT_TYPE_OPEN);
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Decorate action event override with a passed in user.
$actionevent = mod_scorm_core_calendar_provide_event_action($event, $factory, $this->student->id);
$actionevent2 = mod_scorm_core_calendar_provide_event_action($event, $factory);
// Only students see scorm events.
$this->assertNull($actionevent2);
// Confirm the event was decorated.
$this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
$this->assertEquals(get_string('enter', 'scorm'), $actionevent->get_name());
$this->assertInstanceOf('moodle_url', $actionevent->get_url());
$this->assertEquals(1, $actionevent->get_item_count());
$this->assertFalse($actionevent->is_actionable());
}
public function test_scorm_core_calendar_provide_event_action_no_time_specified(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a scorm activity.
$scorm = $this->getDataGenerator()->create_module('scorm', array('course' => $course->id));
// Create a calendar event.
$event = $this->create_action_event($course->id, $scorm->id, SCORM_EVENT_TYPE_OPEN);
// Only students see scorm events.
$this->setUser($this->student);
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Decorate action event.
$actionevent = mod_scorm_core_calendar_provide_event_action($event, $factory);
// Confirm the event was decorated.
$this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
$this->assertEquals(get_string('enter', 'scorm'), $actionevent->get_name());
$this->assertInstanceOf('moodle_url', $actionevent->get_url());
$this->assertEquals(1, $actionevent->get_item_count());
$this->assertTrue($actionevent->is_actionable());
}
public function test_scorm_core_calendar_provide_event_action_already_completed(): void {
$this->resetAfterTest();
set_config('enablecompletion', 1);
$this->setAdminUser();
// Create the activity.
$course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
$scorm = $this->getDataGenerator()->create_module('scorm', array('course' => $course->id),
array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS));
// Get some additional data.
$cm = get_coursemodule_from_instance('scorm', $scorm->id);
// Create a calendar event.
$event = $this->create_action_event($course->id, $scorm->id,
\core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
// Mark the activity as completed.
$completion = new \completion_info($course);
$completion->set_module_viewed($cm);
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Decorate action event.
$actionevent = mod_scorm_core_calendar_provide_event_action($event, $factory);
// Ensure result was null.
$this->assertNull($actionevent);
}
public function test_scorm_core_calendar_provide_event_action_already_completed_for_user(): void {
$this->resetAfterTest();
set_config('enablecompletion', 1);
$this->setAdminUser();
// Create the activity.
$course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
$scorm = $this->getDataGenerator()->create_module('scorm', array('course' => $course->id),
array('completion' => 2, 'completionview' => 1, 'completionexpected' => time() + DAYSECS));
// Enrol a student in the course.
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Get some additional data.
$cm = get_coursemodule_from_instance('scorm', $scorm->id);
// Create a calendar event.
$event = $this->create_action_event($course->id, $scorm->id,
\core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED);
// Mark the activity as completed for the student.
$completion = new \completion_info($course);
$completion->set_module_viewed($cm, $student->id);
// Create an action factory.
$factory = new \core_calendar\action_factory();
// Decorate action event for the student.
$actionevent = mod_scorm_core_calendar_provide_event_action($event, $factory, $student->id);
// Ensure result was null.
$this->assertNull($actionevent);
}
/**
* Creates an action event.
*
* @param int $courseid
* @param int $instanceid The data id.
* @param string $eventtype The event type. eg. DATA_EVENT_TYPE_OPEN.
* @param int|null $timestart The start timestamp for the event
* @return bool|calendar_event
*/
private function create_action_event($courseid, $instanceid, $eventtype, $timestart = null) {
$event = new \stdClass();
$event->name = 'Calendar event';
$event->modulename = 'scorm';
$event->courseid = $courseid;
$event->instance = $instanceid;
$event->type = CALENDAR_EVENT_TYPE_ACTION;
$event->eventtype = $eventtype;
$event->eventtype = $eventtype;
if ($timestart) {
$event->timestart = $timestart;
} else {
$event->timestart = time();
}
return \calendar_event::create($event);
}
/**
* Test the callback responsible for returning the completion rule descriptions.
* This function should work given either an instance of the module (cm_info), such as when checking the active rules,
* or if passed a stdClass of similar structure, such as when checking the the default completion settings for a mod type.
*/
public function test_mod_scorm_completion_get_active_rule_descriptions(): void {
$this->resetAfterTest();
$this->setAdminUser();
// Two activities, both with automatic completion. One has the 'completionsubmit' rule, one doesn't.
$course = $this->getDataGenerator()->create_course(['enablecompletion' => 2]);
$scorm1 = $this->getDataGenerator()->create_module('scorm', [
'course' => $course->id,
'completion' => 2,
'completionstatusrequired' => 6,
'completionscorerequired' => 5,
'completionstatusallscos' => 1
]);
$scorm2 = $this->getDataGenerator()->create_module('scorm', [
'course' => $course->id,
'completion' => 2,
'completionstatusrequired' => null,
'completionscorerequired' => null,
'completionstatusallscos' => null
]);
$cm1 = \cm_info::create(get_coursemodule_from_instance('scorm', $scorm1->id));
$cm2 = \cm_info::create(get_coursemodule_from_instance('scorm', $scorm2->id));
// Data for the stdClass input type.
// This type of input would occur when checking the default completion rules for an activity type, where we don't have
// any access to cm_info, rather the input is a stdClass containing completion and customdata attributes, just like cm_info.
$moddefaults = new \stdClass();
$moddefaults->customdata = ['customcompletionrules' => [
'completionstatusrequired' => 6,
'completionscorerequired' => 5,
'completionstatusallscos' => 1
]];
$moddefaults->completion = 2;
// Determine the selected statuses using a bitwise operation.
$cvalues = array();
foreach (scorm_status_options(true) as $key => $value) {
if (($scorm1->completionstatusrequired & $key) == $key) {
$cvalues[] = $value;
}
}
$statusstring = implode(', ', $cvalues);
$activeruledescriptions = [
get_string('completionstatusrequireddesc', 'scorm', $statusstring),
get_string('completionscorerequireddesc', 'scorm', $scorm1->completionscorerequired),
get_string('completionstatusallscos', 'scorm'),
];
$this->assertEquals(mod_scorm_get_completion_active_rule_descriptions($cm1), $activeruledescriptions);
$this->assertEquals(mod_scorm_get_completion_active_rule_descriptions($cm2), []);
$this->assertEquals(mod_scorm_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions);
$this->assertEquals(mod_scorm_get_completion_active_rule_descriptions(new \stdClass()), []);
}
/**
* An unkown event type should not change the scorm instance.
*/
public function test_mod_scorm_core_calendar_event_timestart_updated_unknown_event(): void {
global $CFG, $DB;
require_once($CFG->dirroot . "/calendar/lib.php");
$this->resetAfterTest(true);
$this->setAdminUser();
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$scormgenerator = $generator->get_plugin_generator('mod_scorm');
$timeopen = time();
$timeclose = $timeopen + DAYSECS;
$scorm = $scormgenerator->create_instance(['course' => $course->id]);
$scorm->timeopen = $timeopen;
$scorm->timeclose = $timeclose;
$DB->update_record('scorm', $scorm);
// Create a valid event.
$event = new \calendar_event([
'name' => 'Test event',
'description' => '',
'format' => 1,
'courseid' => $course->id,
'groupid' => 0,
'userid' => 2,
'modulename' => 'scorm',
'instance' => $scorm->id,
'eventtype' => SCORM_EVENT_TYPE_OPEN . "SOMETHING ELSE",
'timestart' => 1,
'timeduration' => 86400,
'visible' => 1
]);
mod_scorm_core_calendar_event_timestart_updated($event, $scorm);
$scorm = $DB->get_record('scorm', ['id' => $scorm->id]);
$this->assertEquals($timeopen, $scorm->timeopen);
$this->assertEquals($timeclose, $scorm->timeclose);
}
/**
* A SCORM_EVENT_TYPE_OPEN event should update the timeopen property of
* the scorm activity.
*/
public function test_mod_scorm_core_calendar_event_timestart_updated_open_event(): void {
global $CFG, $DB;
require_once($CFG->dirroot . "/calendar/lib.php");
$this->resetAfterTest(true);
$this->setAdminUser();
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$scormgenerator = $generator->get_plugin_generator('mod_scorm');
$timeopen = time();
$timeclose = $timeopen + DAYSECS;
$timemodified = 1;
$newtimeopen = $timeopen - DAYSECS;
$scorm = $scormgenerator->create_instance(['course' => $course->id]);
$scorm->timeopen = $timeopen;
$scorm->timeclose = $timeclose;
$scorm->timemodified = $timemodified;
$DB->update_record('scorm', $scorm);
// Create a valid event.
$event = new \calendar_event([
'name' => 'Test event',
'description' => '',
'format' => 1,
'courseid' => $course->id,
'groupid' => 0,
'userid' => 2,
'modulename' => 'scorm',
'instance' => $scorm->id,
'eventtype' => SCORM_EVENT_TYPE_OPEN,
'timestart' => $newtimeopen,
'timeduration' => 86400,
'visible' => 1
]);
// Trigger and capture the event when adding a contact.
$sink = $this->redirectEvents();
mod_scorm_core_calendar_event_timestart_updated($event, $scorm);
$triggeredevents = $sink->get_events();
$moduleupdatedevents = array_filter($triggeredevents, function($e) {
return is_a($e, 'core\event\course_module_updated');
});
$scorm = $DB->get_record('scorm', ['id' => $scorm->id]);
// Ensure the timeopen property matches the event timestart.
$this->assertEquals($newtimeopen, $scorm->timeopen);
// Ensure the timeclose isn't changed.
$this->assertEquals($timeclose, $scorm->timeclose);
// Ensure the timemodified property has been changed.
$this->assertNotEquals($timemodified, $scorm->timemodified);
// Confirm that a module updated event is fired when the module
// is changed.
$this->assertNotEmpty($moduleupdatedevents);
}
/**
* A SCORM_EVENT_TYPE_CLOSE event should update the timeclose property of
* the scorm activity.
*/
public function test_mod_scorm_core_calendar_event_timestart_updated_close_event(): void {
global $CFG, $DB;
require_once($CFG->dirroot . "/calendar/lib.php");
$this->resetAfterTest(true);
$this->setAdminUser();
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$scormgenerator = $generator->get_plugin_generator('mod_scorm');
$timeopen = time();
$timeclose = $timeopen + DAYSECS;
$timemodified = 1;
$newtimeclose = $timeclose + DAYSECS;
$scorm = $scormgenerator->create_instance(['course' => $course->id]);
$scorm->timeopen = $timeopen;
$scorm->timeclose = $timeclose;
$scorm->timemodified = $timemodified;
$DB->update_record('scorm', $scorm);
// Create a valid event.
$event = new \calendar_event([
'name' => 'Test event',
'description' => '',
'format' => 1,
'courseid' => $course->id,
'groupid' => 0,
'userid' => 2,
'modulename' => 'scorm',
'instance' => $scorm->id,
'eventtype' => SCORM_EVENT_TYPE_CLOSE,
'timestart' => $newtimeclose,
'timeduration' => 86400,
'visible' => 1
]);
// Trigger and capture the event when adding a contact.
$sink = $this->redirectEvents();
mod_scorm_core_calendar_event_timestart_updated($event, $scorm);
$triggeredevents = $sink->get_events();
$moduleupdatedevents = array_filter($triggeredevents, function($e) {
return is_a($e, 'core\event\course_module_updated');
});
$scorm = $DB->get_record('scorm', ['id' => $scorm->id]);
// Ensure the timeclose property matches the event timestart.
$this->assertEquals($newtimeclose, $scorm->timeclose);
// Ensure the timeopen isn't changed.
$this->assertEquals($timeopen, $scorm->timeopen);
// Ensure the timemodified property has been changed.
$this->assertNotEquals($timemodified, $scorm->timemodified);
// Confirm that a module updated event is fired when the module
// is changed.
$this->assertNotEmpty($moduleupdatedevents);
}
/**
* An unkown event type should not have any limits
*/
public function test_mod_scorm_core_calendar_get_valid_event_timestart_range_unknown_event(): void {
global $CFG, $DB;
require_once($CFG->dirroot . "/calendar/lib.php");
$this->resetAfterTest(true);
$this->setAdminUser();
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$timeopen = time();
$timeclose = $timeopen + DAYSECS;
$scorm = new \stdClass();
$scorm->timeopen = $timeopen;
$scorm->timeclose = $timeclose;
// Create a valid event.
$event = new \calendar_event([
'name' => 'Test event',
'description' => '',
'format' => 1,
'courseid' => $course->id,
'groupid' => 0,
'userid' => 2,
'modulename' => 'scorm',
'instance' => 1,
'eventtype' => SCORM_EVENT_TYPE_OPEN . "SOMETHING ELSE",
'timestart' => 1,
'timeduration' => 86400,
'visible' => 1
]);
list ($min, $max) = mod_scorm_core_calendar_get_valid_event_timestart_range($event, $scorm);
$this->assertNull($min);
$this->assertNull($max);
}
/**
* The open event should be limited by the scorm's timeclose property, if it's set.
*/
public function test_mod_scorm_core_calendar_get_valid_event_timestart_range_open_event(): void {
global $CFG, $DB;
require_once($CFG->dirroot . "/calendar/lib.php");
$this->resetAfterTest(true);
$this->setAdminUser();
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$timeopen = time();
$timeclose = $timeopen + DAYSECS;
$scorm = new \stdClass();
$scorm->timeopen = $timeopen;
$scorm->timeclose = $timeclose;
// Create a valid event.
$event = new \calendar_event([
'name' => 'Test event',
'description' => '',
'format' => 1,
'courseid' => $course->id,
'groupid' => 0,
'userid' => 2,
'modulename' => 'scorm',
'instance' => 1,
'eventtype' => SCORM_EVENT_TYPE_OPEN,
'timestart' => 1,
'timeduration' => 86400,
'visible' => 1
]);
// The max limit should be bounded by the timeclose value.
list ($min, $max) = mod_scorm_core_calendar_get_valid_event_timestart_range($event, $scorm);
$this->assertNull($min);
$this->assertEquals($timeclose, $max[0]);
// No timeclose value should result in no upper limit.
$scorm->timeclose = 0;
list ($min, $max) = mod_scorm_core_calendar_get_valid_event_timestart_range($event, $scorm);
$this->assertNull($min);
$this->assertNull($max);
}
/**
* The close event should be limited by the scorm's timeopen property, if it's set.
*/
public function test_mod_scorm_core_calendar_get_valid_event_timestart_range_close_event(): void {
global $CFG, $DB;
require_once($CFG->dirroot . "/calendar/lib.php");
$this->resetAfterTest(true);
$this->setAdminUser();
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$timeopen = time();
$timeclose = $timeopen + DAYSECS;
$scorm = new \stdClass();
$scorm->timeopen = $timeopen;
$scorm->timeclose = $timeclose;
// Create a valid event.
$event = new \calendar_event([
'name' => 'Test event',
'description' => '',
'format' => 1,
'courseid' => $course->id,
'groupid' => 0,
'userid' => 2,
'modulename' => 'scorm',
'instance' => 1,
'eventtype' => SCORM_EVENT_TYPE_CLOSE,
'timestart' => 1,
'timeduration' => 86400,
'visible' => 1
]);
// The max limit should be bounded by the timeclose value.
list ($min, $max) = mod_scorm_core_calendar_get_valid_event_timestart_range($event, $scorm);
$this->assertEquals($timeopen, $min[0]);
$this->assertNull($max);
// No timeclose value should result in no upper limit.
$scorm->timeopen = 0;
list ($min, $max) = mod_scorm_core_calendar_get_valid_event_timestart_range($event, $scorm);
$this->assertNull($min);
$this->assertNull($max);
}
/**
* A user who does not have capabilities to add events to the calendar should be able to create a SCORM.
*/
public function test_creation_with_no_calendar_capabilities(): void {
$this->resetAfterTest();
$course = self::getDataGenerator()->create_course();
$context = \context_course::instance($course->id);
$user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher');
$roleid = self::getDataGenerator()->create_role();
self::getDataGenerator()->role_assign($roleid, $user->id, $context->id);
assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
$generator = self::getDataGenerator()->get_plugin_generator('mod_scorm');
// Create an instance as a user without the calendar capabilities.
$this->setUser($user);
$time = time();
$params = array(
'course' => $course->id,
'timeopen' => $time + 200,
'timeclose' => $time + 2000,
);
$generator->create_instance($params);
}
}
+238
View File
@@ -0,0 +1,238 @@
<?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/>.
/**
* File containing the SCORM module local library function tests.
*
* @package mod_scorm
* @category test
* @copyright 2017 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_scorm;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/scorm/lib.php');
/**
* Class containing the SCORM module local library function tests.
*
* @package mod_scorm
* @category test
* @copyright 2017 Mark Nelson <markn@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class locallib_test extends \advanced_testcase {
public function setUp(): void {
$this->resetAfterTest();
}
public function test_scorm_update_calendar(): void {
global $DB;
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a scorm activity.
$time = time();
$scorm = $this->getDataGenerator()->create_module('scorm',
array(
'course' => $course->id,
'timeopen' => $time
)
);
// Check that there is now an event in the database.
$events = $DB->get_records('event');
$this->assertCount(1, $events);
// Get the event.
$event = reset($events);
// Confirm the event is correct.
$this->assertEquals('scorm', $event->modulename);
$this->assertEquals($scorm->id, $event->instance);
$this->assertEquals(CALENDAR_EVENT_TYPE_ACTION, $event->type);
$this->assertEquals(DATA_EVENT_TYPE_OPEN, $event->eventtype);
$this->assertEquals($time, $event->timestart);
$this->assertEquals($time, $event->timesort);
}
public function test_scorm_update_calendar_time_open_update(): void {
global $DB;
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a scorm activity.
$time = time();
$scorm = $this->getDataGenerator()->create_module('scorm',
array(
'course' => $course->id,
'timeopen' => $time
)
);
// Set the time open and update the event.
$scorm->timeopen = $time + DAYSECS;
scorm_update_calendar($scorm, $scorm->cmid);
// Check that there is an event in the database.
$events = $DB->get_records('event');
$this->assertCount(1, $events);
// Get the event.
$event = reset($events);
// Confirm the event time was updated.
$this->assertEquals('scorm', $event->modulename);
$this->assertEquals($scorm->id, $event->instance);
$this->assertEquals(CALENDAR_EVENT_TYPE_ACTION, $event->type);
$this->assertEquals(DATA_EVENT_TYPE_OPEN, $event->eventtype);
$this->assertEquals($time + DAYSECS, $event->timestart);
$this->assertEquals($time + DAYSECS, $event->timesort);
}
public function test_scorm_update_calendar_time_open_delete(): void {
global $DB;
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a scorm activity.
$scorm = $this->getDataGenerator()->create_module('scorm', array('course' => $course->id));
// Create a scorm activity.
$time = time();
$scorm = $this->getDataGenerator()->create_module('scorm',
array(
'course' => $course->id,
'timeopen' => $time
)
);
// Set the time open to 0 and update the event.
$scorm->timeopen = 0;
scorm_update_calendar($scorm, $scorm->cmid);
// Confirm the event was deleted.
$this->assertEquals(0, $DB->count_records('event'));
}
public function test_scorm_update_calendar_time_close(): void {
global $DB;
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a scorm activity.
$time = time();
$scorm = $this->getDataGenerator()->create_module('scorm',
array(
'course' => $course->id,
'timeclose' => $time
)
);
// Check that there is now an event in the database.
$events = $DB->get_records('event');
$this->assertCount(1, $events);
// Get the event.
$event = reset($events);
// Confirm the event is correct.
$this->assertEquals('scorm', $event->modulename);
$this->assertEquals($scorm->id, $event->instance);
$this->assertEquals(CALENDAR_EVENT_TYPE_ACTION, $event->type);
$this->assertEquals(DATA_EVENT_TYPE_CLOSE, $event->eventtype);
$this->assertEquals($time, $event->timestart);
$this->assertEquals($time, $event->timesort);
}
public function test_scorm_update_calendar_time_close_update(): void {
global $DB;
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a scorm activity.
$time = time();
$scorm = $this->getDataGenerator()->create_module('scorm',
array(
'course' => $course->id,
'timeclose' => $time
)
);
// Set the time close and update the event.
$scorm->timeclose = $time + DAYSECS;
scorm_update_calendar($scorm, $scorm->cmid);
// Check that there is an event in the database.
$events = $DB->get_records('event');
$this->assertCount(1, $events);
// Get the event.
$event = reset($events);
// Confirm the event time was updated.
$this->assertEquals('scorm', $event->modulename);
$this->assertEquals($scorm->id, $event->instance);
$this->assertEquals(CALENDAR_EVENT_TYPE_ACTION, $event->type);
$this->assertEquals(DATA_EVENT_TYPE_CLOSE, $event->eventtype);
$this->assertEquals($time + DAYSECS, $event->timestart);
$this->assertEquals($time + DAYSECS, $event->timesort);
}
public function test_scorm_update_calendar_time_close_delete(): void {
global $DB;
$this->setAdminUser();
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a scorm activity.
$scorm = $this->getDataGenerator()->create_module('scorm',
array(
'course' => $course->id,
'timeclose' => time()
)
);
// Set the time close to 0 and update the event.
$scorm->timeclose = 0;
scorm_update_calendar($scorm, $scorm->cmid);
// Confirm the event time was deleted.
$this->assertEquals(0, $DB->count_records('event'));
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,23 @@
Description of test packages
============================================
Sample Packages downloaded from http://scorm.com/scorm-explained/technical-scorm/golf-examples/
* singlescobasic.zip - Single SCO with basic runtime calls. SCORM 1.2.
* singlesco_scorm12.zip - Single SCO content packaging example. SCORM 1.2.
* RuntimeMinimumCalls_SCORM12.zip - Multi-SCO packaging example. SCORM 1.2.
* RuntimeBasicCalls_SCORM20043rdEdition.zip - Multi-SCO packaging example. SCORM 2004 3rd edition.
* RuntimeMinimumCalls_SCORM12-mini.zip - modified 2 SCO version of RuntimeMinimumCalls_SCORM12.zip
These packages were downloaded from http://scorm.com/ website, the website
disclaimer states that *Content on this site is licensed under a Creative
Commons Attribution 3.0 License*. http://creativecommons.org/licenses/by/3.0/
Other test packages
* badscorm.zip - contains a fake imsmanifest.xml inside a directory, used for validation check.
* invalid.zip - zip file with an single html file, no SCORM config files, used for validation check.
* validscorm.zip - non functional package with an imsmanifest.xml, used for validation check.
* validaicc.zip - non functional package with AICC config files, used for validation check.
* complexscorm.zip - copied from: https://github.com/jleyva/scorm-debugger.
* singlescobasic_missingorg.zip - copy of scorm.com package but with missing org definition.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+336
View File
@@ -0,0 +1,336 @@
<?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/>.
/**
* Base class for unit tests for mod_scorm.
*
* @package mod_scorm
* @category test
* @copyright 2018 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_scorm\privacy;
defined('MOODLE_INTERNAL') || die();
use mod_scorm\privacy\provider;
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;
/**
* Unit tests for mod\scorm\classes\privacy\provider.php
*
* @copyright 2018 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends provider_testcase {
/** @var stdClass User without any AICC/SCORM attempt. */
protected $student0;
/** @var stdClass User with some AICC/SCORM attempt. */
protected $student1;
/** @var stdClass User with some AICC/SCORM attempt. */
protected $student2;
/** @var context context_module of the SCORM 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->scorm_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 SCORM 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->scorm_setup_test_scenario_data();
$component = 'mod_scorm';
$userlist = new \core_privacy\local\request\userlist($this->context, $component);
provider::get_users_in_context($userlist);
// Students 1 and 2 have attempts in the SCORM 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->scorm_setup_test_scenario_data();
// Validate exported data for student0 (without any AICC/SCORM attempt).
$this->setUser($this->student0);
$writer = writer::with_context($this->context);
$this->export_context_data_for_user($this->student0->id, $this->context, 'mod_scorm');
$subcontextattempt1 = [
get_string('myattempts', 'scorm'),
get_string('attempt', 'scorm'). " 1"
];
$subcontextaicc = [
get_string('myaiccsessions', 'scorm')
];
$data = $writer->get_data($subcontextattempt1);
$this->assertEmpty($data);
$data = $writer->get_data($subcontextaicc);
$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, 'mod_scorm');
$data = $writer->get_data([]);
$this->assertEquals('SCORM1', $data->name);
$data = (array)$writer->get_data($subcontextattempt1);
$this->assertCount(1, $data);
$this->assertCount(2, (array) reset($data));
$subcontextattempt2 = [
get_string('myattempts', 'scorm'),
get_string('attempt', 'scorm'). " 2"
];
$data = (array)$writer->get_data($subcontextattempt2);
$this->assertCount(2, (array) reset($data));
// The student1 has only 2 scoes_track attempts.
$subcontextattempt3 = [
get_string('myattempts', 'scorm'),
get_string('attempt', 'scorm'). " 3"
];
$data = $writer->get_data($subcontextattempt3);
$this->assertEmpty($data);
// The student1 has only 1 aicc_session.
$data = $writer->get_data($subcontextaicc);
$this->assertCount(1, (array) $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->scorm_setup_test_scenario_data();
// Before deletion, we should have 8 entries in the scorm_scoes_value table.
$count = $DB->count_records('scorm_scoes_value');
$this->assertEquals(8, $count);
// Before deletion, we should have 4 entries in the scorm_aicc_session table.
$count = $DB->count_records('scorm_aicc_session');
$this->assertEquals(4, $count);
// Delete data based on the context.
provider::delete_data_for_all_users_in_context($this->context);
// After deletion, the scorm_scoes_value entries should have been deleted.
$count = $DB->count_records('scorm_scoes_value');
$this->assertEquals(0, $count);
// After deletion, the scorm_aicc_session entries should have been deleted.
$count = $DB->count_records('scorm_aicc_session');
$this->assertEquals(0, $count);
}
/**
* Test for provider::delete_data_for_user().
*/
public function test_delete_data_for_user(): void {
global $DB;
$this->resetAfterTest(true);
$this->setAdminUser();
$this->scorm_setup_test_scenario_data();
// Before deletion, we should have 8 entries in the scorm_scoes_value table.
$count = $DB->count_records('scorm_scoes_value');
$this->assertEquals(8, $count);
// Before deletion, we should have 4 entries in the scorm_aicc_session table.
$count = $DB->count_records('scorm_aicc_session');
$this->assertEquals(4, $count);
$approvedcontextlist = new approved_contextlist($this->student1, 'scorm', [$this->context->id]);
provider::delete_data_for_user($approvedcontextlist);
// After deletion, the scorm_attempt entries for the first student should have been deleted.
$count = $DB->count_records('scorm_attempt', ['userid' => $this->student1->id]);
$this->assertEquals(0, $count);
$count = $DB->count_records('scorm_scoes_value');
$this->assertEquals(4, $count);
// After deletion, the scorm_aicc_session entries for the first student should have been deleted.
$count = $DB->count_records('scorm_aicc_session', ['userid' => $this->student1->id]);
$this->assertEquals(0, $count);
$count = $DB->count_records('scorm_aicc_session');
$this->assertEquals(2, $count);
// Confirm that the SCORM hasn't been removed.
$scormcount = $DB->get_records('scorm');
$this->assertCount(1, (array) $scormcount);
// Delete scoes_track for student0 (nothing has to be removed).
$approvedcontextlist = new approved_contextlist($this->student0, 'scorm', [$this->context->id]);
provider::delete_data_for_user($approvedcontextlist);
$count = $DB->count_records('scorm_scoes_value');
$this->assertEquals(4, $count);
$count = $DB->count_records('scorm_aicc_session');
$this->assertEquals(2, $count);
}
/**
* Test for provider::delete_data_for_users().
*/
public function test_delete_data_for_users(): void {
global $DB;
$component = 'mod_scorm';
$this->resetAfterTest(true);
$this->setAdminUser();
$this->scorm_setup_test_scenario_data();
// Before deletion, we should have 8 entries in the scorm_scoes_value table.
$count = $DB->count_records('scorm_scoes_value');
$this->assertEquals(8, $count);
// Before deletion, we should have 4 entries in the scorm_aicc_session table.
$count = $DB->count_records('scorm_aicc_session');
$this->assertEquals(4, $count);
// Delete only student 1's data, retain student 2's data.
$approveduserids = [$this->student1->id];
$approvedlist = new approved_userlist($this->context, $component, $approveduserids);
provider::delete_data_for_users($approvedlist);
// After deletion, the scorm_attempt entries for the first student should have been deleted.
$count = $DB->count_records('scorm_attempt', ['userid' => $this->student1->id]);
$this->assertEquals(0, $count);
$count = $DB->count_records('scorm_scoes_value');
$this->assertEquals(4, $count);
// After deletion, the scorm_aicc_session entries for the first student should have been deleted.
$count = $DB->count_records('scorm_aicc_session', ['userid' => $this->student1->id]);
$this->assertEquals(0, $count);
$count = $DB->count_records('scorm_aicc_session');
$this->assertEquals(2, $count);
// Confirm that the SCORM hasn't been removed.
$scormcount = $DB->get_records('scorm');
$this->assertCount(1, (array) $scormcount);
// Delete scoes_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);
$count = $DB->count_records('scorm_scoes_value');
$this->assertEquals(4, $count);
$count = $DB->count_records('scorm_aicc_session');
$this->assertEquals(2, $count);
}
/**
* Helper function to setup 3 users and 2 SCORM attempts for student1 and student2.
* $this->student0 is always created without any attempt.
*/
protected function scorm_setup_test_scenario_data() {
global $DB;
set_config('allowaicchacp', 1, 'scorm');
// Setup test data.
$course = $this->getDataGenerator()->create_course();
$params = array('course' => $course->id, 'name' => 'SCORM1');
$scorm = $this->getDataGenerator()->create_module('scorm', $params);
$this->context = \context_module::instance($scorm->cmid);
// Users enrolments.
$studentrole = $DB->get_record('role', array('shortname' => 'student'));
// Create student0 withot any SCORM attempt.
$this->student0 = self::getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($this->student0->id, $course->id, $studentrole->id, 'manual');
// Create student1 with 2 SCORM attempts and 1 AICC session.
$this->student1 = self::getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($this->student1->id, $course->id, $studentrole->id, 'manual');
static::scorm_insert_attempt($scorm, $this->student1->id, 1);
static::scorm_insert_attempt($scorm, $this->student1->id, 2);
// Create student2 with 2 SCORM attempts and 1 AICC session.
$this->student2 = self::getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($this->student2->id, $course->id, $studentrole->id, 'manual');
static::scorm_insert_attempt($scorm, $this->student2->id, 1);
static::scorm_insert_attempt($scorm, $this->student2->id, 2);
}
/**
* Create a SCORM attempt.
*
* @param object $scorm SCORM activity.
* @param int $userid Userid who is doing the attempt.
* @param int $attempt Number of attempt.
*/
protected function scorm_insert_attempt($scorm, $userid, $attempt) {
global $DB;
$newattempt = 'on';
$mode = 'normal';
scorm_check_mode($scorm, $newattempt, $attempt, $userid, $mode);
$scoes = scorm_get_scoes($scorm->id);
$sco = array_pop($scoes);
scorm_insert_track($userid, $scorm->id, $sco->id, $attempt, 'cmi.core.lesson_status', 'completed');
scorm_insert_track($userid, $scorm->id, $sco->id, $attempt, 'cmi.score.min', '0');
$now = time();
$hacpsession = [
'scormid' => $scorm->id,
'attempt' => $attempt,
'hacpsession' => random_string(20),
'userid' => $userid,
'timecreated' => $now,
'timemodified' => $now
];
$DB->insert_record('scorm_aicc_session', $hacpsession);
}
}
+88
View File
@@ -0,0 +1,88 @@
<?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_scorm;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/scorm/locallib.php');
/**
* Unit tests for {@link mod_scorm}.
*
* @package mod_scorm
* @category test
* @copyright 2013 Dan Marsden
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class validatepackage_test extends \advanced_testcase {
/**
* Convenience to take a fixture test file and create a stored_file.
*
* @param string $filepath
* @return stored_file
*/
protected function create_stored_file_from_path($filepath) {
$syscontext = \context_system::instance();
$filerecord = array(
'contextid' => $syscontext->id,
'component' => 'mod_scorm',
'filearea' => 'unittest',
'itemid' => 0,
'filepath' => '/',
'filename' => basename($filepath)
);
$fs = get_file_storage();
return $fs->create_file_from_pathname($filerecord, $filepath);
}
public function test_validate_package(): void {
global $CFG;
$this->resetAfterTest(true);
$filename = "validscorm.zip";
$file = $this->create_stored_file_from_path($CFG->dirroot.'/mod/scorm/tests/packages/'.$filename, \file_archive::OPEN);
$errors = scorm_validate_package($file);
$this->assertEmpty($errors);
$filename = "validaicc.zip";
$file = $this->create_stored_file_from_path($CFG->dirroot.'/mod/scorm/tests/packages/'.$filename, \file_archive::OPEN);
$errors = scorm_validate_package($file);
$this->assertEmpty($errors);
$filename = "invalid.zip";
$file = $this->create_stored_file_from_path($CFG->dirroot.'/mod/scorm/tests/packages/'.$filename, \file_archive::OPEN);
$errors = scorm_validate_package($file);
$this->assertArrayHasKey('packagefile', $errors);
if (isset($errors['packagefile'])) {
$this->assertEquals(get_string('nomanifest', 'scorm'), $errors['packagefile']);
}
$filename = "badscorm.zip";
$file = $this->create_stored_file_from_path($CFG->dirroot.'/mod/scorm/tests/packages/'.$filename, \file_archive::OPEN);
$errors = scorm_validate_package($file);
$this->assertArrayHasKey('packagefile', $errors);
if (isset($errors['packagefile'])) {
$this->assertEquals(get_string('badimsmanifestlocation', 'scorm'), $errors['packagefile']);
}
}
}