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
+29
View File
@@ -0,0 +1,29 @@
@core
Feature: Navigate action menu
In order to navigate an action menu
As a user
I need to be able to use the keyboard
@javascript
Scenario: The menu does not close on keyboard navigation
When I log in as "admin"
# Click to open the user menu.
And I click on ".usermenu a.toggle-display" "css_element" in the ".usermenu" "css_element"
# The menu should now be visible.
Then ".usermenu [role='menu']" "css_element" should be visible
# Press down arrow.
And I press the down key
# The menu should still be visible.
And ".usermenu [role='menu']" "css_element" should be visible
@javascript
Scenario: The menu closes when it clicked outside
When I log in as "admin"
# Click to open the user menu.
And I click on ".usermenu a.toggle-display" "css_element" in the ".usermenu" "css_element"
# The menu should now be visible.
Then ".usermenu [role='menu']" "css_element" should be visible
# Click outside the menu.
And I click on "adminsearchquery" "field"
# The menu should now be hidden.
And ".usermenu [role='menu']" "css_element" should not be visible
@@ -0,0 +1,249 @@
@core @javascript
Feature: Navigate action menu subpanels
In order to navigate an action menu subpanel
As a user
I need to be able to use both keyboard and mouse to open the subpanel
Background:
Given I log in as "admin"
And I am on fixture page "/lib/tests/behat/fixtures/action_menu_subpanel_output_testpage.php"
Scenario: Navigate several action menus subpanels with mouse
Given I click on "Actions menu" "button" in the "regularscenario" "region"
And I click on "Subpanel example" "menuitem" in the "regularscenario" "region"
And I should see "Status A" in the "regularscenario" "region"
And I should see "Status B" in the "regularscenario" "region"
And I should not see "Status C" in the "regularscenario" "region"
And I should not see "Status D" in the "regularscenario" "region"
When I click on "Another subpanel" "menuitem" in the "regularscenario" "region"
Then I should not see "Status A" in the "regularscenario" "region"
And I should not see "Status B" in the "regularscenario" "region"
And I should see "Status C" in the "regularscenario" "region"
And I should see "Status D" in the "regularscenario" "region"
And I click on "Status D" "link" in the "regularscenario" "region"
And I should see "Foo param value: Donkey" in the "paramcheck" "region"
Scenario: Check extra data in subpanel action menu items
When I should see "Adding data attributes to menu item" in the "dataattributes" "region"
# the page have a javascript script to check that for us.
Then "[data-extra='some other value']" "css_element" should exist in the "dataattributes" "region"
And "[data-extra='some other value']" "css_element" should exist in the "dataattributes" "region"
And I should see "Extra data attribute detected: some extra value" in the "datachecks" "region"
And I should see "Extra data attribute detected: some other value" in the "datachecks" "region"
Scenario: User can navigate left menus subpanels
Given I click on "Actions menu" "button" in the "menuleft" "region"
And I click on "Subpanel example" "menuitem" in the "menuleft" "region"
And I should see "Status A" in the "menuleft" "region"
And I should see "Status B" in the "menuleft" "region"
And I should not see "Status C" in the "menuleft" "region"
And I should not see "Status D" in the "menuleft" "region"
When I click on "Another subpanel" "menuitem" in the "menuleft" "region"
Then I should not see "Status A" in the "menuleft" "region"
And I should not see "Status B" in the "menuleft" "region"
And I should see "Status C" in the "menuleft" "region"
And I should see "Status D" in the "menuleft" "region"
And I click on "Status D" "link" in the "menuleft" "region"
And I should see "Foo param value: Donkey" in the "paramcheck" "region"
Scenario: User can show the subpanels content using keyboard
Given I click on "Actions menu" "button" in the "regularscenario" "region"
# Move to the first subpanel element.
And I press the down key
And I press the down key
And I press the down key
And I should see "Status A" in the "regularscenario" "region"
And I should see "Status B" in the "regularscenario" "region"
And I should not see "Status C" in the "regularscenario" "region"
And I should not see "Status D" in the "regularscenario" "region"
# Move to the next subpanel.
When I press the down key
Then I should not see "Status A" in the "regularscenario" "region"
And I should not see "Status B" in the "regularscenario" "region"
And I should see "Status C" in the "regularscenario" "region"
And I should see "Status D" in the "regularscenario" "region"
Scenario: User can browse the subpanel content using the arrow keys
Given I click on "Actions menu" "button" in the "regularscenario" "region"
# Move to the first subpanel element.
And I press the down key
And I press the down key
And I press the down key
# Move in the subpanel with arrow keys and loop the links with up and down.
When I press the right key
And the focused element is "Status A" "link" in the "regularscenario" "region"
And I press the down key
And the focused element is "Status B" "link" in the "regularscenario" "region"
And I press the down key
And the focused element is "Status A" "link" in the "regularscenario" "region"
And I press the up key
And the focused element is "Status B" "link" in the "regularscenario" "region"
# Leave the subpanel with right and left key.
Then I press the right key
And the focused element is "Subpanel example" "menuitem" in the "regularscenario" "region"
And I press the right key
And the focused element is "Status A" "link" in the "regularscenario" "region"
And I press the left key
And the focused element is "Subpanel example" "menuitem" in the "regularscenario" "region"
And I press the left key
And the focused element is "Status A" "link" in the "regularscenario" "region"
And I press the left key
And the focused element is "Subpanel example" "menuitem" in the "regularscenario" "region"
# Move to the next subpanel with enter.
And I press the down key
And I press the right key
And the focused element is "Status C" "link" in the "regularscenario" "region"
And I press the down key
And the focused element is "Status D" "link" in the "regularscenario" "region"
# Select the current link of the panel with enter.
And I press the enter key
And I should see "Foo param value: Donkey" in the "paramcheck" "region"
Scenario: User can open and close subpanels in mobile
Given I change the viewport size to "mobile"
And I click on "Actions menu" "button" in the "regularscenario" "region"
And I should not see "Status A" in the "regularscenario" "region"
And I should not see "Status B" in the "regularscenario" "region"
And I should not see "Status C" in the "regularscenario" "region"
And I should not see "Status D" in the "regularscenario" "region"
When I click on "Subpanel example" "menuitem" in the "regularscenario" "region"
And I should see "Status A" in the "regularscenario" "region"
And I should see "Status B" in the "regularscenario" "region"
And I should not see "Status C" in the "regularscenario" "region"
And I should not see "Status D" in the "regularscenario" "region"
# In mobile click the menu item toggles the subpanel.
Then I click on "Subpanel example" "menuitem" in the "regularscenario" "region"
And I should not see "Status A" in the "regularscenario" "region"
And I should not see "Status B" in the "regularscenario" "region"
And I should not see "Status C" in the "regularscenario" "region"
And I should not see "Status D" in the "regularscenario" "region"
And I click on "Another subpanel" "menuitem" in the "regularscenario" "region"
And I should not see "Status A" in the "regularscenario" "region"
And I should not see "Status B" in the "regularscenario" "region"
And I should see "Status C" in the "regularscenario" "region"
And I should see "Status D" in the "regularscenario" "region"
And I click on "Status D" "link" in the "regularscenario" "region"
And I should see "Foo param value: Donkey" in the "paramcheck" "region"
Scenario: User can browse the subpanels using keys in extra small windows
Given I change the viewport size to "mobile"
And I click on "Actions menu" "button" in the "regularscenario" "region"
# Go to the seconds subpanel and open it with enter.
And I press the down key
And I press the down key
And I press the down key
And I press the down key
And the focused element is "Another subpanel" "menuitem" in the "regularscenario" "region"
And I press the enter key
When I should not see "Status A" in the "regularscenario" "region"
And I should not see "Status B" in the "regularscenario" "region"
And I should see "Status C" in the "regularscenario" "region"
And I should see "Status D" in the "regularscenario" "region"
And the focused element is "Status C" "link" in the "regularscenario" "region"
# Loop the subpanel links wand the menu item with up and down.
Then I press the down key
And the focused element is "Status D" "link" in the "regularscenario" "region"
And I press the down key
And the focused element is "Another subpanel" "menuitem" in the "regularscenario" "region"
And I press the down key
And the focused element is "Status C" "link" in the "regularscenario" "region"
And I press the down key
And the focused element is "Status D" "link" in the "regularscenario" "region"
And I press the up key
And the focused element is "Status C" "link" in the "regularscenario" "region"
And I press the up key
And the focused element is "Another subpanel" "menuitem" in the "regularscenario" "region"
# Use up in the item to close the panel.
And I press the up key
And I should not see "Status A" in the "regularscenario" "region"
And I should not see "Status B" in the "regularscenario" "region"
And I should not see "Status C" in the "regularscenario" "region"
And I should not see "Status D" in the "regularscenario" "region"
And the focused element is "Subpanel example" "menuitem" in the "regularscenario" "region"
# Enter the panel and select the second link.
And I press the enter key
And I press the down key
And the focused element is "Status B" "link" in the "regularscenario" "region"
And I press the enter key
And I should see "Foo param value: Beetle" in the "paramcheck" "region"
Scenario: action menu subpanels can display optional icons in the menu item
Given I click on "Actions menu" "button" in the "regularscenario" "region"
And "Locked icon" "icon" should not exist in the "regularscenario" "region"
And "Message icon" "icon" should not exist in the "regularscenario" "region"
And I click on "Actions menu" "button" in the "menuleft" "region"
And "Locked icon" "icon" should not exist in the "menuleft" "region"
And "Message icon" "icon" should not exist in the "menuleft" "region"
When I click on "Actions menu" "button" in the "itemicon" "region"
Then "Locked icon" "icon" should exist in the "itemicon" "region"
And "Message icon" "icon" should exist in the "itemicon" "region"
And I click on "Actions menu" "button" in the "itemiconleft" "region"
And "Locked icon" "icon" should exist in the "itemiconleft" "region"
And "Message icon" "icon" should exist in the "itemiconleft" "region"
@accessibility
Scenario: User can browse the subpanels using keys in a drawer action menu
Given I click on "Actions menu" "button" in the "drawersimulation" "region"
# Go to the seconds subpanel and open it with enter.
And I press the down key
And I press the down key
And I press the down key
And I press the down key
And the focused element is "Another subpanel" "menuitem" in the "drawersimulation" "region"
And I press the enter key
When I should not see "Status A" in the "drawersimulation" "region"
And I should not see "Status B" in the "drawersimulation" "region"
And I should see "Status C" in the "drawersimulation" "region"
And I should see "Status D" in the "drawersimulation" "region"
And the focused element is "Status C" "link" in the "drawersimulation" "region"
# Loop the subpanel links wand the menu item with up and down.
Then I press the down key
And the focused element is "Status D" "link" in the "drawersimulation" "region"
And I press the down key
And the focused element is "Another subpanel" "menuitem" in the "drawersimulation" "region"
And I press the down key
And the focused element is "Status C" "link" in the "drawersimulation" "region"
And I press the down key
And the focused element is "Status D" "link" in the "drawersimulation" "region"
And I press the up key
And the focused element is "Status C" "link" in the "drawersimulation" "region"
And I press the up key
And the focused element is "Another subpanel" "menuitem" in the "drawersimulation" "region"
# Use up in the item to close the panel.
And I press the up key
And I should not see "Status A" in the "drawersimulation" "region"
And I should not see "Status B" in the "drawersimulation" "region"
And I should not see "Status C" in the "drawersimulation" "region"
And I should not see "Status D" in the "drawersimulation" "region"
And the focused element is "Subpanel example" "menuitem" in the "drawersimulation" "region"
And the page should meet accessibility standards with "wcag143" extra tests
# Enter the panel and select the second link.
And I press the enter key
And I press the down key
And the focused element is "Status B" "link" in the "drawersimulation" "region"
And I press the enter key
And I should see "Foo param value: Beetle" in the "paramcheck" "region"
Scenario: User can browse the menu using the WCAG recommended compount keyboard navigation
Given I click on "Actions menu" "button" in the "regularscenario" "region"
And I press the down key
And I press the down key
And I press the down key
And the focused element is "Subpanel example" "menuitem" in the "regularscenario" "region"
When I press the tab key
And the focused element is "Status A" "link" in the "regularscenario" "region"
And I should see "Status A" in the "regularscenario" "region"
And I should see "Status B" in the "regularscenario" "region"
And I press the down key
Then I press the shift tab key
And the focused element is "Subpanel example" "menuitem" in the "regularscenario" "region"
And I should not see "Status A" in the "regularscenario" "region"
And I should not see "Status B" in the "regularscenario" "region"
And I press the enter key
And the focused element is "Status A" "link" in the "regularscenario" "region"
And I should see "Status A" in the "regularscenario" "region"
And I should see "Status B" in the "regularscenario" "region"
And I press the escape key
And the focused element is "Subpanel example" "menuitem" in the "regularscenario" "region"
And I should not see "Status A" in the "regularscenario" "region"
And I should not see "Status B" in the "regularscenario" "region"
+46
View File
@@ -0,0 +1,46 @@
@core
Feature: Close modals by clicking outside them
In order to easily close the currently open pop-up
As a user
Clicking outside the modal should close it if it doesn't contain a form.
@javascript
Scenario: The popup closes when clicked on dead space - YUI
Given the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Test quiz name | Test quiz description | C1 | quiz1 |
And I am on the "quiz1" "Activity" page logged in as "admin"
And I follow "Add question"
And I click on "Add" "link"
And I click on "a new question" "link"
# Cannot use the normal I click on here, because the pop-up gets in the way.
And I click on ".moodle-dialogue-lightbox" "css_element" skipping visibility check
# The modal does not close because it contains a form.
Then I should see "Choose a question type to add"
@javascript
Scenario: The popup closes when clicked on dead space - Modal
Given I log in as "admin"
And I follow "Full calendar"
And I press "New event"
When I click on "[data-region='modal-container']" "css_element"
# The modal does not close becaue it contains a form.
Then ".modal-backdrop" "css_element" should be visible
# Confirm that the contents of the new calendar event modal are visible.
And I should see "New event" in the ".modal-title" "css_element"
And I should see "Event title" in the ".modal-body" "css_element"
@javascript
Scenario: The popup help closes when clicked
Given the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Test quiz name | Test quiz description | C1 | quiz1 |
And I am on the "quiz1" "Activity" page logged in as "admin"
And I follow "Add question"
Then I should not see "More help"
+315
View File
@@ -0,0 +1,315 @@
@core
Feature: Initials bar
In order to filter users from user list
As an admin
I need to be able to use letter filters
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher | Ateacher | Teacher | teacher@example.com |
| student1 | Astudent | Astudent | student1@example.com |
| student2 | Bstudent | Astudent | student2@example.com |
| student3 | Cstudent | Cstudent | student3@example.com |
| student4 | Cstudent | Cstudent | student4@example.com |
| student5 | Cstudent | Cstudent | student5@example.com |
| student6 | Cstudent | Cstudent | student6@example.com |
| student7 | Cstudent | Cstudent | student7@example.com |
| student8 | Cstudent | Cstudent | student8@example.com |
| student9 | Cstudent | Cstudent | student9@example.com |
| student10 | Cstudent | Cstudent | student10@example.com |
| student11 | Cstudent | Cstudent | student11@example.com |
| student12 | Cstudent | Cstudent | student12@example.com |
| student13 | Cstudent | Cstudent | student13@example.com |
| student14 | Cstudent | Cstudent | student14@example.com |
| student15 | Cstudent | Cstudent | student15@example.com |
| student16 | Cstudent | Cstudent | student16@example.com |
| student17 | Cstudent | Cstudent | student17@example.com |
| student18 | Cstudent | Cstudent | student18@example.com |
| student19 | Cstudent | Cstudent | student19@example.com |
| student20 | Cstudent | Cstudent | student20@example.com |
| student21 | Cstudent | Cstudent | student21@example.com |
| student22 | Cstudent | Cstudent | student22@example.com |
| student23 | Cstudent | Cstudent | student23@example.com |
| student24 | Cstudent | Cstudent | student24@example.com |
And the following "courses" exist:
| fullname | shortname | category |enablecompletion |
| Course 1 | C1 | 0 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
| student3 | C1 | student |
| student4 | C1 | student |
| student5 | C1 | student |
| student6 | C1 | student |
| student7 | C1 | student |
| student8 | C1 | student |
| student9 | C1 | student |
| student10 | C1 | student |
| student11 | C1 | student |
| student12 | C1 | student |
| student13 | C1 | student |
| student14 | C1 | student |
| student15 | C1 | student |
| student16 | C1 | student |
| student17 | C1 | student |
| student18 | C1 | student |
| student19 | C1 | student |
| student20 | C1 | student |
| student21 | C1 | student |
| student22 | C1 | student |
| student23 | C1 | student |
| student24 | C1 | student |
Scenario: Filter users on assignment submission page
Given the following "activities" exist:
| activity | course | idnumber | name | intro | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled |
| assign | C1 | assign1 | TestAssignment | Test assignment description | 0 | 0 |
And I am on the "assign1" "Activity" page logged in as "teacher"
When I follow "View all submissions"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And I should see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should see "Cstudent Cstudent"
And I click on "A" "link" in the ".initialbar.lastinitial .page-item.A" "css_element"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And I should see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should not see "Cstudent Cstudent"
And I click on "B" "link" in the ".initialbar.firstinitial .page-item.B" "css_element"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And I should not see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should not see "Cstudent Cstudent"
And I am on the "assign1" "Activity" page
When I follow "View all submissions"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And I should not see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should not see "Cstudent Cstudent"
And I click on "All" "link" in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And I should see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should not see "Cstudent Cstudent"
And I click on "All" "link" in the ".initialbar.lastinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And I should see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should see "Cstudent Cstudent"
@javascript
Scenario: Filter users on view gradebook page
Given the following "activities" exist:
| activity | course | idnumber | name | intro | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled |
| assign | C1 | assign1 | TestAssignment | Test assignment description | 0 | 0 |
And I am on the "assign1" "Activity" page logged in as "teacher"
When I follow "View all submissions"
And I select "View gradebook" from the "jump" singleselect
And I click on "Filter by name" "combobox"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And I should see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should see "Cstudent Cstudent"
And I select "A" in the "Last name" "core_grades > initials bar"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And I press "Apply"
And I should see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should not see "Cstudent Cstudent"
And I click on "Last (A)" "combobox"
And I select "B" in the "First name" "core_grades > initials bar"
And I press "Apply"
And I wait until the page is ready
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And I should not see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should not see "Cstudent Cstudent"
And I am on the "assign1" "Activity" page
When I follow "View all submissions"
And I select "View gradebook" from the "jump" singleselect
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And I should not see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should not see "Cstudent Cstudent"
And I click on "First (B) Last (A)" "combobox"
And I select "All" in the "First name" "core_grades > initials bar"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And I press "Apply"
And I wait until the page is ready
And I should see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should not see "Cstudent Cstudent"
And I click on "Last (A)" "combobox"
And I select "All" in the "Last name" "core_grades > initials bar"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And I press "Apply"
And I wait until the page is ready
And I should see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should see "Cstudent Cstudent"
Scenario: Filter users on course participants page
Given the following "activities" exist:
| activity | course | idnumber | name | intro | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled |
| assign | C1 | assign1 | TestAssignment | Test assignment description | 0 | 0 |
And I am on the "C1" "Course" page logged in as "student1"
And I log out
And I am on the "C1" "Course" page logged in as "student2"
And I log out
And I am on the "C1" "Course" page logged in as "teacher"
And I follow "Participants"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And I should see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should see "Cstudent Cstudent"
And I click on "A" "link" in the ".initialbar.lastinitial .page-item.A" "css_element"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And I should see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should not see "Cstudent Cstudent"
And I click on "B" "link" in the ".initialbar.firstinitial .page-item.B" "css_element"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And I should not see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should not see "Cstudent Cstudent"
And I am on "Course 1" course homepage
And I follow "Participants"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And I should not see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should not see "Cstudent Cstudent"
And I click on "All" "link" in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And I should see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should not see "Cstudent Cstudent"
And I click on "All" "link" in the ".initialbar.lastinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And I should see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should see "Cstudent Cstudent"
@javascript
Scenario: Filter users on activity completion page
Given the following "activities" exist:
| activity | course | idnumber | name | intro | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled |
| assign | C1 | assign1 | TestAssignment | Test assignment description | 0 | 0 |
And I am on the "assign1" "assign Activity editing" page logged in as "admin"
And I expand all fieldsets
And I set the field "Students must manually mark the activity as done" to "1"
And I click on "Save and return to course" "button"
And I navigate to "Course completion" in current page administration
And I expand all fieldsets
And I click on "Assignment - TestAssignment" "checkbox"
And I click on "Save changes" "button"
And I log out
And I am on the "C1" "Course" page logged in as "teacher"
And I navigate to "Reports" in current page administration
And I click on "Activity completion" "link"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And I should see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should see "Cstudent Cstudent"
And I click on "A" "link" in the ".initialbar.lastinitial .page-item.A" "css_element"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And I should see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should not see "Cstudent Cstudent"
And I click on "B" "link" in the ".initialbar.firstinitial .page-item.B" "css_element"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And I should not see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should not see "Cstudent Cstudent"
And I am on "Course 1" course homepage
And I navigate to "Reports" in current page administration
And I click on "Activity completion" "link"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And I should not see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should not see "Cstudent Cstudent"
And I click on "All" "link" in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And I should see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should not see "Cstudent Cstudent"
And I click on "All" "link" in the ".initialbar.lastinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.firstinitial" "css_element"
And ".initialbarall.page-item.active" "css_element" should exist in the ".initialbar.lastinitial" "css_element"
And ".page-item.active.B" "css_element" should not exist in the ".initialbar.firstinitial" "css_element"
And ".page-item.active.A" "css_element" should not exist in the ".initialbar.lastinitial" "css_element"
And I should see "Astudent Astudent"
And I should see "Bstudent Astudent"
And I should see "Cstudent Cstudent"
+223
View File
@@ -0,0 +1,223 @@
<?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/>.
/**
* Steps definitions to open and close action menus.
*
* @package core
* @category test
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use Behat\Mink\Exception\{DriverException, ExpectationException};
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/../../behat/behat_base.php');
/**
* Steps definitions to assist with accessibility testing.
*
* @package core
* @category test
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_accessibility extends behat_base {
/**
* Run the axe-core accessibility tests.
*
* There are standard tags to ensure WCAG 2.1 A, WCAG 2.1 AA, and Section 508 compliance.
* It is also possible to specify any desired optional tags.
*
* The list of available tags can be found at
* https://github.com/dequelabs/axe-core/blob/v4.8.4/doc/rule-descriptions.md.
*
* @Then the page should meet accessibility standards
* @Then the page should meet accessibility standards with :extratags extra tests
* @Then the page should meet :standardtags accessibility standards
* @param string $standardtags Comma-separated list of standard tags to run
* @param string $extratags Comma-separated list of tags to run in addition to the standard tags
*/
public function run_axe_validation_for_tags(string $standardtags = '', string $extratags = ''): void {
$this->run_axe_for_tags(
// Turn the comma-separated string into an array of trimmed values, filtering out empty values.
array_filter(array_map('trim', explode(',', $standardtags))),
array_filter(array_map('trim', explode(',', $extratags)))
);
}
/**
* Run the Axe tests.
*
* See https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md for details of the supported
* tags.
*
* @param array $standardtags The list of standard tags to run
* @param array $extratags The list of tags, in addition to the standard tags, to run
*/
protected function run_axe_for_tags(array $standardtags = [], array $extratags = []): void {
if (!behat_config_manager::get_behat_run_config_value('axe')) {
return;
}
if (!$this->has_tag('accessibility')) {
throw new DriverException(
'Accessibility tests using Axe must have the @accessibility tag on either the scenario or feature.'
);
}
$this->require_javascript();
$axeurl = (new \moodle_url('/lib/behat/axe/axe.min.js'))->out(false);
$axeconfig = $this->get_axe_config_for_tags($standardtags, $extratags);
$runaxe = <<<EOF
(axeurl => {
const runTests = () => {
const axeTag = document.querySelector('script[data-purpose="axe"]');
axeTag.dataset.results = null;
axe.run({$axeconfig})
.then(results => {
axeTag.dataset.results = JSON.stringify({
violations: results.violations,
exception: null,
});
})
.catch(exception => {
axeTag.dataset.results = JSON.stringify({
violations: [],
exception: exception,
});
});
};
if (document.querySelector('script[data-purpose="axe"]')) {
runTests();
} else {
// Inject the axe content.
const axeTag = document.createElement('script');
axeTag.src = axeurl,
axeTag.dataset.purpose = 'axe';
axeTag.onload = () => runTests();
document.head.append(axeTag);
}
})('{$axeurl}');
EOF;
$this->execute_script($runaxe);
$getresults = <<<EOF
return (() => {
const axeTag = document.querySelector('script[data-purpose="axe"]');
return axeTag.dataset.results;
})()
EOF;
for ($i = 0; $i < self::get_extended_timeout() * 10; $i++) {
$results = json_decode($this->evaluate_script($getresults) ?? '');
if ($results) {
break;
}
}
if (empty($results)) {
throw new \Exception('No data');
}
if ($results->exception !== null) {
throw new ExpectationException($results->exception, $this->getSession());
}
$violations = $results->violations;
if (!count($violations)) {
return;
}
$violationdata = "Accessibility violations found:\n";
foreach ($violations as $violation) {
$nodedata = '';
foreach ($violation->nodes as $node) {
$failedchecks = [];
foreach (array_merge($node->any, $node->all, $node->none) as $check) {
$failedchecks[$check->id] = $check->message;
}
$nodedata .= sprintf(
" - %s:\n %s\n\n",
implode(', ', $failedchecks),
implode("\n ", $node->target)
);
}
$violationdata .= sprintf(
" %.03d violations of '%s' (severity: %s)\n%s\n",
count($violation->nodes),
$violation->description,
$violation->impact,
$nodedata
);
}
throw new ExpectationException($violationdata, $this->getSession());
}
/**
* Get the configuration to use with Axe.
*
* See https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md for details of the rules.
*
* @param array|null $standardtags The list of standard tags to run
* @param array|null $extratags The list of tags, in addition to the standard tags, to run
* @return string The JSON-encoded configuration.
*/
protected function get_axe_config_for_tags(?array $standardtags = null, ?array $extratags = null): string {
if (empty($standardtags)) {
$standardtags = [
// Meet WCAG 2.2 Level A success criteria.
'wcag22a',
// Meet WCAG 2.2 Level AA success criteria.
'wcag22aa',
// Meet Section 508 requirements.
// See https://www.epa.gov/accessibility/what-section-508 for detail.
'section508',
// Ensure that ARIA attributes are correctly defined.
'cat.aria',
// Requirements for sensory and visual cues.
// These largely related to viewport scale and zoom functionality.
'cat.sensory-and-visual-cues',
// Meet WCAG 1.3.4 requirements for orientation.
// See https://www.w3.org/WAI/WCAG21/Understanding/orientation.html for detail.
'wcag134',
];
}
return json_encode([
'runOnly' => [
'type' > 'tag',
'values' => array_merge($standardtags, $extratags),
],
]);
}
}
+211
View File
@@ -0,0 +1,211 @@
<?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/>.
/**
* Steps definitions to open and close action menus.
*
* @package core
* @category test
* @copyright 2016 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/../../behat/behat_base.php');
use Behat\Mink\Element\NodeElement;
use Behat\Mink\Exception\DriverException;
/**
* Steps definitions to open and close action menus.
*
* @package core
* @category test
* @copyright 2016 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_action_menu extends behat_base {
/**
* Open the action menu in
*
* @Given /^I open the action menu in "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)"$/
* @param string $element
* @param string $selector
* @return void
*/
public function i_open_the_action_menu_in($element, $selectortype) {
// Gets the node based on the requested selector type and locator.
$node = $this->get_node_in_container(
"css_element",
"[role=button][aria-haspopup=true],button[aria-haspopup=true],[role=menuitem][aria-haspopup=true]",
$selectortype,
$element
);
// Check if it is not already opened.
if ($node->getAttribute('aria-expanded') === 'true') {
return;
}
$node->click();
}
/**
* When an action menu is open, follow one of the items in it.
*
* The > is used to indicate a sub-menu. For example "Group mode > Visible groups"
* will do two clicks, one on the Group mode menu item, and one on the Visible groups link
* in the sub-menu.
*
* @Given /^I choose "(?P<link_string>(?:[^"]|\\")*)" in the open action menu$/
* @param string $linkstring
* @return void
*/
public function i_choose_in_the_open_action_menu($menuitemstring) {
if (!$this->running_javascript()) {
throw new DriverException('Action menu steps are not available with Javascript disabled');
}
// Check for sub-menus.
$menuitems = explode('>', $menuitemstring);
foreach ($menuitems as $menuitem) {
// Gets the node based on the requested selector type and locator.
$menuselector = ".moodle-actionmenu .dropdown.show .dropdown-menu";
$node = $this->get_node_in_container("link", trim($menuitem), "css_element", $menuselector);
$node->click();
}
}
/**
* Select a specific item in an action menu.
*
* @When /^I choose the "(?P<item_string>(?:[^"]|\\")*)" item in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu$/
* @param string $item The item to choose
* @param string $actionmenu The text used in the description of the action menu
*/
public function i_choose_in_the_named_menu(string $item, string $actionmenu): void {
$menu = $this->find('actionmenu', $actionmenu);
$this->select_item_in_action_menu($item, $menu);
}
/**
* Select a specific item in an action menu within a container.
*
* @When /^I choose the "(?P<item_string>(?:[^"]|\\")*)" item in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu of the "(?P<locator_string>(?:[^"]|\\")*)" "(?P<type_string>(?:[^"]|\\")*)"$/
* @param string $item The item to choose
* @param string $actionmenu The text used in the description of the action menu
* @param string|NodeElement $locator The identifer used for the container
* @param string $selector The type of container to locate
*/
public function i_choose_in_the_named_menu_in_container(string $item, string $actionmenu, $locator, $selector): void {
$container = $this->find($selector, $locator);
$menu = $this->find('actionmenu', $actionmenu, false, $container);
$this->select_item_in_action_menu($item, $menu);
}
/**
* Select an item in the specified menu.
*
* Note: This step does work both with, and without, JavaScript.
*
* @param string $item Item string value
* @param NodeElement $menu The menu NodeElement to select from
*/
protected function select_item_in_action_menu(string $item, NodeElement $menu): void {
if ($this->running_javascript()) {
// Open the menu by clicking on the trigger.
$this->execute(
'behat_general::i_click_on_in_the',
['a.dropdown-toggle', 'css_element', $menu, "NodeElement"]
);
}
// Select the menu item.
$this->execute(
'behat_general::i_click_on_in_the',
[$item, "link", $menu, "NodeElement"]
);
}
/**
* The action menu item should not exist.
*
* @Then /^the "(?P<item_string>(?:[^"]|\\")*)" item should not exist in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu$/
* @param string $item The item to check
* @param string $actionmenu The text used in the description of the action menu
*/
public function item_should_not_exist(string $item, string $actionmenu): void {
$menu = $this->find('actionmenu', $actionmenu);
$this->execute('behat_general::should_not_exist_in_the', [
$item, 'link',
$menu, 'NodeElement'
]);
}
/**
* The action menu item should not exist within a container.
*
* @Then /^the "(?P<item_string>(?:[^"]|\\")*)" item should not exist in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu of the "(?P<locator_string>(?:[^"]|\\")*)" "(?P<type_string>(?:[^"]|\\")*)"$/
* @param string $item The item to check
* @param string $actionmenu The text used in the description of the action menu
* @param string|NodeElement $locator The identifer used for the container
* @param string $selector The type of container to locate
*/
public function item_should_not_exist_in_the(string $item, string $actionmenu, $locator, $selector): void {
$container = $this->find($selector, $locator);
$menu = $this->find('actionmenu', $actionmenu, false, $container);
$this->execute('behat_general::should_not_exist_in_the', [
$item, 'link',
$menu, 'NodeElement'
]);
}
/**
* The action menu item should exist.
*
* @Then /^the "(?P<item_string>(?:[^"]|\\")*)" item should exist in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu$/
* @param string $item The item to check
* @param string $actionmenu The text used in the description of the action menu
*/
public function item_should_exist(string $item, string $actionmenu): void {
$menu = $this->find('actionmenu', $actionmenu);
$this->execute('behat_general::should_exist_in_the', [
$item, 'link',
$menu, 'NodeElement'
]);
}
/**
* The action menu item should exist within a container.
*
* @Then /^the "(?P<item_string>(?:[^"]|\\")*)" item should exist in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu of the "(?P<locator_string>(?:[^"]|\\")*)" "(?P<type_string>(?:[^"]|\\")*)"$/
* @param string $item The item to check
* @param string $actionmenu The text used in the description of the action menu
* @param string|NodeElement $locator The identifer used for the container
* @param string $selector The type of container to locate
*/
public function item_should_exist_in_the(string $item, string $actionmenu, $locator, $selector): void {
$container = $this->find($selector, $locator);
$menu = $this->find('actionmenu', $actionmenu, false, $container);
$this->execute('behat_general::should_exist_in_the', [
$item, 'link',
$menu, 'NodeElement'
]);
}
}
+248
View File
@@ -0,0 +1,248 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Data generators for acceptance testing.
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/../../behat/behat_base.php');
use Behat\Gherkin\Node\TableNode as TableNode;
use Behat\Behat\Tester\Exception\PendingException as PendingException;
/**
* Class to set up quickly a Given environment.
*
* The entry point is the Behat steps:
* the following "entity types" exist:
* | test | data |
*
* Entity type will either look like "users" or "activities" for core entities, or
* "mod_forum > subscription" or "core_message > message" for entities belonging
* to components.
*
* Generally, you only need to specify properties relevant to your test,
* and everything else gets set to sensible defaults.
*
* The actual generation of entities is done by {@link behat_generator_base}.
* There is one subclass for each component, e.g. {@link behat_core_generator}
* or {@link behat_mod_quiz_generator}. To see the types of entity
* that can be created for each component, look at the arrays returned
* by the get_creatable_entities() method in each class.
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_data_generators extends behat_base {
/**
* Convert legacy entity names to the new component-specific form.
*
* In the past, there was no support for plugins, and everything that
* could be created was handled by the core generator. Now, we can
* support plugins, and so some thing should probably be moved.
*
* For example, in the future we should probably add
* 'message contacts' => 'core_message > contact'] to
* this array, and move generation of message contact
* from core to core_message.
*
* @var array old entity type => new entity type.
*/
protected $movedentitytypes = [
];
/**
* Creates the specified elements.
*
* See the class comment for an overview.
*
* @Given /^the following "(?P<element_string>(?:[^"]|\\")*)" exist:$/
*
* @param string $entitytype The name of the type entity to add
* @param TableNode $data
*/
public function the_following_entities_exist($entitytype, TableNode $data) {
if (isset($this->movedentitytypes[$entitytype])) {
$entitytype = $this->movedentitytypes[$entitytype];
}
list($component, $entity) = $this->parse_entity_type($entitytype);
$this->get_instance_for_component($component)->generate_items($entity, $data);
}
/**
* Create multiple entities of one entity type.
*
* @Given :count :entitytype exist with the following data:
*
* @param string $entitytype The name of the type entity to add
* @param int $count
* @param TableNode $data
*/
public function the_following_repeated_entities_exist(string $entitytype, int $count, TableNode $data): void {
$rows = $data->getRowsHash();
$tabledata = [array_keys($rows)];
for ($current = 1; $current < $count + 1; $current++) {
$rowdata = [];
foreach ($rows as $fieldname => $fieldtemplate) {
$rowdata[$fieldname] = str_replace('[count]', $current, $fieldtemplate);
}
$tabledata[] = $rowdata;
}
if (isset($this->movedentitytypes[$entitytype])) {
$entitytype = $this->movedentitytypes[$entitytype];
}
list($component, $entity) = $this->parse_entity_type($entitytype);
$this->get_instance_for_component($component)->generate_items($entity, new TableNode($tabledata), false);
}
/**
* Creates the specified (singular) element.
*
* See the class comment for an overview.
*
* @Given the following :entitytype exists:
*
* @param string $entitytype The name of the type entity to add
* @param TableNode $data
*/
public function the_following_entity_exists($entitytype, TableNode $data) {
if (isset($this->movedentitytypes[$entitytype])) {
$entitytype = $this->movedentitytypes[$entitytype];
}
list($component, $entity) = $this->parse_entity_type($entitytype);
$this->get_instance_for_component($component)->generate_items($entity, $data, true);
}
/**
* Parse a full entity type like 'users' or 'mod_forum > subscription'.
*
* E.g. parsing 'course' gives ['core', 'course'] and
* parsing 'core_message > message' gives ['core_message', 'message'].
*
* @param string $entitytype the entity type
* @return string[] with two elements, component and entity type.
*/
protected function parse_entity_type(string $entitytype): array {
$dividercount = substr_count($entitytype, ' > ');
if ($dividercount === 0) {
return ['core', $entitytype];
} else if ($dividercount === 1) {
list($component, $type) = explode(' > ', $entitytype);
if ($component === 'core') {
throw new coding_exception('Do not specify the component "core > ..." for entity types.');
}
return [$component, $type];
} else {
throw new coding_exception('The entity type must be in the form ' .
'"{entity-type}" for core entities, or "{component} > {entity-type}" ' .
'for entities belonging to other components. ' .
'For example "users" or "mod_forum > subscriptions".');
}
}
/**
* Get an instance of the appropriate subclass of this class for a given component.
*
* @param string $component The name of the component to generate entities for.
* @return behat_generator_base the subclass of this class for the requested component.
*/
protected function get_instance_for_component(string $component): behat_generator_base {
global $CFG;
// Ensure the generator class is loaded.
require_once($CFG->libdir . '/behat/classes/behat_generator_base.php');
if ($component === 'core') {
$lib = $CFG->libdir . '/behat/classes/behat_core_generator.php';
} else {
$dir = core_component::get_component_directory($component);
$lib = $dir . '/tests/generator/behat_' . $component . '_generator.php';
if (!$dir || !is_readable($lib)) {
throw new coding_exception("Component {$component} does not support " .
"behat generators yet. Missing {$lib}.");
}
}
require_once($lib);
// Create an instance.
$componentclass = "behat_{$component}_generator";
if (!class_exists($componentclass)) {
throw new PendingException($component .
' does not yet support the Behat data generator mechanism. Class ' .
$componentclass . ' not found in file ' . $lib . '.');
}
$instance = new $componentclass($component);
return $instance;
}
/**
* Get all entities that can be created in all components using the_following_entities_exist()
*
* @return array
* @throws coding_exception
*/
public function get_all_entities(): array {
global $CFG;
// Ensure the generator class is loaded.
require_once($CFG->libdir . '/behat/classes/behat_generator_base.php');
$componenttypes = core_component::get_component_list();
$coregenerator = $this->get_instance_for_component('core');
$pluginswithentities = ['core' => array_keys($coregenerator->get_available_generators())];
foreach ($componenttypes as $components) {
foreach ($components as $component => $componentdir) {
try {
$plugingenerator = $this->get_instance_for_component($component);
$entities = array_keys($plugingenerator->get_available_generators());
if (!empty($entities)) {
$pluginswithentities[$component] = $entities;
}
} catch (Exception $e) {
// The component has no generator, skip it.
continue;
}
}
}
return $pluginswithentities;
}
/**
* Get the required fields for a specific creatable entity.
*
* @param string $entitytype
* @return mixed
* @throws coding_exception
*/
public function get_entity(string $entitytype): array {
[$component, $entity] = $this->parse_entity_type($entitytype);
$generator = $this->get_instance_for_component($component);
$entities = $generator->get_available_generators();
if (!array_key_exists($entity, $entities)) {
throw new coding_exception('No generator for ' . $entity . ' in component ' . $component);
}
return $entities[$entity];
}
}
+34
View File
@@ -0,0 +1,34 @@
<?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/>.
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/../../../lib/behat/behat_deprecated_base.php');
/**
* Steps definitions that are now deprecated and will be removed in the next releases.
*
* This file only contains the steps that previously were in the behat_*.php files in the SAME DIRECTORY.
* When deprecating steps from other components or plugins, create a behat_COMPONENT_deprecated.php
* file in the same directory where the steps were defined.
*
* @package core
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_deprecated extends behat_deprecated_base {
}
+211
View File
@@ -0,0 +1,211 @@
<?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/>.
/**
* Steps definitions to verify a downloaded file.
*
* @package core
* @category test
* @copyright 2024 Simey Lameze <simey@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use Behat\Gherkin\Node\TableNode;
use Behat\Mink\Exception\ExpectationException;
require_once(__DIR__ . '/../../behat/behat_base.php');
/**
* Steps definitions to verify a downloaded file.
*
* @package core
* @category test
* @copyright 2024 Simey Lameze <simey@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_download extends behat_base {
/**
* Downloads the file from a link on the page and verify the type and content.
*
* @Then following :link_text should download a file that:
*
* @param string $linktext the text of the link.
* @param TableNode $table the table of assertions to use the check the file contents.
* @throws ExpectationException if the file cannot be downloaded, or if the download does not pass all the checks.
*/
public function following_should_download_a_file_that(string $linktext, TableNode $table): void {
$this->following_in_element_should_download_a_file_that($linktext, '', '', $table);
}
/**
* Downloads the file from a link on the page and verify the type and content.
*
* @Then following :link_text in the :element_container_string :text_selector_string should download a file that:
*
* @param string $linktext the text of the link.
* @param string $containerlocator the container element.
* @param string $containertype the container selector type.
* @param TableNode $table the table of assertions to use the check the file contents.
* @throws ExpectationException if the file cannot be downloaded, or if the download does not pass all the checks.
*/
public function following_in_element_should_download_a_file_that(string $linktext, string $containerlocator,
string $containertype, TableNode $table): void {
$filecontent = $this->download_file($linktext, $containerlocator, $containertype);
$this->verify_file_content($filecontent, $table);
}
/**
* Download a file from the given link.
*
* @param string $linktext the text of the link.
* @param string $containerlocator the container element.
* @param string $containertype the container selector type.
* @return string the file contents.
* @throws ExpectationException if the download fails.
*/
protected function download_file(string $linktext, string $containerlocator, string $containertype): string {
return behat_context_helper::get('behat_general')->download_file_from_link($linktext, $containerlocator, $containertype);
}
/**
* Checks the content of the downloaded file.
*
* @param string $filecontent the content of the file.
* @param TableNode $table the table of assertions to check.
* @throws ExpectationException if the file content does not pass all the checks.
*/
private function verify_file_content(string $filecontent, TableNode $table): void {
foreach ($table->getRows() as $row) {
switch (strtolower(trim($row[0]))) {
case 'contains text':
$this->verify_file_contains_text($filecontent, $row[1]);
break;
case 'contains text in xml element':
$this->verify_xml_element_contains($filecontent, $row[1]);
break;
case 'has mimetype':
$this->verify_file_mimetype($filecontent, $row[1]);
break;
case 'contains file in zip':
$this->verify_zip_file_content($filecontent, $row[1]);
break;
default:
throw new ExpectationException(
'Invalid type of file assertion: ' . $row[0], $this->getSession());
}
}
}
/**
* Validates the downloaded file appears to be of the mimetype.
*
* @param string $filecontent the content of the file.
* @param string $expectedmimetype the expected file mimetype e.g. 'application/xml'.
* @throws ExpectationException if the file does not appear to be of the expected type.
*/
protected function verify_file_mimetype(string $filecontent, string $expectedmimetype): void {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$actualmimetype = $finfo->buffer($filecontent);
if ($actualmimetype !== $expectedmimetype) {
throw new ExpectationException(
"The file downloaded should have been a $expectedmimetype file, " .
"but got $actualmimetype instead.",
$this->getSession(),
);
}
}
/**
* Asserts that the given string is present in the file content.
*
* @param string $filecontent the content of the file.
* @param string $expectedcontent the string to search for.
* @throws ExpectationException if verification fails.
*/
protected function verify_file_contains_text(string $filecontent, string $expectedcontent): void {
if (!str_contains($filecontent, $expectedcontent)) {
throw new ExpectationException(
"The string '$expectedcontent' was not found in the file content.",
$this->getSession(),
);
}
}
/**
* Asserts that the given XML file is valid and contains the expected string.
*
* @param string $filecontent the content of the file.
* @param string $expectedcontent the string to search for.
* @throws ExpectationException
*/
protected function verify_xml_element_contains(string $filecontent, string $expectedcontent): void {
$xml = new SimpleXMLElement($filecontent);
$result = $xml->xpath("//*[contains(text(), '$expectedcontent')]");
if (empty($result)) {
throw new ExpectationException(
"The string '$expectedcontent' was not found in the content of any element in this XML file.",
$this->getSession(),
);
}
}
/**
* Save the downloaded file to tempdir and return the path.
*
* @param string $filecontent the content of the file.
* @param string $fileextension the expected file type, given as a file extension, e.g. 'txt', 'xml'.
* @return string path where the file was saved temporarily.
*/
protected function save_to_temp_file(string $filecontent, string $fileextension): string {
// Then perform additional image-specific validations.
$tempdir = make_request_directory();
$filepath = $tempdir . '/downloaded.' . $fileextension;
file_put_contents($filepath, $filecontent);
return $filepath;
}
/**
* Asserts that the given zip archive contains the expected file(s).
*
* @param string $filecontent the content of the file.
* @param string $expectedfile the name of the file to search for.
* @throws ExpectationException if the zip file does not contain the expected files.
*/
protected function verify_zip_file_content(string $filecontent, string $expectedfile): void {
$zip = new ZipArchive();
$res = $zip->open($this->save_to_temp_file($filecontent, 'zip'));
if ($res !== true) {
throw new ExpectationException(
"Failed to open zip file.",
$this->getSession(),
);
}
if ($zip->locateName($expectedfile) === false) {
throw new ExpectationException(
"The file '$expectedfile' was not found in the downloaded zip archive.",
$this->getSession(),
);
}
}
}
+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/>.
/**
* Steps definitions related to filters.
*
* @package core
* @category test
* @copyright 2018 the Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// Note: You cannot use MOODLE_INTERNAL test here, or include files which do so.
// This file is required by behat before including /config.php.
require_once(__DIR__ . '/../../behat/behat_base.php');
/**
* Steps definitions related to filters.
*
* @package core
* @category test
* @copyright 2018 the Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_filters extends behat_base {
/**
* Set the global filter configuration.
*
* @Given /^the "(?P<filter_name>(?:[^"]|\\")*)" filter is "(on|off|disabled)"$/
*
* @param string $filtername the name of a filter, e.g. 'glossary'.
* @param string $statename 'on', 'off' or 'disabled'.
*/
public function the_filter_is($filtername, $statename) {
require_once(__DIR__ . '/../../filterlib.php');
switch ($statename) {
case 'on':
$state = TEXTFILTER_ON;
break;
case 'off':
$state = TEXTFILTER_OFF;
break;
case 'disabled':
$state = TEXTFILTER_DISABLED;
break;
default:
throw new coding_exception('Unknown filter state: ' . $statename);
}
filter_set_global_state($filtername, $state);
}
/**
* Set the global filter target.
*
* @Given /^the "(?P<filter_name>(?:[^"]|\\")*)" filter applies to "(content|content and headings)"$/
*
* @param string $filtername the name of a filter, e.g. 'glossary'.
* @param string $filtertarget 'content' or 'content and headings'.
*/
public function the_filter_applies_to($filtername, $filtertarget) {
switch ($filtertarget) {
case 'content and headings':
filter_set_applies_to_strings($filtername, 1);
break;
case 'content':
filter_set_applies_to_strings($filtername, 0);
break;
default:
throw new coding_exception('Unknown filter target: ' . $filtertarget);
}
}
}
+837
View File
@@ -0,0 +1,837 @@
<?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/>.
/**
* Steps definitions related with forms.
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
require_once(__DIR__ . '/../../../lib/behat/behat_field_manager.php');
use Behat\Gherkin\Node\{TableNode, PyStringNode};
use Behat\Mink\Element\NodeElement;
use Behat\Mink\Exception\{ElementNotFoundException, ExpectationException};
/**
* Forms-related steps definitions.
*
* Note, Behat tests to verify that the steps defined here work as advertised
* are kept in admin/tool/behat/tests/behat.
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_forms extends behat_base {
/**
* Presses button with specified id|name|title|alt|value.
*
* @When /^I press "(?P<button_string>(?:[^"]|\\")*)"$/
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $button
*/
public function press_button($button) {
$this->execute('behat_general::i_click_on', [$button, 'button']);
}
/**
* Press button with specified id|name|title|alt|value and switch to main window.
*
* @When /^I press "(?P<button_string>(?:[^"]|\\")*)" and switch to main window$/
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $button
*/
public function press_button_and_switch_to_main_window($button) {
// Ensures the button is present, before pressing.
$buttonnode = $this->find_button($button);
$buttonnode->press();
$this->wait_for_pending_js();
$this->look_for_exceptions();
// Switch to main window.
$this->execute('behat_general::switch_to_the_main_window');
}
/**
* Fills a form with field/value data.
*
* @Given /^I set the following fields to these values:$/
* @throws ElementNotFoundException Thrown by behat_base::find
* @param TableNode $data
*/
public function i_set_the_following_fields_to_these_values(TableNode $data) {
// Expand all fields in case we have.
$this->expand_all_fields();
$datahash = $data->getRowsHash();
// The action depends on the field type.
foreach ($datahash as $locator => $value) {
$this->set_field_value($locator, $value);
}
}
/**
* Fills a form with field/value data.
*
* @Given /^I set the following fields in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" to these values:$/
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $containerelement Element we look in
* @param string $containerselectortype The type of selector where we look in
* @param TableNode $data
*/
public function i_set_the_following_fields_in_container_to_these_values(
$containerelement, $containerselectortype, TableNode $data) {
// Expand all fields in case we have.
$this->expand_all_fields();
$datahash = $data->getRowsHash();
// The action depends on the field type.
foreach ($datahash as $locator => $value) {
$this->set_field_value_in_container($locator, $value, $containerselectortype, $containerelement);
}
}
/**
* Expands all moodleform's fields, including collapsed fieldsets and advanced fields if they are present.
* @Given /^I expand all fieldsets$/
*/
public function i_expand_all_fieldsets() {
$this->expand_all_fields();
}
/**
* Expands all moodle form fieldsets if they exists.
*
* Externalized from i_expand_all_fields to call it from
* other form-related steps without having to use steps-group calls.
*
* @throws ElementNotFoundException Thrown by behat_base::find_all
* @return void
*/
protected function expand_all_fields() {
// Expand only if JS mode, else not needed.
if (!$this->running_javascript()) {
return;
}
// We already know that we waited for the DOM and the JS to be loaded, even the editor
// so, we will use the reduced timeout as it is a common task and we should save time.
try {
$this->wait_for_pending_js();
// Expand all fieldsets link - which will only be there if there is more than one collapsible section.
$expandallxpath = "//div[@class='collapsible-actions']" .
"//a[contains(concat(' ', @class, ' '), ' collapsed ')]" .
"//span[contains(concat(' ', @class, ' '), ' expandall ')]";
// Else, look for the first expand fieldset link (old theme structure).
$expandsectionold = "//legend[@class='ftoggler']" .
"//a[contains(concat(' ', @class, ' '), ' icons-collapse-expand ') and @aria-expanded = 'false']";
// Else, look for the first expand fieldset link (current theme structure).
$expandsectioncurrent = "//fieldset//div[contains(concat(' ', @class, ' '), ' ftoggler ')]" .
"//a[contains(concat(' ', @class, ' '), ' icons-collapse-expand ') and @aria-expanded = 'false']";
$collapseexpandlink = $this->find('xpath', $expandallxpath . '|' . $expandsectionold . '|' . $expandsectioncurrent,
false, false, behat_base::get_reduced_timeout());
$collapseexpandlink->click();
$this->wait_for_pending_js();
} catch (ElementNotFoundException $e) {
// The behat_base::find() method throws an exception if there are no elements,
// we should not fail a test because of this. We continue if there are not expandable fields.
}
// Different try & catch as we can have expanded fieldsets with advanced fields on them.
try {
// Expand all fields xpath.
$showmorexpath = "//a[normalize-space(.)='" . get_string('showmore', 'form') . "']" .
"[contains(concat(' ', normalize-space(@class), ' '), ' moreless-toggler')]";
// We don't wait here as we already waited when getting the expand fieldsets links.
if (!$showmores = $this->getSession()->getPage()->findAll('xpath', $showmorexpath)) {
return;
}
if ($this->getSession()->getDriver() instanceof \DMore\ChromeDriver\ChromeDriver) {
// Chrome Driver produces unique xpaths for each element.
foreach ($showmores as $showmore) {
$showmore->click();
}
} else {
// Funny thing about this, with findAll() we specify a pattern and each element matching the pattern
// is added to the array with of xpaths with a [0], [1]... sufix, but when we click on an element it
// does not matches the specified xpath anymore (now is a "Show less..." link) so [1] becomes [0],
// that's why we always click on the first XPath match, will be always the next one.
$iterations = count($showmores);
for ($i = 0; $i < $iterations; $i++) {
$showmores[0]->click();
}
}
} catch (ElementNotFoundException $e) {
// We continue with the test.
}
}
/**
* Sets the field to wwwroot plus the given path. Include the first slash.
*
* @Given /^I set the field "(?P<field_string>(?:[^"]|\\")*)" to local url "(?P<field_path_string>(?:[^"]|\\")*)"$/
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $field
* @param string $path
* @return void
*/
public function i_set_the_field_to_local_url($field, $path) {
global $CFG;
$this->set_field_value($field, $CFG->wwwroot . $path);
}
/**
* Sets the specified value to the field.
*
* @Given /^I set the field "(?P<field_string>(?:[^"]|\\")*)" to "(?P<field_value_string>(?:[^"]|\\")*)"$/
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $field
* @param string $value
* @return void
*/
public function i_set_the_field_to($field, $value) {
$this->set_field_value($field, $value);
}
/**
* Sets the specified value to the field.
*
* @Given /^I set the field "(?P<field_string>(?:[^"]|\\")*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" to "(?P<field_value_string>(?:[^"]|\\")*)"$/
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $field
* @param string $containerelement Element we look in
* @param string $containerselectortype The type of selector where we look in
* @param string $value
*/
public function i_set_the_field_in_container_to($field, $containerelement, $containerselectortype, $value) {
$this->set_field_value_in_container($field, $value, $containerselectortype, $containerelement);
}
/**
* Press the key in the field to trigger the javascript keypress event
*
* Note that the character key will not actually be typed in the input field
*
* @Given /^I press key "(?P<key_string>(?:[^"]|\\")*)" in the field "(?P<field_string>(?:[^"]|\\")*)"$/
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $key either char-code or character itself,
* may optionally be prefixed with ctrl-, alt-, shift- or meta-
* @param string $field
* @return void
*/
public function i_press_key_in_the_field($key, $field) {
if (!$this->running_javascript()) {
throw new DriverException('Key press step is not available with Javascript disabled');
}
$fld = behat_field_manager::get_form_field_from_label($field, $this);
$modifier = null;
$char = $key;
if (preg_match('/-/', $key)) {
list($modifier, $char) = preg_split('/-/', $key, 2);
}
if (is_numeric($char)) {
$char = (int)$char;
}
$fld->key_press($char, $modifier);
}
/**
* Sets the specified value to the field.
*
* @Given /^I set the field "(?P<field_string>(?:[^"]|\\")*)" to multiline:$/
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $field
* @param PyStringNode $value
* @return void
*/
public function i_set_the_field_to_multiline($field, PyStringNode $value) {
$this->set_field_value($field, (string)$value);
}
/**
* Sets the specified value to the field with xpath.
*
* @Given /^I set the field with xpath "(?P<fieldxpath_string>(?:[^"]|\\")*)" to "(?P<field_value_string>(?:[^"]|\\")*)"$/
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $field
* @param string $value
* @return void
*/
public function i_set_the_field_with_xpath_to($fieldxpath, $value) {
$this->set_field_node_value($this->find('xpath', $fieldxpath), $value);
}
/**
* Checks, the field matches the value.
*
* @Then /^the field "(?P<field_string>(?:[^"]|\\")*)" matches value "(?P<field_value_string>(?:[^"]|\\")*)"$/
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $field
* @param string $value
* @return void
*/
public function the_field_matches_value($field, $value) {
// Get the field.
$formfield = behat_field_manager::get_form_field_from_label($field, $this);
// Checks if the provided value matches the current field value.
if (!$formfield->matches($value)) {
$fieldvalue = $formfield->get_value();
throw new ExpectationException(
'The \'' . $field . '\' value is \'' . $fieldvalue . '\', \'' . $value . '\' expected' ,
$this->getSession()
);
}
}
/**
* Checks, the field contains the value.
*
* @Then /^the field "(?P<field_string>(?:[^"]|\\")*)" (?P<doesnot_bool>does not )?match(?:es)* expression "(?P<expression_string>(?:[^"]|\\")*)"$/
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $field The naem or reference to the field
* @param bool $doesnot
* @param string $expression The Perl-like regular expression, including any delimeters and flag
* @return void
*/
public function the_field_matches_expression(
string $field,
bool $doesnot,
string $expression,
): void {
// Get the field.
$formfield = behat_field_manager::get_form_field_from_label($field, $this);
// Checks if the provided value matches the current field value.
$fieldvalue = $formfield->get_value();
$matches = preg_match($expression, $fieldvalue);
if ($matches === 1 && $doesnot) {
throw new ExpectationException(
"The '{$field}' field matches the expression '{$expression}' and it should not",
$this->getSession()
);
} else if ($matches === 0 && !$doesnot) {
throw new ExpectationException(
"The '{$field}' field does not match the expression '{$expression}'",
$this->getSession()
);
} else if ($matches === false) {
throw new coding_exception(
"The expression '{$expression}' was not valid",
);
}
}
/**
* Checks, the field matches the value.
*
* @Then /^the field "(?P<field_string>(?:[^"]|\\")*)" matches multiline:$/
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $field
* @param PyStringNode $value
* @return void
*/
public function the_field_matches_multiline($field, PyStringNode $value) {
$this->the_field_matches_value($field, (string)$value);
}
/**
* Checks, the field does not match the value.
*
* @Then /^the field "(?P<field_string>(?:[^"]|\\")*)" does not match value "(?P<field_value_string>(?:[^"]|\\")*)"$/
* @throws ExpectationException
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $field
* @param string $value
*/
public function the_field_does_not_match_value($field, $value) {
// Get the field.
$formfield = behat_field_manager::get_form_field_from_label($field, $this);
// Checks if the provided value matches the current field value.
if ($formfield->matches($value)) {
throw new ExpectationException(
'The \'' . $field . '\' value matches \'' . $value . '\' and it should not match it' ,
$this->getSession()
);
}
}
/**
* Checks, the field matches the value.
*
* @Then /^the field "(?P<field_string>(?:[^"]|\\")*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" matches value "(?P<field_value_string>(?:[^"]|\\")*)"$/
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $field
* @param string $containerelement Element we look in
* @param string $containerselectortype The type of selector where we look in
* @param string $value
*/
public function the_field_in_container_matches_value($field, $containerelement, $containerselectortype, $value) {
// Get the field.
$node = $this->get_node_in_container('field', $field, $containerselectortype, $containerelement);
$formfield = behat_field_manager::get_form_field($node, $this->getSession());
// Checks if the provided value matches the current field value.
if (!$formfield->matches($value)) {
$fieldvalue = $formfield->get_value();
throw new ExpectationException(
'The \'' . $field . '\' value is \'' . $fieldvalue . '\', \'' . $value . '\' expected' ,
$this->getSession()
);
}
}
/**
* Checks, the field does not match the value.
*
* @Then /^the field "(?P<field_string>(?:[^"]|\\")*)" in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" does not match value "(?P<field_value_string>(?:[^"]|\\")*)"$/
* @throws ExpectationException
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $field
* @param string $containerelement Element we look in
* @param string $containerselectortype The type of selector where we look in
* @param string $value
*/
public function the_field_in_container_does_not_match_value($field, $containerelement, $containerselectortype, $value) {
// Get the field.
$node = $this->get_node_in_container('field', $field, $containerselectortype, $containerelement);
$formfield = behat_field_manager::get_form_field($node, $this->getSession());
// Checks if the provided value matches the current field value.
if ($formfield->matches($value)) {
throw new ExpectationException(
'The \'' . $field . '\' value matches \'' . $value . '\' and it should not match it' ,
$this->getSession()
);
}
}
/**
* Checks, the field matches the value.
*
* @Then /^the field with xpath "(?P<xpath_string>(?:[^"]|\\")*)" matches value "(?P<field_value_string>(?:[^"]|\\")*)"$/
* @throws ExpectationException
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $fieldxpath
* @param string $value
* @return void
*/
public function the_field_with_xpath_matches_value($fieldxpath, $value) {
// Get the field.
$fieldnode = $this->find('xpath', $fieldxpath);
$formfield = behat_field_manager::get_form_field($fieldnode, $this->getSession());
// Checks if the provided value matches the current field value.
if (!$formfield->matches($value)) {
$fieldvalue = $formfield->get_value();
throw new ExpectationException(
'The \'' . $fieldxpath . '\' value is \'' . $fieldvalue . '\', \'' . $value . '\' expected' ,
$this->getSession()
);
}
}
/**
* Checks, the field does not match the value.
*
* @Then /^the field with xpath "(?P<xpath_string>(?:[^"]|\\")*)" does not match value "(?P<field_value_string>(?:[^"]|\\")*)"$/
* @throws ExpectationException
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $fieldxpath
* @param string $value
* @return void
*/
public function the_field_with_xpath_does_not_match_value($fieldxpath, $value) {
// Get the field.
$fieldnode = $this->find('xpath', $fieldxpath);
$formfield = behat_field_manager::get_form_field($fieldnode, $this->getSession());
// Checks if the provided value matches the current field value.
if ($formfield->matches($value)) {
throw new ExpectationException(
'The \'' . $fieldxpath . '\' value matches \'' . $value . '\' and it should not match it' ,
$this->getSession()
);
}
}
/**
* Checks, the provided field/value matches.
*
* @Then /^the following fields match these values:$/
* @throws ExpectationException
* @param TableNode $data Pairs of | field | value |
*/
public function the_following_fields_match_these_values(TableNode $data) {
// Expand all fields in case we have.
$this->expand_all_fields();
$datahash = $data->getRowsHash();
// The action depends on the field type.
foreach ($datahash as $locator => $value) {
$this->the_field_matches_value($locator, $value);
}
}
/**
* Checks that the provided field/value pairs don't match.
*
* @Then /^the following fields do not match these values:$/
* @throws ExpectationException
* @param TableNode $data Pairs of | field | value |
*/
public function the_following_fields_do_not_match_these_values(TableNode $data) {
// Expand all fields in case we have.
$this->expand_all_fields();
$datahash = $data->getRowsHash();
// The action depends on the field type.
foreach ($datahash as $locator => $value) {
$this->the_field_does_not_match_value($locator, $value);
}
}
/**
* Checks, the provided field/value matches.
*
* @Then /^the following fields in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" match these values:$/
* @throws ExpectationException
* @param string $containerelement Element we look in
* @param string $containerselectortype The type of selector where we look in
* @param TableNode $data Pairs of | field | value |
*/
public function the_following_fields_in_container_match_these_values(
$containerelement, $containerselectortype, TableNode $data) {
// Expand all fields in case we have.
$this->expand_all_fields();
$datahash = $data->getRowsHash();
// The action depends on the field type.
foreach ($datahash as $locator => $value) {
$this->the_field_in_container_matches_value($locator, $containerelement, $containerselectortype, $value);
}
}
/**
* Checks that the provided field/value pairs don't match.
*
* @Then /^the following fields in the "(?P<element_container_string>(?:[^"]|\\")*)" "(?P<text_selector_string>[^"]*)" do not match these values:$/
* @throws ExpectationException
* @param string $containerelement Element we look in
* @param string $containerselectortype The type of selector where we look in
* @param TableNode $data Pairs of | field | value |
*/
public function the_following_fields_in_container_do_not_match_these_values(
$containerelement, $containerselectortype, TableNode $data) {
// Expand all fields in case we have.
$this->expand_all_fields();
$datahash = $data->getRowsHash();
// The action depends on the field type.
foreach ($datahash as $locator => $value) {
$this->the_field_in_container_does_not_match_value($locator, $containerelement, $containerselectortype, $value);
}
}
/**
* Checks, that given select box contains the specified option.
*
* @Then /^the "(?P<select_string>(?:[^"]|\\")*)" select box should contain "(?P<option_string>(?:[^"]|\\")*)"$/
* @throws ExpectationException
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $select The select element name
* @param string $option The option text/value. Plain value or comma separated
* values if multiple. Commas in multiple values escaped with backslash.
*/
public function the_select_box_should_contain($select, $option) {
$selectnode = $this->find_field($select);
$multiple = $selectnode->hasAttribute('multiple');
$optionsarr = array(); // Array of passed value/text options to test.
if ($multiple) {
// Can pass multiple comma separated, with valuable commas escaped with backslash.
foreach (preg_replace('/\\\,/', ',', preg_split('/(?<!\\\),/', $option)) as $opt) {
$optionsarr[] = trim($opt);
}
} else {
// Only one option has been passed.
$optionsarr[] = trim($option);
}
// Now get all the values and texts in the select.
$options = $selectnode->findAll('xpath', '//option');
$values = array();
foreach ($options as $opt) {
$values[trim($opt->getValue())] = trim($opt->getText());
}
foreach ($optionsarr as $opt) {
// Verify every option is a valid text or value.
if (!in_array($opt, $values) && !array_key_exists($opt, $values)) {
throw new ExpectationException(
'The select box "' . $select . '" does not contain the option "' . $opt . '"',
$this->getSession()
);
}
}
}
/**
* Checks, that given select box does not contain the specified option.
*
* @Then /^the "(?P<select_string>(?:[^"]|\\")*)" select box should not contain "(?P<option_string>(?:[^"]|\\")*)"$/
* @throws ExpectationException
* @throws ElementNotFoundException Thrown by behat_base::find
* @param string $select The select element name
* @param string $option The option text/value. Plain value or comma separated
* values if multiple. Commas in multiple values escaped with backslash.
*/
public function the_select_box_should_not_contain($select, $option) {
$selectnode = $this->find_field($select);
$multiple = $selectnode->hasAttribute('multiple');
$optionsarr = array(); // Array of passed value/text options to test.
if ($multiple) {
// Can pass multiple comma separated, with valuable commas escaped with backslash.
foreach (preg_replace('/\\\,/', ',', preg_split('/(?<!\\\),/', $option)) as $opt) {
$optionsarr[] = trim($opt);
}
} else {
// Only one option has been passed.
$optionsarr[] = trim($option);
}
// Now get all the values and texts in the select.
$options = $selectnode->findAll('xpath', '//option');
$values = array();
foreach ($options as $opt) {
$values[trim($opt->getValue())] = trim($opt->getText());
}
foreach ($optionsarr as $opt) {
// Verify every option is not a valid text or value.
if (in_array($opt, $values) || array_key_exists($opt, $values)) {
throw new ExpectationException(
'The select box "' . $select . '" contains the option "' . $opt . '"',
$this->getSession()
);
}
}
}
/**
* Generic field setter.
*
* Internal API method, a generic *I set "VALUE" to "FIELD" field*
* could be created based on it.
*
* @param string $fieldlocator The pointer to the field, it will depend on the field type.
* @param string $value
* @return void
*/
protected function set_field_value($fieldlocator, $value) {
// We delegate to behat_form_field class, it will
// guess the type properly as it is a select tag.
$field = behat_field_manager::get_form_field_from_label($fieldlocator, $this);
$field->set_value($value);
}
/**
* Generic field setter to be used by chainable steps.
*
* @param NodeElement $fieldnode
* @param string $value
*/
public function set_field_node_value(NodeElement $fieldnode, string $value): void {
$field = behat_field_manager::get_form_field($fieldnode, $this->getSession());
$field->set_value($value);
}
/**
* Generic field setter.
*
* Internal API method, a generic *I set "VALUE" to "FIELD" field*
* could be created based on it.
*
* @param string $fieldlocator The pointer to the field, it will depend on the field type.
* @param string $value the value to set
* @param string $containerselectortype The type of selector where we look in
* @param string $containerelement Element we look in
*/
protected function set_field_value_in_container($fieldlocator, $value, $containerselectortype, $containerelement) {
$node = $this->get_node_in_container('field', $fieldlocator, $containerselectortype, $containerelement);
$this->set_field_node_value($node, $value);
}
/**
* Select a value from single select and redirect.
*
* @Given /^I select "(?P<singleselect_option_string>(?:[^"]|\\")*)" from the "(?P<singleselect_name_string>(?:[^"]|\\")*)" singleselect$/
*/
public function i_select_from_the_singleselect($option, $singleselect) {
$this->execute('behat_forms::i_set_the_field_to', array($this->escape($singleselect), $this->escape($option)));
if (!$this->running_javascript()) {
// Press button in the specified select container.
$containerxpath = "//div[" .
"(contains(concat(' ', normalize-space(@class), ' '), ' singleselect ') " .
"or contains(concat(' ', normalize-space(@class), ' '), ' urlselect ')".
") and (
.//label[contains(normalize-space(string(.)), '" . $singleselect . "')] " .
"or .//select[(./@name='" . $singleselect . "' or ./@id='". $singleselect . "')]" .
")]";
$this->execute('behat_general::i_click_on_in_the',
array(get_string('go'), "button", $containerxpath, "xpath_element")
);
}
}
/**
* Select item from autocomplete list.
*
* @Given /^I click on "([^"]*)" item in the autocomplete list$/
*
* @param string $item
*/
public function i_click_on_item_in_the_autocomplete_list($item) {
$xpathtarget = "//ul[@class='form-autocomplete-suggestions']//*" .
"[contains(concat('|', normalize-space(.), '|'),'|" . $item . "|')]";
$this->execute('behat_general::i_click_on', [$xpathtarget, 'xpath_element']);
}
/**
* Open the auto-complete suggestions list (Assuming there is only one on the page.).
*
* @Given I open the autocomplete suggestions list
* @Given I open the autocomplete suggestions list in the :container :containertype
*/
public function i_open_the_autocomplete_suggestions_list($container = null, $containertype = null) {
$csstarget = ".form-autocomplete-downarrow";
if ($container && $containertype) {
$this->execute('behat_general::i_click_on_in_the', [$csstarget, 'css_element', $container, $containertype]);
} else {
$this->execute('behat_general::i_click_on', [$csstarget, 'css_element']);
}
}
/**
* Expand the given autocomplete list
*
* @Given /^I expand the "(?P<field_string>(?:[^"]|\\")*)" autocomplete$/
*
* @param string $field Field name
*/
public function i_expand_the_autocomplete($field) {
$csstarget = '.form-autocomplete-downarrow';
$node = $this->get_node_in_container('css_element', $csstarget, 'form_row', $field);
$node->click();
}
/**
* Assert the given option exist in the given autocomplete list
*
* @Given /^I should see "(?P<option_string>(?:[^"]|\\")*)" in the list of options for the "(?P<field_string>(?:[^"]|\\")*)" autocomplete$$/
*
* @param string $option Name of option
* @param string $field Field name
*/
public function i_should_see_in_the_list_of_option_for_the_autocomplete($option, $field) {
$xpathtarget = "//div[contains(@class, 'form-autocomplete-selection') and contains(.//div, '" . $option . "')]";
$node = $this->get_node_in_container('xpath_element', $xpathtarget, 'form_row', $field);
$this->ensure_node_is_visible($node);
}
/**
* Checks whether the select menu contains an option with specified text or not.
*
* @Then the :name select menu should contain :option
* @Then the :name select menu should :not contain :option
*
* @throws ExpectationException When the expectation is not satisfied
* @param string $label The label of the select menu element
* @param string $option The string that is used to identify an option within the select menu. If the string
* has two items separated by '>' (ex. "Group > Option"), the first item ("Group") will be
* used to identify a particular group within the select menu, while the second ("Option")
* will be used to identify an option within that group. Otherwise, a string with a single
* item (ex. "Option") will be used to identify an option within the select menu regardless
* of any existing groups.
* @param string|null $not If set, the select menu should not contain the specified option. If null, the option
* should be present.
*/
public function the_select_menu_should_contain(string $label, string $option, ?string $not = null) {
$field = behat_field_manager::get_form_field_from_label($label, $this);
if (!method_exists($field, 'has_option')) {
throw new coding_exception('Field does not support the has_option function.');
}
// If the select menu contains the specified option but it should not.
if ($field->has_option($option) && $not) {
throw new ExpectationException(
"The select menu should not contain \"{$option}\" but it does.",
$this->getSession()
);
}
// If the select menu does not contain the specified option but it should.
if (!$field->has_option($option) && !$not) {
throw new ExpectationException(
"The select menu should contain \"{$option}\" but it does not.",
$this->getSession()
);
}
}
}
File diff suppressed because it is too large Load Diff
+890
View File
@@ -0,0 +1,890 @@
<?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/>.
/**
* Behat hooks steps definitions.
*
* This methods are used by Behat CLI command.
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/../../behat/behat_base.php');
use Behat\Testwork\Hook\Scope\BeforeSuiteScope,
Behat\Testwork\Hook\Scope\AfterSuiteScope,
Behat\Behat\Hook\Scope\BeforeFeatureScope,
Behat\Behat\Hook\Scope\AfterFeatureScope,
Behat\Behat\Hook\Scope\BeforeScenarioScope,
Behat\Behat\Hook\Scope\AfterScenarioScope,
Behat\Behat\Hook\Scope\BeforeStepScope,
Behat\Behat\Hook\Scope\AfterStepScope,
Behat\Mink\Exception\ExpectationException,
Behat\Mink\Exception\DriverException,
Facebook\WebDriver\Exception\UnexpectedAlertOpenException,
Facebook\WebDriver\Exception\WebDriverCurlException,
Facebook\WebDriver\Exception\UnknownErrorException;
/**
* Hooks to the behat process.
*
* Behat accepts hooks after and before each
* suite, feature, scenario and step.
*
* They can not call other steps as part of their process
* like regular steps definitions does.
*
* Throws generic Exception because they are captured by Behat.
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_hooks extends behat_base {
/**
* @var For actions that should only run once.
*/
protected static $initprocessesfinished = false;
/** @var bool Whether the first javascript scenario has been seen yet */
protected static $firstjavascriptscenarioseen = false;
/**
* @var bool Scenario running
*/
protected $scenariorunning = false;
/**
* Some exceptions can only be caught in a before or after step hook,
* they can not be thrown there as they will provoke a framework level
* failure, but we can store them here to fail the step in i_look_for_exceptions()
* which result will be parsed by the framework as the last step result.
*
* @var ?Exception Null or the exception last step throw in the before or after hook.
*/
protected static $currentstepexception = null;
/**
* If an Exception is thrown in the BeforeScenario hook it will cause the Scenario to be skipped, and the exit code
* to be non-zero triggering a potential rerun.
*
* To combat this the exception is stored and re-thrown when looking for exceptions.
* This allows the test to instead be failed and re-run correctly.
*
* @var null|Exception
*/
protected static $currentscenarioexception = null;
/**
* If we are saving any kind of dump on failure we should use the same parent dir during a run.
*
* @var The parent dir name
*/
protected static $faildumpdirname = false;
/**
* Keeps track of time taken by feature to execute.
*
* @var array list of feature timings
*/
protected static $timings = array();
/**
* Keeps track of current running suite name.
*
* @var string current running suite name
*/
protected static $runningsuite = '';
/**
* @var array Array (with tag names in keys) of all tags in current scenario.
*/
protected static $scenariotags;
/**
* Gives access to moodle codebase, ensures all is ready and sets up the test lock.
*
* Includes config.php to use moodle codebase with $CFG->behat_* instead of $CFG->prefix and $CFG->dataroot, called
* once per suite.
*
* @BeforeSuite
* @param BeforeSuiteScope $scope scope passed by event fired before suite.
*/
public static function before_suite_hook(BeforeSuiteScope $scope) {
global $CFG;
// If behat has been initialised then no need to do this again.
if (!self::is_first_scenario()) {
return;
}
// Defined only when the behat CLI command is running, the moodle init setup process will
// read this value and switch to $CFG->behat_dataroot and $CFG->behat_prefix instead of
// the normal site.
if (!defined('BEHAT_TEST')) {
define('BEHAT_TEST', 1);
}
if (!defined('CLI_SCRIPT')) {
define('CLI_SCRIPT', 1);
}
// With BEHAT_TEST we will be using $CFG->behat_* instead of $CFG->dataroot, $CFG->prefix and $CFG->wwwroot.
require_once(__DIR__ . '/../../../config.php');
// Now that we are MOODLE_INTERNAL.
require_once(__DIR__ . '/../../behat/classes/behat_command.php');
require_once(__DIR__ . '/../../behat/classes/behat_selectors.php');
require_once(__DIR__ . '/../../behat/classes/behat_context_helper.php');
require_once(__DIR__ . '/../../behat/classes/util.php');
require_once(__DIR__ . '/../../testing/classes/test_lock.php');
require_once(__DIR__ . '/../../testing/classes/nasty_strings.php');
// Avoids vendor/bin/behat to be executed directly without test environment enabled
// to prevent undesired db & dataroot modifications, this is also checked
// before each scenario (accidental user deletes) in the BeforeScenario hook.
if (!behat_util::is_test_mode_enabled()) {
self::log_and_stop('Behat only can run if test mode is enabled. More info in ' . behat_command::DOCS_URL);
}
// Reset all data, before checking for check_server_status.
// If not done, then it can return apache error, while running tests.
behat_util::clean_tables_updated_by_scenario_list();
behat_util::reset_all_data();
// Check if the web server is running and using same version for cli and apache.
behat_util::check_server_status();
// Prevents using outdated data, upgrade script would start and tests would fail.
if (!behat_util::is_test_data_updated()) {
$commandpath = 'php admin/tool/behat/cli/init.php';
$message = <<<EOF
Your behat test site is outdated, please run the following command from your Moodle dirroot to drop, and reinstall the Behat test site.
{$commandpath}
EOF;
self::log_and_stop($message);
}
// Avoid parallel tests execution, it continues when the previous lock is released.
test_lock::acquire('behat');
if (!empty($CFG->behat_faildump_path) && !is_writable($CFG->behat_faildump_path)) {
self::log_and_stop(
"The \$CFG->behat_faildump_path value is set to a non-writable directory ({$CFG->behat_faildump_path})."
);
}
// Handle interrupts on PHP7.
if (extension_loaded('pcntl')) {
$disabled = explode(',', ini_get('disable_functions'));
if (!in_array('pcntl_signal', $disabled)) {
declare(ticks = 1);
}
}
}
/**
* Run final tests before running the suite.
*
* @BeforeSuite
* @param BeforeSuiteScope $scope scope passed by event fired before suite.
*/
public static function before_suite_final_checks(BeforeSuiteScope $scope) {
$happy = defined('BEHAT_TEST');
$happy = $happy && defined('BEHAT_SITE_RUNNING');
$happy = $happy && php_sapi_name() == 'cli';
$happy = $happy && behat_util::is_test_mode_enabled();
$happy = $happy && behat_util::is_test_site();
if (!$happy) {
error_log('Behat only can modify the test database and the test dataroot!');
exit(1);
}
}
/**
* Gives access to moodle codebase, to keep track of feature start time.
*
* @param BeforeFeatureScope $scope scope passed by event fired before feature.
* @BeforeFeature
*/
public static function before_feature(BeforeFeatureScope $scope) {
if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
return;
}
$file = $scope->getFeature()->getFile();
self::$timings[$file] = microtime(true);
}
/**
* Gives access to moodle codebase, to keep track of feature end time.
*
* @param AfterFeatureScope $scope scope passed by event fired after feature.
* @AfterFeature
*/
public static function after_feature(AfterFeatureScope $scope) {
if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
return;
}
$file = $scope->getFeature()->getFile();
self::$timings[$file] = microtime(true) - self::$timings[$file];
// Probably didn't actually run this, don't output it.
if (self::$timings[$file] < 1) {
unset(self::$timings[$file]);
}
}
/**
* Gives access to moodle codebase, to keep track of suite timings.
*
* @param AfterSuiteScope $scope scope passed by event fired after suite.
* @AfterSuite
*/
public static function after_suite(AfterSuiteScope $scope) {
if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
return;
}
$realroot = realpath(__DIR__.'/../../../').'/';
foreach (self::$timings as $k => $v) {
$new = str_replace($realroot, '', $k);
self::$timings[$new] = round($v, 1);
unset(self::$timings[$k]);
}
if ($existing = @json_decode(file_get_contents(BEHAT_FEATURE_TIMING_FILE), true)) {
self::$timings = array_merge($existing, self::$timings);
}
arsort(self::$timings);
@file_put_contents(BEHAT_FEATURE_TIMING_FILE, json_encode(self::$timings, JSON_PRETTY_PRINT));
}
/**
* Helper function to restart the Mink session.
*/
protected function restart_session(): void {
$session = $this->getSession();
if ($session->isStarted()) {
$session->restart();
} else {
$this->start_session();
}
if ($this->running_javascript() && $this->getSession()->getDriver()->getWebDriverSessionId() === 'session') {
throw new DriverException('Unable to create a valid session');
}
}
/**
* Start the Session, applying any initial configuratino required.
*/
protected function start_session(): void {
$this->getSession()->start();
$this->set_test_timeout_factor(1);
}
/**
* Restart the session before each non-javascript scenario.
*
* @BeforeScenario @~javascript
* @param BeforeScenarioScope $scope scope passed by event fired before scenario.
*/
public function before_browserkit_scenarios(BeforeScenarioScope $scope) {
if ($this->running_javascript()) {
// A bug in the BeforeScenario filtering prevents the @~javascript filter on this hook from working
// properly.
// See https://github.com/Behat/Behat/issues/1235 for further information.
return;
}
$this->restart_session();
}
/**
* Start the session before the first javascript scenario.
*
* This is treated slightly differently to try to capture when Selenium is not running at all.
*
* @BeforeScenario @javascript
* @param BeforeScenarioScope $scope scope passed by event fired before scenario.
*/
public function before_first_scenario_start_session(BeforeScenarioScope $scope) {
if (!self::is_first_javascript_scenario()) {
// The first Scenario has started.
// The `before_subsequent_scenario_start_session` function will restart the session instead.
return;
}
$docsurl = behat_command::DOCS_URL;
$driverexceptionmsg = <<<EOF
The Selenium or WebDriver server is not running. You must start it to run tests that involve Javascript.
See {$docsurl} for more information.
The following debugging information is available:
EOF;
try {
$this->restart_session();
} catch (WebDriverCurlException | DriverException $e) {
// Thrown by WebDriver.
self::log_and_stop(
$driverexceptionmsg . '. ' .
$e->getMessage() . "\n\n" .
format_backtrace($e->getTrace(), true)
);
} catch (UnknownErrorException $e) {
// Generic 'I have no idea' Selenium error. Custom exception to provide more feedback about possible solutions.
self::log_and_stop(
$e->getMessage() . "\n\n" .
format_backtrace($e->getTrace(), true)
);
}
}
/**
* Start the session before each javascript scenario.
*
* Note: Before the first scenario the @see before_first_scenario_start_session() function is used instead.
*
* @BeforeScenario @javascript
* @param BeforeScenarioScope $scope scope passed by event fired before scenario.
*/
public function before_subsequent_scenario_start_session(BeforeScenarioScope $scope) {
if (self::is_first_javascript_scenario()) {
// The initial init has not yet finished.
// The `before_first_scenario_start_session` function will have started the session instead.
return;
}
self::$currentscenarioexception = null;
try {
$this->restart_session();
} catch (Exception $e) {
self::$currentscenarioexception = $e;
}
}
/**
* Resets the test environment.
*
* @BeforeScenario
* @param BeforeScenarioScope $scope scope passed by event fired before scenario.
*/
public function before_scenario_hook(BeforeScenarioScope $scope) {
global $DB;
if (self::$currentscenarioexception) {
// A BeforeScenario hook triggered an exception and marked this test as failed.
// Skip this hook as it will likely fail.
return;
}
$suitename = $scope->getSuite()->getName();
// Register behat selectors for theme, if suite is changed. We do it for every suite change.
if ($suitename !== self::$runningsuite) {
self::$runningsuite = $suitename;
behat_context_helper::set_environment($scope->getEnvironment());
// We need the Mink session to do it and we do it only before the first scenario.
$namedpartialclass = 'behat_partial_named_selector';
$namedexactclass = 'behat_exact_named_selector';
// If override selector exist, then set it as default behat selectors class.
$overrideclass = behat_config_util::get_behat_theme_selector_override_classname($suitename, 'named_partial', true);
if (class_exists($overrideclass)) {
$namedpartialclass = $overrideclass;
}
// If override selector exist, then set it as default behat selectors class.
$overrideclass = behat_config_util::get_behat_theme_selector_override_classname($suitename, 'named_exact', true);
if (class_exists($overrideclass)) {
$namedexactclass = $overrideclass;
}
$this->getSession()->getSelectorsHandler()->registerSelector('named_partial', new $namedpartialclass());
$this->getSession()->getSelectorsHandler()->registerSelector('named_exact', new $namedexactclass());
// Register component named selectors.
foreach (\core_component::get_component_names() as $component) {
$this->register_component_selectors_for_component($component);
}
}
// Reset $SESSION.
\core\session\manager::init_empty_session();
// Ignore E_NOTICE and E_WARNING during reset, as this might be caused because of some existing process
// running ajax. This will be investigated in another issue.
$errorlevel = error_reporting();
error_reporting($errorlevel & ~E_NOTICE & ~E_WARNING);
behat_util::reset_all_data();
error_reporting($errorlevel);
if ($this->running_javascript()) {
// Fetch the user agent.
// This isused to choose between the SVG/Non-SVG versions of themes.
$useragent = $this->getSession()->evaluateScript('return navigator.userAgent;');
\core_useragent::instance(true, $useragent);
// Restore the saved themes.
behat_util::restore_saved_themes();
}
// Assign valid data to admin user (some generator-related code needs a valid user).
$user = $DB->get_record('user', array('username' => 'admin'));
\core\session\manager::set_user($user);
// Set the theme if not default.
if ($suitename !== "default") {
set_config('theme', $suitename);
}
// Reset the scenariorunning variable to ensure that Step 0 occurs.
$this->scenariorunning = false;
// Set up the tags for current scenario.
self::fetch_tags_for_scenario($scope);
// If scenario requires the Moodle app to be running, set this up.
if ($this->has_tag('app')) {
$this->execute('behat_app::start_scenario');
return;
}
// Run all test with medium (1024x768) screen size, to avoid responsive problems.
$this->resize_window('medium');
}
/**
* Mark the first Javascript Scenario as have been seen.
*
* @BeforeScenario
* @param BeforeScenarioScope $scope scope passed by event fired before scenario.
*/
public function mark_first_js_scenario_as_seen(BeforeScenarioScope $scope) {
self::$firstjavascriptscenarioseen = true;
}
/**
* Hook to open the site root before the first step in the suite.
* Yes, this is in a strange location and should be in the BeforeScenario hook, but failures in the test setUp lead
* to the test being incorrectly marked as skipped with no way to force the test to be failed.
*
* @param BeforeStepScope $scope
* @BeforeStep
*/
public function before_step(BeforeStepScope $scope) {
global $CFG;
if (!$this->scenariorunning) {
// We need to visit / before the first step in any Scenario.
// This is our Step 0.
// Ideally this would be in the BeforeScenario hook, but any exception in there will lead to the test being
// skipped rather than it being failed.
//
// We also need to check that the site returned is a Behat site.
// Again, this would be better in the BeforeSuite hook, but that does not have access to the selectors in
// order to perform the necessary searches.
$session = $this->getSession();
$this->execute('behat_general::i_visit', ['/']);
// Checking that the root path is a Moodle test site.
if (self::is_first_scenario()) {
$message = "The base URL ({$CFG->wwwroot}) is not a behat test site. " .
'Ensure that you started the built-in web server in the correct directory, ' .
'or that your web server is correctly set up and started.';
$this->find(
"xpath", "//head/child::title[contains(., '" . behat_util::BEHATSITENAME . "')]",
new ExpectationException($message, $session)
);
}
$this->scenariorunning = true;
}
}
/**
* Sets up the tags for the current scenario.
*
* @param \Behat\Behat\Hook\Scope\BeforeScenarioScope $scope Scope
*/
protected static function fetch_tags_for_scenario(\Behat\Behat\Hook\Scope\BeforeScenarioScope $scope) {
self::$scenariotags = array_flip(array_merge(
$scope->getScenario()->getTags(),
$scope->getFeature()->getTags()
));
}
/**
* Gets the tags for the current scenario
*
* @return array Array where key is tag name and value is an integer
*/
public static function get_tags_for_scenario(): array {
return self::$scenariotags;
}
/**
* Wait for JS to complete before beginning interacting with the DOM.
*
* Executed only when running against a real browser. We wrap it
* all in a try & catch to forward the exception to i_look_for_exceptions
* so the exception will be at scenario level, which causes a failure, by
* default would be at framework level, which will stop the execution of
* the run.
*
* @param BeforeStepScope $scope scope passed by event fired before step.
* @BeforeStep
*/
public function before_step_javascript(BeforeStepScope $scope) {
if (self::$currentscenarioexception) {
// A BeforeScenario hook triggered an exception and marked this test as failed.
// Skip this hook as it will likely fail.
return;
}
self::$currentstepexception = null;
// Only run if JS.
if ($this->running_javascript()) {
try {
$this->wait_for_pending_js();
} catch (Exception $e) {
self::$currentstepexception = $e;
}
}
}
/**
* Wait for JS to complete after finishing the step.
*
* With this we ensure that there are not AJAX calls
* still in progress.
*
* Executed only when running against a real browser. We wrap it
* all in a try & catch to forward the exception to i_look_for_exceptions
* so the exception will be at scenario level, which causes a failure, by
* default would be at framework level, which will stop the execution of
* the run.
*
* @param AfterStepScope $scope scope passed by event fired after step..
* @AfterStep
*/
public function after_step_javascript(AfterStepScope $scope) {
global $CFG, $DB;
// If step is undefined then throw exception, to get failed exit code.
if ($scope->getTestResult()->getResultCode() === Behat\Behat\Tester\Result\StepResult::UNDEFINED) {
throw new coding_exception("Step '" . $scope->getStep()->getText() . "'' is undefined.");
}
$isfailed = $scope->getTestResult()->getResultCode() === Behat\Testwork\Tester\Result\TestResult::FAILED;
// Abort any open transactions to prevent subsequent tests hanging.
// This does the same as abort_all_db_transactions(), but doesn't call error_log() as we don't
// want to see a message in the behat output.
if (($scope->getTestResult() instanceof \Behat\Behat\Tester\Result\ExecutedStepResult) &&
$scope->getTestResult()->hasException()) {
if ($DB && $DB->is_transaction_started()) {
$DB->force_transaction_rollback();
}
}
if ($isfailed && !empty($CFG->behat_faildump_path)) {
// Save the page content (html).
$this->take_contentdump($scope);
if ($this->running_javascript()) {
// Save a screenshot.
$this->take_screenshot($scope);
}
}
if ($isfailed && !empty($CFG->behat_pause_on_fail)) {
$exception = $scope->getTestResult()->getException();
$message = "<colour:lightRed>Scenario failed. ";
$message .= "<colour:lightYellow>Paused for inspection. Press <colour:lightRed>Enter/Return<colour:lightYellow> to continue.<newline>";
$message .= "<colour:lightRed>Exception follows:<newline>";
$message .= trim($exception->getMessage());
behat_util::pause($this->getSession(), $message);
}
// Only run if JS.
if (!$this->running_javascript()) {
return;
}
try {
$this->wait_for_pending_js();
self::$currentstepexception = null;
} catch (UnexpectedAlertOpenException $e) {
self::$currentstepexception = $e;
// Accepting the alert so the framework can continue properly running
// the following scenarios. Some browsers already closes the alert, so
// wrapping in a try & catch.
try {
$this->getSession()->getDriver()->getWebDriver()->switchTo()->alert()->accept();
} catch (Exception $e) {
// Catching the generic one as we never know how drivers reacts here.
}
} catch (Exception $e) {
self::$currentstepexception = $e;
}
}
/**
* Reset the session between each scenario.
*
* @param AfterScenarioScope $scope scope passed by event fired after scenario.
* @AfterScenario
*/
public function reset_webdriver_between_scenarios(AfterScenarioScope $scope) {
try {
$this->getSession()->stop();
} catch (Exception $e) {
$error = <<<EOF
Error while stopping WebDriver: %s (%d) '%s'
Attempting to continue with test run. Stacktrace follows:
%s
EOF;
error_log(sprintf(
$error,
get_class($e),
$e->getCode(),
$e->getMessage(),
format_backtrace($e->getTrace(), true)
));
}
}
/**
* Getter for self::$faildumpdirname
*
* @return string
*/
protected function get_run_faildump_dir() {
return self::$faildumpdirname;
}
/**
* Take screenshot when a step fails.
*
* @throws Exception
* @param AfterStepScope $scope scope passed by event after step.
*/
protected function take_screenshot(AfterStepScope $scope) {
// BrowserKit can't save screenshots.
if (!$this->running_javascript()) {
return false;
}
// Some drivers (e.g. chromedriver) may throw an exception while trying to take a screenshot. If this isn't handled,
// the behat run dies. We don't want to lose the information about the failure that triggered the screenshot,
// so let's log the exception message to a file (to explain why there's no screenshot) and allow the run to continue,
// handling the failure as normal.
try {
list ($dir, $filename) = $this->get_faildump_filename($scope, 'png');
$this->saveScreenshot($filename, $dir);
} catch (Exception $e) {
// Catching all exceptions as we don't know what the driver might throw.
list ($dir, $filename) = $this->get_faildump_filename($scope, 'txt');
$message = "Could not save screenshot due to an error\n" . $e->getMessage();
file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $message);
}
}
/**
* Take a dump of the page content when a step fails.
*
* @throws Exception
* @param AfterStepScope $scope scope passed by event after step.
*/
protected function take_contentdump(AfterStepScope $scope) {
list ($dir, $filename) = $this->get_faildump_filename($scope, 'html');
try {
// Driver may throw an exception during getContent(), so do it first to avoid getting an empty file.
$content = $this->getSession()->getPage()->getContent();
} catch (Exception $e) {
// Catching all exceptions as we don't know what the driver might throw.
$content = "Could not save contentdump due to an error\n" . $e->getMessage();
}
file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $content);
}
/**
* Determine the full pathname to store a failure-related dump.
*
* This is used for content such as the DOM, and screenshots.
*
* @param AfterStepScope $scope scope passed by event after step.
* @param String $filetype The file suffix to use. Limited to 4 chars.
*/
protected function get_faildump_filename(AfterStepScope $scope, $filetype) {
global $CFG;
// All the contentdumps should be in the same parent dir.
if (!$faildumpdir = self::get_run_faildump_dir()) {
$faildumpdir = self::$faildumpdirname = date('Ymd_His');
$dir = $CFG->behat_faildump_path . DIRECTORY_SEPARATOR . $faildumpdir;
if (!is_dir($dir) && !mkdir($dir, $CFG->directorypermissions, true)) {
// It shouldn't, we already checked that the directory is writable.
throw new Exception('No directories can be created inside $CFG->behat_faildump_path, check the directory permissions.');
}
} else {
// We will always need to know the full path.
$dir = $CFG->behat_faildump_path . DIRECTORY_SEPARATOR . $faildumpdir;
}
// The scenario title + the failed step text.
// We want a i-am-the-scenario-title_i-am-the-failed-step.$filetype format.
$filename = $scope->getFeature()->getTitle() . '_' . $scope->getStep()->getText();
// As file name is limited to 255 characters. Leaving 5 chars for line number and 4 chars for the file.
// extension as we allow .png for images and .html for DOM contents.
$filenamelen = 245;
// Suffix suite name to faildump file, if it's not default suite.
$suitename = $scope->getSuite()->getName();
if ($suitename != 'default') {
$suitename = '_' . $suitename;
$filenamelen = $filenamelen - strlen($suitename);
} else {
// No need to append suite name for default.
$suitename = '';
}
$filename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $filename);
$filename = substr($filename, 0, $filenamelen) . $suitename . '_' . $scope->getStep()->getLine() . '.' . $filetype;
return array($dir, $filename);
}
/**
* Internal step definition to find exceptions, debugging() messages and PHP debug messages.
*
* Part of behat_hooks class as is part of the testing framework, is auto-executed
* after each step so no features will splicitly use it.
*
* @Given /^I look for exceptions$/
* @throw Exception Unknown type, depending on what we caught in the hook or basic \Exception.
* @see Moodle\BehatExtension\EventDispatcher\Tester\ChainedStepTester
*/
public function i_look_for_exceptions() {
// If the scenario already failed in a hook throw the exception.
if (!is_null(self::$currentscenarioexception)) {
throw self::$currentscenarioexception;
}
// If the step already failed in a hook throw the exception.
if (!is_null(self::$currentstepexception)) {
throw self::$currentstepexception;
}
$this->look_for_exceptions();
}
/**
* Returns whether the first scenario of the suite is running
*
* @return bool
*/
protected static function is_first_scenario() {
return !(self::$initprocessesfinished);
}
/**
* Returns whether the first scenario of the suite is running
*
* @return bool
*/
protected static function is_first_javascript_scenario(): bool {
return !self::$firstjavascriptscenarioseen;
}
/**
* Register a set of component selectors.
*
* @param string $component
*/
public function register_component_selectors_for_component(string $component): void {
$context = behat_context_helper::get_component_context($component);
if ($context === null) {
return;
}
$namedpartial = $this->getSession()->getSelectorsHandler()->getSelector('named_partial');
$namedexact = $this->getSession()->getSelectorsHandler()->getSelector('named_exact');
// Replacements must come before selectors as they are used in the selectors.
foreach ($context->get_named_replacements() as $replacement) {
$namedpartial->register_replacement($component, $replacement);
$namedexact->register_replacement($component, $replacement);
}
foreach ($context->get_partial_named_selectors() as $selector) {
$namedpartial->register_component_selector($component, $selector);
}
foreach ($context->get_exact_named_selectors() as $selector) {
$namedexact->register_component_selector($component, $selector);
}
}
/**
* Mark the first step as having been completed.
*
* This must be the last BeforeStep hook in the setup.
*
* @param BeforeStepScope $scope
* @BeforeStep
*/
public function first_step_setup_complete(BeforeStepScope $scope): void {
self::$initprocessesfinished = true;
}
/**
* Log a notification, and then exit.
*
* @param string $message The content to dispaly
*/
protected static function log_and_stop(string $message): void {
error_log($message);
exit(1);
}
}
+56
View File
@@ -0,0 +1,56 @@
<?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/>.
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
use Moodle\BehatExtension\Exception\SkippedException;
require_once(__DIR__ . '/../../behat/behat_base.php');
/**
* Steps definitions related to MoodleNet.
*
* @package core
* @category test
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_moodlenet extends behat_base {
/**
* Check that the TEST_MOODLENET_MOCK_SERVER is defined, so we can connect to the mock server.
*
* @Given /^a MoodleNet mock server is configured$/
*/
public function mock_is_configured(): void {
if (!defined('TEST_MOODLENET_MOCK_SERVER')) {
throw new SkippedException(
'The TEST_MOODLENET_MOCK_SERVER constant must be defined to run MoodleNet tests'
);
}
}
/**
* Change the service base url to the TEST_MOODLENET_MOCK_SERVER url.
*
* @Given /^I change the MoodleNet field "(?P<field_string>(?:[^"]|\\")*)" to mock server$/
* @param string $field Field name
*/
public function change_service_base_url_to_mock_url(string $field): void {
$field = behat_field_manager::get_form_field_from_label($field, $this);
$field->set_value(TEST_MOODLENET_MOCK_SERVER);
}
}
File diff suppressed because it is too large Load Diff
+322
View File
@@ -0,0 +1,322 @@
<?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/>.
/**
* Steps definitions related with permissions.
*
* @package core
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/../../behat/behat_base.php');
use Behat\Mink\Exception\ExpectationException as ExpectationException,
Behat\Gherkin\Node\TableNode as TableNode;
/**
* Steps definitions to set up permissions to capabilities.
*
* @package core
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_permissions extends behat_base {
/**
* Set system level permissions to the specified role. Expects a table with capability name and permission (Inherit/Allow/Prevent/Prohibit) columns.
* @Given /^I set the following system permissions of "(?P<rolefullname_string>(?:[^"]|\\")*)" role:$/
* @param string $rolename
* @param TableNode $table
*/
public function i_set_the_following_system_permissions_of_role($rolename, $table) {
// Applied in the System context.
$context = \context_system::instance();
// Translate the specified rolename into a role.
$rolenames = role_get_names($context);
$matched = array_filter($rolenames, function($role) use ($rolename) {
return ($role->localname === $rolename) || ($role->shortname === $rolename) || ($role->description === $rolename);
});
if (count($matched) === 0) {
throw new ExpectationException("Unable to find a role with name '{$rolename}'", $this->getSession());
} else if (count($matched) > 1) {
throw new ExpectationException("Multiple roles matched '{$rolename}'", $this->getSession());
}
$role = reset($matched);
$permissionmap = [
get_string('inherit', 'role') => 'inherit',
get_string('allow', 'role') => 'allow',
get_string('prevent', 'role') => 'prevent',
get_string('prohibit', 'role') => 'prohibit',
];
$columns = ['role'];
$newtabledata = [$role->shortname];
foreach ($table as $data) {
$columns[] = $data['capability'];
$newtabledata[] = $permissionmap[$data['permission']];
}
$this->execute(
'behat_data_generators::the_following_entities_exist',
[
'role capabilities',
new TableNode([
0 => $columns,
1 => $newtabledata,
])
]
);
}
/**
* Overrides system capabilities at category, course and module levels. This step begins after clicking 'Permissions' link. Expects a table with capability name and permission (Inherit/Allow/Prevent/Prohibit) columns.
* @Given /^I override the system permissions of "(?P<rolefullname_string>(?:[^"]|\\")*)" role with:$/
* @param string $rolename
* @param TableNode $table
*/
public function i_override_the_system_permissions_of_role_with($rolename, $table) {
// We don't know the number of overrides so we have to get it to match the option contents.
$roleoption = $this->find('xpath', '//select[@name="roleid"]/option[contains(.,"' . $this->escape($rolename) . '")]');
$this->execute('behat_forms::i_set_the_field_to',
array(get_string('advancedoverride', 'role'), $this->escape($roleoption->getText()))
);
if (!$this->running_javascript()) {
$xpath = "//div[@class='advancedoverride']/div/form/noscript";
$this->execute("behat_general::i_click_on_in_the", [
get_string('go'), 'button',
$this->escape($xpath),
'xpath_element']
);
}
$this->execute("behat_permissions::i_fill_the_capabilities_form_with_the_following_permissions", $table);
$this->execute('behat_forms::press_button', get_string('savechanges'));
}
/**
* Fills the advanced permissions form with the provided data. Expects a table with capability name and permission (Inherit/Allow/Prevent/Prohibit) columns.
* @Given /^I fill the capabilities form with the following permissions:$/
* @param TableNode $table
* @return void
*/
public function i_fill_the_capabilities_form_with_the_following_permissions($table) {
// Ensure we are using the advanced view.
// Wrapped in a try/catch to capture the exception and continue execution, we don't know if advanced mode was already enabled.
try {
$advancedtoggle = $this->find_button(get_string('showadvanced', 'form'));
if ($advancedtoggle) {
$advancedtoggle->click();
// Wait for the page to load.
$this->getSession()->wait(self::get_timeout() * 1000, self::PAGE_READY_JS);
}
} catch (Exception $e) {
// We already are in advanced mode.
}
// Using getRows() as we are not sure if tests writers will add the header.
foreach ($table->getRows() as $key => $row) {
if (count($row) !== 2) {
throw new ExpectationException('You should specify a table with capability/permission columns', $this->getSession());
}
list($capability, $permission) = $row;
// Skip the headers row if it was provided
if (strtolower($capability) == 'capability' || strtolower($capability) == 'capabilities') {
continue;
}
// Checking the permission value.
$permissionconstant = 'CAP_'. strtoupper($permission);
if (!defined($permissionconstant)) {
throw new ExpectationException(
'The provided permission value "' . $permission . '" is not valid. Use Inherit, Allow, Prevent or Prohibited',
$this->getSession()
);
}
// Converting from permission to constant value.
$permissionvalue = constant($permissionconstant);
// Here we wait for the element to appear and exception if it does not exist.
$radio = $this->find('xpath', '//input[@name="' . $capability . '" and @value="' . $permissionvalue . '"]');
$field = behat_field_manager::get_field_instance('radio', $radio, $this->getSession());
$field->set_value(1);
}
}
/**
* Checks if the capability has the specified permission. Works in the role definition advanced page.
*
* @Then /^"(?P<capability_string>(?:[^"]|\\")*)" capability has "(?P<permission_string>Not set|Allow|Prevent|Prohibit)" permission$/
* @throws ExpectationException
* @param string $capabilityname
* @param string $permission
* @return void
*/
public function capability_has_permission($capabilityname, $permission) {
// We already know the name, so we just need the value.
$radioxpath = "//table[contains(concat(' ',
normalize-space(@class), ' '), ' rolecap ')]/descendant::input[@type='radio']" .
"[@name='" . $capabilityname . "'][@checked]";
$checkedradio = $this->find('xpath', $radioxpath);
switch ($permission) {
case get_string('notset', 'role'):
$perm = CAP_INHERIT;
break;
case get_string('allow', 'role'):
$perm = CAP_ALLOW;
break;
case get_string('prevent', 'role'):
$perm = CAP_PREVENT;
break;
case get_string('prohibit', 'role'):
$perm = CAP_PROHIBIT;
break;
default:
throw new ExpectationException('"' . $permission . '" permission does not exist', $this->getSession());
break;
}
if ($checkedradio->getAttribute('value') != $perm) {
throw new ExpectationException('"' . $capabilityname . '" permission is not "' . $permission . '"', $this->getSession());
}
}
/**
* Set the allowed role assignments for the specified role.
*
* @Given /^I define the allowed role assignments for the "(?P<rolefullname_string>(?:[^"]|\\")*)" role as:$/
* @param string $rolename
* @param TableNode $table
* @return void Executes other steps
*/
public function i_define_the_allowed_role_assignments_for_a_role_as($rolename, $table) {
$parentnodes = get_string('users', 'admin') . ' > ' .
get_string('permissions', 'role');
// Go to home page.
$this->execute("behat_general::i_am_on_homepage");
// Navigate to Define roles page via site administration menu.
$this->execute("behat_navigation::i_navigate_to_in_site_administration",
$parentnodes .' > '. get_string('defineroles', 'role')
);
$this->execute("behat_general::click_link", "Allow role assignments");
$this->execute("behat_permissions::i_fill_in_the_allowed_role_assignments_form_for_a_role_with",
array($rolename, $table)
);
$this->execute('behat_forms::press_button', get_string('savechanges'));
}
/**
* Fill in the allowed role assignments form for the specied role.
*
* Takes a table with two columns. Each row should contain the target
* role, and either "Assignable" or "Not assignable".
*
* @Given /^I fill in the allowed role assignments form for the "(?P<rolefullname_string>(?:[^"]|\\")*)" role with:$/
* @param String $sourcerole
* @param TableNode $table
* @return void
*/
public function i_fill_in_the_allowed_role_assignments_form_for_a_role_with($sourcerole, $table) {
foreach ($table->getRows() as $key => $row) {
list($targetrole, $allowed) = $row;
$node = $this->find('xpath', '//input[@title="Allow users with role ' .
$sourcerole .
' to assign the role ' .
$targetrole . '"]');
if ($allowed == 'Assignable') {
if (!$node->isChecked()) {
$node->check();
}
} else if ($allowed == 'Not assignable') {
if ($node->isChecked()) {
$node->uncheck();
}
} else {
throw new ExpectationException(
'The provided permission value "' . $allowed . '" is not valid. Use Assignable, or Not assignable',
$this->getSession()
);
}
}
}
/**
* Mark context as frozen.
*
* @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" is context frozen$/
* @throws ExpectationException if the context cannot be frozen or found
* @param string $element Element we look on
* @param string $selector The type of where we look (activity, course)
*/
public function the_context_is_context_frozen(string $element, string $selector) {
// Enable context freeze if it is not done yet.
set_config('contextlocking', 1);
// Find context.
$context = self::get_context($selector, $element);
// Freeze context.
$context->set_locked(true);
}
/**
* Unmark context as frozen.
*
* @Then /^the "(?P<element_string>(?:[^"]|\\")*)" "(?P<selector_string>[^"]*)" is not context frozen$/
* @throws ExpectationException if the context cannot be frozen or found
* @param string $element Element we look on
* @param string $selector The type of where we look (activity, course)
*/
public function the_context_is_not_context_frozen(string $element, string $selector) {
// Enable context freeze if it is not done yet.
set_config('contextlocking', 1);
// Find context.
$context = self::get_context($selector, $element);
// Freeze context.
$context->set_locked(false);
}
}
+208
View File
@@ -0,0 +1,208 @@
<?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/>.
/**
* Behat arguments transformations.
*
* This methods are used by Behat CLI command.
*
* @package core
* @category test
* @copyright 2012 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/../../behat/behat_base.php');
use Behat\Gherkin\Node\TableNode;
/**
* Transformations to apply to steps arguments.
*
* This methods are applied to the steps arguments that matches
* the regular expressions specified in the @Transform tag.
*
* @package core
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_transformations extends behat_base {
/**
* @deprecated since Moodle 3.2
*/
public function prefixed_tablenode_transformations() {
throw new coding_exception('prefixed_tablenode_transformations() can not be used anymore. ' .
'Please use tablenode_transformations() instead.');
}
/**
* Removes escaped argument delimiters.
*
* We use double quotes as arguments delimiters and
* to add the " as part of an argument we escape it
* with a backslash, this method removes this backslash.
*
* @Transform /^((.*)"(.*))$/
* @param string $string
* @return string The string with the arguments fixed.
*/
public function arg_replace_slashes($string) {
if (!is_scalar($string)) {
return $string;
}
return str_replace('\"', '"', $string);
}
/**
* Replaces $NASTYSTRING vars for a nasty string.
*
* @Transform /^((.*)\$NASTYSTRING(\d)(.*))$/
* @param string $argument The whole argument value.
* @return string
*/
public function arg_replace_nasty_strings($argument) {
if (!is_scalar($argument)) {
return $argument;
}
return $this->replace_nasty_strings($argument);
}
/**
* Convert string time to timestamp.
* Use ::time::STRING_TIME_TO_CONVERT::DATE_FORMAT::
*
* @Transform /^##(.*)##$/
* @param string $time
* @return int timestamp.
*/
public function arg_time_to_string($time) {
return $this->get_transformed_timestamp($time);
}
/**
* Transformations for TableNode arguments.
*
* Transformations applicable to TableNode arguments should also
* be applied, adding them in a different method for Behat API restrictions.
*
* @Transform table:*
* @param TableNode $tablenode
* @return TableNode The transformed table
*/
public function tablenode_transformations(TableNode $tablenode) {
global $CFG;
// Walk through all values including the optional headers.
$rows = $tablenode->getRows();
foreach ($rows as $rowkey => $row) {
foreach ($row as $colkey => $value) {
// Transforms vars into nasty strings.
if (preg_match('/\$NASTYSTRING(\d)/', $rows[$rowkey][$colkey])) {
$rows[$rowkey][$colkey] = $this->replace_nasty_strings($rows[$rowkey][$colkey]);
}
// Transform time.
if (preg_match('/^##(.*)##$/', $rows[$rowkey][$colkey], $match)) {
if (isset($match[1])) {
$rows[$rowkey][$colkey] = $this->get_transformed_timestamp($match[1]);
}
}
// Transform wwwroot.
if (preg_match('/#wwwroot#/', $rows[$rowkey][$colkey])) {
$rows[$rowkey][$colkey] = $this->replace_wwwroot($rows[$rowkey][$colkey]);
}
}
}
// Return the transformed TableNode.
unset($tablenode);
$tablenode = new TableNode($rows);
return $tablenode;
}
/**
* Convert #wwwroot# to the wwwroot config value, so it is
* possible to reference fully qualified URLs within the site.
*
* @Transform /^((.*)#wwwroot#(.*))$/
* @param string $string
* @return string
*/
public function arg_insert_wwwroot(string $string): string {
return $this->replace_wwwroot($string);
}
/**
* Replaces $NASTYSTRING vars for a nasty string.
*
* Method reused by TableNode tranformation.
*
* @param string $string
* @return string
*/
public function replace_nasty_strings($string) {
return preg_replace_callback(
'/\$NASTYSTRING(\d)/',
function ($matches) {
return nasty_strings::get($matches[0]);
},
$string
);
}
/**
* Return timestamp for the time passed.
*
* @param string $time time to convert
* @return string
*/
protected function get_transformed_timestamp($time) {
$timepassed = explode('##', $time);
// If not a valid time string, then just return what was passed.
if ((($timestamp = strtotime($timepassed[0])) === false)) {
return $time;
}
$count = count($timepassed);
if ($count === 2) {
// If timestamp with specified strftime format, then return formatted date string.
return userdate($timestamp, $timepassed[1]);
} else if ($count === 1) {
return $timestamp;
} else {
// If not a valid time string, then just return what was passed.
return $time;
}
}
/**
* Replace #wwwroot# with the actual wwwroot config value.
*
* @param string $string String to attempt the replacement in.
* @return string
*/
protected function replace_wwwroot(string $string): string {
global $CFG;
return str_replace('#wwwroot#', $CFG->wwwroot, $string);
}
}
+60
View File
@@ -0,0 +1,60 @@
@core @javascript @core_form
Feature: Any day / month / year combination in date form elements works ok.
In order to use date / datetime elements with Behat
as a user
Any day / month / year combination must work ok
@javascript
Scenario Outline: Verify that setting any date / datetime is possible with enabled fields
Given the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "activity" exist:
| activity | name | intro | course | idnumber |
| assign | Assignment 01 | Assign activity to test some dates | C1 | assign01 |
And I am on the "Assignment 01" "assign activity editing" page logged in as admin
And I expand all fieldsets
And I set the field "Due date" to "<initial_date>"
And I set the field "Due date" to "<final_date>"
When I press "Save and display"
Then the activity date in "Assignment 01" should contain "Due:"
And the activity date in "Assignment 01" should contain "<date_result>"
Examples:
| initial_date | final_date | date_result | case_explanation (times Australia/Perth) |
| ##today## | ##tomorrow noon## | ##tomorrow noon##%A, %d %B %Y, %I:%M## | change of day, any day, back and forth |
| ##tomorrow## | ##today noon## | ##today noon##%A, %d %B %Y, %I:%M## | |
| 1617256800 | 1617170400 | Wednesday, 31 March 2021, 2:00 | change of month, back and forth |
| 1617170400 | 1617256800 | Thursday, 1 April 2021, 2:00 | |
| 1740808800 | 1709186400 | Thursday, 29 February 2024, 2:00 | change of month, leap year, back and forth |
| 1709186400 | 1740808800 | Saturday, 1 March 2025, 2:00 | |
| 1577858400 | 1577772000 | Tuesday, 31 December 2019, 2:00 | change of year, back and forth |
| 1577772000 | 1577858400 | Wednesday, 1 January 2020, 2:00 | |
@javascript
Scenario Outline: Verify that setting any date / datetime is possible with disabled fields
Given the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "activity" exist:
| activity | name | intro | course | idnumber |
| assign | Assignment 01 | Assign activity to test some dates | C1 | assign01 |
And I am on the "Assignment 01" "assign activity editing" page logged in as admin
And I expand all fieldsets
And I set the field "Due date" to "<initial_date>"
And I set the field "Due date" to "disabled"
And I set the field "Due date" to "<final_date>"
When I press "Save and display"
Then the activity date in "Assignment 01" should contain "Due:"
And the activity date in "Assignment 01" should contain "<date_result>"
Examples:
| initial_date | final_date | date_result | case_explanation (times Australia/Perth) |
| ##today## | ##tomorrow noon## | ##tomorrow noon##%A, %d %B %Y, %I:%M## | change of day, any day, back and forth |
| ##tomorrow## | ##today noon## | ##today noon##%A, %d %B %Y, %I:%M## | |
| 1617256800 | 1617170400 | Wednesday, 31 March 2021, 2:00 | change of month, back and forth |
| 1617170400 | 1617256800 | Thursday, 1 April 2021, 2:00 | |
| 1740808800 | 1709186400 | Thursday, 29 February 2024, 2:00 | change of month, leap year, back and forth |
| 1709186400 | 1740808800 | Saturday, 1 March 2025, 2:00 | |
| 1577858400 | 1577772000 | Tuesday, 31 December 2019, 2:00 | change of year, back and forth |
| 1577772000 | 1577858400 | Wednesday, 1 January 2020, 2:00 | |
+168
View File
@@ -0,0 +1,168 @@
@core @javascript
Feature: Test dropdown output module
In order to show extra information to the user
As a user
I need to interact with the dropdown output modules
Background:
# Get to the fixture page.
Given I log in as "admin"
And I am on fixture page "/lib/tests/behat/fixtures/dropdown_output_testpage.php"
And I should not see "Dialog content"
Scenario: User can open a dropdown dialog
When I click on "Open dialog" "button" in the "regularscenario" "region"
Then I should see "Dialog content" in the "regularscenario" "region"
Scenario: Dropdown dialog can have rich content inside
When I click on "Open dialog" "button" in the "richcontent" "region"
Then I should see "Some rich content" in the "richcontent" "region"
And "Link 1" "link" should exist in the "richcontent" "region"
And "Eye icon" "icon" should exist in the "richbutton" "region"
Scenario: HTML attributtes can be overriden in dropdowns
When I click on "Open dialog" "button" in the "cssoverride" "region"
Then I should see "Dialog content" in the "cssoverride" "region"
And ".extraclass" "css_element" should exist in the "cssoverride" "region"
And "[data-foo='bar']" "css_element" should exist in the "extraattributes" "region"
And I should see "Custom ID button found" in the "customid" "region"
And "#CustomDropdownButtonId" "css_element" should exist in the "customid" "region"
And ".dialog-big" "css_element" should exist in the "widths" "region"
And ".dialog-small" "css_element" should exist in the "widths" "region"
Scenario: User can open a dropdown status
When I click on "Open dialog" "button" in the "statusregularscenario" "region"
Then I should see "Option 1" in the "statusregularscenario" "region"
And I should see "Option 1 description" in the "statusregularscenario" "region"
And I should see "Option 2" in the "statusregularscenario" "region"
And I should see "Option 2 description" in the "statusregularscenario" "region"
And "Eye icon 1" "icon" should exist in the "statusregularscenario" "region"
And "Eye icon 2" "icon" should exist in the "statusregularscenario" "region"
Scenario: Dropdown status can have as selected option
When I click on "Open dialog" "button" in the "statusselectedscenario" "region"
Then "Selected" "icon" in the "#statusselectedscenario [data-optionnumber='2']" "css_element" should be visible
And "Selected" "icon" in the "#statusselectedscenario [data-optionnumber='1']" "css_element" should not be visible
And "Selected" "icon" in the "#statusselectedscenario [data-optionnumber='3']" "css_element" should not be visible
Scenario: Dropdown status can have a disabled option
When I click on "Open dialog" "button" in the "statusdisablescenario" "region"
Then ".disabled" "css_element" should exist in the "#statusdisablescenario [data-optionnumber='2']" "css_element"
And ".disabled" "css_element" should not exist in the "#statusdisablescenario [data-optionnumber='1']" "css_element"
And ".disabled" "css_element" should not exist in the "#statusdisablescenario [data-optionnumber='3']" "css_element"
Scenario: Dropdown status can have a extra attribute in the options
When I click on "Open dialog" "button" in the "statusoptionextrasscenario" "region"
Then "[data-foo='bar']" "css_element" should exist in the "#statusoptionextrasscenario [data-optionnumber='2']" "css_element"
And "[data-foo='bar']" "css_element" should not exist in the "#statusoptionextrasscenario [data-optionnumber='1']" "css_element"
And "[data-foo='bar']" "css_element" should not exist in the "#statusoptionextrasscenario [data-optionnumber='3']" "css_element"
Scenario: Dropdown status can define urls in options
Given I should see "Foo param value: none"
When I click on "Open dialog" "button" in the "statusoptionurl" "region"
And I click on "Option 2" "link" in the "statusoptionurl" "region"
Then I should see "Foo param value: bar"
Scenario: Dropdowns dialogs can be controlled via javascript
Given "Open dialog" "button" should exist in the "dialogjscontrolssection" "region"
And I should see "The dropdown is hidden" in the "dialogjscontrolssection" "region"
# Change button text.
When I click on "Change button text" "button" in the "dialogjscontrolssection" "region"
Then "New button text" "button" should exist in the "dialogjscontrolssection" "region"
# Open dropdown.
And I click on "Open" "button" in the "dialogjscontrolssection" "region"
And I should see "Dialog content" in the "dialogjscontrolssection" "region"
And I should see "The dropdown is visible" in the "dialogjscontrolssection" "region"
# Close dropdown.
And I click on "Close" "button" in the "dialogjscontrolssection" "region"
And I should not see "Dialog content" in the "dialogjscontrolssection" "region"
And I should see "The dropdown is hidden" in the "dialogjscontrolssection" "region"
Scenario: Dropdown status can sync the clicked option with the button text
Given I should see "Option 2" in the "statussyncbutton" "region"
When I click on "Option 2" "button" in the "statussyncbutton" "region"
And "Selected" "icon" in the "#statussyncbutton [data-optionnumber='2']" "css_element" should be visible
And "Selected" "icon" in the "#statussyncbutton [data-optionnumber='3']" "css_element" should not be visible
And I click on "Option 3" "link" in the "statussyncbutton" "region"
Then I should see "Option 3" in the "statussyncbutton" "region"
And I should not see "Option 2" in the "statussyncbutton" "region"
And I click on "Option 3" "button" in the "statussyncbutton" "region"
And "Selected" "icon" in the "#statussyncbutton [data-optionnumber='2']" "css_element" should not be visible
And "Selected" "icon" in the "#statussyncbutton [data-optionnumber='3']" "css_element" should be visible
Scenario: Dropdowns status can be controlled via javascript
Given "Open dialog" "button" should exist in the "statusjscontrolsection" "region"
And I should see "The status value is option2" in the "statusjscontrolsection" "region"
# Change value.
When I click on "Change selected value" "button" in the "statusjscontrolsection" "region"
Then I should see "The status value is option3" in the "statusjscontrolsection" "region"
And I click on "Open dialog" "button" in the "statusjscontrolsection" "region"
And "Selected" "icon" in the "#statusjscontrolsection [data-optionnumber='2']" "css_element" should not be visible
And "Selected" "icon" in the "#statusjscontrolsection [data-optionnumber='3']" "css_element" should be visible
# Enable button sync.
And I click on "Enable sync" "button" in the "statusjscontrolsection" "region"
And I should see "Option 3" in the "statusjscontrolsection" "region"
And I click on "Option 3" "button" in the "statusjscontrolsection" "region"
And I click on "Option 2" "link" in the "statusjscontrolsection" "region"
And I should see "The status value is option2" in the "statusjscontrolsection" "region"
And I should see "Option 2" in the "statusjscontrolsection" "region"
# Trigger change event with button text sync.
And I click on "Change selected value" "button" in the "statusjscontrolsection" "region"
And I should see "Option 3" in the "statusjscontrolsection" "region"
And I should see "The status value is option3" in the "statusjscontrolsection" "region"
# Disable button text sync.
And I click on "Disable sync" "button" in the "statusjscontrolsection" "region"
And I click on "Option 3" "button" in the "statusjscontrolsection" "region"
And I click on "Option 1" "link" in the "statusjscontrolsection" "region"
And I should see "Option 3" in the "statusjscontrolsection" "region"
And I should see "The status value is option1" in the "statusjscontrolsection" "region"
And I click on "Change selected value" "button" in the "statusjscontrolsection" "region"
And I should see "Option 3" in the "statusjscontrolsection" "region"
And I should see "The status value is option2" in the "statusjscontrolsection" "region"
# Disable update.
And I click on "Disable update" "button" in the "statusjscontrolsection" "region"
And I click on "Option 3" "button" in the "statusjscontrolsection" "region"
And I click on "Option 1" "link" in the "statusjscontrolsection" "region"
And I should see "The status value is option2" in the "statusjscontrolsection" "region"
And I click on "Option 3" "button" in the "statusjscontrolsection" "region"
And "Selected" "icon" in the "#statusjscontrolsection [data-optionnumber='1']" "css_element" should not be visible
And "Selected" "icon" in the "#statusjscontrolsection [data-optionnumber='2']" "css_element" should be visible
Scenario: Dropdown status content is accessible with keyboard
Given I click on "Focus helper" "button" in the "statussyncbutton" "region"
When I press the tab key
# Open and close dropdown with enter key.
Then I press the enter key
And the focused element is "[data-for='dropdowndialog_button']" "css_element" in the "statussyncbutton" "region"
And I should see "Option 1" in the "statussyncbutton" "region"
And I press the enter key
And the focused element is "[data-for='dropdowndialog_button']" "css_element" in the "statussyncbutton" "region"
And I should not see "Option 1" in the "statussyncbutton" "region"
# Open and close with down and up keys.
And I press the down key
And the focused element is "[data-optionnumber='1'] a" "css_element" in the "statussyncbutton" "region"
And I should see "Option 1" in the "statussyncbutton" "region"
And I press the up key
And the focused element is "[data-for='dropdowndialog_button']" "css_element" in the "statussyncbutton" "region"
And I should see "Option 1" in the "statussyncbutton" "region"
And I press the up key
And the focused element is "[data-for='dropdowndialog_button']" "css_element" in the "statussyncbutton" "region"
And I should not see "Option 1" in the "statussyncbutton" "region"
# Select to option 3 and check user cannot go beyond that.
And I press the down key
And the focused element is "[data-optionnumber='1'] a" "css_element" in the "statussyncbutton" "region"
And I press the down key
And the focused element is "[data-optionnumber='2'] a" "css_element" in the "statussyncbutton" "region"
And I press the down key
And the focused element is "[data-optionnumber='3'] a" "css_element" in the "statussyncbutton" "region"
And I press the down key
And the focused element is "[data-optionnumber='3'] a" "css_element" in the "statussyncbutton" "region"
And I press the enter key
And I should see "Option 3" in the "statussyncbutton" "region"
# Close dropdown with escape key.
And I press the down key
And the focused element is "[data-optionnumber='1'] a" "css_element" in the "statussyncbutton" "region"
And I should see "Option 1" in the "statussyncbutton" "region"
And I press the escape key
And the focused element is "[data-for='dropdowndialog_button']" "css_element" in the "statussyncbutton" "region"
And I should not see "Option 1" in the "statussyncbutton" "region"
+21
View File
@@ -0,0 +1,21 @@
@core
Feature: Enable dashboard setting
In order to hide/show dashboard in navigation
As an administrator
I can enable or disable it
Scenario: Hide setting when dashboard is disabled
Given the following config values are set as admin:
| enabledashboard | 0 |
# 2 = User preference.
| defaulthomepage | 2 |
When I log in as "admin"
And I navigate to "Appearance > Navigation" in site administration
Then the field "Enable Dashboard" matches value "0"
And I should not see "Allow guest access to Dashboard"
And I should not see "Dashboard" in the "Start page for users" "select"
And I follow "Appearance"
And I should not see "Default Dashboard page"
And I follow "Preferences" in the user menu
And I follow "Start page"
And I should not see "Dashboard" in the "Start page" "select"
@@ -0,0 +1,17 @@
@core
Feature: Expand single fieldset in Behat tests
In order to expand all fieldsets when there is only one
As a developer
I need Behat to successfully expand that fieldset
@javascript
Scenario: Test expand all fieldsets when there is only one fieldset
Given I log in as "admin"
# This page was selected because it only has one fieldset.
When I navigate to "Users > Accounts > Upload users" in site administration
# Close the fieldset manually...
And I click on "//a[@data-toggle='collapse']" "xpath_element"
And I should not see "Example text file"
# Expand using 'expand all' step.
And I expand all fieldsets
Then I should see "Example text file"
@@ -0,0 +1,253 @@
<?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/>.
/**
* Test page for action menu subpanel output component.
*
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @package core
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../../config.php');
defined('BEHAT_SITE_RUNNING') || die();
$foo = optional_param('foo', 'none', PARAM_TEXT);
global $CFG, $PAGE, $OUTPUT;
$PAGE->set_url('/lib/tests/behat/fixtures/action_menu_subpanel_output_testpage.php');
$PAGE->add_body_class('limitedwidth');
require_login();
$PAGE->set_context(core\context\system::instance());
$PAGE->set_title('Action menu subpanel test page');
echo $OUTPUT->header();
$choice1 = new core\output\choicelist('Choice example');
$choice1->add_option("statusa", "Status A", [
'url' => new moodle_url($PAGE->url, ['foo' => 'Aardvark']),
'description' => 'Status A description',
'icon' => new pix_icon('t/user', '', ''),
]);
$choice1->add_option("statusb", "Status B", [
'url' => new moodle_url($PAGE->url, ['foo' => 'Beetle']),
'description' => 'Status B description',
'icon' => new pix_icon('t/groupv', '', ''),
]);
$choice1->set_selected_value('statusb');
$choice2 = new core\output\choicelist('Choice example');
$choice2->add_option("statusc", "Status C", [
'url' => new moodle_url($PAGE->url, ['foo' => 'Caterpillar']),
'description' => 'Status C description',
'icon' => new pix_icon('t/groups', '', ''),
]);
$choice2->add_option("statusd", "Status D", [
'url' => new moodle_url($PAGE->url, ['foo' => 'Donkey']),
'description' => 'Status D description',
'icon' => new pix_icon('t/hide', '', ''),
]);
$choice2->set_selected_value('statusc');
$normalactionlink = new action_menu_link(
new moodle_url($PAGE->url, ['foo' => 'bar']),
new pix_icon('t/emptystar', ''),
'Action link example',
false
);
echo "<h2>Action menu subpanel test page</h2>";
echo '<div id="paramcheck" class="mb-4">';
echo "<p>Foo param value: $foo</p>";
echo '</div>';
echo '<div id="regularscenario" class="mb-4">';
echo "<h3>Basic example</h3>";
$menu = new action_menu();
$menu->add($normalactionlink);
$menu->add($normalactionlink);
$menu->add(
new core\output\local\action_menu\subpanel(
'Subpanel example',
$choice1
)
);
$menu->add(
new core\output\local\action_menu\subpanel(
'Another subpanel',
$choice2
)
);
echo '<div class="border p-2 d-flex flex-row">';
echo '<div class="flex-fill">Menu right example</div><div>';
echo $OUTPUT->render($menu);
echo '</div></div>';
echo '</div>';
echo '<div id="menuleft" class="mb-4">';
echo "<h3>Menu left</h3>";
$menu = new action_menu();
$menu->set_menu_left();
$menu->add($normalactionlink);
$menu->add($normalactionlink);
$menu->add(
new core\output\local\action_menu\subpanel(
'Subpanel example',
$choice1,
null,
null
)
);
$menu->add(
new core\output\local\action_menu\subpanel(
'Another subpanel',
$choice2,
null,
null
)
);
echo '<div class="border p-2 d-flex flex-row"><div>';
echo $OUTPUT->render($menu);
echo '</div><div class="flex-fill ml-2">Menu left example</div></div>';
echo '</div>';
echo '<div id="itemicon" class="mb-4">';
echo "<h3>Menu item with icon</h3>";
$menu = new action_menu();
$menu->add($normalactionlink);
$menu->add($normalactionlink);
$menu->add(
new core\output\local\action_menu\subpanel(
'Subpanel example',
$choice1,
null,
new pix_icon('t/locked', 'Locked icon')
)
);
$menu->add(
new core\output\local\action_menu\subpanel(
'Another subpanel',
$choice2,
null,
new pix_icon('t/message', 'Message icon')
)
);
echo '<div class="border p-2 d-flex flex-row">';
echo '<div class="flex-fill">Menu right example</div><div>';
echo $OUTPUT->render($menu);
echo '</div></div>';
echo '</div>';
echo '<div id="itemiconleft" class="mb-4">';
echo "<h3>Left menu with item icon</h3>";
$menu = new action_menu();
$menu->set_menu_left();
$menu->add($normalactionlink);
$menu->add($normalactionlink);
$menu->add(
new core\output\local\action_menu\subpanel(
'Subpanel example',
$choice1,
null,
new pix_icon('t/locked', 'Locked icon')
)
);
$menu->add(
new core\output\local\action_menu\subpanel(
'Another subpanel',
$choice2,
null,
new pix_icon('t/message', 'Message icon')
)
);
echo '<div class="border p-2 d-flex flex-row"><div>';
echo $OUTPUT->render($menu);
echo '</div><div class="flex-fill ml-2">Menu left example</div></div>';
echo '</div>';
echo '<div id="dataattributes" class="mb-4">';
echo "<h3>Adding data attributes to menu item</h3>";
$menu = new action_menu();
$menu->add($normalactionlink);
$menu->add($normalactionlink);
$menu->add(
new core\output\local\action_menu\subpanel(
'Subpanel example',
$choice1,
['data-extra' => 'some extra value']
)
);
$menu->add(
new core\output\local\action_menu\subpanel(
'Another subpanel',
$choice2,
['data-extra' => 'some other value']
)
);
echo '<div class="border p-2 d-flex flex-row">';
echo '<div class="flex-fill">Menu right example</div><div>';
echo $OUTPUT->render($menu);
echo '</div></div>';
echo '<div class="mt-1 p-2 border" id="datachecks">Nothing here.</div>';
echo '</div>';
$inlinejs = <<<EOF
const datachecks = document.getElementById('datachecks');
const dataitems = document.querySelectorAll('[data-extra]');
let dataitemshtml = '';
for (let i = 0; i < dataitems.length; i++) {
dataitemshtml += '<p>Extra data attribute detected: ' + dataitems[i].getAttribute('data-extra') + '</p>';
}
datachecks.innerHTML = dataitemshtml;
EOF;
$PAGE->requires->js_amd_inline($inlinejs);
echo '<div id="drawersimulation" class="mb-4">';
echo "<h3>Drawer like example</h3>";
$menu = new action_menu();
$menu->add($normalactionlink);
$menu->add($normalactionlink);
$menu->add(
new core\output\local\action_menu\subpanel(
'Subpanel example',
$choice1
)
);
$menu->add(
new core\output\local\action_menu\subpanel(
'Another subpanel',
$choice2
)
);
echo '<div class="border p-2 d-flex flex-row" data-region="fixed-drawer" data-behat-fake-drawer="true" style="width: 350px;">';
echo '<div class="flex-fill">Drawer example</div><div>';
echo $OUTPUT->render($menu);
echo '</div></div>';
echo '</div>';
echo $OUTPUT->footer();
@@ -0,0 +1,366 @@
<?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/>.
/**
* Test page for dropdown dialog output component.
*
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @package core
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../../config.php');
defined('BEHAT_SITE_RUNNING') || die();
global $CFG, $PAGE, $OUTPUT;
$PAGE->set_url('/lib/tests/behat/fixtures/dropdown_dialog_testpage.php');
$PAGE->add_body_class('limitedwidth');
require_login();
$PAGE->set_context(core\context\system::instance());
echo $OUTPUT->header();
echo "<h2>Dropdown dialog test page</h2>";
echo '<div id="regularscenario" class="mb-4">';
echo "<h3>Basic example</h3>";
$dialog = new core\output\local\dropdown\dialog('Open dialog', 'Dialog content');
echo $OUTPUT->render($dialog);
echo '</div>';
echo '<div id="richcontent" class="mb-4">';
echo "<h3>Rich content example</h3>";
$content = '
<p>Some rich content <b>element</b>.</p>
<ul>
<li>Item 1 <a href="#">Link 1</a></li>
<li>Item 2 <a href="#">Link 2</a></li>
</ul>
';
$dialog = new core\output\local\dropdown\dialog('Open dialog', $content);
echo $OUTPUT->render($dialog);
echo '</div>';
echo '<div id="richbutton" class="mb-4">';
echo "<h3>Rich button example</h3>";
$button = $OUTPUT->pix_icon('t/hide', 'Eye icon') . ' Click to <b>open</b></a>';
$dialog = new core\output\local\dropdown\dialog($button, 'Dialog content');
echo $OUTPUT->render($dialog);
echo '</div>';
echo '<div id="cssoverride" class="mb-4">';
echo "<h3>CSS override example</h3>";
$dialog = new core\output\local\dropdown\dialog(
'Open dialog',
'Dialog content',
[
'buttonclasses' => 'btn btn-primary extraclass',
]
);
echo $OUTPUT->render($dialog);
echo '</div>';
echo '<div id="extraattributes" class="mb-4">';
echo "<h3>Extra data attributes example</h3>";
$dialog = new core\output\local\dropdown\dialog('Open dialog', 'Dialog content', [
'extras' => ['data-foo' => 'bar'],
]);
echo $OUTPUT->render($dialog);
echo '</div>';
echo '<div id="customid" class="mb-4">';
echo "<h3>Custom element id</h3>";
$dialog = new core\output\local\dropdown\dialog('Open dialog', 'Dialog content', [
'extras' => ['id' => 'CustomDropdownButtonId'],
]);
echo $OUTPUT->render($dialog);
echo '</div>';
$inlinejs = "document.querySelector('#CustomDropdownButtonId button').innerHTML = 'Custom ID button found';";
$PAGE->requires->js_amd_inline($inlinejs);
echo '<div id="position" class="mb-4">';
echo "<h3>Dropdown position example</h3>";
$dialog = new core\output\local\dropdown\dialog('Open dialog', 'Dialog content');
$dialog->set_position(core\output\local\dropdown\dialog::POSITION['end']);
echo $OUTPUT->render($dialog);
echo '</div>';
echo '<div id="widths" class="mb-4">';
echo "<h3>Dropdown max width values example</h3>";
$content = '
Some long long content. Some long long content. Some long long content. Some long long content.
Some long long content. Some long long content. Some long long content. Some long long content.
Some long long content. Some long long content. Some long long content. Some long long content.
';
$dialog = new core\output\local\dropdown\dialog('Default dialog (adaptative)', $content);
$dialog->set_classes('mb-3');
echo $OUTPUT->render($dialog);
$dialog = new core\output\local\dropdown\dialog('Big dialog', $content);
$dialog->set_dialog_width(core\output\local\dropdown\dialog::WIDTH['big']);
$dialog->set_classes('mb-3');
echo $OUTPUT->render($dialog);
$dialog = new core\output\local\dropdown\dialog('Small dialog', $content);
$dialog->set_dialog_width(core\output\local\dropdown\dialog::WIDTH['small']);
$dialog->set_classes('mb-3');
echo $OUTPUT->render($dialog);
echo '</div>';
echo '<div id="dialogjscontrolssection" class="mb-4">';
echo "<h3>Dropdown JS module controls</h3>";
echo '<div class="mb-2">
<button class="btn btn-secondary" id="buttontext">Change button text</button>
<button class="btn btn-secondary" id="opendropdown">Open</button>
<button class="btn btn-secondary" id="closedropdown">Close</button>
<span id="dialogvisibility"></span>
</div>';
$dialog = new core\output\local\dropdown\dialog('Open dialog', 'Dialog content', [
'extras' => ['id' => 'dialogjscontrols'],
]);
echo $OUTPUT->render($dialog);
echo '</div>';
$inlinejs = <<<EOF
require(
['core/local/dropdown/dialog', 'jquery'],
(Module, jQuery) => {
const dialog = Module.getDropdownDialog('#dialogjscontrols');
document.querySelector('#buttontext').addEventListener('click', () => {
dialog.setButtonContent('New button text');
});
document.querySelector('#opendropdown').addEventListener('click', (e) => {
e.stopPropagation();
dialog.setVisible(true);
});
document.querySelector('#closedropdown').addEventListener('click', (e) => {
e.stopPropagation();
dialog.setVisible(false);
});
const visibility = () => {
const text = 'The dropdown is ' + (dialog.isVisible() ? 'visible' : 'hidden') + '.';
document.querySelector('#dialogvisibility').innerHTML = text;
}
visibility();
// Bootstrap 4 events are still jQuery.
jQuery(dialog.getElement()).on('shown.bs.dropdown', (e) => {
visibility();
});
jQuery(dialog.getElement()).on('hidden.bs.dropdown', (e) => {
visibility();
});
}
);
EOF;
$PAGE->requires->js_amd_inline($inlinejs);
echo "<h2>Dropdown status test page</h2>";
echo '<div id="statusregularscenario" class="mb-4">';
echo "<h3>Basic example</h3>";
$choice = new core\output\choicelist('Dialog content');
$choice->add_option('option1', 'Option 1', [
'description' => 'Option 1 description'
]);
$choice->add_option('option2', 'Option 2', [
'description' => 'Option 2 description',
'icon' => new pix_icon('t/hide', 'Eye icon 1')
]);
$choice->add_option('option3', 'Option 3', [
'icon' => new pix_icon('t/show', 'Eye icon 2')
]);
$dialog = new core\output\local\dropdown\status('Open dialog', $choice);
echo $OUTPUT->render($dialog);
echo '</div>';
echo '<div id="statusselectedscenario" class="mb-4">';
echo "<h3>Selected element example</h3>";
$choice = new core\output\choicelist('Dialog content');
$choice->add_option('option1', 'Option 1', [
'description' => 'Option 1 description',
'icon' => new pix_icon('t/show', 'Eye icon 1')
]);
$choice->add_option('option2', 'Option 2', [
'description' => 'Option 2 description',
'icon' => new pix_icon('t/hide', 'Eye icon 2')
]);
$choice->add_option('option3', 'Option 3', [
'description' => 'Option 3 description',
'icon' => new pix_icon('t/stealth', 'Eye icon 3')
]);
$choice->set_selected_value('option2');
$dialog = new core\output\local\dropdown\status('Open dialog', $choice);
echo $OUTPUT->render($dialog);
echo '</div>';
echo '<div id="statusdisablescenario" class="mb-4">';
echo "<h3>Disable option example</h3>";
$choice = new core\output\choicelist('Dialog content');
$choice->add_option('option1', 'Option 1');
$choice->add_option('option2', 'Option 2', [
'description' => 'Option 2 description',
'icon' => new pix_icon('t/hide', 'Eye icon')
]);
$choice->add_option('option3', 'Option 3');
$choice->set_option_disabled('option2', true);
$dialog = new core\output\local\dropdown\status('Open dialog', $choice);
echo $OUTPUT->render($dialog);
echo '</div>';
echo '<div id="statusoptionextrasscenario" class="mb-4">';
echo "<h3>Set option extra attributes example</h3>";
$choice = new core\output\choicelist('Dialog content');
$choice->add_option('option1', 'Option 1');
$choice->add_option('option2', 'Option 2', [
'description' => 'Option 2 description',
'icon' => new pix_icon('t/hide', 'Eye icon')
]);
$choice->add_option('option3', 'Option 3');
$choice->set_option_extras('option2', ['data-foo' => 'bar']);
$dialog = new core\output\local\dropdown\status('Open dialog', $choice);
echo $OUTPUT->render($dialog);
echo '</div>';
echo '<div id="statusoptionurl" class="mb-4">';
echo "<h3>Set option url example</h3>";
$choice = new core\output\choicelist('Dialog content');
$choice->add_option('option1', 'Option 1');
$choice->add_option('option2', 'Option 2', [
'url' => new moodle_url('/lib/tests/behat/fixtures/dropdown_output_testpage.php', ['foo' => 'bar']),
]);
$choice->add_option('option3', 'Option 3');
$choice->set_option_extras('option2', ['data-foo' => 'bar']);
$dialog = new core\output\local\dropdown\status('Open dialog', $choice);
echo $OUTPUT->render($dialog);
$foo = optional_param('foo', 'none', PARAM_TEXT);
echo "<p>Foo param value: $foo</p>";
echo '</div>';
echo '<div id="statussyncbutton" class="mb-4">';
echo "<h3>Sync button text</h3>";
$choice = new core\output\choicelist('Dialog content');
$choice->add_option('option1', 'Option 1', [
'description' => 'Option 1 description',
'icon' => new pix_icon('t/show', 'Eye icon 1')
]);
$choice->add_option('option2', 'Option 2', [
'description' => 'Option 2 description',
'icon' => new pix_icon('t/hide', 'Eye icon 2')
]);
$choice->add_option('option3', 'Option 3', [
'description' => 'Option 3 description',
'icon' => new pix_icon('t/stealth', 'Eye icon 3')
]);
$choice->set_selected_value('option2');
$dialog = new core\output\local\dropdown\status(
'Open dialog',
$choice,
['buttonsync' => true, 'updatestatus' => true]
);
echo '<button class="btn">Focus helper</button>';
echo $OUTPUT->render($dialog);
echo '</div>';
echo '<div id="statusjscontrolsection" class="mb-4">';
echo "<h3>Status JS controls</h3>";
echo '<div class="mb-2">
<button class="btn btn-secondary" id="setselected">Change selected value</button>
<button class="btn btn-secondary" id="syncbutton">Enable sync</button>
<button class="btn btn-secondary" id="updatestatus">Disable update</button>
<span id="statusvalue"></span>
</div>';
$choice = new core\output\choicelist('Dialog content');
$choice->add_option('option1', 'Option 1', [
'description' => 'Option 1 description',
'icon' => new pix_icon('t/show', 'Eye icon 1')
]);
$choice->add_option('option2', 'Option 2', [
'description' => 'Option 2 description',
'icon' => new pix_icon('t/hide', 'Eye icon 2')
]);
$choice->add_option('option3', 'Option 3', [
'description' => 'Option 3 description',
'icon' => new pix_icon('t/stealth', 'Eye icon 3')
]);
$choice->set_selected_value('option2');
$dialog = new core\output\local\dropdown\status(
'Open dialog',
$choice,
[
'extras' => ['id' => 'statusjscontrols'],
'updatestatus' => true
],
);
echo $OUTPUT->render($dialog);
echo '</div>';
$inlinejs = <<<EOF
require(
['core/local/dropdown/status', 'jquery'],
(Module, jQuery) => {
const status = Module.getDropdownStatus('#statusjscontrols');
const printValue = () => {
const text = 'The status value is ' + status.getSelectedValue() + '.';
document.querySelector('#statusvalue').innerHTML = text;
}
printValue();
document.querySelector('#setselected').addEventListener('click', () => {
if (status.getSelectedValue() == 'option2') {
status.setSelectedValue('option3');
} else {
status.setSelectedValue('option2');
}
});
document.querySelector('#syncbutton').addEventListener('click', (e) => {
if (status.isButtonSyncEnabled()) {
status.setButtonSyncEnabled(false);
} else {
status.setButtonSyncEnabled(true);
}
e.target.innerHTML = (status.isButtonSyncEnabled()) ? 'Disable sync': 'Enable sync';
});
document.querySelector('#updatestatus').addEventListener('click', (e) => {
if (status.isUpdateStatusEnabled()) {
status.setUpdateStatusEnabled(false);
} else {
status.setUpdateStatusEnabled(true);
}
e.target.innerHTML = (status.isUpdateStatusEnabled()) ? 'Disable update': 'Enable update';
});
status.getElement().addEventListener('change', () => {
printValue();
});
}
);
EOF;
$PAGE->requires->js_amd_inline($inlinejs);
echo $OUTPUT->footer();
+62
View File
@@ -0,0 +1,62 @@
@core
Feature: Forms with a large number of fields
In order to use certain forms on large Moodle installations
As an admin
I need forms to work with more fields than the PHP max_input_vars setting
Background:
# Get to the fixture page.
Given the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| label | L1 | <a href="../lib/tests/fixtures/max_input_vars.php">FixtureLink</a> | C1 | label1 |
When I am on the "C1" "Course" page logged in as "admin"
And I click on "FixtureLink" "link" in the "region-main" "region"
# Note: These tests do not actually use JavaScript but they don't work with
# the headless 'browser'.
@javascript
Scenario: Small form with checkboxes (not using workaround)
When I follow "Advanced checkboxes / Small"
And I press "Submit here!"
Then I should see "_qf__core_max_input_vars_form=1"
And I should see "mform_isexpanded_id_general=1"
And I should see "arraytest=[13,42]"
And I should see "array2test=[13,42]"
And I should see "submitbutton=Submit here!"
And I should see "Bulk checkbox success: true"
@javascript
Scenario: Small form with array fields (not using workaround)
When I follow "Select options / Small"
And I press "Submit here!"
Then I should see "_qf__core_max_input_vars_form=1"
And I should see "mform_isexpanded_id_general=1"
And I should see "arraytest=[13,42]"
And I should see "array2test=[13,42]"
And I should see "submitbutton=Submit here!"
And I should see "Bulk array success: true"
@javascript
Scenario: Below limit form with array fields (uses workaround but doesn't need it)
When I follow "Select options / Below limit"
And I press "Submit here!"
Then I should see "_qf__core_max_input_vars_form=1"
And I should see "mform_isexpanded_id_general=1"
And I should see "arraytest=[13,42]"
And I should see "array2test=[13,42]"
And I should see "submitbutton=Submit here!"
And I should see "Bulk array success: true"
@javascript
Scenario: Exact PHP limit length form with array fields (uses workaround but doesn't need it)
When I follow "Select options / Exact PHP limit"
And I press "Submit here!"
Then I should see "_qf__core_max_input_vars_form=1"
And I should see "mform_isexpanded_id_general=1"
And I should see "arraytest=[13,42]"
And I should see "array2test=[13,42]"
And I should see "submitbutton=Submit here!"
And I should see "Bulk array success: true"
+197
View File
@@ -0,0 +1,197 @@
@core
Feature: Context freezing apply to child contexts
In order to preserve content
As a manager
I can disbale writes at different areas
Background:
Given the following config values are set as admin:
| contextlocking | 1 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher | Ateacher | Teacher | teacher@example.com |
| student1 | Astudent | Astudent | student1@example.com |
And the following "categories" exist:
| name | category | idnumber |
| cata | 0 | cata |
| cataa | cata | cataa |
| catb | 0 | catb |
And the following "courses" exist:
| fullname | shortname | category |
| courseaa1 | courseaa1 | cataa |
| courseaa2 | courseaa2 | cataa |
| courseb | courseb | catb |
And the following "activities" exist:
| activity | name | course | idnumber |
| forum | faa1 | courseaa1 | faa1 |
| forum | faa1b | courseaa1 | faa1b |
| forum | faa2 | courseaa2 | faa2 |
| forum | fb | courseb | fb |
And the following "course enrolments" exist:
| user | course | role |
| teacher | courseaa1 | editingteacher |
| student1 | courseaa1 | student |
| teacher | courseaa2 | editingteacher |
| student1 | courseaa2 | student |
| teacher | courseb | editingteacher |
| student1 | courseb | student |
Scenario: Freeze course module module should freeze just that module
Given I am on the "courseaa1" "Course" page logged in as "admin"
And I follow "faa1"
And "Add discussion topic" "link" should exist
When I follow "Freeze this context"
And I click on "Continue" "button"
Then "Add discussion topic" "link" should not exist
When I am on "courseaa1" course homepage
Then edit mode should be available on the current page
When I follow "faa1b"
Then "Add discussion topic" "link" should exist
When I am on "courseaa2" course homepage
Then edit mode should be available on the current page
When I follow "faa2"
Then "Add discussion topic" "link" should exist
When I am on "courseb" course homepage
Then edit mode should be available on the current page
When I follow "fb"
Then "Add discussion topic" "link" should exist
And I log out
When I am on the "courseaa1" "Course" page logged in as "teacher"
And I follow "faa1"
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa1" course homepage
Then edit mode should be available on the current page
When I follow "faa1b"
Then "Add discussion topic" "link" should exist
When I am on "courseaa2" course homepage
Then edit mode should be available on the current page
When I follow "faa2"
Then "Add discussion topic" "link" should exist
When I am on "courseb" course homepage
Then edit mode should be available on the current page
When I follow "fb"
And "Add discussion topic" "link" should exist
And I log out
When I am on the "courseaa1" "Course" page logged in as "student1"
And I follow "faa1"
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa1" course homepage
When I follow "faa1b"
Then "Add discussion topic" "link" should exist
When I am on "courseaa2" course homepage
When I follow "faa2"
Then "Add discussion topic" "link" should exist
When I am on "courseb" course homepage
When I follow "fb"
Then "Add discussion topic" "link" should exist
Scenario: Freeze course should freeze all children
Given I am on the "courseaa1" "Course" page logged in as "admin"
Then edit mode should be available on the current page
When I follow "Freeze this context"
And I click on "Continue" "button"
Then edit mode should not be available on the current page
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa1" course homepage
Then edit mode should not be available on the current page
And "Unfreeze this context" "link" should exist in current page administration
When I follow "faa1b"
Then "Add a new discussion topic" "link" should not exist
And "Unfreeze this context" "link" should not exist in current page administration
When I am on "courseaa2" course homepage
Then edit mode should be available on the current page
When I follow "faa2"
Then "Add discussion topic" "link" should exist
When I am on "courseb" course homepage
Then edit mode should be available on the current page
When I follow "fb"
Then "Add discussion topic" "link" should exist
And I log out
When I am on the "courseaa1" "Course" page logged in as "teacher"
And I follow "faa1"
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa1" course homepage
Then edit mode should not be available on the current page
When I follow "faa1b"
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa2" course homepage
Then edit mode should be available on the current page
When I follow "faa2"
Then "Add discussion topic" "link" should exist
When I am on "courseb" course homepage
Then edit mode should be available on the current page
When I follow "fb"
Then "Add discussion topic" "link" should exist
And I log out
When I am on the "courseaa1" "Course" page logged in as "student1"
And I follow "faa1"
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa1" course homepage
When I follow "faa1b"
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa2" course homepage
When I follow "faa2"
Then "Add discussion topic" "link" should exist
When I am on "courseb" course homepage
When I follow "fb"
Then "Add discussion topic" "link" should exist
Scenario: Freeze course category should freeze all children
Given I log in as "admin"
And I go to the courses management page
And I click on "managecontextlock" action for "cata" in management category listing
And I click on "Continue" "button"
And I am on "courseaa1" course homepage
Then edit mode should not be available on the current page
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa1" course homepage
Then edit mode should not be available on the current page
And "Unfreeze this context" "link" should not exist in current page administration
When I follow "faa1b"
Then "Add a new discussion topic" "link" should not exist
And "Unfreeze this context" "link" should not exist in current page administration
When I am on "courseaa2" course homepage
Then edit mode should not be available on the current page
When I follow "faa2"
Then "Add a new discussion topic" "link" should not exist
And "Unfreeze this context" "link" should not exist in current page administration
When I am on "courseb" course homepage
Then edit mode should be available on the current page
When I follow "fb"
Then "Add discussion topic" "link" should exist
And I log out
When I am on the "courseaa1" "Course" page logged in as "teacher"
Then edit mode should not be available on the current page
And I follow "faa1"
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa1" course homepage
Then edit mode should not be available on the current page
When I follow "faa1b"
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa2" course homepage
Then edit mode should not be available on the current page
When I follow "faa2"
Then "Add a new discussion topic" "link" should not exist
When I am on "courseb" course homepage
Then edit mode should be available on the current page
When I follow "fb"
Then "Add discussion topic" "link" should exist
And I log out
When I am on the "courseaa1" "Course" page logged in as "student1"
And I follow "faa1"
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa1" course homepage
When I follow "faa1b"
Then "Add a new discussion topic" "link" should not exist
When I am on "courseaa2" course homepage
When I follow "faa2"
Then "Add a new discussion topic" "link" should not exist
When I am on "courseb" course homepage
When I follow "fb"
Then "Add discussion topic" "link" should exist
@@ -0,0 +1,78 @@
@core
Feature: Menu navigation has accurate checkmarks in single activity course format
In order to correctly navigate the menu items
As an admin
I need to see accurate checkmarks besides the menu items I am currently on while in a single activity format course
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | format | activitytype |
| Course 1 | C1 | singleactivity | quiz |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 for testing the Add menu | C1 | quiz1 |
Scenario: Admin can see checkmark beside menu item they are currently on in a single activity format course
Given I log in as "admin"
And I am on "Course 1" course homepage
When I navigate to "Backup" in current page administration
Then menu item "Backup" should be active
When I navigate to "Permissions" in current page administration
Then menu item "Permissions" should be active
And menu item "Backup" should not be active
When I navigate to "Participants" in current page administration
Then menu item "Participants" should be active
And menu item "Backup" should not be active
And menu item "Permissions" should not be active
When I navigate to "Grades" in current page administration
Then menu item "Grades" should be active
And menu item "Backup" should not be active
And menu item "Permissions" should not be active
And menu item "Participants" should not be active
Scenario: Admin can see checkmark beside menu item they are currently on after pressing browser back button in a single
activity format course
Given I log in as "admin"
And I am on "Course 1" course homepage
When I navigate to "Backup" in current page administration
Then menu item "Backup" should be active
When I navigate to "Permissions" in current page administration
Then menu item "Permissions" should be active
And menu item "Backup" should not be active
When I press the "back" button in the browser
Then menu item "Backup" should be active
And menu item "Permissions" should not be active
Scenario: Admin can see checkmark beside menu item they are currently on after pressing browser back button when
jumping between course and activity menu in a single activity format course
Given I log in as "admin"
And I am on "Course 1" course homepage
When I navigate to "Backup" in current page administration
Then menu item "Backup" should be active
When I navigate to "Participants" in current page administration
Then menu item "Participants" should be active
And menu item "Backup" should not be active
When I press the "back" button in the browser
Then menu item "Backup" should be active
And menu item "Participants" should not be active
@javascript
Scenario: Admin should not see checkmark if link is not navigated to in current browser for single activity format quiz
Given I log in as "admin"
And I am on "Course 1" course homepage
And I update the href of the "//*//a/following-sibling::*//a[contains(text(), 'Participants')]" "xpath" link to "#"
When I navigate to "Participants" in current page administration
Then menu item "Participants" should not be active
And I update the href of the "//*//a/following-sibling::*//a[contains(text(), 'Backup')]" "xpath" link to "#"
When I click on "//*//a[contains(text(),'Activity')]" "xpath"
And I click on "//*//a/following-sibling::*//a[contains(text(), 'Backup')]" "xpath"
Then menu item "Backup" should not be active
@@ -0,0 +1,92 @@
@core
Feature: Menu navigation has accurate checkmarks in topic course format
In order to correctly navigate the menu items
As an admin
I need to see accurate checkmarks besides the menu items I am currently on while in topics format
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 for testing the Add menu | C1 | quiz1 |
@javascript
Scenario: Admin can see checkmark beside menu item they are currently on in the quiz page of a topics format course
Given I log in as "admin"
And I am on "Course 1" course homepage
And I follow "Quiz 1"
When I navigate to "Filters" in current page administration
Then menu item "Filters" should be active
When I navigate to "Permissions" in current page administration
Then menu item "Permissions" should be active
And menu item "Filters" should not be active
When I navigate to "Backup" in current page administration
Then menu item "Backup" should be active
And menu item "Filters" should not be active
And menu item "Permissions" should not be active
@javascript
Scenario: Admin can see checkmark beside menu item they are currently on in the course page of a topics format course
Given I log in as "admin"
And I am on "Course 1" course homepage
When I navigate to "Filters" in current page administration
Then menu item "Filters" should be active
When I navigate to "Course reuse" in current page administration
Then menu item "Course reuse" should be active
And menu item "Filters" should not be active
@javascript
Scenario: Admin can see checkmark beside menu item they are currently on after pressing browser back button in the
quiz page of a topics format course
Given I log in as "admin"
And I am on "Course 1" course homepage
And I follow "Quiz 1"
When I navigate to "Filters" in current page administration
Then menu item "Filters" should be active
When I navigate to "Permissions" in current page administration
Then menu item "Permissions" should be active
And menu item "Filters" should not be active
When I press the "back" button in the browser
Then menu item "Filters" should be active
And menu item "Permissions" should not be active
@javascript
Scenario: Admin can see checkmark beside menu item they are currently on after pressing browser back button in the
course page of a topics format course
Given I log in as "admin"
And I am on "Course 1" course homepage
When I navigate to "Filters" in current page administration
Then menu item "Filters" should be active
When I navigate to "Course reuse" in current page administration
Then menu item "Course reuse" should be active
And menu item "Filters" should not be active
When I press the "back" button in the browser
Then menu item "Filters" should be active
And menu item "Course reuse" should not be active
@javascript
Scenario: Admin should not see checkmark if link is not navigated to in current browser in course view for topics format
Given I log in as "admin"
And I am on "Course 1" course homepage
And I update the href of the "//*//a/following-sibling::*//a[contains(text(), 'Filters')]" "xpath" link to "#"
And I navigate to "Question bank" in current page administration
Then menu item "Filters" should not be active
@javascript
Scenario: Admin should not see checkmark if link is not navigated to in current browser in quiz view for topics format
Given I log in as "admin"
And I am on "Course 1" course homepage
And I follow "Quiz 1"
And I update the href of the "//*//a/following-sibling::*//a[contains(text(), 'Backup')]" "xpath" link to "#"
And I navigate to "Backup" in current page administration
Then menu item "Backup" should not be active
+27
View File
@@ -0,0 +1,27 @@
@core
Feature: Check for minimum or maximimum version of Moodle
In order adapt acceptance tests for different versions of Moodle
As a developer
I should be able to skip tests according to the Moodle version present on a site
Scenario: Minimum version too low
Given the site is running Moodle version 99.0 or higher
# The following steps should not be executed. If they are, the test will fail.
When I log in as "admin"
Then I should not see "Home"
Scenario: Maximum version too high
Given the site is running Moodle version 3.0 or lower
# The following steps should not be executed. If they are, the test will fail.
When I log in as "admin"
Then I should not see "Home"
Scenario: Minimum version OK
Given the site is running Moodle version 3.0 or higher
When I log in as "admin"
Then I should see "Home"
Scenario: Maximum version OK
Given the site is running Moodle version 99.0 or lower
When I log in as "admin"
Then I should see "Home"
@@ -0,0 +1,94 @@
@core
Feature: MoodleNet outbound send activity
In order to send activity to MoodleNet server
As a teacher
I need to be able package the activity and share to MoodleNet
Background:
Given I log in as "admin"
And a MoodleNet mock server is configured
And the following config values are set as admin:
| enablesharingtomoodlenet | 1 |
And I navigate to "Server > OAuth 2 services" in site administration
And I press "MoodleNet"
And I should see "Create new service: MoodleNet"
And I change the MoodleNet field "Service base URL" to mock server
And I press "Save changes"
And I navigate to "MoodleNet > MoodleNet outbound settings" in site administration
And I set the field "Auth 2 service" to "MoodleNet"
And I press "Save changes"
And the following course exists:
| name | Test course |
| shortname | C1 |
And the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| teacher1 | Teacher | 1 | teacher1@example.com |
| manager1 | Manager | 1 | manager1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| manager1 | C1 | manager |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "activities" exist:
| activity | course | idnumber | name | intro |
| assign | C1 | assign1 | Test Assignment 1 | Test Assignment 1 |
Scenario: Share to MoodleNet menu only be available for teachers and managers
Given I am on the "Test Assignment 1" "assign activity" page logged in as student1
Then "Share to MoodleNet" "link" should not exist in current page administration
And I am on the "Test Assignment 1" "assign activity" page logged in as teacher1
And "Share to MoodleNet" "link" should exist in current page administration
And I am on the "Test Assignment 1" "assign activity" page logged in as manager1
And "Share to MoodleNet" "link" should exist in current page administration
Scenario: Share to MoodleNet menu only be available for user that has capability only
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/moodlenet:shareactivity | Prohibit | editingteacher | Course | C1 |
When I am on the "Test Assignment 1" "assign activity" page logged in as teacher1
Then "Share to MoodleNet" "link" should not exist in current page administration
And I am on the "Test Assignment 1" "assign activity" page logged in as manager1
And "Share to MoodleNet" "link" should exist in current page administration
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/moodlenet:shareactivity | Prohibit | manager | Course | C1 |
And I am on the "Test Assignment 1" "assign activity" page logged in as manager1
And "Share to MoodleNet" "link" should not exist in current page administration
@javascript
Scenario: User can share activity to MoodleNet
Given I am on the "Test Assignment 1" "assign activity" page logged in as teacher1
When I navigate to "Share to MoodleNet" in current page administration
Then I should see "Assignment" in the "Share to MoodleNet" "dialogue"
And I should see "Test Assignment 1" in the "Share to MoodleNet" "dialogue"
And I should see "This activity is being shared with MoodleNet as a resource." in the "Share to MoodleNet" "dialogue"
And I click on "Share" "button" in the "Share to MoodleNet" "dialogue"
And I switch to "moodlenet_auth" window
And I press "Allow" and switch to main window
And I should see "Saved to MoodleNet drafts"
And "Go to MoodleNet drafts" "link" should exist in the "Share to MoodleNet" "dialogue"
@javascript
Scenario: User can see their shared resources on the MoodleNet share progress page
Given I am on the "C1" course page logged in as teacher1
When I navigate to "MoodleNet share progress" in current page administration
And I should see "There are no shared resources to display at this time."
And I am on "C1" course homepage
And I navigate to "Share to MoodleNet" in current page administration
And I click on "Share" "button" in the "Share to MoodleNet" "dialogue"
And I switch to "moodlenet_auth" window
And I press "Allow" and switch to main window
And I click on "Close" "button" in the "Share to MoodleNet" "dialogue"
And I am on the "Test Assignment 1" "assign activity" page
And I navigate to "Share to MoodleNet" in current page administration
And I click on "Share" "button" in the "Share to MoodleNet" "dialogue"
And I click on "Close" "button" in the "Share to MoodleNet" "dialogue"
And I am on "C1" course homepage
And I navigate to "MoodleNet share progress" in current page administration
Then "Test course 1" row "Name" column of "generaltable" table should contain "Test course 1"
And "Test course 1" row "Type" column of "generaltable" table should contain "Course"
And "Test course 1" row "Send status" column of "generaltable" table should contain "Sent"
And "Test Assignment 1" row "Name" column of "generaltable" table should contain "Test Assignment 1"
And "Test Assignment 1" row "Type" column of "generaltable" table should contain "Assignment"
And "Test Assignment 1" row "Send status" column of "generaltable" table should contain "Sent"
@@ -0,0 +1,67 @@
@core
Feature: MoodleNet outbound share course
In order to send a course to MoodleNet server
As a teacher
I need to be able to backup the course and share to MoodleNet
Background:
Given the following course exists:
| name | Test course |
| shortname | C1 |
And the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| teacher1 | Teacher | 1 | teacher1@example.com |
| manager1 | Manager | 1 | manager1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| manager1 | C1 | manager |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And I log in as "admin"
And a MoodleNet mock server is configured
And the following config values are set as admin:
| enablesharingtomoodlenet | 1 |
And I navigate to "Server > OAuth 2 services" in site administration
And I press "MoodleNet"
And I should see "Create new service: MoodleNet"
And I change the MoodleNet field "Service base URL" to mock server
And I press "Save changes"
And I navigate to "MoodleNet > MoodleNet outbound settings" in site administration
And I set the field "Auth 2 service" to "MoodleNet"
And I press "Save changes"
Scenario: Share course to MoodleNet option only be available for teachers and managers
Given I am on the "C1" "course" page logged in as student1
And "Share to MoodleNet" "link" should not exist in current page administration
When I am on the "C1" "course" page logged in as teacher1
And "Share to MoodleNet" "link" should exist in current page administration
Then I am on the "C1" "course" page logged in as manager1
And "Share to MoodleNet" "link" should exist in current page administration
Scenario: Share course to MoodleNet option only be available for user that has capability only
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/moodlenet:sharecourse | Prohibit | editingteacher | Course | C1 |
When I am on the "C1" "course" page logged in as teacher1
Then "Share to MoodleNet" "link" should not exist in current page administration
And I am on the "C1" "course" page logged in as manager1
And "Share to MoodleNet" "link" should exist in current page administration
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/moodlenet:sharecourse | Prohibit | manager | Course | C1 |
And I am on the "C1" "course" page logged in as manager1
And "Share to MoodleNet" "link" should not exist in current page administration
@javascript
Scenario: User can share course to MoodleNet
Given I am on the "C1" "course" page logged in as teacher1
When I navigate to "Share to MoodleNet" in current page administration
Then I should see "Course" in the "Share to MoodleNet" "dialogue"
And I should see "Test course 1" in the "Share to MoodleNet" "dialogue"
And I should see "This course is being shared with MoodleNet as a resource." in the "Share to MoodleNet" "dialogue"
And I click on "Share" "button" in the "Share to MoodleNet" "dialogue"
And I switch to "moodlenet_auth" window
And I press "Allow" and switch to main window
And I should see "Saved to MoodleNet drafts"
And "Go to MoodleNet drafts" "link" should exist in the "Share to MoodleNet" "dialogue"
@@ -0,0 +1,97 @@
@core
Feature: MoodleNet outbound share selected activities in a course
In order to send a number of selected activities in a course to MoodleNet server
As a teacher
I need to be able to backup the selected activities in a course and share to MoodleNet
Background:
Given the following course exists:
| name | Test course |
| shortname | C1 |
And the following "activities" exist:
| activity | course | idnumber | name | intro |
| assign | C1 | assign1 | Test Assignment 1 | Test Assignment 1 |
| assign | C1 | assign2 | Test Assignment 2 | Test Assignment 2 |
| assign | C1 | assign3 | Test Assignment 3 | Test Assignment 3 |
And the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | 1 | student1@example.com |
| teacher1 | Teacher | 1 | teacher1@example.com |
| manager1 | Manager | 1 | manager1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| manager1 | C1 | manager |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And I log in as "admin"
And a MoodleNet mock server is configured
And the following config values are set as admin:
| enablesharingtomoodlenet | 1 |
And I navigate to "Server > OAuth 2 services" in site administration
And I press "MoodleNet"
And I should see "Create new service: MoodleNet"
And I change the MoodleNet field "Service base URL" to mock server
And I press "Save changes"
And I navigate to "MoodleNet > MoodleNet outbound settings" in site administration
And I set the field "Auth 2 service" to "MoodleNet"
And I press "Save changes"
@javascript
Scenario: Share to MoodleNet bulk option should only be available for users with the capability
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/moodlenet:sharecourse | Prohibit | editingteacher | Course | C1 |
When I am on the "C1" "course" page logged in as teacher1
And I turn editing mode on
And I click on "Bulk actions" "button"
And I click on "Select activity Test Assignment 1" "checkbox"
Then "Share to MoodleNet" "button" should not exist in the "sticky-footer" "region"
And I am on the "C1" "course" page logged in as manager1
And I turn editing mode on
And I click on "Bulk actions" "button"
And I click on "Select activity Test Assignment 1" "checkbox"
And "Share to MoodleNet" "button" should exist in the "sticky-footer" "region"
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/moodlenet:sharecourse | Prohibit | manager | Course | C1 |
And I am on the "C1" "course" page logged in as manager1
And I turn editing mode on
And I click on "Bulk actions" "button"
And I click on "Select activity Test Assignment 1" "checkbox"
And "Share to MoodleNet" "button" should not exist in the "sticky-footer" "region"
@javascript
Scenario: User can share selected activities in a course to MoodleNet
Given I am on the "C1" "course" page logged in as teacher1
And I turn editing mode on
And I click on "Bulk actions" "button"
When I click on "Share to MoodleNet" "button" in the "sticky-footer" "region"
Then "Share to MoodleNet" "dialogue" should not exist
And I click on "Select activity Test Assignment 1" "checkbox"
And I click on "Select activity Test Assignment 2" "checkbox"
And I click on "Share to MoodleNet" "button" in the "sticky-footer" "region"
And "Share to MoodleNet" "dialogue" should exist
And I should see "Test course 1" in the "Share to MoodleNet" "dialogue"
And I should see "The selected activities are being shared with MoodleNet as a resource." in the "Share to MoodleNet" "dialogue"
And I should see "2 activities will be included in the course." in the "Share to MoodleNet" "dialogue"
And I click on "Share" "button" in the "Share to MoodleNet" "dialogue"
And I switch to "moodlenet_auth" window
And I press "Allow" and switch to main window
And I should see "Saved to MoodleNet drafts"
And "Go to MoodleNet drafts" "link" should exist in the "Share to MoodleNet" "dialogue"
@javascript
Scenario: User can share activity directly in a course bulk mode to MoodleNet
Given I am on the "C1" "course" page logged in as teacher1
And I turn editing mode on
And I click on "Bulk actions" "button"
And I click on "Select activity Test Assignment 1" "checkbox"
When I click on "Share to MoodleNet" "button" in the "sticky-footer" "region"
Then I should see "Test Assignment 1" in the "Share to MoodleNet" "dialogue"
And I should see "This activity is being shared with MoodleNet as a resource." in the "Share to MoodleNet" "dialogue"
And I should not see "1 activities will be included in the course." in the "Share to MoodleNet" "dialogue"
And I click on "Share" "button" in the "Share to MoodleNet" "dialogue"
And I switch to "moodlenet_auth" window
And I press "Allow" and switch to main window
And I should see "Saved to MoodleNet drafts"
And "Go to MoodleNet drafts" "link" should exist in the "Share to MoodleNet" "dialogue"
+69
View File
@@ -0,0 +1,69 @@
@core @javascript
Feature: Override permissions on a context
In order to extend and restrict moodle features
As an admin or a teacher
I need to allow/deny the existing capabilities at different levels
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | t1@example.com |
And the following "courses" exist:
| fullname | shortname | enablecompletion |
| Course 1 | C1 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
Scenario: Default system capabilities modification
Given I am on the "C1" "permissions" page logged in as "admin"
When I click on "Allow" "icon" in the "mod/forum:addnews" "table_row"
And I press "Student"
Then "Add announcementsmod/forum:addnews" row "Roles with permission" column of "permissions" table should contain "Student"
When I reload the page
And I click on "Delete Student role" "link" in the "mod/forum:addnews" "table_row"
And I click on "Remove" "button" in the "Confirm role change" "dialogue"
Then "Add announcementsmod/forum:addnews" row "Roles with permission" column of "permissions" table should not contain "Student"
When I reload the page
And I click on "Prohibit" "icon" in the "mod/forum:addnews" "table_row"
And I press "Student"
Then "Add announcementsmod/forum:addnews" row "Prohibited" column of "permissions" table should contain "Student"
Scenario: Module capabilities overrides
Given the following "activity" exists:
| course | C1 |
| activity | forum |
| name | Forum 1 |
And I am on the "Forum 1" "forum activity permissions" page logged in as admin
When I click on "Allow" "icon" in the "mod/forum:addnews" "table_row"
And I press "Student"
Then "Add announcementsmod/forum:addnews" row "Roles with permission" column of "permissions" table should contain "Student"
When I reload the page
And I click on "Delete Student role" "link" in the "mod/forum:addnews" "table_row"
And I click on "Remove" "button" in the "Confirm role change" "dialogue"
Then "Add announcementsmod/forum:addnews" row "Roles with permission" column of "permissions" table should not contain "Student"
When I reload the page
And I click on "Prohibit" "icon" in the "mod/forum:addnews" "table_row"
And I press "Student"
Then "Add announcementsmod/forum:addnews" row "Prohibited" column of "permissions" table should contain "Student"
Scenario: Dates, completion and description are not shown in permission and override pages
Given the following "activity" exists:
| course | C1 |
| activity | feedback |
| name | Test Feedback |
| intro | Test feedback description |
| completion | 1 |
| timeopen | ##1 Jan 2040 08:00## |
And I am on the "Test Feedback" "feedback activity" page logged in as teacher1
And I should see "Test feedback description"
And "Mark as done" "button" should exist
And I should see "1 January 2040"
When I am on the "Test Feedback" "feedback activity permissions" page
Then I should not see "Test feedback description"
And "Mark as done" "button" should not exist
And I should not see "1 January 2040"
And I set the field "Advanced role override" to "Student"
And I should not see "Test feedback description"
And "Mark as done" "button" should not exist
And I should not see "1 January 2040"
+28
View File
@@ -0,0 +1,28 @@
@core @core_form
Feature: Read-only forms should work
In order to use certain forms on large Moodle installations
As a user
Relevant featuers of non-editable forms should still work
@javascript
Scenario: Shortforms expand collapsing should work for read-only forms - one-section form
Given I log in as "admin"
And I visit "/lib/tests/fixtures/readonlyform.php?sections=1"
When I press "First section"
Then "Name" "field" should be visible
And the field "Name" matches value "Important information"
And I press "First section"
And "Name" "field" should not be visible
@javascript
Scenario: Shortforms expand collapsing should work for read-only forms - two-section form
Given I log in as "admin"
And I visit "/lib/tests/fixtures/readonlyform.php?sections=2"
When I press "Expand all"
Then "Name" "field" should be visible
And the field "Name" matches value "Important information"
And "Other" "field" should be visible
And the field "Other" matches value "Other information"
And I press "Collapse all"
And "Name" "field" should not be visible
And "Other" "field" should not be visible
+38
View File
@@ -0,0 +1,38 @@
@core
Feature: Page displaying with secure layout
In order to securely perform tasks
As a student
I need not to be able to exit the page using the header logo
Background:
# Get to the fixture page.
Given the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| label | L1 | <a href="../lib/tests/fixtures/securetestpage.php">Fixture link</a> | C1 | label1 |
Scenario: Confirm that there is no header link
Given I am on the "C1" "Course" page logged in as "admin"
When I follow "Fixture link"
Then I should see "Acceptance test site" in the "nav" "css_element"
But "Acceptance test site" "link" should not exist
Scenario: Confirm that the user name is displayed in the navbar without a link
Given I log in as "admin"
And the following config values are set as admin:
| logininfoinsecurelayout | 1 |
And I am on "Course 1" course homepage
When I follow "Fixture link"
Then I should see "You are logged in as Admin User" in the "nav" "css_element"
But "Logout" "link" should not exist
Scenario: Confirm that the custom menu items do not appear when language selection is enabled
Given I log in as "admin"
And the following config values are set as admin:
| langmenuinsecurelayout | 1 |
| custommenuitems | -This is a custom item\|/customurl/ |
And I am on "Course 1" course homepage
When I follow "Fixture link"
Then I should not see "This is a custom item" in the "nav" "css_element"
+83
View File
@@ -0,0 +1,83 @@
@core
Feature: Select user identity fields
In order to see who users are at my institution
As an administrator
I can configure which user fields show with lists of users
Background:
Given the following "custom profile fields" exist:
| datatype | shortname | name | param2 |
| text | speciality | Speciality | 255 |
| checkbox | fool | Foolish | |
| text | thesis | Thesis | 100000 |
And the following "users" exist:
| username | department | profile_field_speciality | email |
| user1 | Amphibians | Frogs | email1@example.org |
| user2 | Undead | Zombies | email2@example.org |
And the following "courses" exist:
| shortname | fullname |
| C1 | Course 1 |
And the following "course enrolments" exist:
| user | course | role |
| user1 | C1 | manager |
| user2 | C1 | manager |
Scenario: The admin settings screen should show text custom fields of certain length (and let you choose them)
When I log in as "admin"
And I navigate to "Users > Permissions > User policies" in site administration
Then I should see "Speciality" in the "#admin-showuseridentity" "css_element"
And I should not see "Foolish" in the "#admin-showuseridentity" "css_element"
And I should not see "Thesis" in the "#admin-showuseridentity" "css_element"
And I set the field "Speciality" to "1"
And I press "Save changes"
And the field "Speciality" matches value "1"
Scenario: The admin settings screen correctly formats custom field names
Given the "multilang" filter is "on"
And the "multilang" filter applies to "content and headings"
And the following "custom profile field" exists:
| datatype | text |
| name | <span class="multilang" lang="en">Field (EN)</span><span class="multilang" lang="de">Field (DE)</span> |
| shortname | stuff |
| param2 | 100 |
When I log in as "admin"
And I navigate to "Users > Permissions > User policies" in site administration
Then I should see "Field (EN)" in the "#admin-showuseridentity" "css_element"
And I should not see "Field (DE)" in the "#admin-showuseridentity" "css_element"
Scenario: When you choose custom fields, these should be displayed in the 'Browse list of users' screen
Given the following config values are set as admin:
| showuseridentity | username,department,profile_field_speciality |
When I log in as "admin"
And I navigate to "Users > Accounts > Browse list of users" in site administration
Then I should see "Speciality" in the "thead" "css_element"
And I should see "Department" in the "thead" "css_element"
And I should not see "Email" in the "thead" "css_element"
Then I should see "Amphibians" in the "user1" "table_row"
And I should see "Frogs" in the "user1" "table_row"
And I should not see "email1@example.org"
And I should see "Undead" in the "user2" "table_row"
And I should see "Zombies" in the "user2" "table_row"
And I should not see "email2@example.org"
Scenario: When you choose custom fields, these should be displayed in the 'Participants' screen
Given the following config values are set as admin:
| showuseridentity | username,department,profile_field_speciality |
When I am on the "C1" "Course" page logged in as "user1"
And I navigate to course participants
Then I should see "Frogs" in the "user1" "table_row"
And I should see "Zombies" in the "user2" "table_row"
@javascript
Scenario: The user filtering options on the participants screen should work for custom profile fields
Given the following config values are set as admin:
| showuseridentity | username,department,profile_field_speciality |
When I am on the "C1" "Course" page logged in as "admin"
And I navigate to course participants
And I set the field "type" in the "Filter 1" "fieldset" to "Keyword"
And I set the field "Type..." in the "Filter 1" "fieldset" to "Frogs"
# You have to tab out to make it actually apply.
And I press tab
And I click on "Apply filters" "button"
Then I should see "user1" in the "participants" "table"
And I should not see "user2" in the "participants" "table"
@@ -0,0 +1,71 @@
@core @turn_edit_mode_on @javascript
Feature: Turn editing mode on
Users should be able to turn editing mode on and off
Background:
Given the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I turn editing mode off
And I log out
Scenario: Edit mode on page Gradebook
Given the following "activities" exist:
| activity | course | idnumber | name | intro |
| assign | C1 | assign1 | Test Assignment 1 | Test Assignment 1 |
And I am on the "Course 1" "grades > Grader report > View" page logged in as "teacher1"
And I turn editing mode on
And I click on grade item menu "Test Assignment 1" of type "gradeitem" on "grader" page
And "Edit grade item" "link" should exist
And I turn editing mode off
And I click on grade item menu "Test Assignment 1" of type "gradeitem" on "grader" page
Then "Edit grade item" "link" should not exist
Scenario: Edit mode on page Homepage
Given I log in as "admin"
And I am on site homepage
And I turn editing mode on
And I should see "Add an activity or resource"
And I turn editing mode off
Then I should not see "Add an activity or resource"
Scenario: Edit mode on page Default profile
Given I log in as "admin"
And I navigate to "Appearance > Default profile page" in site administration
And I turn editing mode on
And I should see "Add a block"
And I turn editing mode off
Then I should not see "Add a block"
Scenario: Edit mode on page Profile
Given I log in as "admin"
And I follow "View profile"
And I turn editing mode on
And I should see "Add a block"
And I turn editing mode off
Then I should not see "Add a block"
Scenario: Edit mode on page Default dashboard
Given I log in as "admin"
And I navigate to "Appearance > Default Dashboard page" in site administration
And I turn editing mode on
And I should see "Add a block"
And I turn editing mode off
Then I should not see "Add a block"
Scenario: Edit mode on page Dashboard
And I log in as "teacher1"
And I turn editing mode on
And I should see "Add a block"
Then I turn editing mode off
Then I should not see "Add a block"
+11
View File
@@ -0,0 +1,11 @@
@core
Feature: View timezone defaults
In order to run all other behat tests
As an admin
I need to verify the default timezone is Australia/Perth
Scenario: Admin sees default timezone Australia/Perth
When I log in as "admin"
And I navigate to "Location > Location settings" in site administration
Then I should see "Default: Australia/Perth"
And the field "Default timezone" matches value "Australia/Perth"
+50
View File
@@ -0,0 +1,50 @@
@core
Feature: Gathering user feedback
In order to facilitate data collection from as broad a sample of Moodle users as possible
As Moodle HQ
We should add a link within Moodle to a permanent URL on which surveys will be placed
Scenario: Users should see a feedback link on footer when the feature is enabled
Given the following config values are set as admin:
| enableuserfeedback | 1 |
When I log in as "admin"
Then I should see "Give feedback" in the "page-footer" "region"
Scenario: Users should not see a feedback link on footer when the feature is disabled
Given the following config values are set as admin:
| enableuserfeedback | 0 |
When I log in as "admin"
Then I should not see "Give feedback" in the "page-footer" "region"
Scenario: Visitors should not see a feedback link on footer when they are not logged in
Given the following config values are set as admin:
| enableuserfeedback | 1 |
When I am on site homepage
Then I should not see "Give feedback" in the "page-footer" "region"
@javascript
Scenario: Users should not see the notification after they click on the remind me later link
Given the following config values are set as admin:
| enableuserfeedback | 1 |
| userfeedback_nextreminder | 2 |
| userfeedback_remindafter | 90 |
When I log in as "admin"
And I follow "Dashboard"
And I click on "Remind me later" "link"
And I reload the page
Then I should not see "Give feedback" in the "region-main" "region"
And I should not see "Remind me later" in the "region-main" "region"
@javascript
Scenario: Users should not see the notification after they click on the give feedback link
Given the following config values are set as admin:
| enableuserfeedback | 1 |
| userfeedback_nextreminder | 2 |
| userfeedback_remindafter | 90 |
When I log in as "admin"
And I follow "Dashboard"
And I click on "Give feedback" "link"
And I close all opened windows
And I reload the page
Then I should not see "Give feedback" in the "region-main" "region"
And I should not see "Remind me later" in the "region-main" "region"