Files
fanuzu/app/test/f0_screens_test.dart
T

260 lines
8.5 KiB
Dart

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<void> 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);
}
});
});
test('캠페인 알림은 문서 ID 앞머리로 달성 · 환불 · 취소 · 결과보고를 가른다(10/8 r11)', () {
String label(String id) => AppNotification(
id: id,
kind: NotificationKind.campaign,
title: 't',
at: DateTime(2026, 10, 8),
).sourceLabel;
expect(label('campaignSuccess_c1'), '캠페인 달성');
expect(label('campaignRefund_tx1'), '캠페인 환불');
expect(label('campaignCancel_c1'), '캠페인 취소');
expect(label('campaignReport_c1_r1'), '결과 보고');
expect(label('n1'), '캠페인 달성', reason: '모르는 앞머리는 종류 이름');
});
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<String>? returned;
await pumpWide(
tester,
Builder(
builder: (context) => TextButton(
onPressed: () async {
returned = await Navigator.of(context).push<Set<String>>(
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 = <String>[];
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<Text>(find.byType(Text))) {
final data = text.data ?? '';
expect(data, isNot(contains('lib/')));
expect(data, isNot(contains('.dart')));
}
});
});
}