// Surface-level tests for the FlowRunDriver approval hooks // added in editor 0.21.0. The card itself is a private widget; // these tests cover the driver-side contract that the card // depends on so that legacy / mis-implementing hosts surface // in CI rather than at runtime. import 'dart:async'; import 'dart:typed_data'; import 'package:fai_studio_flow_editor/src/run_driver.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('FlowRunDriver default approval implementation', () { test('pendingApprovalIdForStep returns null when not overridden', () async { final d = _NullDriver(); final id = await d.pendingApprovalIdForStep( flowName: 'f', stepId: 's', ); expect(id, isNull); }); test('approveApproval throws UnsupportedError by default', () async { final d = _NullDriver(); await expectLater( d.approveApproval(approvalId: 'x', reviewer: 'r'), throwsA(isA()), ); }); test('rejectApproval throws UnsupportedError by default', () async { final d = _NullDriver(); await expectLater( d.rejectApproval(approvalId: 'x', reviewer: 'r', reason: 'y'), throwsA(isA()), ); }); }); group('FlowRunDriver fake host implementation', () { test('full happy path: lookup → approve → state observable', () async { final d = _FakeApprovalDriver(); d.pending['flowA|stepX'] = 'a-123'; final id = await d.pendingApprovalIdForStep( flowName: 'flowA', stepId: 'stepX', ); expect(id, 'a-123'); await d.approveApproval(approvalId: 'a-123', reviewer: 'alice'); expect(d.approvals, hasLength(1)); expect(d.approvals.first, ('a-123', 'alice')); }); test('reject path carries reason through', () async { final d = _FakeApprovalDriver(); d.pending['flowA|stepX'] = 'a-123'; await d.rejectApproval( approvalId: 'a-123', reviewer: 'alice', reason: 'missing signature', ); expect(d.rejections, hasLength(1)); expect(d.rejections.first.$3, 'missing signature'); }); }); } /// Minimal driver that overrides nothing. Used to verify the /// default-throw stubs from `run_driver.dart`. class _NullDriver extends FlowRunDriver { @override Future> runFlow({ required String flowName, required Map textInputs, required Map fileInputs, required Map fileMimes, }) async => const {}; @override Stream events() => const Stream.empty(); } /// Studio-shaped driver mock — implements every approval hook /// + tracks side effects so tests can assert on them. class _FakeApprovalDriver extends FlowRunDriver { final Map pending = {}; final List<(String, String)> approvals = []; final List<(String, String, String)> rejections = []; @override Future> runFlow({ required String flowName, required Map textInputs, required Map fileInputs, required Map fileMimes, }) async => const {}; @override Stream events() => const Stream.empty(); @override Future pendingApprovalIdForStep({ required String flowName, required String stepId, }) async => pending['$flowName|$stepId']; @override Future approveApproval({ required String approvalId, required String reviewer, }) async { approvals.add((approvalId, reviewer)); } @override Future rejectApproval({ required String approvalId, required String reviewer, required String reason, }) async { rejections.add((approvalId, reviewer, reason)); } }