// 화면 점검용 캡처 (일반 시험이 아니다). 폰 크기(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/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/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, '결제 서버가 멈췄어요'); // 김민수가 새 폰으로 다시 연결을 청함 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'); }); 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'); }); }