// SPDX-FileCopyrightText: 2026 FocusFlow contributors // SPDX-License-Identifier: AGPL-3.0-only /// Tests Export Controller behavior for the Scheduler Export package. library; import 'dart:convert'; import 'package:scheduler_core/scheduler_core.dart' as core; import 'package:scheduler_export/export.dart'; import 'package:scheduler_persistence_memory/persistence_memory.dart'; import 'package:test/test.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() { test('JSON writer exports owner-scoped tasks through controller', () async { final ownerId = core.OwnerId('owner-a'); final repository = InMemoryTaskRepository(); await repository.insert(ownerId: ownerId, task: _task('task-b')); await repository.insert(ownerId: ownerId, task: _task('task-a')); await repository.insert( ownerId: core.OwnerId('owner-b'), task: _task('task-hidden'), ); final output = StringBuffer(); await ExportController(taskRepository: repository).exportTasksToSink( context: ExportContext( ownerId: ownerId, timezone: 'America/Los_Angeles', version: 1, ), format: 'json', sink: output, pageSize: 1, ); final decoded = jsonDecode(output.toString()) as Map; final context = decoded['context'] as Map; final tasks = decoded['tasks'] as List; expect(context['ownerId'], 'owner-a'); expect(context['timezone'], 'America/Los_Angeles'); expect(tasks.map((task) => (task as Map)['_id']), ['task-a', 'task-b']); }); test('CSV writer exports task rows', () async { final ownerId = core.OwnerId('owner-a'); final repository = InMemoryTaskRepository(); await repository.insert(ownerId: ownerId, task: _task('task-a')); final output = StringBuffer(); await ExportController(taskRepository: repository).exportTasksToSink( context: ExportContext( ownerId: ownerId, timezone: 'UTC', version: 1, ), format: 'csv', sink: output, ); final lines = output.toString().trim().split('\n'); expect(lines.first, startsWith('id,title,projectId,type,status')); expect(lines.singleWhere((line) => line.startsWith('task-a,')), contains('Task task-a')); }); test('writer registry supports custom writer factories', () async { final registry = ExportWriterRegistry(); registry.register('custom', _CountingWriter.new); final writer = registry.create(' CUSTOM ', StringBuffer()); expect(writer, isA<_CountingWriter>()); }); } /// Top-level helper that performs the `_task` operation for this file. /// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail. core.Task _task(String id) { return core.Task( id: id, title: 'Task $id', projectId: 'project-a', type: core.TaskType.flexible, status: core.TaskStatus.backlog, durationMinutes: 30, createdAt: DateTime.utc(2026, 1), updatedAt: DateTime.utc(2026, 1), ); } /// Private implementation type for `_CountingWriter` 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 _CountingWriter implements ExportWriter { /// Creates a `_CountingWriter` 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. _CountingWriter(StringSink sink); /// Stores the `count` value for this object. /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior. var count = 0; /// Runs the `begin` 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 begin(ExportContext context) async {} /// Runs the `writeTask` 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 writeTask(core.Task task) async { count += 1; } /// Runs the `end` 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 end() async {} }