import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:wrnexus_inbox/wrnexus_inbox.dart'; class IdleStreamClient extends http.BaseClient { final controller = StreamController>(); @override Future send(http.BaseRequest request) async => http.StreamedResponse(controller.stream, 200); @override void close() { unawaited(controller.close()); } } void main() { setUpAll(() async { final loader = FontLoader('Inter'); loader.addFont( Future.value( ByteData.sublistView(await File('test/fonts/Inter.ttf').readAsBytes()), ), ); await loader.load(); final iconPath = '${Platform.environment['FLUTTER_ROOT'] ?? '../../.wrnexus/tools/flutter'}/bin/cache/artifacts/material_fonts/MaterialIcons-Regular.otf'; final icons = FontLoader('MaterialIcons'); icons.addFont( Future.value(ByteData.sublistView(await File(iconPath).readAsBytes())), ); await icons.load(); }); testWidgets('branding, actions, read/archive and recipient change', ( tester, ) async { var read = false, archived = false; final writes = >[]; final streams = []; final client = InboxClient( token: () => 'recipient', streamTransport: () { final c = IdleStreamClient(); streams.add(c); return c; }, transport: MockClient((r) async { if (r.method == 'POST') { final body = jsonDecode(r.body) as Map; writes.add(body); read = body['action'] == 'read' || read; archived = body['action'] == 'archive' || archived; return http.Response('{"id":"m"}', 200); } if (r.url.path.endsWith('preferences')) { return http.Response( jsonEncode({ 'branding': {'displayName': 'Acme', 'brandColor': '#123456'}, 'deliveryRules': { 'timezone': 'UTC', 'quietHours': null, 'marketingDailyLimit': null, }, 'preferences': [], 'channelPreferences': [], }), 200, ); } return http.Response( jsonEncode({ 'messages': archived ? [] : [ { 'id': 'm', 'recipient_id': 'r', 'title': 'Order ready', 'body': 'Your order is ready for collection.', 'purpose': 'transactional', 'category': 'orders', 'created_at': '2026-10-09T00:00:00Z', 'read_at': read ? '2026-10-09' : null, 'archived_at': null, 'url': 'javascript:alert(1)', 'actions': [ {'id': 'track', 'label': 'Track order'}, ], }, ], 'unreadCount': read || archived ? 0 : 1, }), 200, ); }), ); final actions = []; await tester.pumpWidget( MaterialApp( theme: ThemeData( fontFamily: 'Inter', scaffoldBackgroundColor: Colors.white, ), home: Scaffold( body: RepaintBoundary( key: const Key('inbox'), child: ColoredBox( color: Colors.white, child: WRNexusInboxView( client: client, onAction: (m, a, u) => actions.add(a!.id), ), ), ), ), ), ); await tester.pumpAndSettle(); expect(find.text('Acme'), findsOneWidget); expect(find.text('Order ready'), findsOneWidget); expect(find.text('Open'), findsNothing); await tester.tap(find.text('Track order')); expect(actions, ['track']); await expectLater( find.byKey(const Key('inbox')), matchesGoldenFile('goldens/branded-inbox.png'), ); await tester.tap(find.text('Mark read')); await tester.pumpAndSettle(); expect(writes.last, {'id': 'm', 'action': 'read'}); expect(find.text('Mark read'), findsNothing); await tester.tap(find.text('Archive')); await tester.pumpAndSettle(); expect(find.text('No notifications yet'), findsOneWidget); await tester.pumpWidget(const SizedBox()); client.close(); expect(streams.every((s) => s.controller.isClosed), isTrue); }); testWidgets( 'replacing the client clears the previous recipient before the new request resolves', (tester) async { final pending = Completer(); String settings(String name) => jsonEncode({ 'branding': {'displayName': name}, 'deliveryRules': {'timezone': 'UTC'}, 'preferences': [], 'channelPreferences': [], }); InboxClient make(String name, {bool wait = false}) => InboxClient( token: () => name, streamTransport: IdleStreamClient.new, transport: MockClient((request) async { if (request.url.path.endsWith('preferences')) { return wait ? pending.future : http.Response(settings(name), 200); } return http.Response( jsonEncode({ 'messages': [ { 'id': name, 'recipient_id': name, 'title': '$name message', 'body': 'Private content', 'purpose': 'transactional', 'category': 'orders', 'created_at': '2026-10-09', }, ], 'unreadCount': 1, }), 200, ); }), ); final first = make('First'); final second = make('Second', wait: true); Widget app(InboxClient client) => MaterialApp( home: Scaffold( body: WRNexusInboxView( key: const ValueKey('same-view'), client: client, ), ), ); await tester.pumpWidget(app(first)); await tester.pumpAndSettle(); expect(find.text('First message'), findsOneWidget); await tester.pumpWidget(app(second)); await tester.pump(); expect(find.text('First message'), findsNothing); expect(find.text('First'), findsNothing); pending.complete(http.Response(settings('Second'), 200)); await tester.pumpAndSettle(); expect(find.text('Second message'), findsOneWidget); expect(find.text('First message'), findsNothing); await tester.pumpWidget(const SizedBox()); first.close(); second.close(); }, ); }