import 'package:fancake/core/ui/app_icons.dart'; import 'package:fancake/features/notification/notification_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; /// F0 공통 — 화면 대조 원장 2화면. /// Bottom Navigation(`350:215853`)은 5탭 셸이라 `app_smoke_test` 가 검증한다. Future pumpWide(WidgetTester tester, Widget child) async { tester.view.physicalSize = const Size(1080, 2400); tester.view.devicePixelRatio = 3.0; addTearDown(tester.view.reset); await tester.pumpWidget(MaterialApp(home: child)); await tester.pump(); } void main() { group('날짜 머리글 (F0 · 518:88353)', () { final now = DateTime(2026, 9, 1, 12); test('오늘과 어제만 이름으로 부른다', () { expect(notificationDayLabel(DateTime(2026, 9, 1, 3), now: now), '오늘'); expect(notificationDayLabel(DateTime(2026, 8, 31, 23), now: now), '어제'); expect( notificationDayLabel(DateTime(2026, 8, 30), now: now), '2026.08.30', ); }); test('생성일시는 명세 형식을 그대로 쓴다', () { expect( notificationTimestamp(DateTime(2026, 1, 2, 3, 4)), '2026.01.02 03:04', ); }); }); group('날짜 그룹화', () { final now = DateTime(2026, 9, 1, 12); test('날짜로 묶고 최신이 앞에 온다', () { final items = [ AppNotification( id: 'a', kind: NotificationKind.like, title: 'a', at: DateTime(2026, 8, 30, 9), ), AppNotification( id: 'b', kind: NotificationKind.like, title: 'b', at: DateTime(2026, 9, 1, 9), ), AppNotification( id: 'c', kind: NotificationKind.like, title: 'c', at: DateTime(2026, 9, 1, 11), ), ]; final groups = groupNotifications(items, now: now); expect(groups.map((g) => g.label).toList(), ['오늘', '2026.08.30']); expect(groups.first.items.map((i) => i.id).toList(), ['c', 'b']); }); }); group('종류별 이동 (F0 · 518:88353)', () { test('이클립스와 아이템 사용은 이동하지 않는다', () { expect(NotificationKind.eclipse.navigable, isFalse); expect(NotificationKind.itemUsed.navigable, isFalse); }); test('나머지 넷은 갈 곳이 있다', () { for (final kind in const [ NotificationKind.like, NotificationKind.comment, NotificationKind.campaign, NotificationKind.quest, ]) { expect(kind.navigable, isTrue, reason: kind.label); } }); }); group('알림 화면 (F0 · 229:11265)', () { testWidgets('날짜 머리글과 항목을 함께 보여 준다', (tester) async { // 표본은 DateTime.now() 기준이라 자정 전후로 「오늘/어제」가 갈린다. // 시각을 못박아 넣는다. final now = DateTime.now(); await pumpWide( tester, NotificationScreen( repository: SampleNotificationRepository( items: [ AppNotification( id: 'q', kind: NotificationKind.quest, title: '만보기 퀘스트를 완료했습니다', at: now, ), AppNotification( id: 'o', kind: NotificationKind.like, title: '오래된 알림', at: now.subtract(const Duration(days: 9)), ), ], ), ), ); expect(find.text('오늘'), findsOneWidget); expect(find.textContaining('만보기 퀘스트를 완료'), findsOneWidget); expect(find.text('오래된 알림'), findsOneWidget); }); testWidgets('이동하지 않는 알림에는 화살표를 그리지 않는다', (tester) async { await pumpWide( tester, NotificationScreen( repository: SampleNotificationRepository( items: [ AppNotification( id: 'e', kind: NotificationKind.eclipse, title: '이클립스', at: DateTime.now(), ), ], ), ), ); expect( find.byWidgetPredicate( (w) => w is ImageIcon && w.image == AppIcons.chevronRight, ), findsNothing, ); }); testWidgets('누르면 읽음으로 바뀌고 뒤로가기가 읽은 목록을 돌려준다', (tester) async { final items = [ AppNotification( id: 'n1', kind: NotificationKind.quest, title: '만보기 퀘스트를 완료했습니다', at: DateTime.now(), ), ]; Set? returned; await pumpWide( tester, Builder( builder: (context) => TextButton( onPressed: () async { returned = await Navigator.of(context).push>( MaterialPageRoute( builder: (_) => NotificationScreen( repository: SampleNotificationRepository(items: items), ), ), ); }, child: const Text('열기'), ), ), ); await tester.tap(find.text('열기')); await tester.pumpAndSettle(); await tester.tap(find.textContaining('만보기 퀘스트를 완료')); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.arrow_back)); await tester.pumpAndSettle(); expect(returned, contains('n1')); }); testWidgets('누르면 스낵바가 아니라 대상 화면으로 간다 — 이클립스는 경로가 있어도 이동 X', (tester) async { final opened = []; final now = DateTime.now(); await pumpWide( tester, NotificationTargets( screenFor: (path) { opened.add(path); return Scaffold(body: Text('열린 화면 $path')); }, child: NotificationScreen( repository: SampleNotificationRepository( items: [ AppNotification( id: 'q', kind: NotificationKind.quest, title: '[만보기] 퀘스트를 완료했어요!', at: now, targetPath: 'quests/steps-1', ), AppNotification( id: 'e', kind: NotificationKind.eclipse, title: '내 행성의 이클립스가 발생했어요.', at: now, targetPath: 'fandoms/bts', ), ], ), ), ), ); await tester.tap(find.text('내 행성의 이클립스가 발생했어요.')); await tester.pumpAndSettle(); expect(find.byType(SnackBar), findsNothing); expect(find.textContaining('열린 화면'), findsNothing); await tester.tap(find.text('[만보기] 퀘스트를 완료했어요!')); await tester.pumpAndSettle(); expect(find.text('열린 화면 quests/steps-1'), findsOneWidget); expect(find.byType(SnackBar), findsNothing); }); testWidgets('비어 있으면 그렇게 알린다', (tester) async { await pumpWide( tester, NotificationScreen( repository: const SampleNotificationRepository(items: []), ), ); expect(find.text('알림이 없습니다.'), findsOneWidget); }); testWidgets('문구에 내부 경로가 새지 않는다', (tester) async { await pumpWide( tester, NotificationScreen(repository: const SampleNotificationRepository()), ); await tester.pump(); for (final text in tester.widgetList(find.byType(Text))) { final data = text.data ?? ''; expect(data, isNot(contains('lib/'))); expect(data, isNot(contains('.dart'))); } }); }); }