// 화면 점검용 캡처 (일반 시험이 아니다). 폰 크기(360×800)로 주요 화면을 그려 test_ux/shots/*.png 로 남긴다. // flutter test test_ux/shots_test.dart --update-goldens import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:friendly_map/modules/app/app.dart'; import 'package:friendly_map/modules/core/core.dart'; import 'package:friendly_map/modules/kinship/kinship.dart'; import 'package:friendly_map/modules/platform/platform.dart'; import 'package:friendly_map/modules/storage/storage.dart'; import 'package:friendly_map/modules/transport/transport.dart'; import 'package:friendly_map/modules/ui/src/screens/chat_room_screen.dart'; import 'package:friendly_map/modules/ui/src/screens/contact_import_screen.dart'; import 'package:friendly_map/modules/ui/src/screens/circle_group_screen.dart'; import 'package:friendly_map/modules/ui/src/screens/circle_hub_screen.dart'; import 'package:friendly_map/modules/ui/src/screens/family_tree_screen.dart'; import 'package:friendly_map/modules/ui/src/screens/group_detail_screen.dart'; import 'package:friendly_map/modules/ui/src/screens/person_screen.dart'; import 'package:friendly_map/modules/ui/src/screens/requests_screen.dart'; import 'package:friendly_map/modules/ui/src/screens/settings_screen.dart'; import 'package:friendly_map/modules/ui/ui.dart'; import '../test/support/app_device.dart'; import '../test/support/fake_cipher.dart'; import '../test/support/storage_contract.dart' show FakeProbe; import '../test/widget_test.dart' show FakeBootstrap; class _Contacts implements ContactsProvider { @override Future requestPermission() async => true; @override Future> all() async => const [ DeviceContact(id: '1', name: '김민수', phones: ['010-3333-4444'], labels: ['가족']), DeviceContact(id: '2', name: '이영희', phones: ['010-5555-6666'], labels: ['고등학교 동창']), DeviceContact(id: '3', name: '박철수', phones: ['010-7777-8888'], labels: ['회사']), DeviceContact(id: '4', name: '최지은', phones: ['010-1212-3434']), DeviceContact(id: '5', name: '정우성', phones: ['02-555-1234']), ]; } Future _font(String family, String path) async { final bytes = File(path).readAsBytesSync(); await (FontLoader(family)..addFont(Future.value(ByteData.view(Uint8List.fromList(bytes).buffer)))).load(); } void main() { late AppDevice me, minsu; late FriendlyApp app; setUpAll(() async { await _font('Roboto', 'build/ux/kr.ttf'); await _font('MaterialIcons', '/sdks/flutter/bin/cache/artifacts/material_fonts/MaterialIcons-Regular.otf'); }); Future seed(WidgetTester tester) async { await tester.runAsync(() async { final hub = MemoryHub(); me = await AppDevice.create(hub, '010-1111-2222', '+821011112222', '이미영', contacts: _Contacts()); minsu = await AppDevice.create(hub, '010-3333-4444', '+821033334444', '김민수'); app = me.app; await app.friends.add(minsu.phone, minsu.name); await AppDevice.settleAll(); await minsu.app.friends.accept(me.phone); await AppDevice.settleAll(); for (final (p, n) in [('+821055556666', '이영희'), ('+821077778888', '박철수'), ('+821012123434', '최지은')]) { await app.friends.add(p, n); } final people = {for (final p in await app.friends.people()) p.name: p.id}; await app.relate.into(people['이영희']!, 'school.high'); final hb = (await app.groups.add('한빛전자', CardKinds.company, parent: CardKinds.company))!; final team = (await app.groups.add('결제팀', CardKinds.company, parent: hb.id))!; await app.relate.into(people['박철수']!, team.id); await app.relate.into(people['김민수']!, team.id); final room = (await app.chats.openDirect(people['김민수']!, CardKinds.personal))!; await app.chats.send(room.id, '안녕하세요, 내일 회의 몇 시죠?'); await app.work.assign(room.id, '결제 API 정리', assigneeId: people['김민수'], due: DateTime(2026, 10, 30)); await app.work.urgent(room.id, '결제 서버가 멈췄어요'); // 가족: 나(남) · 아내 · 아들 · 부모 · 할아버지 · 장인 await app.kin.setMe(gender: Gender.male, birthYear: 1990); Future rel(String n, Gender g) async => (await app.kin.addRelative(n, gender: g))!.id; final dad = await rel('이정훈', Gender.male); await app.relate.asFamily(dad, link: KinLink.parent, gender: Gender.male); final mom = await rel('박순자', Gender.female); await app.relate.asFamily(mom, link: KinLink.parent, gender: Gender.female); await app.kin.setSpouse(dad, mom); await app.relate.asFamily(await rel('이만복', Gender.male), link: KinLink.parent, to: dad, gender: Gender.male); final wife = await rel('최유진', Gender.female); await app.relate.asFamily(wife, link: KinLink.spouse, gender: Gender.female); await app.relate.asFamily(await rel('최상철', Gender.male), link: KinLink.parent, to: wife, gender: Gender.male); final son = await rel('이도윤', Gender.male); await app.relate.asFamily(son, link: KinLink.child, gender: Gender.male); await app.kin.setParent(son, wife); await app.facts.save(son, (await app.facts.get(son)).copyWith(birthYear: 2020, birthMonth: 11, birthDay: 3)); // 학교: 고등학교 동문 여럿 (연도·반·대화) final mineCards = await app.cards.mine(); await app.cards.edit(mineCards.firstWhere((c) => c.kind == CardKinds.school).id, fields: {'name': '이미영', 'school': '가람고', 'year': '2006'}); final cls = (await app.groups.add('27회 3반', CardKinds.school, parent: 'school.high'))!; var seq = 0; for (final (nm, yr) in [('정수빈', 2004), ('한지민', 2005), ('오세훈', 2006), ('윤아름', 2006), ('강동원', 2007), ('송혜교', 2007), ('유재석', 2008), ('조인성', 2003)]) { final p = (await app.friends.add('+82107000${(1000 + seq++)}', nm))!; await app.deps.storage.receivedCards.put(ReceivedCard(id: 'sc$seq', personId: p.id, kind: CardKinds.school, fieldsJson: encodeFields({'year': '$yr'}), receivedAt: 1, updatedAt: 1)); await app.groups.addMembers('school.high', [p.id]); if (yr == 2006) await app.groups.addMembers(cls.id, [p.id]); final room = (await app.chats.openDirect(p.id, CardKinds.personal))!; for (var i = 0; i < (seq * 2) % 7; i++) { await app.chats.send(room.id, '안녕 $i'); } } // 회사: 한빛전자 직원들 (직책·대화) await app.cards.edit(mineCards.firstWhere((c) => c.kind == CardKinds.company).id, fields: {'name': '이미영', 'company': '한빛전자', 'position': '과장'}); final all = await app.groups.all(); final hb2 = all.firstWhere((v) => v.group.name == '한빛전자').group; final pay = all.firstWhere((v) => v.group.name == '결제팀').group; for (final (nm, pos, g2) in [('김부장', '부장', hb2.id), ('박차장', '차장', pay.id), ('최과장', '과장', pay.id), ('정대리', '대리', pay.id), ('한사원', '사원', pay.id), ('오사원', '사원', hb2.id)]) { final p = (await app.friends.add('+82108000${(1000 + seq++)}', nm))!; await app.deps.storage.receivedCards.put(ReceivedCard(id: 'cc$seq', personId: p.id, kind: CardKinds.company, fieldsJson: encodeFields({'position': pos}), receivedAt: 1, updatedAt: 1)); await app.groups.addMembers(g2, [p.id]); final room = (await app.chats.openDirect(p.id, CardKinds.personal))!; for (var i = 0; i < (seq * 3) % 6; i++) { await app.chats.send(room.id, '안녕 $i'); } } // 김민수가 새 폰으로 다시 연결을 청함 final again = await AppDevice.create(hub, '010-3333-4444', '+821033334444', '김민수 새폰'); await again.app.friends.add(me.phone, '이미영'); await AppDevice.settleAll(); }); } Future open(WidgetTester tester) async { tester.view.physicalSize = const Size(1080, 2400); tester.view.devicePixelRatio = 3; addTearDown(tester.view.reset); await seed(tester); await tester.pumpWidget(FriendlyMapApp(bootstrap: FakeBootstrap(keys: true, app: app))); await tester.pumpAndSettle(); } Future shot(WidgetTester tester, String name) async { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 30))); await tester.pumpAndSettle(); await expectLater(find.byType(MaterialApp), matchesGoldenFile('shots/$name.png')); } Future push(WidgetTester tester, Widget page) async { tester.state(find.byType(Navigator).first).push(MaterialPageRoute(builder: (_) => page)); await tester.pumpAndSettle(); } Future tab(WidgetTester tester, String label) async { await tester.tap(find.descendant(of: find.byType(NavigationBar), matching: find.text(label))); await tester.pumpAndSettle(); } testWidgets('처음 시작', (tester) async { tester.view.physicalSize = const Size(1080, 2400); tester.view.devicePixelRatio = 3; addTearDown(tester.view.reset); final fresh = FriendlyApp(AppDeps(storage: Storage.memory(), transport: MemoryTransport('+82100', MemoryHub()), cipher: FakeCipher('K'), identity: FakeIdentity('K'), probe: FakeProbe(10))); await tester.pumpWidget(FriendlyMapApp(bootstrap: FakeBootstrap(app: fresh))); await tester.pumpAndSettle(); await shot(tester, '00_onboarding'); }); testWidgets('아래 메뉴 다섯 칸', (tester) async { await open(tester); await shot(tester, '01_chats'); await tab(tester, '관계'); await shot(tester, '02_relations'); await tab(tester, '업무'); await shot(tester, '03_tasks'); await tab(tester, '명함'); await shot(tester, '04_cards'); await tab(tester, '나'); await shot(tester, '05_me'); }); testWidgets('요청 · 그룹 · 설정', (tester) async { await open(tester); await push(tester, const RequestsScreen()); await shot(tester, '10_requests'); await push(tester, const GroupDetailScreen(groupId: CardKinds.school)); await shot(tester, '11_group_school'); await push(tester, const GroupDetailScreen(groupId: CardKinds.company)); await shot(tester, '12_group_company'); await push(tester, const SettingsScreen()); await shot(tester, '13_settings'); await push(tester, const FamilyTreeScreen()); await shot(tester, '14_family_tree'); await push(tester, const CircleHubScreen(rootId: CardKinds.school)); await shot(tester, '15_school_hub'); await push(tester, const CircleGroupScreen(groupId: 'school.high')); await shot(tester, '16_school_ring'); await tester.tap(find.text('목록')); await tester.pumpAndSettle(); await shot(tester, '17_school_list'); await tester.tap(find.text('고르기')); await tester.pumpAndSettle(); await tester.tap(find.textContaining('모두')); await tester.pumpAndSettle(); await shot(tester, '18_school_pick'); await push(tester, const CircleHubScreen(rootId: CardKinds.company)); await shot(tester, '19_company_hub'); await push(tester, const CircleGroupScreen(groupId: 'g1')); await tester.tap(find.text('목록')); await tester.pumpAndSettle(); await shot(tester, '19b_company_list'); }); testWidgets('주소록 · 사람 · 대화', (tester) async { await open(tester); await push(tester, const ContactImportScreen()); await shot(tester, '20_contacts'); final people = {for (final p in (await tester.runAsync(() => app.friends.people()))!) p.name: p.id}; await push(tester, PersonScreen(personId: people['김민수']!)); await shot(tester, '21_person'); await tester.tap(find.text('관계')); await tester.pumpAndSettle(); await shot(tester, '22_relate'); await tester.tapAt(const Offset(10, 10)); await tester.pumpAndSettle(); final rooms = (await tester.runAsync(() => app.chats.summaries()))!; await push(tester, ChatRoomScreen(roomId: rooms.first.room.id)); await shot(tester, '23_chat_room'); }); }