focus-flow/packages/scheduler_export/test/export_controller_test.dart

122 lines
4.8 KiB
Dart

// 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<String, Object?>;
final context = decoded['context'] as Map<String, Object?>;
final tasks = decoded['tasks'] as List<Object?>;
expect(context['ownerId'], 'owner-a');
expect(context['timezone'], 'America/Los_Angeles');
expect(tasks.map((task) => (task as Map<String, Object?>)['_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<void> 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<void> 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<void> end() async {}
}