// 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()); }); test('linux backend invokes notify-send', () async { final calls = <_ProcessCall>[]; final logs = []; final backend = createDefaultDesktopNotificationBackend( platform: DesktopNotificationPlatform.linux, processRunner: (String executable, List 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 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 = []; 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()); }); } /// 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 _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 delivered = []; /// 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 cancelled = []; /// 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 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 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 arguments; }