From 208ec01b6c529d6ddf2f5124ef552147c03d9895 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Mon, 29 Jan 2024 14:51:20 +0100 Subject: [PATCH] MOBILE-4496 reminders: Improve notification tests The previous implementation was too flaky because it relied on test execution time --- .../tests/behat/behat_app.php | 76 +++++++++++++++--- .../emulator/services/local-notifications.ts | 79 ++++++++++++------- .../tests/behat/activity_reminders.feature | 17 ++-- src/testing/services/behat-runtime.ts | 25 ++++++ 4 files changed, 148 insertions(+), 49 deletions(-) diff --git a/local_moodleappbehat/tests/behat/behat_app.php b/local_moodleappbehat/tests/behat/behat_app.php index 696b3a1e2..cbb164b32 100644 --- a/local_moodleappbehat/tests/behat/behat_app.php +++ b/local_moodleappbehat/tests/behat/behat_app.php @@ -1055,29 +1055,83 @@ class behat_app extends behat_app_helper { } + /** + * Send pending notifications. + * + * @Then /^I flush pending notifications in the app$/ + */ + public function i_flush_notifications() { + $this->runtime_js("flushNotifications()"); + } + /** * Check if a notification has been triggered and is present. * - * @Then /^a notification with title (".+") is( not)? present in the app$/ + * @Then /^a notification with title (".+") should( not)? be present in the app$/ * @param string $title Notification title * @param bool $not Whether assert that the notification was not found */ public function notification_present_in_the_app(string $title, bool $not = false) { - $result = $this->runtime_js("notificationIsPresentWithText($title)"); + $this->spin(function() use ($not, $title) { + $result = $this->runtime_js("notificationIsPresentWithText($title)"); - if ($not && $result === 'YES') { - throw new ExpectationException("Notification is present", $this->getSession()->getDriver()); + if ($not && $result === 'YES') { + throw new ExpectationException("Notification is present", $this->getSession()->getDriver()); + } + + if (!$not && $result === 'NO') { + throw new ExpectationException("Notification is not present", $this->getSession()->getDriver()); + } + + if ($result !== 'YES' && $result !== 'NO') { + throw new DriverException('Error checking notification - ' . $result); + } + + return true; + }); + } + + /** + * Check if a notification has been scheduled. + * + * @Then /^a notification with title (".+") should( not)? be scheduled(?: (\d+) minutes before the "(.+)" assignment due date)? in the app$/ + * @param string $title Notification title + * @param bool $not Whether assert that the notification was not scheduled + * @param int $minutes Minutes before the assignment at which the notification was scheduled + * @param string $assignment Assignment for which the notification was scheduled + */ + public function notification_scheduled_in_the_app(string $title, bool $not = false, ?int $minutes = null, ?string $assignment = null) { + if (!is_null($minutes)) { + global $DB; + + $assign = $DB->get_record('assign', ['name' => $assignment]); + + if (!$assign) { + throw new ExpectationException("Couldn't find '$assignment' assignment", $this->getSession()->getDriver()); + } + + $date = ($assign->duedate - $minutes * 60) * 1000; + } else { + $date = 'undefined'; } - if (!$not && $result === 'NO') { - throw new ExpectationException("Notification is not present", $this->getSession()->getDriver()); - } + $this->spin(function() use ($not, $title, $date) { + $result = $this->runtime_js("notificationIsScheduledWithText($title, $date)"); - if ($result !== 'YES' && $result !== 'NO') { - throw new DriverException('Error checking notification - ' . $result); - } + if ($not && $result === 'YES') { + throw new ExpectationException("Notification is scheduled", $this->getSession()->getDriver()); + } - return true; + if (!$not && $result === 'NO') { + throw new ExpectationException("Notification is not scheduled", $this->getSession()->getDriver()); + } + + if ($result !== 'YES' && $result !== 'NO') { + throw new DriverException('Error checking scheduled notification - ' . $result); + } + + return true; + }); } /** diff --git a/src/core/features/emulator/services/local-notifications.ts b/src/core/features/emulator/services/local-notifications.ts index 02ef0cdd9..965dc05ba 100644 --- a/src/core/features/emulator/services/local-notifications.ts +++ b/src/core/features/emulator/services/local-notifications.ts @@ -67,6 +67,17 @@ export class LocalNotificationsMock extends LocalNotifications { }); } + /** + * Flush pending notifications. + */ + flush(): void { + for (const notification of this.scheduledNotifications) { + this.sendNotification(notification); + } + + this.scheduledNotifications = []; + } + /** * Sets timeout for next nofitication. */ @@ -104,36 +115,7 @@ export class LocalNotificationsMock extends LocalNotifications { const notificationTime = nextNotification.trigger?.at?.getTime() || 0; if (notificationTime === 0 || notificationTime <= dateNow) { - const body = Array.isArray(nextNotification.text) ? nextNotification.text.join() : nextNotification.text; - const notification = new Notification(nextNotification.title || '', { - body, - data: nextNotification.data, - icon: nextNotification.icon, - requireInteraction: true, - tag: nextNotification.data?.component, - }); - - this.triggeredNotifications.push(nextNotification); - - this.observables.trigger.next(nextNotification); - - notification.addEventListener('click', () => { - this.observables.click.next(nextNotification); - - notification.close(); - if (nextNotification.id) { - delete(this.presentNotifications[nextNotification.id]); - } - }); - - if (nextNotification.id) { - this.presentNotifications[nextNotification.id] = notification; - - notification.addEventListener('close', () => { - delete(this.presentNotifications[nextNotification.id ?? 0]); - }); - } - + this.sendNotification(nextNotification); this.scheduledNotifications.shift(); this.triggerNextNotification(); } else { @@ -141,6 +123,43 @@ export class LocalNotificationsMock extends LocalNotifications { } } + /** + * Send notification. + * + * @param localNotification Notification. + */ + protected sendNotification(localNotification: ILocalNotification): void { + const body = Array.isArray(localNotification.text) ? localNotification.text.join() : localNotification.text; + const notification = new Notification(localNotification.title || '', { + body, + data: localNotification.data, + icon: localNotification.icon, + requireInteraction: true, + tag: localNotification.data?.component, + }); + + this.triggeredNotifications.push(localNotification); + + this.observables.trigger.next(localNotification); + + notification.addEventListener('click', () => { + this.observables.click.next(localNotification); + + notification.close(); + if (localNotification.id) { + delete(this.presentNotifications[localNotification.id]); + } + }); + + if (localNotification.id) { + this.presentNotifications[localNotification.id] = notification; + + notification.addEventListener('close', () => { + delete(this.presentNotifications[localNotification.id ?? 0]); + }); + } + } + /** * @inheritdoc */ diff --git a/src/core/features/reminders/tests/behat/activity_reminders.feature b/src/core/features/reminders/tests/behat/activity_reminders.feature index dc7e63f93..5a9f41401 100644 --- a/src/core/features/reminders/tests/behat/activity_reminders.feature +++ b/src/core/features/reminders/tests/behat/activity_reminders.feature @@ -62,20 +62,21 @@ Feature: Set a new reminder on activity And I press "Custom..." in the app Then I should find "Custom reminder" in the app When I set the following fields to these values in the app: - | Value | 69 | + | Value | 40 | | Units | minutes | And I press "Set reminder" in the app Then I should find "Reminder set for" in the app - When I wait "50" seconds - Then a notification with title "Due: Assignment 01" is present in the app - And I close a notification with title "Due: Assignment 01" in the app + And a notification with title "Due: Assignment 01" should be scheduled 40 minutes before the "Assignment 01" assignment due date in the app + When I flush pending notifications in the app + Then a notification with title "Due: Assignment 01" should be present in the app # Set and check reminder is cancelled - When I press "Set a reminder for \"Assignment 01\" (Due)" in the app + When I close a notification with title "Due: Assignment 01" in the app + And I press "Set a reminder for \"Assignment 01\" (Due)" in the app And I press "Custom..." in the app Then I should find "Custom reminder" in the app When I set the following fields to these values in the app: - | Value | 68 | + | Value | 20 | | Units | minutes | And I press "Set reminder" in the app Then I should find "Reminder set for" in the app @@ -83,8 +84,8 @@ Feature: Set a new reminder on activity Then I should find "Reminder set for" in the app When I press "Delete reminder" in the app Then I should find "Reminder deleted" in the app - When I wait "50" seconds - Then a notification with title "Due: Assignment 01" is not present in the app + But a notification with title "Due: Assignment 01" should not be scheduled in the app + And a notification with title "Due: Assignment 01" should not be present in the app Scenario: Check toast is correct Given I entered the assign activity "Assignment 02" on course "Course 1" as "student1" in the app diff --git a/src/testing/services/behat-runtime.ts b/src/testing/services/behat-runtime.ts index a55962086..aa4945adc 100644 --- a/src/testing/services/behat-runtime.ts +++ b/src/testing/services/behat-runtime.ts @@ -30,6 +30,7 @@ import { CoreSites, CoreSitesProvider } from '@services/sites'; import { CoreNavigator, CoreNavigatorService } from '@services/navigator'; import { CoreSwipeNavigationDirective } from '@directives/swipe-navigation'; import { Swiper } from 'swiper'; +import { LocalNotificationsMock } from '@features/emulator/services/local-notifications'; /** * Behat runtime servive with public API. @@ -585,6 +586,13 @@ export class TestingBehatRuntimeService { console.log('BEHAT: ' + nowFormatted, ...args); // eslint-disable-line no-console } + /** + * Flush pending notifications. + */ + flushNotifications(): void { + (LocalNotifications as unknown as LocalNotificationsMock).flush(); + } + /** * Check a notification is present. * @@ -608,6 +616,23 @@ export class TestingBehatRuntimeService { return (await LocalNotifications.isPresent(notification.id)) ? 'YES' : 'NO'; } + /** + * Check a notification is scheduled. + * + * @param title Title of the notification + * @param date Scheduled notification date. + * @returns YES or NO: depending on the result. + */ + async notificationIsScheduledWithText(title: string, date?: number): Promise { + const notifications = await LocalNotifications.getAllScheduled(); + + const notification = notifications.find( + (notification) => notification.title?.includes(title) && (!date || notification.trigger?.at?.getTime() === date), + ); + + return notification ? 'YES' : 'NO'; + } + /** * Close notification. *