diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2023-04-13 08:57:54 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2023-04-17 14:14:07 +0200 |
| commit | 124e7774c82d8b6ec80be2af5d973b350698bb9c (patch) | |
| tree | 5c088bbe618cb5c3a876c26bd3275183e2cb985f | |
| parent | 025a88ecd26d85a1ab8fcedf421ce774de361a0a (diff) | |
| download | mullvadvpn-124e7774c82d8b6ec80be2af5d973b350698bb9c.tar.xz mullvadvpn-124e7774c82d8b6ec80be2af5d973b350698bb9c.zip | |
Add tests for notification evaluation
| -rw-r--r-- | gui/src/main/notification-controller.ts | 36 | ||||
| -rw-r--r-- | gui/test/unit/notification-evaluation.spec.ts | 142 |
2 files changed, 164 insertions, 14 deletions
diff --git a/gui/src/main/notification-controller.ts b/gui/src/main/notification-controller.ts index 04cb8afcb9..f5b7b631ad 100644 --- a/gui/src/main/notification-controller.ts +++ b/gui/src/main/notification-controller.ts @@ -85,7 +85,7 @@ export default class NotificationController { hasExcludedApps: boolean, isWindowVisible: boolean, areSystemNotificationsEnabled: boolean, - ) { + ): boolean { const notificationProviders: SystemNotificationProvider[] = [ new ConnectingNotificationProvider({ tunnelState, reconnecting: this.reconnecting }), new ConnectedNotificationProvider(tunnelState), @@ -98,11 +98,14 @@ export default class NotificationController { notification.mayDisplay(), ); + this.reconnecting = + tunnelState.state === 'disconnecting' && tunnelState.details === 'reconnect'; + if (notificationProvider) { const notification = notificationProvider.getSystemNotification(); if (notification) { - this.notify(notification, isWindowVisible, areSystemNotificationsEnabled); + return this.notify(notification, isWindowVisible, areSystemNotificationsEnabled); } else { log.error( `Notification providers mayDisplay() returned true but getSystemNotification() returned undefined for ${notificationProvider.constructor.name}`, @@ -112,8 +115,7 @@ export default class NotificationController { this.closeNotificationsInCategory(SystemNotificationCategory.tunnelState); } - this.reconnecting = - tunnelState.state === 'disconnecting' && tunnelState.details === 'reconnect'; + return false; } // Closes still relevant notifications but still lets them affect notification dot in tray icon. @@ -150,7 +152,7 @@ export default class NotificationController { systemNotification: SystemNotification, windowVisible: boolean, infoNotificationsEnabled: boolean, - ) { + ): boolean { const notificationSuppressReason = this.evaluateNotification( systemNotification, windowVisible, @@ -165,7 +167,7 @@ export default class NotificationController { this.updateNotificationIcon(); } - return; + return false; } // Cancel throttled notifications within the same category @@ -186,8 +188,10 @@ export default class NotificationController { }, THROTTLE_DELAY); this.throttledNotifications.set(systemNotification, scheduler); + return true; } else { this.notifyImpl(systemNotification); + return true; } } @@ -210,14 +214,7 @@ export default class NotificationController { } private createNotification(systemNotification: SystemNotification): Notification { - const notification = new ElectronNotification({ - title: this.notificationTitle, - body: systemNotification.message, - silent: true, - icon: this.notificationIcon, - timeoutType: - systemNotification.severity == SystemNotificationSeverityType.high ? 'never' : 'default', - }); + const notification = this.createElectronNotification(systemNotification); // Action buttons are only available on macOS. if (process.platform === 'darwin') { @@ -242,6 +239,17 @@ export default class NotificationController { return { specification: systemNotification, notification }; } + private createElectronNotification(systemNotification: SystemNotification): ElectronNotification { + return new ElectronNotification({ + title: this.notificationTitle, + body: systemNotification.message, + silent: true, + icon: this.notificationIcon, + timeoutType: + systemNotification.severity == SystemNotificationSeverityType.high ? 'never' : 'default', + }); + } + private performAction(action?: NotificationAction) { if (action && action.type === 'open-url') { void this.notificationControllerDelegate.openLink(action.url, action.withAuth); diff --git a/gui/test/unit/notification-evaluation.spec.ts b/gui/test/unit/notification-evaluation.spec.ts new file mode 100644 index 0000000000..33a86328bd --- /dev/null +++ b/gui/test/unit/notification-evaluation.spec.ts @@ -0,0 +1,142 @@ +import { expect } from 'chai'; +import { it, describe } from 'mocha'; +import sinon from 'sinon'; + +import { + UnsupportedVersionNotificationProvider, UpdateAvailableNotificationProvider, + // UpdateAvailableNotificationProvider, +} from '../../src/shared/notifications/notification'; +import NotificationController from '../../src/main/notification-controller'; +import { TunnelState } from '../../src/shared/daemon-rpc-types'; +import { ErrorStateCause } from '../../src/shared/daemon-rpc-types'; +import { FirewallPolicyErrorType } from '../../src/shared/daemon-rpc-types'; + +function createController() { + return new NotificationController({ + openApp: () => { /* no-op */ }, + openLink: (_url: string, _withAuth?: boolean) => Promise.resolve(), + showNotificationIcon: (_value: boolean) => { /* no-op */ }, + }); +} + +describe('System notifications', () => { + let sandbox: sinon.SinonSandbox; + + before(() => { + sandbox = sinon.createSandbox(); + // @ts-ignore + sandbox.stub(NotificationController.prototype, 'createElectronNotification').returns({ + show: () => { /* no-op */ }, + close: () => { /* no-op */ }, + on: () => { /* no-op */ }, + removeAllListeners: () => { /* no-op */ }, + }); + }); + + it('should evaluate unspupported version notification to show', () => { + const controller1 = createController(); + const controller2 = createController(); + const notification = new UnsupportedVersionNotificationProvider({ + supported: false, + consistent: true, + suggestedUpgrade: '2100.1', + suggestedIsBeta: false, + }); + + expect(notification.mayDisplay()).to.be.true; + + const systemNotification = notification.getSystemNotification(); + const result1 = controller1.notify(systemNotification, false, true); + const result2 = controller2.notify(systemNotification, false, false); + + expect(result1).to.be.true; + expect(result2).to.be.true; + }); + + it('should evaluate update available notification to show', () => { + const controller1 = createController(); + const controller2 = createController(); + const notification = new UpdateAvailableNotificationProvider({ + suggestedUpgrade: '2100.1', + suggestedIsBeta: false, + }); + + expect(notification.mayDisplay()).to.be.true; + + const systemNotification = notification.getSystemNotification(); + const result1 = controller1.notify(systemNotification, false, true); + const result2 = controller2.notify(systemNotification, false, false); + + expect(result1).to.be.true; + expect(result2).to.be.true; + }); + + it('should show unsupported version notification only once', () => { + const controller = createController(); + const notification = new UnsupportedVersionNotificationProvider({ + supported: false, + consistent: true, + suggestedUpgrade: '2100.1', + suggestedIsBeta: false, + }); + + const systemNotification = notification.getSystemNotification(); + const result1 = controller.notify(systemNotification, false, true); + const result2 = controller.notify(systemNotification, false, true); + + expect(result1).to.be.true; + expect(result2).to.be.false; + }); + + it('should not show notification when window is open', () => { + const controller = createController(); + const notification = new UnsupportedVersionNotificationProvider({ + supported: false, + consistent: true, + suggestedUpgrade: '2100.1', + suggestedIsBeta: false, + }); + + const systemNotification = notification.getSystemNotification(); + const result = controller.notify(systemNotification, true, true); + + expect(result).to.be.false; + }); + + it('Tunnel state notifications should respect notification setting', () => { + const controller = createController(); + + const disconnectedState: TunnelState = { state: 'disconnected' }; + const connectingState: TunnelState = { state: 'connecting' }; + const result1 = controller.notifyTunnelState(disconnectedState, false, false, false, true); + const result2 = controller.notifyTunnelState(disconnectedState, false, false, false, false); + const result3 = controller.notifyTunnelState(connectingState, false, false, false, true); + const result4 = controller.notifyTunnelState(connectingState, false, false, false, false); + + expect(result1).to.be.true; + expect(result2).to.be.false; + expect(result3).to.be.true; + expect(result4).to.be.false; + + const blockingErrorState: TunnelState = { + state: 'error', + details: { + cause: ErrorStateCause.isOffline, + }, + }; + const result5 = controller.notifyTunnelState(blockingErrorState, false, false, false, false); + expect(result5).to.be.false; + + const nonBlockingErrorState: TunnelState = { + state: 'error', + details: { + cause: ErrorStateCause.isOffline, + blockingError: { + type: FirewallPolicyErrorType.generic, + } + }, + }; + const result6 = controller.notifyTunnelState(nonBlockingErrorState, false, false, false, false); + expect(result6).to.be.true; + }); +}); |
