207 lines
6.8 KiB
Dart
207 lines
6.8 KiB
Dart
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<List<int>>();
|
|
@override
|
|
Future<http.StreamedResponse> 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 = <Map<String, dynamic>>[];
|
|
final streams = <IdleStreamClient>[];
|
|
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<String, dynamic>;
|
|
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 = <String>[];
|
|
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<http.Response>();
|
|
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();
|
|
},
|
|
);
|
|
}
|