186 lines
7.4 KiB
Dart
186 lines
7.4 KiB
Dart
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
/// Tests Desktop Notification Adapter behavior for the Scheduler Notifications Desktop package.
|
|
library;
|
|
|
|
import 'dart:io';
|
|
|
|
import 'package:scheduler_notifications/notifications.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
import '../../scheduler_notifications/test/notification_adapter_contract.dart';
|
|
|
|
// ignore: avoid_relative_lib_imports
|
|
import '../lib/desktop_notifications.dart';
|
|
|
|
/// Entry point for this executable Dart file.
|
|
/// It wires the local command, test, or application behavior needed by this repository without exposing additional public API.
|
|
void main() {
|
|
runNotificationAdapterContractTests('desktop', () {
|
|
final backend = _RecordingBackend();
|
|
return NotificationAdapterContractHarness(
|
|
adapter: DesktopNotificationAdapter(backend: backend),
|
|
scheduledRequests: () => backend.delivered,
|
|
cancelledIds: () => backend.cancelled,
|
|
);
|
|
});
|
|
|
|
test('desktop adapter delegates schedule and cancel to backend', () async {
|
|
final backend = _RecordingBackend();
|
|
final adapter = DesktopNotificationAdapter(backend: backend);
|
|
final request = _request('reminder-1');
|
|
|
|
await adapter.schedule(request);
|
|
await adapter.cancel(' reminder-1 ');
|
|
|
|
expect(backend.delivered, [request]);
|
|
expect(backend.cancelled, ['reminder-1']);
|
|
expect(adapter, isA<NotificationAdapter>());
|
|
});
|
|
|
|
test('linux backend invokes notify-send', () async {
|
|
final calls = <_ProcessCall>[];
|
|
final logs = <String>[];
|
|
final backend = createDefaultDesktopNotificationBackend(
|
|
platform: DesktopNotificationPlatform.linux,
|
|
processRunner: (String executable, List<String> arguments) async {
|
|
calls.add(_ProcessCall(executable, arguments));
|
|
return ProcessResult(1, 0, '', '');
|
|
},
|
|
logSink: logs.add,
|
|
);
|
|
|
|
await backend.deliver(_request('reminder-1'));
|
|
|
|
expect(backend.isSupported, isTrue);
|
|
expect(calls.single.executable, 'notify-send');
|
|
expect(calls.single.arguments, contains('Start task'));
|
|
expect(logs, isEmpty);
|
|
});
|
|
|
|
test('macOS backend invokes osascript', () async {
|
|
final calls = <_ProcessCall>[];
|
|
final backend = createDefaultDesktopNotificationBackend(
|
|
platform: DesktopNotificationPlatform.macos,
|
|
processRunner: (String executable, List<String> arguments) async {
|
|
calls.add(_ProcessCall(executable, arguments));
|
|
return ProcessResult(1, 0, '', '');
|
|
},
|
|
);
|
|
|
|
await backend.deliver(_request('reminder-1'));
|
|
|
|
expect(backend.isSupported, isTrue);
|
|
expect(calls.single.executable, 'osascript');
|
|
expect(calls.single.arguments, contains('-e'));
|
|
});
|
|
|
|
test('unsupported and Windows platforms fall back to log output', () async {
|
|
final logs = <String>[];
|
|
final windows = createDefaultDesktopNotificationBackend(
|
|
platform: DesktopNotificationPlatform.windows,
|
|
logSink: logs.add,
|
|
);
|
|
final unsupported = createDefaultDesktopNotificationBackend(
|
|
platform: DesktopNotificationPlatform.unsupported,
|
|
logSink: logs.add,
|
|
);
|
|
|
|
await windows.deliver(_request('windows-reminder'));
|
|
await unsupported.cancel(' unsupported-reminder ');
|
|
|
|
expect(windows.isSupported, isFalse);
|
|
expect(unsupported.isSupported, isFalse);
|
|
expect(logs.first, contains('windows-reminder'));
|
|
expect(logs.last, contains('unsupported-reminder'));
|
|
});
|
|
|
|
test('fake adapter can stand in for scheduling workflows', () async {
|
|
final adapter = FakeNotificationAdapter();
|
|
addTearDown(adapter.close);
|
|
await _scheduleAndCancelReminder(adapter, _request('reminder-1'));
|
|
|
|
expect(adapter.scheduledRequests, isEmpty);
|
|
expect(adapter.cancelledIds, ['reminder-1']);
|
|
});
|
|
|
|
test('default desktop adapter can be created on supported test platforms',
|
|
() {
|
|
if (!Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) {
|
|
markTestSkipped(
|
|
'Desktop notifications are unsupported on this platform.');
|
|
}
|
|
|
|
expect(DesktopNotificationAdapter.defaultInstance(),
|
|
isA<NotificationAdapter>());
|
|
});
|
|
}
|
|
|
|
/// Top-level helper that performs the `_scheduleAndCancelReminder` operation for this file.
|
|
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
|
|
Future<void> _scheduleAndCancelReminder(
|
|
NotificationAdapter adapter,
|
|
NotificationRequest request,
|
|
) async {
|
|
await adapter.schedule(request);
|
|
await adapter.cancel(request.id);
|
|
}
|
|
|
|
/// Top-level helper that performs the `_request` operation for this file.
|
|
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
|
|
NotificationRequest _request(String id) {
|
|
return NotificationRequest(
|
|
id: id,
|
|
title: 'Start task',
|
|
body: 'Time to begin.',
|
|
scheduledDateTimeUtc: DateTime.utc(2026, 1, 1, 9),
|
|
);
|
|
}
|
|
|
|
/// Private implementation type for `_RecordingBackend` in this library.
|
|
/// It keeps supporting state and behavior scoped to the owning file so the public API stays focused on the scheduler concepts that callers use directly.
|
|
final class _RecordingBackend implements DesktopNotificationBackend {
|
|
/// Stores the `delivered` value for this object.
|
|
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
|
final List<NotificationRequest> delivered = <NotificationRequest>[];
|
|
|
|
/// Stores the `cancelled` value for this object.
|
|
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
|
final List<String> cancelled = <String>[];
|
|
|
|
/// Returns the derived `isSupported` value for this object.
|
|
/// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated.
|
|
@override
|
|
bool get isSupported => true;
|
|
|
|
/// Runs the `deliver` operation and completes after its asynchronous work finishes.
|
|
/// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
|
|
@override
|
|
Future<void> deliver(NotificationRequest request) async {
|
|
delivered.add(request);
|
|
}
|
|
|
|
/// Runs the `cancel` operation and completes after its asynchronous work finishes.
|
|
/// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
|
|
@override
|
|
Future<void> cancel(String id) async {
|
|
cancelled.add(id);
|
|
}
|
|
}
|
|
|
|
/// Private implementation type for `_ProcessCall` in this library.
|
|
/// It keeps supporting state and behavior scoped to the owning file so the public API stays focused on the scheduler concepts that callers use directly.
|
|
final class _ProcessCall {
|
|
/// Creates a `_ProcessCall` instance with the values required by its invariants.
|
|
/// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code.
|
|
const _ProcessCall(this.executable, this.arguments);
|
|
|
|
/// Stores the `executable` value for this object.
|
|
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
|
final String executable;
|
|
|
|
/// Stores the `arguments` value for this object.
|
|
/// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
|
|
final List<String> arguments;
|
|
}
|