diff --git a/local_moodleappbehat/tests/behat/behat_app.php b/local_moodleappbehat/tests/behat/behat_app.php index 012aff300af..a8e5bbc334a 100644 --- a/local_moodleappbehat/tests/behat/behat_app.php +++ b/local_moodleappbehat/tests/behat/behat_app.php @@ -604,6 +604,7 @@ public function i_open_a_custom_link(TableNode $data) { $data = $data->getColumnsHash()[0]; $title = array_keys($data)[0]; $data = (object) $data; + $username = $data->user ?? ''; switch ($title) { case 'discussion': @@ -645,7 +646,7 @@ public function i_open_a_custom_link(TableNode $data) { throw new DriverException('Invalid custom link title - ' . $title); } - $this->open_moodleapp_custom_url($pageurl); + $this->open_moodleapp_custom_url($pageurl, '', $username); } /** diff --git a/local_moodleappbehat/tests/behat/behat_app_helper.php b/local_moodleappbehat/tests/behat/behat_app_helper.php index 4621b6a038e..d255237d239 100644 --- a/local_moodleappbehat/tests/behat/behat_app_helper.php +++ b/local_moodleappbehat/tests/behat/behat_app_helper.php @@ -366,12 +366,15 @@ protected function runtime_js(string $script) { * * @param string $script * @param bool $blocking + * @param string $texttofind If set, when this text is found the operation is considered finished. This is useful for + * operations that might expect user input before finishing, like a confirm modal. * @return mixed Result. */ - protected function zone_js(string $script, bool $blocking = false) { + protected function zone_js(string $script, bool $blocking = false, string $texttofind = '') { $blockingjson = json_encode($blocking); + $locatortofind = !empty($texttofind) ? json_encode((object) ['text' => $texttofind]) : null; - return $this->runtime_js("runInZone(() => window.behat.$script, $blockingjson)"); + return $this->runtime_js("runInZone(() => window.behat.$script, $blockingjson, $locatortofind)"); } /** @@ -411,16 +414,14 @@ protected function open_moodleapp_custom_login_url($username, $path = '', string $privatetoken = $usertoken->privatetoken; } - // Generate custom URL. - $parsed_url = parse_url($CFG->behat_wwwroot); - $site = $parsed_url['host'] ?? ''; - $site .= isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; - $site .= $parsed_url['path'] ?? ''; - $url = $this->get_mobile_url_scheme() . "://$username@$site?token=$token&privatetoken=$privatetoken"; + $url = $this->generate_custom_url([ + 'username' => $username, + 'token' => $token, + 'privatetoken' => $privatetoken, + 'redirect' => $path, + ]); - if (!empty($path)) { - $url .= '&redirect='.urlencode($CFG->behat_wwwroot.$path); - } else { + if (empty($path)) { $successXPath = '//page-core-mainmenu'; } @@ -434,14 +435,54 @@ protected function open_moodleapp_custom_login_url($username, $path = '', string * * @param string $path To navigate. * @param string $successXPath The XPath of the element to lookat after navigation. + * @param string $username The username to use. + */ + protected function open_moodleapp_custom_url(string $path, string $successXPath = '', string $username = '') { + global $CFG; + + $url = $this->generate_custom_url([ + 'username' => $username, + 'redirect' => $path, + ]); + + $this->handle_url($url, $successXPath, $username ? 'This link belongs to another site' : ''); + } + + /** + * Generates a custom URL to be treated by the app. + * + * @param array $data Data to generate the URL. */ - protected function open_moodleapp_custom_url(string $path, string $successXPath = '') { + protected function generate_custom_url(array $data): string { global $CFG; - $urlscheme = $this->get_mobile_url_scheme(); - $url = "$urlscheme://link=" . urlencode($CFG->behat_wwwroot.$path); + $parsed_url = parse_url($CFG->behat_wwwroot); + $parameters = []; + + $url = $this->get_mobile_url_scheme() . '://' . ($parsed_url['scheme'] ?? 'http') . '://'; + if (!empty($data['username'])) { + $url .= $data['username'] . '@'; + } + $url .= $parsed_url['host'] ?? ''; + $url .= isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; + $url .= $parsed_url['path'] ?? ''; + + if (!empty($data['token'])) { + $parameters[] = 'token=' . $data['token']; + if (!empty($data['privatetoken'])) { + $parameters[] = 'privatetoken=' . $data['privatetoken']; + } + } + + if (!empty($data['redirect'])) { + $parameters[] = 'redirect=' . urlencode($data['redirect']); + } + + if (!empty($parameters)) { + $url .= '?' . implode('&', $parameters); + } - $this->handle_url($url); + return $url; } /** @@ -449,9 +490,11 @@ protected function open_moodleapp_custom_url(string $path, string $successXPath * * @param string $customurl To navigate. * @param string $successXPath The XPath of the element to lookat after navigation. + * @param string $texttofind If set, when this text is found the operation is considered finished. This is useful for + * operations that might expect user input before finishing, like a confirm modal. */ - protected function handle_url(string $customurl, string $successXPath = '') { - $result = $this->zone_js("customUrlSchemes.handleCustomURL('$customurl')"); + protected function handle_url(string $customurl, string $successXPath = '', string $texttofind = '') { + $result = $this->zone_js("customUrlSchemes.handleCustomURL('$customurl')", false, $texttofind); if ($result !== 'OK') { throw new DriverException('Error handling url - ' . $customurl . ' - '.$result); diff --git a/src/core/features/login/tests/behat/basic_usage.feature b/src/core/features/login/tests/behat/basic_usage.feature index 2285a9d933b..39bde734152 100755 --- a/src/core/features/login/tests/behat/basic_usage.feature +++ b/src/core/features/login/tests/behat/basic_usage.feature @@ -66,13 +66,6 @@ Feature: Test basic usage of login in app And I press "Connect to your site" in the app Then I should find "Can't connect to site" in the app - Scenario: Log out from the app - Given I entered the app as "student1" - And I press the user menu button in the app - When I press "Log out" in the app - And I wait the app to restart - Then the header should be "Accounts" in the app - Scenario: Delete an account Given I entered the app as "student1" When I log out in the app diff --git a/src/core/features/login/tests/behat/logout.feature b/src/core/features/login/tests/behat/logout.feature new file mode 100755 index 00000000000..95f859d9a4e --- /dev/null +++ b/src/core/features/login/tests/behat/logout.feature @@ -0,0 +1,161 @@ +@core_login @app @javascript +Feature: Test different cases of logout and switch account + I need different logout use cases to work + + Background: + Given the following "users" exist: + | username | firstname | lastname | + | student1 | david | student | + | student2 | pau | student2 | + + Scenario: Log out and re-login + Given I entered the app as "student1" + When I press the user menu button in the app + And I press "Log out" in the app + And I wait the app to restart + Then the header should be "Accounts" in the app + + When I press "david student" in the app + Then the header should be "Reconnect" in the app + And I should find "david student" in the app + + When I set the following fields to these values in the app: + | Password | student1 | + And I press "Log in" near "Lost password?" in the app + Then the header should be "Acceptance test site" in the app + + Scenario: Exit account using switch account and re-enter + Given I entered the app as "student1" + When I press the user menu button in the app + And I press "Switch account" in the app + And I press "Add" in the app + And I wait the app to restart + Then I should find "Connect to Moodle" in the app + + When I go back in the app + And I press "david student" in the app + Then the header should be "Acceptance test site" in the app + + Scenario: Exit account using switch account and re-enter when forcelogout is enabled + Given the following config values are set as admin: + | forcelogout | 1 | tool_mobile | + | defaulthomepage | 0 | | + And I entered the app as "student1" + When I press the user menu button in the app + And I press "Switch account" in the app + And I press "Add" in the app + And I wait the app to restart + And I go back in the app + And I press "david student" in the app + Then the header should be "Reconnect" in the app + And I should find "david student" in the app + + Scenario: Switch to a different account + Given I entered the app as "student1" + And I entered the app as "student2" + When I press the user menu button in the app + Then I should find "pau student2" in the app + + When I press "Switch account" in the app + And I press "david student" in the app + And I wait the app to restart + Then the header should be "Acceptance test site" in the app + + When I press the user menu button in the app + Then I should find "david student" in the app + + Scenario: Logout when there is unsaved data + Given the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + And the following "activities" exist: + | activity | name | intro | course | idnumber | + | forum | Test forum | Test forum | C1 | forum | + And the following forum discussions exist in course "Course 1": + | forum | user | name | message | + | Test forum | student1 | Forum topic 1 | Forum message 1 | + | Test forum | student1 | Forum topic 2 | Forum message 2 | + And I entered the course "Course 1" as "student1" in the app + And I change viewport size to "1200x640" in the app + + When I press "Test forum" in the app + And I press "Add discussion topic" in the app + And I set the following fields to these values in the app: + | Subject | My happy subject | + | Message | An awesome message | + And I press the user menu button in the app + And I press "Log out" in the app + Then I should find "Are you sure you want to leave this page?" in the app + + # Check that the app continues working fine if the user cancels the logout. + When I press "Cancel" in the app + And I press "Forum topic 1" in the app + And I press "OK" in the app + Then I should find "Forum message 1" in the app + + When I press "Forum topic 2" in the app + Then I should find "Forum message 2" in the app + + # Now confirm the logout. + When I press "Add discussion topic" in the app + And I set the following fields to these values in the app: + | Subject | My happy subject | + | Message | An awesome message | + And I press the user menu button in the app + And I press "Log out" in the app + And I press "OK" in the app + And I wait the app to restart + Then the header should be "Accounts" in the app + + Scenario: Switch account when there is unsaved data + Given the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + And the following "activities" exist: + | activity | name | intro | course | idnumber | + | forum | Test forum | Test forum | C1 | forum | + And the following forum discussions exist in course "Course 1": + | forum | user | name | message | + | Test forum | student1 | Forum topic 1 | Forum message 1 | + | Test forum | student1 | Forum topic 2 | Forum message 2 | + And I entered the app as "student2" + And I entered the course "Course 1" as "student1" in the app + And I change viewport size to "1200x640" in the app + + When I press "Test forum" in the app + And I press "Add discussion topic" in the app + And I set the following fields to these values in the app: + | Subject | My happy subject | + | Message | An awesome message | + And I press the user menu button in the app + And I press "Switch account" in the app + And I press "pau student2" in the app + Then I should find "Are you sure you want to leave this page?" in the app + + # Check that the app continues working fine if the user cancels the switch account. + When I press "Cancel" in the app + And I press "Forum topic 1" in the app + And I press "OK" in the app + Then I should find "Forum message 1" in the app + + When I press "Forum topic 2" in the app + Then I should find "Forum message 2" in the app + + # Now confirm the switch account. + When I press "Add discussion topic" in the app + And I set the following fields to these values in the app: + | Subject | My happy subject | + | Message | An awesome message | + And I press the user menu button in the app + And I press "Switch account" in the app + And I press "pau student2" in the app + And I press "OK" in the app + And I wait the app to restart + And I press the user menu button in the app + Then I should find "pau student2" in the app diff --git a/src/core/tests/behat/navigation_deeplinks.feature b/src/core/tests/behat/navigation_deeplinks.feature index 41bd254c252..7f858d53be5 100644 --- a/src/core/tests/behat/navigation_deeplinks.feature +++ b/src/core/tests/behat/navigation_deeplinks.feature @@ -3,9 +3,9 @@ Feature: It navigates properly using deep links. Background: Given the following "users" exist: - | username | - | student1 | - | student2 | + | username | firstname | lastname | + | student1 | david | student | + | student2 | pau | student2 | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | @@ -20,7 +20,6 @@ Feature: It navigates properly using deep links. | forum | user | name | message | | Test forum | student1 | Forum topic | Forum message | And the following config values are set as admin: - | forcelogout | 1 | tool_mobile | | defaulthomepage | 0 | | Scenario: Receive a push notification @@ -78,3 +77,98 @@ Feature: It navigates properly using deep links. When I go back in the app Then I should find "Site home" in the app But I should not find "Test forum" in the app + + Scenario: Open a deep link in a different account not stored in the app + Given I entered the app as "student1" + When I open a custom link in the app for: + | discussion | user | + | Forum topic | student2 | + Then I should find "This link belongs to another site" in the app + + When I press "OK" in the app + And I wait the app to restart + Then the header should be "Log in" in the app + + When I set the following fields to these values in the app: + | Password | student2 | + And I press "Log in" near "Lost password?" in the app + Then I should find "Forum topic" in the app + And I should find "Forum message" in the app + + When I go back to the root page in the app + And I press the user menu button in the app + Then I should find "pau student2" in the app + + Scenario: Open a deep link in a different account stored in the app + Given I entered the app as "student2" + And I entered the app as "student1" + When I open a custom link in the app for: + | discussion | user | + | Forum topic | student2 | + Then I should find "This link belongs to another site" in the app + + When I press "OK" in the app + And I wait the app to restart + Then I should find "Forum topic" in the app + And I should find "Forum message" in the app + + When I go back to the root page in the app + And I press the user menu button in the app + Then I should find "pau student2" in the app + + Scenario: Open a deep link in a different account stored in the app but logged out + Given I entered the app as "student2" + And I press the user menu button in the app + And I press "Log out" in the app + And I wait the app to restart + And I entered the app as "student1" + When I open a custom link in the app for: + | discussion | user | + | Forum topic | student2 | + Then I should find "This link belongs to another site" in the app + + When I press "OK" in the app + And I wait the app to restart + Then the header should be "Reconnect" in the app + And I should find "pau student2" in the app + + When I set the following fields to these values in the app: + | Password | student2 | + And I press "Log in" near "Lost password?" in the app + Then I should find "Forum topic" in the app + And I should find "Forum message" in the app + + When I go back to the root page in the app + And I press the user menu button in the app + Then I should find "pau student2" in the app + + Scenario: Open a deep link in a different account when there is unsaved data + Given I entered the app as "student2" + And I entered the forum activity "Test forum" on course "Course 1" as "student1" in the app + When I press "Add discussion topic" in the app + And I set the following fields to these values in the app: + | Subject | My happy subject | + | Message | An awesome message | + And I open a custom link in the app for: + | discussion | user | + | Forum topic | student2 | + Then I should find "This link belongs to another site" in the app + + When I press "OK" in the app + Then I should find "Are you sure you want to leave this page?" in the app + + When I press "Cancel" in the app + Then I should not find "Forum message" in the app + + When I open a custom link in the app for: + | discussion | user | + | Forum topic | student2 | + And I press "OK" in the app + And I press "OK" in the app + And I wait the app to restart + Then I should find "Forum topic" in the app + And I should find "Forum message" in the app + + When I go back to the root page in the app + And I press the user menu button in the app + Then I should find "pau student2" in the app diff --git a/src/testing/services/behat-runtime.ts b/src/testing/services/behat-runtime.ts index f6c7d7ed036..74d0b717a63 100644 --- a/src/testing/services/behat-runtime.ts +++ b/src/testing/services/behat-runtime.ts @@ -136,19 +136,39 @@ export class TestingBehatRuntimeService { * Run an operation inside the angular zone and return result. * * @param operation Operation callback. + * @param blocking Whether the operation is blocking or not. + * @param locatorToFind If set, when this locator is found the operation is considered finished. This is useful for + * operations that might expect user input before finishing, like a confirm modal. * @returns OK if successful, or ERROR: followed by message. */ - async runInZone(operation: () => unknown, blocking: boolean = false): Promise { + async runInZone( + operation: () => unknown, + blocking: boolean = false, + locatorToFind?: TestingBehatElementLocator, + ): Promise { const blockKey = blocking && TestingBehatBlocking.block(); + let interval: number | undefined; try { - await NgZone.run(operation); + await new Promise((resolve, reject) => { + Promise.resolve(NgZone.run(operation)).then(resolve).catch(reject); + + if (locatorToFind) { + interval = window.setInterval(() => { + if (TestingBehatDomUtils.findElementBasedOnText(locatorToFind, { onlyClickable: false })) { + clearInterval(interval); + resolve(); + } + }, 500); + } + }); return 'OK'; } catch (error) { return 'ERROR: ' + error.message; } finally { blockKey && TestingBehatBlocking.unblock(blockKey); + window.clearInterval(interval); } }